Linux Ptrace 详解
转 https://blog.csdn.net/u012417380/article/details/60470075
Linux Ptrace 详解
一、系统调用
操作系统提供一系列系统调用函数来为应用程序提供服务。关于系统调用的详细相关知识,可以查看<<程序员的自我修养》第十二章。
对于x86操作系统来说,用中断命令“int 0x80”来进行系统调用,系统调用前,需要将系统调用号放入到%EAX
寄存器中,将系统的参数依次放入到寄存器%ebx
、%ecx
、%edx
以及%esi
和%edi
中。
以write系统调用为例:
write(2,"Hello",5);
- 1
在32位系统中会转换成:
movl $1,%eax
movl $2,%ebx
movl $hello,%ecx
movl $5,%edx
int $0x80
- 1
- 2
- 3
- 4
- 5
其中1
为write的系统调用号,所有的系统调用号定义在unistd.h
文件中,$hello 表示字符串“Hello”的地址;32位Linux系统通过0x80中断来进行系统调用。
64位系统用户应用层用整数寄存器%rdi ,%rsi,%rdx,%rcx, %r8以及 %r9
来传参。而内核接口用%rdi ,%rsi,%rdx,%r10,&r8以及%r10
来传参,并且用syscall
指令而不是80中断进行系统调用。
x86和x64都用寄存器rax
来保存调用号和返回值。
二、ptrace 函数简介
#include <sys/ptrace.h>
long ptrace(enum _ptrace_request request,pid_t pid,void * addr ,void *data);
- 1
- 2
- 3
ptrace()系统调用函数提供了一个进程(the “tracer”)监察和控制另一个进程(the “tracee”)的方法。并且可以检查和改变“tracee”进程的内存和寄存器里的数据。它可以用来实现断点调试和系统调用跟踪。
tracee首先需要被附着到tracer。在多线程进程中,每个线程都可以被附着到一个tracer。ptrace命令总是以ptrace(PTARCE_foo,pid,..)
的形式发送到tracee进程。pid是tracee线程ID。
当一个进程可以开始跟踪进程通过调用fork函数创建子进程并让子进程执行PTRACE_TRACEME,然后子进程再调用execve()(如果当前进程被ptrace,execve()成功执行后 SIGTRAP信号量会被发送到该进程)。一个进程也可以使用”PTRACE_ATTACH”或者”PTRACE_SEIZE”来跟踪另一个进程。
当进程被跟踪后,每当信号量传来,甚至信号量会被忽略时,tracee会暂停。tracer会在下次调用waitpid(wstatus)(或者其它wait系统调用)处被通知。该调用会返回一个包含tracee暂停原因信息的状态码。当tracee暂停后,tracer可以使用一系列ptrace请求来查看和修改tracee中的信息。tracer接着可以让tracee继续执行。tracee传递给tracer中的信号量通常被忽略。
当PTRACE_O_TRACEEXEC项未起作用时,所有成功执行execve()的tracee进程会被发送一个 SIGTRAP信号量后暂停,在新程序执行之前,父进程将会取得该进程的控制权。
当tracer结束跟踪后,可以通过调用PTRACE_DETACH继续让tracee执行。
prace更多相关信息可以查看http://man7.org/linux/man-pages/man2/ptrace.2.html官方文档。
三、示例
1.ptrace追踪子进程执行exec()
#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h> /* For constants ORIG_RAX etc */
int main(){
pid_t child;
long orig_rax;
child=fork();
if(child==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls",NULL);
}else{
wait(NULL);
orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
printf("The child made a system call %ld\n",orig_rax);
ptrace(PTRACE_CONT,child,NULL,NULL);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
编译后输出:
The child made a system call 59
user1@user-virtual-machine:~/hookTest$ a.out attach.c~ ex1.c ex1.o ex2.c~ ex3.c ex3.o ex4.c~ victim.c~
attach.c attach.o ex1.c~ ex2.c ex2.o ex3.c~ ex4.c victim.c victim.o
- 1
- 2
- 3
- 4
execl()
函数对应的系统调用为__NR_execve
,系统调用值为59。父进程通过调用fork()
来创建子进程。在子进程中,先运行patrce().请求参数设为PTRACE_TRACE,来告诉内核当前进程被父进程trace,每当有信号量传递到当前进程,该进程会暂停,提醒父进程在wait()调用处继续执行。然后再调用execl()。当execl()函数成功执行后,新程序运行之前,SIGTRAP信号量会被发送到该进程,让子进程停止,这时父进程会在wait相关调用处被通知,获取子进程的控制权,可以查看子进程内存和寄存器相关信息。
当进程进行系统调用时,int
会在内核栈中依次压入用户态的寄存器SS、ESP、EFLAGS、CS、EIP.中断处理程序的SAVE_ALL
宏会将 依次将EAX、EBP、EDI、ESI、EDX、ECX、EBX寄存器值压入内核栈。调用ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL)
获取USER area信息时<sys/reg.h>
文件定义了与内核栈寄存器数组顺序相同的下标:
#ifndef _SYS_REG_H
#define _SYS_REG_H 1
#ifdef __x86_64__
/* Index into an array of 8 byte longs returned from ptrace for
location of the users' stored general purpose registers. */
# define R15 0
# define R14 1
# define R13 2
# define R12 3
# define RBP 4
# define RBX 5
# define R11 6
# define R10 7
# define R9 8
# define R8 9
# define RAX 10
# define RCX 11
# define RDX 12
# define RSI 13
# define RDI 14
# define ORIG_RAX 15
# define RIP 16
# define CS 17
# define EFLAGS 18
# define RSP 19
# define SS 20
# define FS_BASE 21
# define GS_BASE 22
# define DS 23
# define ES 24
# define FS 25
# define GS 26
#else
/* Index into an array of 4 byte integers returned from ptrace for
* location of the users' stored general purpose registers. */
# define EBX 0
# define ECX 1
# define EDX 2
# define ESI 3
# define EDI 4
# define EBP 5
# define EAX 6
# define DS 7
# define ES 8
# define FS 9
# define GS 10
# define ORIG_EAX 11
# define EIP 12
# define CS 13
# define EFL 14
# define UESP 15
# define SS 16
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
这样8*ORIG_RAX
就找到USER area 中 ORIG_RAX
寄存器值的保存地址。ORIG_RAX
保存了系统调用号。
当检查完系统调用之后,可以调用ptrace并设置参数PTRACE_CONT
让子进程继续进行。
2.读取子进程系统调用参数
//64位下乌班图程序
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
int main(){
pid_t child;
long orig_rax;
int status;
int iscalling=0;
struct user_regs_struct regs;
child = fork();
if(child==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls","-l","-h",NULL);
}else{
while(1){
wait(&status);
if(WIFEXITED(status))
break;
orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
if(orig_rax == SYS_write){
ptrace(PTRACE_GETREGS,child,NULL,®s);
if(!iscalling){
iscalling =1;
printf("SYS_write call with %lld, %lld, %lld\n",regs.rdi,regs.rsi,regs.rdx);
} else{
printf("SYS_write call return %lld\n",regs.rax);
iscalling = 0;
}
}
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
}
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
编译后输出:
SYS_write call with 1, 140179049189376, 14
总用量 28K
SYS_write call return 14
SYS_write call with 1, 140179049189376, 51
-rw-rw-r-- 1 user1 user1 534 2月 26 18:02 ex1.c
SYS_write call return 51
SYS_write call with 1, 140179049189376, 52
-rw-rw-r-- 1 user1 user1 534 2月 26 18:02 ex1.c~
SYS_write call return 52
SYS_write call with 1, 140179049189376, 53
-rw-rw-r-- 1 user1 user1 1.1K 3月 2 13:02 hook2.c
SYS_write call return 53
SYS_write call with 1, 140179049189376, 54
-rw-rw-r-- 1 user1 user1 1.1K 3月 2 13:02 hook2.c~
SYS_write call return 54
SYS_write call with 1, 140179049189376, 53
-rwxrwxr-x 1 user1 user1 8.6K 3月 2 13:02 hook2.o
SYS_write call return 53
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
可以看到ls -l -h
执行了六次SYS_write
系统调用。
读取寄存器中的参数时,可以使用PTRACE_PEEKUSER
一个字一个字读取,也可以使用PTRACE_GETREGS
参数直接将寄存器的值读取到结构体user_regs_struct
中,该结构体定义在sys/user.h
中
对于PTRACE_STSCALL
参数,该参数会像PTRACE_CONT
一样使暂停的子进程继续执行,并在子进程下次进行系统调用前或系统调后,向子进程发送SINTRAP信号量,让子进程暂停。
WIFEXITED函数(宏)函数用来检查子进程是暂停还准备退出。
3.修改子进程系统调用参数
val = ptrace(PTRACE_PEEKDATA,child,addr,NULL)
- 1
PTRACE_PEEKDATA
、PTRACE_PEEKTEXT
参数是在tracee内存的addr地址处读取一个字(sizeof(long))的数据,反回值是long 型的,可多次读取addr
+i*sizeof(long)然后再合并得到最终字符串的内容。
现在,我们对系统调用write 输出的字符串参数进行反转:
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/syscall.h>
#include <sys/user.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#define long_size sizeof(long)
void reverse(char * str)
{
int i,j;
char temp;
for(i=0,j=strlen(str)-2;i<=j;++i,--j){
temp=str[i];
str[i]=str[j];
str[j]=temp;
}
}
void getdata(pid_t child,long addr,char * str,int len){
char * laddr;
int i,j;
union u{
long val;
char chars[long_size];
} data;
i=0;
j=len/long_size;
laddr=str;
while(i<j){
data.val=ptrace(PTRACE_PEEKDATA,child,addr+i*long_size,NULL);
if(data.val == -1){
if(errno){
printf("READ error: %s\n",strerror(errno));
}
}
memcpy(laddr,data.chars,long_size);
++i;
laddr +=long_size;
};
j=len % long_size;
if(j!=0){
data.val=ptrace(PTRACE_PEEKDATA,child,addr+i*long_size,NULL);
memcpy(laddr,data.chars,j);
}
str[len]='\0';
}
void putdata(pid_t child,long addr,char * str,int len){
char * laddr;
int i,j;
union u{
long val;
char chars[long_size];
} data;
i=0;
j=len /long_size;
laddr=str;
while(i<j){
memcpy(data.chars,laddr,long_size);
ptrace(PTRACE_POKEDATA,child,addr +i*long_size,data.val);
++i;
laddr+=long_size;
}
j=len%long_size;
if(j!=0){
//注意:由于写入时也是按字写入的,所以正确的做法是先将该字的高地址数据读出保存在data的高地址上 ,然后将该字再写入
memcpy(data.chars,laddr,j);
ptrace(PTRACE_POKEDATA,child,addr +i*long_size,data.val);
}
}
int main(){
pid_t child;
int status;
struct user_regs_struct regs;
child =fork();
if(child ==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls",NULL);
}else{
long orig_eax;
char *str,*laddr;
int toggle =0;
while(1){
wait(&status);
if(WIFEXITED(status))
break;
orig_eax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
if(orig_eax == SYS_write){
if(toggle == 0){
toggle =1;
ptrace(PTRACE_GETREGS,child,NULL,®s);
str=(char * )calloc((regs.rdx+1),sizeof(char));
getdata(child,regs.rsi,str,regs.rdx);
reverse(str);
putdata(child,regs.rsi,str,regs.rdx);
}else{
toggle =0;
}
}
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
}
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
输出:
user1@user-virtual-machine:~/hookTest$ ./hook3.o
o.3kooh ~c.3kooh c.3kooh o.2kooh ~c.2kooh c.2kooh ~c.1xe c.1xe
- 1
- 2
- 3
4.向其它程序注入指令
我们追踪其它独立运行的进程时,需要使用下面的命令:
ptrace(PTRACE_ATTACH, pid, NULL, NULL)
- 1
使pid进程成为被追踪的tracee进程。tracee进程会被发送一个SIGTOP信号量,tracee进程不会立即停止,直到完成本次系统调用。如果要结束追踪,则调用PTRACE_DETACH
即可。
debug 设置断点的功能可以通过ptrace实现。原理是ATTACH正在运行的进程使其停止。然后读取该进程的指令寄存器IR(32位x86为EIP,64w的是RIP)内容所指向的指令,备份后替换成目标指令,再使其继续执行,此时被追踪进程就会执行我们替换的指令,运行完注入的指令之后,我们再恢复原进程的IR
,从而达到改变原程序运行逻辑的目的。
tracee进程代码:
stdio.h>
int main(){
int i=0;
while(1){
printf("Hello,ptrace! [pid:%d]! num is %d\n",getpid(),i++);
sleep(2);
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
tracer进程代码
#include<sys/ptrace.h>
#include<sys/reg.h>
#include<sys/wait.h>
#include<sys/user.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<stdio.h>
#define long_size sizeof(long)
void getdata(pid_t child, long addr ,char * str,int len){
char * laddr =str;
int i,j;
union u{
long val;
char chars [long_size] ;
} data;
i=0;
j=len/long_size;
while(i<j){
data.val=ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);
if(data.val==-1){
if(errno){
printf("READ error: %s\n",strerror(errno));
}
}
memcpy(laddr,data.chars,long_size);
++i;
laddr=laddr+long_size;
}
j= len %long_size;
if(j!=0){
data.val=ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);
if(data.val==-1){
if(errno){
printf("READ error: %s\n",strerror(errno));
}
}
memcpy(laddr,data.chars,j);
}
str[len]='\0';
}
void putdata(pid_t child , long addr,char * str,int len){
char * laddr =str;
int i,j;
j=len/long_size;
i=0;
union u{
long val;
char chars [long_size] ;
} data;
while(i<j){
memcpy(data.chars,laddr,long_size);
ptrace(PTRACE_POKEDATA,child,addr + long_size*i,data.val);
++i;
laddr=laddr+long_size;
}
j=len%long_size;
if(j!=0){
data.val= ptrace(PTRACE_PEEKDATA,child,addr + long_size*i,NULL);
if(data.val==-1){
if(errno){
printf("READ error: %s\n",strerror(errno));
}
}
memcpy(data.chars,laddr,j);
ptrace(PTRACE_POKEDATA,child,addr + long_size*i,data.val);
}
}
int main(int argc,char * argv[]){
if(argc!=2){
printf("Usage: %s pid\n",argv[0]);
}
pid_t tracee = atoi(argv[1]);
struct user_regs_struct regs;
/*int 80(系统调用) int 3(断点)*/
unsigned char code[]={0xcd,0x80,0xcc,0x00,0,0,0,0}; //八个字节,等于long 型的长度
char backup[8]; //备份读取的指令
ptrace(PTRACE_ATTACH,tracee,NULL,NULL);
long inst; //用于保存指令寄存器所指向的下一条将要执行的指令的内存地址
wait(NULL);
ptrace(PTRACE_GETREGS,tracee,NULL,®s);
inst =ptrace(PTRACE_PEEKTEXT,tracee,regs.rip,NULL);
printf("tracee:RIP:0x%llx INST: 0x%lx\n",regs.rip,inst);
//读取子进程将要执行的 7 bytes指令并备份
getdata(tracee,regs.rip,backup,7);
//设置断点
putdata(tracee,regs.rip,code,7);
//让子进程继续执行并执行“int 3”断点指令停止
ptrace(PTRACE_CONT,tracee,NULL,NULL);
wait(NULL);
long rip=ptrace(PTRACE_PEEKUSER,tracee,8*RIP,NULL);//获取子进程停止时,rip的值
long inst2=ptrace(PTRACE_PEEKTEXT,tracee,rip,NULL);
printf("tracee:RIP:0x%lx INST: 0x%lx\n",rip,inst2);
printf("Press Enter to continue tracee process\n");
getchar();
putdata(tracee,regs.rip,backup,7); //重新将备份的指令写回寄存器
ptrace(PTRACE_SETREGS,tracee,NULL,®s);//设置会原来的寄存器值
ptrace(PTRACE_CONT,tracee,NULL,NULL);
ptrace(PTRACE_DETACH,tracee,NULL,NULL);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
先运行tracee.o 文件
$ ./tracee.o
- 1
此时tracee.o输出:
Hello,ptrace! [pid:14384]! num is 0
Hello,ptrace! [pid:14384]! num is 1
Hello,ptrace! [pid:14384]! num is 2
Hello,ptrace! [pid:14384]! num is 3
......
- 1
- 2
- 3
- 4
- 5
再另打开一个shell运行attach.o文件
$ ./.attach.o 14384 //pid
- 1
此时tracee.o执行到int 3断点指令停止,attach1,o输出:
tracee:RIP:0x7f48b0394f20 INST: 0x3173fffff0013d48
tracee:RIP:0x7f48b0394f23 INST: 0x8348c33100000000
Press Enter to continue tracee process
- 1
- 2
- 3
- 4
- 5
按任意键tracee.o恢复执行
参考:
Linux Ptrace 详解的更多相关文章
- Linux命令详解之—tail命令
tail命令也是一个非常常用的文件查看类的命令,今天就为大家介绍下Linux tail命令的用法. 更多Linux命令详情请看:Linux命令速查手册 Linux tail命令主要用来从指定点开始将文 ...
- Linux命令详解之—less命令
Linux下还有一个与more命令非常类似的命令--less命令,相比于more命令,less命令更加灵活强大一些,今天就给大家介绍下Linux下的less命令. 更多Linux命令详情请看:Linu ...
- Linux命令详解之—more命令
Linux more命令同cat命令一样,多用来查看文件内容,本文就为大家介绍下Linux more命令的用法. 更多Linux命令详情请看:Linux命令速查手册 Linux的more命令类似 ca ...
- 【转】linux命令详解:md5sum命令
[转]linux命令详解:md5sum命令 转自:http://blog.itpub.net/29320885/viewspace-1710218/ 前言 在网络传输.设备之间转存.复制大文件等时,可 ...
- Linux命令详解之—cat命令
cat命令的功能是连接文件或标准输入并打印,今天就为大家介绍下Linux中的cat命令. 更多Linux命令详情请看:Linux命令速查手册 Linux 的cat命令通常用来显示文件内容,也可以用来将 ...
- Linux命令详解之—pwd命令
Linux的pwd命令也是一个非常常用的命令,本文为大家介绍下Linux中pwd命令的用法. 更多Linux命令详情请看:Linux命令速查手册 Linux pwd命令用于显示工作目录. 执行pwd指 ...
- Linux命令详解之–cd命令
cd命令是linux实际使用当中另一个非常重要的命令,本文就为大家介绍下Linux中cd命令的用法. 更多Linux命令详情请看:Linux命令速查手册 Linux cd命令用于切换当前工作目录至 d ...
- Linux命令详解之–ls命令
今天开始为大家介绍下Linux中常用的命令,首先给大家介绍下Linux中使用频率最高的命令--ls命令. 更多Linux命令详情请看:Linux命令速查手册 linux ls命令用于显示指定工作目录下 ...
- Linux 系统结构详解
Linux 系统结构详解 Linux系统一般有4个主要部分: 内核.shell.文件系统和应用程序.内核.shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序.管理文件并使用系统 ...
随机推荐
- JDK动态代理 Proxy InvocationHandler
- shell编程 Shell script 的默认变量($0, $1...)
Shell script 的默认变量($0, $1...) 我们知道指令可以带有选项与参数,例如 ls -la 可以察看包含隐藏文件的所有属性与权限.那么 shell script 能不能在脚本文件名 ...
- Mybatis Plugin(拦截器)的开发
1.Plugin MyBatis 允许使用插件来拦截的方法调用包括: • Executor (update, query, flushStatements, commit, rollback, g ...
- JSPs only permit GET POST or HEAD的解决方案(REST风格)
问题:原文链接 https://blog.csdn.net/tiberroot/article/details/76615727 看到很多人解决办法使用 @ResponseBody注解 这个意思是按照 ...
- python大战机器学习——人工神经网络
人工神经网络是有一系列简单的单元相互紧密联系构成的,每个单元有一定数量的实数输入和唯一的实数输出.神经网络的一个重要的用途就是接受和处理传感器产生的复杂的输入并进行自适应性的学习,是一种模式匹配算法, ...
- 如果查询的结果少,可以把查询结果当子集,再查另一张表,比join的速度快得多得多
) as qty from riv_print_history_l phl JOIN tab_user_profile up on up.UP_USER_ID = phl.PHL_PRINT_USER ...
- RTC-IC-PCF2129
一特点: -20~70度,SPI/I2C接口,1.2 V to 4.2 V ,可编程看门狗,时钟输出以便校准,闹钟输出,时间戳输出.低功耗高精度,电源切换,中断输出. SPI接口和I2C接口硬件接法: ...
- Spring-boot原理(附带实现一个spring-boot-starter实例和代码下载)
(我就是个封面) Spring-boot自出现后,到现在火的很,大家貌似都在用,连招聘里面也要求会这个.但是说实话,spring-boot无外乎想实现一种可插拔的编程方式,说是简化配置,其实并没有 ...
- ruby 正则表达式 匹配所有符合规则的信息
假设一个字符串当中有很多符合规则的信息,下面的例子可以把所有匹配到的结果打印出来: message="afhadhffkdf414j9tr3j43i3433094jwoert223jwew1 ...
- webpack.config.js====webpack-dev-server开发服务器配置
1. 安装webpack-dev-server(在指定目录下),一定要先安装完毕webpack webpack-cli之后在安装webpack-dev-server 一个基于expressjs的开发服 ...