kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

Playing with ptrace, Part I — 玩转ptrace(二)

本文地址

版权所有 © 转载时必须以链接形式注明作者和原始出处!

Playing with ptrace, Part II
by Pradeep Padala p_padala@yahoo.com http://www.cise.ufl.edu/~ppadala
Created 2002-11-01 02:00

翻译: Magic.D E-mail: adamgic@163.com

在第一部分中我们已经看到ptrace怎么获取子进程的系统调用以及改变系统调用的参数。在这篇文章中,我们将要研究如何在子进程中设置断点和往运行中的程序里插入代码。实际上调试器就是用这种方法来设置断点和执行调试句柄。与前面一样,这里的所有代码都是针对i386平台的。
附着在进程上

在第一部分钟,我们使用ptrace(PTRACE_TRACEME, …)来跟踪一个子进程,如果你只是想要看进程是怎么进行系统调用和跟踪程序的,这个做法是不错的。但如果你要对运行中的进程进行调试,则需要使用 ptrace( PTRACE_ATTACH, ….)

当 ptrace( PTRACE_ATTACH, …)在被调用的时候传入了子进程的pid时, 它大体是与ptrace( PTRACE_TRACEME, …)的行为相同的,它会向子进程发送SIGSTOP信号,于是我们可以察看和修改子进程,然后使用 ptrace( PTRACE_DETACH, …)来使子进程继续运行下去。

下面是调试程序的一个简单例子

1
2
3
4
5
6
7
8
9
int main()
{
	int i;
	for(i = 0;i < 10; ++i) {
		printf("My counter: %d ", i);
		sleep(2);
	}
	return 0;
}

将上面的代码保存为dummy2.c。按下面的方法编译运行:
gcc -o dummy2 dummy2.c
./dummy2 &

现在我们可以用下面的代码来附着到dummy2上。

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
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>   /* For user_regs_struct
							 etc. */
int main(int argc, char *argv[])
{
	pid_t traced_process;
	struct user_regs_struct regs;
	long ins;
	if(argc != 2) {
		printf("Usage: %s <pid to be traced>\n",
			argv[0], argv[1]);
		exit(1);
	}
	traced_process = atoi(argv[1]);
	ptrace(PTRACE_ATTACH, traced_process,
		NULL, NULL);
	wait(NULL);
	ptrace(PTRACE_GETREGS, traced_process,
		NULL, ®s);
	ins = ptrace(PTRACE_PEEKTEXT, traced_process,
		regs.eip, NULL);
	printf("EIP: %lx Instruction executed: %lx\n",
		regs.eip, ins);
	ptrace(PTRACE_DETACH, traced_process,
		NULL, NULL);
	return 0;
}

上面的程序仅仅是附着在子进程上,等待它结束,并测量它的eip( 指令指针)然后释放子进程。 设置断点

调试器是怎么设置断点的呢?通常是将当前将要执行的指令替换成trap指令,于是被调试的程序就会在这里停滞,这时调试器就可以察看被调试程序的信息了。被调试程序恢复运行以后调试器会把原指令再放回来。这里是一个例子:

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
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>
const int long_size = sizeof(long);
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 * 4, NULL);
		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 * 4, 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 * 4, data.val);
		++i;
		laddr += long_size;
	}
	j = len % long_size;
	if(j != 0) {
		memcpy(data.chars, laddr, j);
		ptrace(PTRACE_POKEDATA, child,
			addr + i * 4, data.val);
	}
}
int main(int argc, char *argv[])
{
	pid_t traced_process;
	struct user_regs_struct regs, newregs;
	long ins;
	/* int 0x80, int3 */
	char code[] = {0xcd,0x80,0xcc,0};
	char backup[4];
	if(argc != 2) {
		printf("Usage: %s <pid to be traced>\n",
			argv[0], argv[1]);
		exit(1);
	}
	traced_process = atoi(argv[1]);
	ptrace(PTRACE_ATTACH, traced_process,
		NULL, NULL);
	wait(NULL);
	ptrace(PTRACE_GETREGS, traced_process,
		NULL, ®s);
	/* Copy instructions into a backup variable */
	getdata(traced_process, regs.eip, backup, 3);
	/* Put the breakpoint */
	putdata(traced_process, regs.eip, code, 3);
	/* Let the process continue and execute
	   the int 3 instruction */
	ptrace(PTRACE_CONT, traced_process, NULL, NULL);
	wait(NULL);
	printf("The process stopped, putting back "
		"the original instructions\n");
	printf("Press <enter> to continue\n");
	getchar();
	putdata(traced_process, regs.eip, backup, 3);
	/* Setting the eip back to the original
	   instruction to let the process continue */
	ptrace(PTRACE_SETREGS, traced_process,
		NULL, ®s);
	ptrace(PTRACE_DETACH, traced_process,
		NULL, NULL);
	return 0;
}

上面的程序将把三个byte的内容进行替换以执行trap指令,等被调试进程停滞以后,我们把原指令再替换回来并把eip修改为原来的值。下面的图中演示了指令的执行过程
1. 进程停滞后
2. 替换入trap指令
3.断点成功,控制权交给了调试器
4. 继续运行,将原指令替换回来并将eip复原
在了解了断点的机制以后,往运行中的程序里面添加指令也不再是难事了,下面的代码会使原程序多出一个”hello world”的输出

这时一个简单的”hello world”程序,当然为了我们的特殊需要作了点修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void main()
{
	__asm__("
		jmp forward
		backward:
			popl   %esi   # Get the address of
					# hello world string
			movl   $4, %eax   # Do write system call
			movl   $2, %ebx
			movl   %esi, %ecx
			movl   $12, %edx
			int   $0x80
			int3      # Breakpoint. Here the
					# program will stop and
					# give control back to
					# the parent
		forward:
			call   backward
			.string \"Hello World\\n\""
	);
}

使用 gcc -o hello hello.c
来编译它。
在backward和forward之间的跳转是为了使程序能够找到”hello world” 字符串的地址。
使用GDB我们可以得到上面那段程序的机器码。启动GDB,然后对程序进行反汇编:

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
(gdb) disassemble main
Dump of assembler code forfunction main:
0x80483e0<main>:       push   %ebp
0x80483e1<main+1>:   mov    %esp,%ebp
0x80483e3<main+3>:   jmp    0x80483fa<forward>
End of assembler dump.
(gdb) disassemble forward
Dump of assembler code forfunction forward:
0x80483fa<forward>: call   0x80483e5<backward>
0x80483ff<forward+5>:  dec  %eax
0x8048400<forward+6>:  gs
0x8048401<forward+7>:  insb   (%dx),%es:(%edi)
0x8048402<forward+8>:  insb   (%dx),%es:(%edi)
0x8048403<forward+9>:  outsl  %ds:(%esi),(%dx)
0x8048404<forward+10>: and  %dl,0x6f(%edi)
0x8048407<forward+13>: jb    0x8048475
0x8048409<forward+15>: or    %fs:(%eax),%al
0x804840c<forward+18>: mov  %ebp,%esp
0x804840e<forward+20>: pop  %ebp
0x804840f<forward+21>: ret
End of assembler dump.
(gdb) disassemble backward
Dump of assembler code forfunction backward:
0x80483e5<backward>:   pop  %esi
0x80483e6<backward+1>: mov  $0x4,%eax
0x80483eb<backward+6>: mov  $0x2,%ebx
0x80483f0<backward+11>:     mov %esi,%ecx
0x80483f2<backward+13>:     mov $0xc,%edx
0x80483f7<backward+18>:int  $0x80
0x80483f9<backward+20>:     int3
End of assembler dump.

我们需要使用从man+3到backward+20之间的字节码,总共41字节。使用GDB中的x命令来察看机器码。

1
2
3
4
5
6
(gdb) x/40bx main+3
<main+3>: eb 15 5e b8 04000000
<backward+6>: bb 0200000089 f1 ba
<backward+14>: 0c 000000 cd 80 cc
<forward+1>: e6 ff ff ff 4865 6c 6c
<forward+9>:6f20576f72 6c 64 0a

已经有了我们想要执行的指令,还等什么呢?只管把它们根前面那个例子一样插入到被调试程序中去!

代码:

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
int main(int argc,char*argv[])
{
	pid_t traced_process;
	struct user_regs_struct regs, newregs;
	long ins;
	int len =41;
	char insertcode[]=
		"\xeb\x15\x5e\xb8\x04\x00"
		"\x00\x00\xbb\x02\x00\x00\x00\x89\xf1\xba"
		"\x0c\x00\x00\x00\xcd\x80\xcc\xe8\xe6\xff"
		"\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f"
		"\x72\x6c\x64\x0a\x00";
	char backup[len];
	if(argc != 2) {
		printf("Usage: %s <pid to be traced>\n",
			argv[0], argv[1]);
		exit(1);
	}
	traced_process = atoi(argv[1]);
	ptrace(PTRACE_ATTACH, traced_process,
		NULL, NULL);
	wait(NULL);
	ptrace(PTRACE_GETREGS, traced_process,
		NULL,®s);
	getdata(traced_process, regs.eip, backup, len);
	putdata(traced_process, regs.eip,
		insertcode, len);
	ptrace(PTRACE_SETREGS, traced_process,
		NULL,®s);
	ptrace(PTRACE_CONT, traced_process,
		NULL, NULL);
	wait(NULL);
	printf("The process stopped, Putting back the original instructions\n");
	putdata(traced_process, regs.eip, backup, len);
	ptrace(PTRACE_SETREGS, traced_process,
		NULL,®s);
	printf("Letting it continue with original flow\n");
	ptrace(PTRACE_DETACH, traced_process,
		NULL, NULL);
	return0;
}

将代码插入到自由空间

在前面的例子中我们将代码直接插入到了正在执行的指令流中,然而,调试器可能会被这种行为弄糊涂,所以我们决定把指令插入到进程中的自由空间中去。通过察看/proc/pid/maps可以知道这个进程中自由空间的分布。接下来这个函数可以找到这个内存映射的起始点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
long freespaceaddr(pid_t pid)
{
	FILE *fp;
	char filename[30];
	char line[85];
	long addr;
	char str[20];
	sprintf(filename,"/proc/%d/maps", pid);
	fp = fopen(filename,"r");
	if(fp == NULL)
		exit(1);
	while(fgets(line,85, fp) != NULL) {
		sscanf(line,"%lx-%*lx %*s %*s %s",&addr,
			str, str, str, str);
		if(strcmp(str,"00:00")==0)
			break;
	}
	fclose(fp);
	return addr;
}

在/proc/pid/maps中的每一行都对应了进程中一段内存区域。主函数的代码如下:

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
int main(int argc,char*argv[])
{
	pid_t traced_process;
	struct user_regs_struct oldregs, regs;
	long ins;
	int len =41;
	char insertcode[]=
		"\xeb\x15\x5e\xb8\x04\x00"
		"\x00\x00\xbb\x02\x00\x00\x00\x89\xf1\xba"
		"\x0c\x00\x00\x00\xcd\x80\xcc\xe8\xe6\xff"
		"\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f"
		"\x72\x6c\x64\x0a\x00";
	char backup[len];
	long addr;
	if(argc !=2){
		printf("Usage: %s <pid to be traced>\n",
			argv[0], argv[1]);
			exit(1);
	}
	traced_process = atoi(argv[1]);
	ptrace(PTRACE_ATTACH, traced_process,
		NULL, NULL);
	wait(NULL);
	ptrace(PTRACE_GETREGS, traced_process,
		NULL,®s);
	addr = freespaceaddr(traced_process);
	getdata(traced_process, addr, backup, len);
	putdata(traced_process, addr, insertcode, len);
	memcpy(&oldregs,®s,sizeof(regs));
	regs.eip= addr;
	ptrace(PTRACE_SETREGS, traced_process,
		NULL,®s);
	ptrace(PTRACE_CONT, traced_process,
		NULL, NULL);
	wait(NULL);
	printf("The process stopped, Putting back the original instructions\n");
	putdata(traced_process, addr, backup, len);
	ptrace(PTRACE_SETREGS, traced_process,
		NULL,&oldregs);
	printf("Letting it continue with original flow\n");
	ptrace(PTRACE_DETACH, traced_process,
		NULL, NULL);
	return0;
}

ptrace的幕后工作

那么,在使用ptrace的时候,内核里发生了声么呢?这里有一段简要的说明:当一个进程调用了 ptrace( PTRACE_TRACEME, …)之后,内核为该进程设置了一个标记,注明该进程将被跟踪。内核中的相关原代码如下:

1
2
3
4
5
6
7
8
9
10
Source: arch/i386/kernel/ptrace.c
  if(request == PTRACE_TRACEME){
      /* are we already being traced? */
      if(current->ptrace & PT_PTRACED)
          goto out;
      /* set the ptrace bit in the process flags. */
      current->ptrace |= PT_PTRACED;
      ret =0;
      goto out;
  }

一次系统调用完成之后,内核察看那个标记,然后执行trace系统调用(如果这个进程正处于被跟踪状态的话)。其汇编的细节可以在 arh/i386/kernel/entry.S中找到。

现在让我们来看看这个sys_trace()函数(位于 arch/i386/kernel/ptrace.c )。它停止子进程,然后发送一个信号给父进程,告诉它子进程已经停滞,这个信号会激活正处于等待状态的父进程,让父进程进行相关处理。父进程在完成相关操作以后就调用ptrace( PTRACE_CONT, …)或者 ptrace( PTRACE_SYSCALL, …), 这将唤醒子进程,内核此时所作的是调用一个叫wake_up_process() 的进程调度函数。其他的一些系统架构可能会通过发送SIGCHLD给子进程来达到这个目的。 小结:

ptrace函数可能会让人们觉得很奇特,因为它居然可以检测和修改一个运行中的程序。这种技术主要是在调试器和系统调用跟踪程序中使用。它使程序员可以在用户级别做更多有意思的事情。已经有过很多在用户级别下扩展操作系统得尝试,比如UFO,一个用户级别的文件系统扩展,它使用ptrace来实现一些安全机制。

Playing with ptrace, Part I — 玩转ptrace(一)

原文
版权所有 © 转载时必须以链接形式注明作者和原始出处!

Playing with ptrace, Part I
by Pradeep Padala p_padala@yahoo.com http://www.cise.ufl.edu/~ppadala
Created 2002-11-01 02:00

翻译: Magic.D E-mail: adamgic@163.com
译者序:

在开发Hust Online Judge的过程中,查阅了不少资料,关于调试器技术的资料在网上是很少,即便是UNIX编程巨著《UNIX环境高级编程》中,相关内容也不多,直到我在 http://www.linuxjournal.com 上找到这篇文章,如获至宝,特翻译之,作为鄙人翻译技术文档的第一次尝试,必定会有不少蹩脚之处,各位就将就一下吧,欢迎大力拍砖。

你想过怎么实现对系统调用的拦截吗?你尝试过通过改变系统调用的参数来愚弄你的系统kernel吗?你想过调试器是如何使运行中的进程暂停并且控制它吗?

你可能会开始考虑怎么使用复杂的kernel编程来达到目的,那么,你错了。实际上Linux提供了一种优雅的机制来完成这些:ptrace系统函数。 ptrace提供了一种使父进程得以监视和控制其它进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪。

使用ptrace,你可以在用户层拦截和修改系统调用(sys call)

在这篇文章中,我们将学习如何拦截一个系统调用,然后修改它的参数。在本文的第二部分我们将学习更先进的技术:设置断点,插入代码到一个正在运行的程序中;我们将潜入到机器内部,偷窥和纂改进程的寄存器和数据段。

基本知识

操作系统提供了一种标准的服务来让程序员实现对底层硬件和服务的控制(比如文件系统),叫做系统调用(system calls)。当一个程序需要作系统调用的时候,它将相关参数放进系统调用相关的寄存器,然后调用软中断0×80,这个中断就像一个让程序得以接触到内核模式的窗口,程序将参数和系统调用号交给内核,内核来完成系统调用的执行。

在i386体系中(本文中所有的代码都是面向i386体系),系统调用号将放入%eax,它的参数则依次放入%ebx, %ecx, %edx, %esi 和 %edi。 比如,在以下的调用

1
Write(2, “Hello”, 5)

的汇编形式大概是这样的

1
2
3
4
5
movl $4, %eax
movl $2, %ebx
movl $hello, %ecx
movl $5, %edx
int $0×80

这里的$hello指向的是标准字符串”Hello”。

那么,ptrace会在什么时候出现呢?在执行系统调用之前,内核会先检查当前进程是否处于被“跟踪”(traced)的状态。如果是的话,内核暂停当前进程并将控制权交给跟踪进程,使跟踪进程得以察看或者修改被跟踪进程的寄存器。

让我们来看一个例子,演示这个跟踪程序的过程

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
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h> /* For constants
			   ORIG_EAX etc */
int main()
{
	pid_t child;
	long orig_eax;
	child = fork();
	if(child ==0){
		ptrace(PTRACE_TRACEME,0, NULL, NULL);
		execl("/bin/ls","ls", NULL);
	} else {
		wait(NULL);
		orig_eax = ptrace(PTRACE_PEEKUSER,
					  child,4* ORIG_EAX,
					  NULL);
		printf("The child made a "
			"system call %ld\n", orig_eax);
		ptrace(PTRACE_CONT, child, NULL, NULL);
	}
	return0;
}

运行这个程序,将会在输出ls命令的结果的同时,输出: The child made a system call 11

说明:11是execve的系统调用号,这是该程序调用的第一个系统调用。
想知道系统调用号的详细内容,察看 /usr/include/asm/unistd.h。

在以上的示例中,父进程fork出了一个子进程,然后跟踪它。在调用exec函数之前,子进程用PTRACE_TRACEME作为第一个参数调用了 ptrace函数,它告诉内核:让别人跟踪我吧!然后,在子进程调用了execve()之后,它将控制权交还给父进程。当时父进程正使用wait()函数来等待来自内核的通知,现在它得到了通知,于是它可以开始察看子进程都作了些什么,比如看看寄存器的值之类。

出现系统调用之后,内核会将eax中的值(此时存的是系统调用号)保存起来,我们可以使用PTRACE_PEEKUSER作为ptrace的第一个参数来读到这个值。
我们察看完系统调用的信息后,可以使用PTRACE_CONT作为ptrace的第一个参数,调用ptrace使子进程继续系统调用的过程。
ptrace函数的参数
Ptrace有四个参数

1
2
3
4
long ptrace(enum __ptrace_request request,
	pid_t pid,
	void *addr,
	void *data);

第一个参数决定了ptrace的行为与其它参数的使用方法,可取的值有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PTRACE_ME
PTRACE_PEEKTEXT
PTRACE_PEEKDATA
PTRACE_PEEKUSER
PTRACE_POKETEXT
PTRACE_POKEDATA
PTRACE_POKEUSER
PTRACE_GETREGS
PTRACE_GETFPREGS,
PTRACE_SETREGS
PTRACE_SETFPREGS
PTRACE_CONT
PTRACE_SYSCALL,
PTRACE_SINGLESTEP
PTRACE_DETACH

在下文中将对这些常量的用法进行说明。
读取系统调用的参数

通过将PTRACE_PEEKUSER作为ptrace 的第一个参数进行调用,可以取得与子进程相关的寄存器值。

先看下面这个例子

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
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>
#include <sys/syscall.h>   /* For SYS_write etc */
int main()
{
	pid_t child;
	long orig_eax, eax;
	long params[3];
	int status;
	int insyscall =0;
	child = fork();
	if(child ==0){
		ptrace(PTRACE_TRACEME,0, NULL, NULL);
		execl("/bin/ls","ls", NULL);
	} else {
		while(1) {
			wait(&status);
			if(WIFEXITED(status))
			break;
	          orig_eax = ptrace(PTRACE_PEEKUSER,
			child,4* ORIG_EAX, NULL);
			if(orig_eax == SYS_write) {
				if(insyscall == 0) {
					/* Syscall entry */
					insyscall =1;
					params[0]= ptrace(PTRACE_PEEKUSER,
							child,4* EBX,
							NULL);
					params[1]= ptrace(PTRACE_PEEKUSER,
							child,4* ECX,
							NULL);
					params[2]= ptrace(PTRACE_PEEKUSER,
                          child,4* EDX,
							NULL);
					printf("Write called with "
							"%ld, %ld, %ld\n",
							params[0], params[1],
							params[2]);
				} else {/* Syscall exit */
					eax = ptrace(PTRACE_PEEKUSER,
						child,4* EAX, NULL);
					printf("Write returned "
						"with %ld\n", eax);
							insyscall =0;
				}
			}
			ptrace(PTRACE_SYSCALL,
				child, NULL, NULL);
		}
	}
	return0;
}

这个程序的输出是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ppadala@linux:~/ptrace > ls
a.out        dummy.s      ptrace.txt
libgpm.html  registers.c  syscallparams.c
dummy        ptrace.html  simple.c
ppadala@linux:~/ptrace > ./a.out
Write called with 1,1075154944,48
a.out        dummy.s      ptrace.txt
Write returned with 48
Write called with 1,1075154944,59
libgpm.html  registers.c  syscallparams.c
Write returned with 59
Write called with 1,1075154944,30
dummy        ptrace.html  simple.c
Write returned with 30

以上的例子中我们跟踪了write系统调用,而ls命令的执行将产生三个write系统调用。使用PTRACE_SYSCALL作为ptrace的第一个参数,使内核在子进程做出系统调用或者准备退出的时候暂停它。这种行为与使用PTRACE_CONT,然后在下一个系统调用/进程退出时暂停它是等价的。

在前一个例子中,我们用PTRACE_PEEKUSER来察看write系统调用的参数。系统调用的返回值会被放入%eax。

wait函数使用status变量来检查子进程是否已退出。它是用来判断子进程是被ptrace暂停掉还是已经运行结束并退出。有一组宏可以通过status的值来判断进程的状态,比如WIFEXITED等,详情可以察看wait(2) man。 读取寄存器的值

如果你想在系统调用或者进程终止的时候读取它的寄存器,使用前面那个例子的方法是可以的,但是这是笨拙的方法。使用PRACE_GETREGS作为ptrace的第一个参数来调用,可以只需一次函数调用就取得所有的相关寄存器值。 获得寄存器值得例子如下:

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
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>
#include <sys/syscall.h>
int main()
{
	pid_t child;
	long orig_eax, eax;
	long params[3];
	int status;
	int insyscall =0;
	struct user_regs_struct regs;
	child = fork();
	if(child == 0) {
		ptrace(PTRACE_TRACEME,0, NULL, NULL);
		execl("/bin/ls","ls", NULL);
	} else {
		while(1) {
			wait(&status);
			if(WIFEXITED(status))
			break;
			orig_eax = ptrace(PTRACE_PEEKUSER,
				child,4* ORIG_EAX,
				NULL);
			if(orig_eax == SYS_write) {
				if(insyscall == 0) {
					/* Syscall entry */
					insyscall =1;
					ptrace(PTRACE_GETREGS, child,
						NULL,®s);
					printf("Write called with "
						"%ld, %ld, %ld\n",
						regs.ebx, regs.ecx,
						regs.edx);
				} else { /* Syscall exit */
					eax = ptrace(PTRACE_PEEKUSER,
						child,4* EAX,
						NULL);
					printf("Write returned "
						"with %ld\n", eax);
						insyscall =0;
				}
			}
			ptrace(PTRACE_SYSCALL, child,
				NULL, NULL);
		}
	}
	return0;
}

这段代码与前面的例子是比较相似的,不同的是它使用了PTRACE_GETREGS。 其中的user_regs_struct结构是在中定义的。
来点好玩的

现在该做点有意思的事情了,我们将要把传给write系统调用的字符串给反转。

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
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>
#include <sys/syscall.h>
constint 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 *4,
			NULL);
		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 *4,
			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 *4, data.val);
			++i;
		laddr += long_size;
	}
	j = len % long_size;
	if(j != 0) {
		memcpy(data.chars, laddr, j);
		ptrace(PTRACE_POKEDATA, child,
		addr + i *4, data.val);
	}
}
int main()
{
	pid_t child;
	child = fork();
	if(child ==0){
		ptrace(PTRACE_TRACEME,0, NULL, NULL);
		execl("/bin/ls","ls", NULL);
	} else {
		long orig_eax;
		long params[3];
		int status;
		char*str,*laddr;
		int toggle =0;
		while(1) {
			wait(&status);
			if(WIFEXITED(status))
			break;
			orig_eax = ptrace(PTRACE_PEEKUSER,
				child,4* ORIG_EAX,
				NULL);
			if(orig_eax == SYS_write){
				if(toggle ==0){
				toggle =1;
				params[0]= ptrace(PTRACE_PEEKUSER,
					child,4* EBX,
					NULL);
				params[1]= ptrace(PTRACE_PEEKUSER,
					child,4* ECX,
					NULL);
				params[2]= ptrace(PTRACE_PEEKUSER,
					child,4* EDX,
					NULL);
				str =(char*)calloc((params[2]+1) * sizeof(char));
				getdata(child, params[1], str, params[2]);
				reverse(str);
				putdata(child, params[1], str, params[2]);
			} else {
				toggle =0;
			}
		}
		ptrace(PTRACE_SYSCALL, child, NULL, NULL);
		}
	}
	return0;
}

输出是这样的:

1
2
3
4
5
6
7
8
ppadala@linux:~/ptrace > ls
a.out dummy.s ptrace.txt
libgpm.html registers.c syscallparams.c
dummy ptrace.html simple.c
ppadala@linux:~/ptrace > ./a.out
txt.ecartp s.ymmud tuo.a
c.sretsiger lmth.mpgbil c.llacys_egnahc
c.elpmis lmth.ecartp ymmud

这个例子中涵盖了前面讨论过的所有知识点,当然还有些新的内容。这里我们用PTRACE_POKEDATA作为第一个参数,以此来改变子进程中的变量值。它以与PTRACE_PEEKDATA相似的方式工作,当然,它不只是偷窥变量的值了,它可以修改它们。

单步

ptrace 提供了对子进程进行单步的功能。 ptrace(PTRACE_SINGLESTEP, …) 会使内核在子进程的每一条指令执行前先将其阻塞,然后将控制权交给父进程。下面的例子可以查出子进程当前将要执行的指令。为了便于理解,我用汇编写了这个受控程序,而不是让你为c的库函数到底会作那些系统调用而头痛。

以下是被控程序的代码 dummy1.s,使用gcc –o dummy1 dummy1.s来编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.data
hello:
	.string"hello world\n"
.globl main
main:
	movl $4,%eax
	movl $2,%ebx
	movl $hello,%ecx
	movl $12,%edx
int $0x80
	movl $1,%eax
	xorl %ebx,%ebx
int $0x80
	ret

以下的程序则用来完成单步

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
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>
#include <sys/syscall.h>
int main()
{
	pid_t child;
	const int long_size =sizeof(long);
	child = fork();
	if(child ==0){
		ptrace(PTRACE_TRACEME,0, NULL, NULL);
		execl("./dummy1","dummy1", NULL);
	} else {
		int status;
		union u {
		long val;
		char chars[long_size];
	} data;
	struct user_regs_struct regs;
	int start =0;
	long ins;
	while(1) {
		wait(&status);
		if(WIFEXITED(status))
		break;
		ptrace(PTRACE_GETREGS, child, NULL,®s);
		if(start ==1){
			ins = ptrace(PTRACE_PEEKTEXT,
				child, regs.eip,
				NULL);
			printf("EIP: %lx Instruction executed: %lx ",
				regs.eip, ins);
		}
		if(regs.orig_eax== SYS_write){
			start =1;
			ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
		}
		else
			ptrace(PTRACE_SYSCALL, child, NULL, NULL);
		}
	}
	return0;
}

程序的输出是这样的:
你可能需要察看Intel的用户手册来了解这些指令代码的意思。
更复杂的单步,比如设置断点,则需要很仔细的设计和更复杂的代码才可以实现。

在第二部分,我们将会看到如何在程序中加入断点,以及将代码插入到已经在运行的程序中

linux ptrace函数

1
2
#include <sys/ptrace.h>
int ptrace(int request, int pid, int addr, int data);
描述

Ptrace提供了一种父进程可以控制子进程运行,并可以检查和改变它的核心image。它主要用于实现断点调试。一个被跟踪的进程运行中,直到发生一个信号。则进程被中止,并且通知其父进程。在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。

AT&T 汇编

AT&T 汇编

1.Register Reference

引用寄存器要在寄存器号前加百分号%,如“movl %eax, %ebx”。
80386 有如下寄存器:

1
2
3
4
5
6
7
8
9
10
[1] 8 个 32-bit 寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp;
( 8 个 16-bit 寄存器,它们事实上是上面 8 个 32-bit 寄存器的低 16 位:%ax,%bx,
%cx,%dx,%di,%si,%bp,%sp;
8 个 8-bit 寄存器:%ah,%al,%bh,%bl,%ch,%cl,%dh,%dl。它们事实上
是寄存器%ax,%bx,%cx,%dx 的高 8 位和低 8 位;)
[2] 6 个段寄存器:%cs(code),%ds(data),%ss(stack), %es,%fs,%gs;
[3] 3 个控制寄存器:%cr0,%cr2,%cr3;
[4] 6 个 debug 寄存器:%db0,%db1,%db2,%db3,%db6,%db7;
[5] 2 个测试寄存器:%tr6,%tr7;
[6] 8 个浮点寄存器栈:%st(0),%st(1),%st(2),%st(3),%st(4),%st(5),%st(6),%st(7)。

2. Operator Sequence

操作数排列是从源(左)到目的(右),如“movl %eax(源), %ebx(目的)”

3. Immediately Operator

使用立即数,要在数前面加符号$, 如“movl $0x04, %ebx”
或者:

1
2
para = 0x04
movl $para, %ebx

指令执行的结果是将立即数 0x04 装入寄存器 ebx。

4. Symbol Constant

符号常数直接引用 如

1
2
value: .long 0x12a3f2de
movl value , %ebx

指令执行的结果是将常数 0x12a3f2de 装入寄存器 ebx。
引用符号地址在符号前加符号$, 如“movl $value, % ebx”则是将符号 value 的地址装入寄存器 ebx。

5. Length of Operator

操作数的长度用加在指令后的符号表示 b(byte, 8-bit), w(word, 16-bits), l(long,32-bits) ,如“movb %al, %bl” ,“movw %ax, %bx”,“movl %eax, %ebx ”。
如 果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。比如指令“mov %ax, %bx”,由于目标操作数 bx 的长度为 word , 那 么 编 译 器 将 把 此 指 令 等 同 于 “ movw %ax,%bx” 。 同 样 道 理 , 指 令 “ mov $4, %ebx” 等 同 于 指 令 “ movl $4, %ebx”,“push %al”等同于“pushb %al”。对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令 “push $4”。

6. Sign and Zero Extension

绝大多数面向 80386 的 AT&T 汇编指令与 Intel 格式的汇编指令都是相同的,但符号扩展指令和零扩展指令有不同格式。符号扩展指令 和零扩展指令需要指定源操作数长度和目的操作数长度,即使在某些指令中这些操作数是隐含的。

在 AT&T 语法中,符号扩展和零扩展指令的格式为,基本部分"movs"和"movz"(对应 Intel 语法的 movsx 和 movzx),后面跟 上源操作数长度和目的操作数长度。 movsbl 意味着 movs (from)byte (to)long;movbw 意味着 movs (from)byte (to)word;movswl 意味着 movs (from)word (to)long。对于 movz 指令也一样。比如指令“movsbl %al,%edx”意味着将 al 寄存器的内容进行符号扩展后放置到 edx 寄存器中。

其它的 Intel 格式的符号扩展指令还有:

1
2
3
4
cbw -- sign-extend byte in %al to word in %ax;
cwde -- sign-extend word in %ax to long in %eax;
cwd -- sign-extend word in %ax to long in %dx:%ax;
cdq -- sign-extend dword in %eax to quad in %edx:%eax;

对应的 AT&T 语法的指令为 cbtw,cwtl,cwtd,cltd。

7. Call and Jump

段内调用和跳转指令为 “call” , “ret” 和 “jmp”,段间调用和跳转指令为 “lcall” , “lret” 和 “ljmp” 。段间调用和跳转指令的格式为
“lcall/ljmp $SECTION, $OFFSET”,而段间返回指令则为“lret $STACK-ADJUST”。

8. Prefix

操作码前缀被用在下列的情况:

1
2
3
4
[1]字符串重复操作指令(rep,repne);
[2]指定被操作的段(cs,ds,ss,es,fs,gs);
[3]进行总线加锁(lock);
[4]指定地址和操作的大小(data16,addr16);

在 AT&T 汇编语法中,操作码前缀通常被单独放在一行,后面不跟任何操作数。例如,对于重复 scas 指令,其写法为:

1
2
repne
scas

上述操作码前缀的意义和用法如下:

1
2
3
4
5
6
7
8
9
[1]指定被操作的段前缀为 cs,ds,ss,es,fs,和 gs。在 AT&T 语法中,只需要按照
section:memory-operand 的格式就指定了相应的段前缀。比如:
lcall %cs:realmode_swtch
[2]操作数/地址大小前缀是“data16”和"addr16",它们被用来在 32-bit 操作数/地址代码中指定 16-bit 的操作数/地址。
[3]总线加锁前缀“lock”,它是为了在多处理器环境中,保证在当前指令执行期间禁止一切中断。这个前缀仅仅对 ADD, ADC, AND,
BTC, BTR, BTS, CMPXCHG,DEC,
INC, NEG, NOT, OR, SBB, SUB, XOR, XADD,XCHG 指令有效,如果将 Lock 前
缀用在其它指令之前,将会引起异常。
[4]字符串重复操作前缀"rep","repe","repne"用来让字符串操作重复“%ecx”次。

9. Memory Reference

Intel 语法的间接内存引用的格式为:

1
section:[base+index*scale+displacement]

而在 AT&T 语法中对应的形式为:

1
section:displacement(base,index,scale)

其中,base 和 index 是任意的 32-bit base 和 index 寄存器。scale 可以取值 1,2,4,8。如果不指定 scale 值,则默认值为 1。 section 可以指定任意的段寄存器作为段前缀,默认的段寄存器在不同的情况下不一样。如果在指令中指定了默认的段前缀,则编译器在 目标代码中不会产生此段前缀代码。

下面是一些例子:
-4(%ebp):base=%ebp,displacement=-4,section 没有指定,由于 base=%ebp,所以默认的 section=%ss,index,scale 没有指定,则 index 为 0。
foo(,%eax,4):index=%eax,scale=4,displacement=foo。其它域没有指定。这里默认的 section=%ds。
foo(,1):这个表达式引用的是指针 foo 指向的地址所存放的值。注意这个表达式中没有 base 和 index,并且只有一个逗号,这是一种 异常语法,但却合法。
%gs:foo:这个表达式引用的是放置于%gs 段里变量 foo 的值。
如果 call 和 jump 操作在操作数前指定前缀“”,则表示是一个绝对地址调用/跳转,也就是说 jmp/call 指令指定的是一个绝对地址。
如果没有指定"
“,则操作数是一个相对地址。
任何指令如果其操作数是一个内存操作, 则指令必须指定它的操作尺寸
(byte,word,long),也就是说必须带有指令后缀(b,w,l)。
Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地
址计算方法:
disp + base + index * scale
下面是一些内存操作数的例子:

1
2
3
4
5
AT&T 格式
movl -4(%ebp), %eax
movl array(, %eax, 4), %eax
movw array(%ebx, %eax, 4), %cx
movb $4, %fs:(%eax)

其中下面这些省略了浮点数及 IA-32 如 SSE FPU 等特殊的指令集部分, 我觉得重要的是学习 linux 汇编的语法及编译原理和程序控制流程, 具体的指令细节就不那么重要了。

LINUX汇编

一, IA-32 硬件特性

寄存器:

1, 通用寄存器, 用于存放正在处理的数据
1
2
3
4
5
6
7
8
EAX 用于操作数和结果数的累加器
EBX 指向数据内存断中的数据的指针
ECX 字符串和循环操作的计数器
EDX IO指针
EDI 用于字符串操作的目标的数据指针
ESI 用于字符串操作的源的数据指针
ESP 堆栈指针
EBP 堆栈数据指针

其中寄存器EAX, EBX, ECX, EDX又可以通过16位和8位寄存器名称引用 如EAX, AX 引用EAX低16位, AL 引用EAX低8位, AH 引用AL之后的高8位

2, 段寄存器:

IA-32平台允许使用3中内存模型: 平坦内存模式 分段内存模式 实地址模式

平坦内存: 把全部的系统内存表示为连续的地址空间, 通过线性地址的特定地址访问内存位置.

分段内存: 把系统内存划分为独立的段组, 通过位于寄存器中的指针进行引用. 每个段用于包含特定类型的数据。 一个段用于包含指令码, 另一个段包含数据元素, 第三个段包含数据堆栈。段中的内存位置是通过逻辑地址引用的, 逻辑地址是由段地址加上偏移量构成, 处理器把逻辑地址转换为相应的线性地址以便访问。

1
2
3
4
5
6
7
段寄存器:
CS 代码段
DS 数据段
SS 堆栈段
ES 附加段指针
FS 附加段指针
GS 附加段指针

每个段寄存器都是16位的, 包含指向内存特定段起始位置的指针,程序不能显示加载或改变CS寄存器, DS, ES, FS, GS都用于指向数据段, 通过4个独立的段, 程序可以分隔数据元素, 确保他们不会重叠, 程序必须加载带有段的正确指针值的数据段寄存器, 并且使用偏移值引用各个内存的位置。SS段寄存器用于指向堆栈段, 堆栈包含传递给函数和过程的数据值。

实地址: 如果实地址模式, 所有段寄存器都指向线性0地址, 并且都不会被程序改动,所有的指令码 数据元素 堆栈元素 都是通过他们的线性地址直接访问的。

3, 指令指针寄存器

是EIP寄存器, 它跟踪要执行程序的下一条指令代码, 应用程序不能修改指令指针本身,不能指定内存地址把它拖放EIP寄存器中,相反必须通过一般的跳转指令来改变预存取缓存的下一条指令。

在平坦内存模型中, 指令指针包含下一条指令码的线性地址, 在分段模型中指令指针包含逻辑地址指针, 通过CS寄存器的内存引用。

4, 控制寄存器
1
2
3
4
5
CRO 控制操作模式 和 处理器当前状态的系统标志
CR1 当前没有使用
CR2 内存页面错误信息
CR3 内存页面目录信息
CR4 支持处理器特性和说明处理器特性能力的标志

不能直接访问控制寄存器, 但是能把控制寄存器中的值传递给通用寄存器,如果必须改动控制寄存器的标志, 可以改动通用寄存器的值, 然后把内容传递给控制寄存器。

标志:

IA-32使用单一的寄存器来包含一组状态控制和系统标志, EFLAGS寄存器包含32位标志信息

1, 状态标志
1
2
3
4
5
6
7
8
标志 位 说明
CF 0 进位标志, 如果无符号数的数学操作产生最高有效位的进位或者借位, 此时值为1
PF 2 奇偶校验标志, 用于表明数学操作的结果寄存器中的是否包含错误数据
AF 4 辅助进位标志, 用于二进制编码的10进制(BCD)的数学操作中, 如果用于运算的
寄存器的第三位发生进位或借位, 该值为1
ZF 6 0标志, 如果操作为0, 则该值为1
SF 7 符号标志, 设置为结果的最高有效位, 这一位是符号位表明结果是正值还是负值
OF 11 溢出标志
2, 控制标志

当前只定义了一个控制标志DF即方向标志, 用于控制处理器处理字符串的方式 如果设置为1, 字符串指令自动递减内存地址以便到达字符串中的下一字节。 反之。

3, 系统标志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
标志 位 说明
TF 8 陷阱标志, 设置为1时启用单步模式, 在单步模式下处理器每次只执行一条命令。
IF 9 中断使能标志, 控制处理器如响应从外部源接收到的信号。
IOPL 12和13 IO特权级别标志, 表明当前正在运行任务的IO特权级别, 它定义IO地址空间的
特权访问级别, 该值必须小于或者等于访问I/O地址空间的级别; 否则任何访问
IO空间的请求都会被拒绝!
NT 14 嵌套任务标志控制当前运行的任务是否连接到前一个任务, 它用于连接被中断
和被调用的任务.
RF 16 恢复标志用于控制在调试模式中如何响应异常。
VM 17 虚拟8086模式, 表明处理器在虚拟8086模式中而不是保护模式或者实模式。
AC 18 对准检查标志, 用于启用内存引用的对准检查
VIF 19 虚拟中断标志, 当处理器在虚拟模式中操作时, 该标志起IF标志的作用.
VIP 20 虚拟中断挂起标志, 在虚拟模式操作时用于表示一个中断正在被挂起。
ID 21 表示CPU是否支持cpuid指令, 如果处理器能够设置或者清零这个标志, 表示
处理器支持该指令。

二,GNU汇编工具系列

1, 二进制工具系列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
addr2line 把地址转换成文件名或者行号
ar 创建 修改或者展开文件存档
as 把汇编语言代码汇编成目标代码
c++filt 还原c++符号的过滤器
gprof 显示程序简档信息的程序
ld 把目标代码文件转换成可执行文件的转换器
nm 列出目标文件中的符号
objcopy 复制或翻译目标文件
objdump 显示来自目标文件的信息
ranlib 生成存档文件内容的索引
readelf 按照elf格式显示目标文件信息
size 列出目标文件或者存档文件的段长度
strings 显示目标文件中可打印字符串
strip 丢弃符号
windres 编译Microsoft Windows资源文件

2, GNU编译器

gcc

3, GNU调试程序

gdb

三, GNU汇编语言结构

主要包括三个常用的段: data 数据段 声明带有初始值的元素
bss 数据段 声明使用0或者null初始化的元素
text 正文段 包含的指令, 每个汇编程序都必须包含此段

使用.section 指令定义段, 如:

1
2
3
.section .data
.section .bss
.section .text

起始点:

gnu汇编器使用start标签表示默认的起始点, 此外如果想要汇编内部的标签能够被外部程序访问, 需要使用.globl 指令, 如:.globl start

使用通用库函数时可以使用:
ld -dynamic-linker /lib/ld-linux.so.2

四, 数据传递

1, 数据段

使用.data声明数据段, 这个段中声明的任何数据元素都保留在内存中并可以被汇编程序的指令读取, 此外还可以使用.rodata声明只读的数据段, 在声明一个数据元素时, 需要使用标签和命令:

标签:用做引用数据元素所使用的标记, 它和c语言的变量很相似, 它对于处理器是没有意义的, 它 只是用做汇编器试图访问内存位置时用做引用指针的一个位置。

指令:这个名字指示汇编器为通过标签引用的数据元素保留特定数量的内存, 声明命令之后必须给出 一个或多个默认值。

声明指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.ascii 文本字符串
.asciz 以空字符结尾的字符串
.byte 字节值
.double 双精度浮点值
.float 单精度浮点值
.int 32位整数
.long 32位整数, 和int相同
.octa 16字节整数
.quad 8字节整数
.short 16位整数
.single 单精度浮点数(和float相同)


例子:
output:
.ascii "hello world."

pi:
.float 2.14

声明可以在一行中定义多个值, 如:
ages:
.int 20, 10, 30, 40

定义静态符号:
使用.equ命令把常量值定义为可以在文本段中使用的符号,如:

1
2
3
4
.section .data
.equ LINUX_SYS_CALL, 0x80
.section .text
movl $LINUX_SYS_CALL, %eax

2, bss段

和data段不同, 无需声明特定的数据类型, 只需声明为所需目的保留的原始内存部分即可。 GNU汇编器使用以下两个命令声明内存区域:

1
2
.comm 声明为未初始化的通用内存区域
.lcomm 声明为未初始化的本地内存区域

两种声明很相似, 但.lcomm是为不会从本地汇编代码之外进行访问的数据保留的, 格式为:

1
.comm/.lcomm symbol, length

例子:

1
2
.section .bss
.lcomm buffer, 1000

该语句把1000字节的内存地址赋予标签buffer, 在声明本地通用内存区域的程序之外的函数是 不能访问他们的.(不能在.globl命令中使用他们)

在bss段声明的好处是, 数据不包含在可执行文件中。在数据段中定义数据时, 它必须被包含在 可执行程序中, 因为必须使用特定值初始化它。 因为不使用数据初始化bss段中声明的数据区域, 所以内存区域被保留在运行时使用, 并且不必包含在最终的程序中

3, 传送数据

move 指令: 格式 movex 源操作数, 目的操作数。 其中x为要传送数据的长度, 取值有: l 用于32位的长字节
w 用于16位的字
b 用于8位的字节值

立即数前面要加一个$符号, 寄存器前面要加%符号。
8个通用的寄存器是用于保存数据的最常用的寄存器, 这些寄存器的内容可以传递 给其他的任何可用的寄存器。 和通用寄存器不同, 专用寄存器(控制, 调试, 段) 的内容只能传送给通用寄存器, 或者接收从通用寄存器传过来的内容。

在对标签进行引用时: 例:

1
2
3
4
5
6
7
8
.section .data
value:
.int 100
_start:
movl value, %eax
movl $value, %eax
movl %ebx, (%edi)
movl %ebx, 4(%edi)

其中:
movl value, %eax 只是把标签value当前引用的内存值传递给eax
movl $value, %eax 把标签value当前引用的内存地址指针传递给eax
movl %ebx, (%edi) 如果edi外面没有括号那么这个指令只是把ebx中的
值加载到edi中, 如果有了括号就表示把ebx中的内容
传送给edi中包含的内存位置。
movl %ebx, 4(%edi) 表示把edi中的值放在edi指向的位置之后的4字节内存位置中
movl %ebx, -4(%edi) 表示把edi中的值放在edi指向的位置之前的4字节内存位置中

cmove 指令(条件转移): cmovex 源操作数, 目的操作数. x的取值为:
无符号数:

1
2
3
4
5
6
7
8
9
10
a/nbe 大于/不小于或者等于
ae/nb 大于或者等于/不小于
nc 无进位
b/nae 小于/不大于等于
c 进位
be/na 小于或等于/不大于
e/z 等于/零
ne/nz 不等于/不为零
p/pe 奇偶校验/偶校验
np/po 非奇偶校验/奇校验

有符号数:

1
2
3
4
5
6
7
ge/nl 大于或者等于/不小于
l/nge 小于/不大于或者等于
le/ng 小于或者等于/不大于
o 溢出
no 未溢出
s 带符号(负)
ns 无符号(非负)

交换数据: xchg 在两个寄存器之间或者寄存器和内存间交换值。如:
xchg 操作数, 操作数, 要求两个操作数必须长度相同且不能同时都是内存位置
其中寄存器可以是32,16,8位的

bswap 反转一个32位寄存器的字节顺序
如: bswap %ebx

xadd 交换两个值 并把两个值只和存储在目标操作数中
如: xadd 源操作数,目标操作数
其中源操作数必须是寄存器, 目标操作数可以是内存位置也可以是寄存器
其中寄存器可以是32,16,8位的

cmpxchg
cmpxchg source, destination
其中source必须是寄存器, destination可以是内存或者寄存器, 用来比较 两者的值, 如果相等,就把源操作数的值加载到目标操作数中, 如果不等就把 目标操作数加载到源操作数中,其中寄存器可以是32,16,8位的, 其中源操作 数是EAX,AX或者AL寄存器中的值

cmpxchg8b 同cmpxchg, 但是它处理8字节值, 同时它只有一个操作数
cmpxchg8b destination
其中destination引用一个内存位置, 其中的8字节值会与EDX和EAX寄存器中 包含的值(EDX高位寄存器, EAX低位寄存器)进行比较, 如果目标值和EDX:EAX 对中的值相等, 就把EDX:EAX对中的64位值传递给内存位置, 如果不匹配就把 内存地址中的值加载到EDX:EAX对中

4, 堆栈

ESP 寄存器保存了当前堆栈的起始位置, 当一个数据压入栈时, 它就会自动递减,反之其自动递增
压入堆栈操作:

1
2
3
pushx source, x取值为:
l 32位长字
w 16位字

*出堆栈操作:

1
popx source

其中source必须是16或32位寄存器或者内存位置, 当pop最后一个元素时ESP值应该和以前的相等

5,压入和*出所有寄存器

1
2
3
4
pusha/popa 压入或者*出所有16位通用寄存器
pushad/popad 压入或者*出所有32位通用寄存器
pushf/popf 压入或者*出EFLAGS寄存器的低16位
pushfd/popfd 压入或者*出EFLAGS寄存器的全部32位

6,数据地址对齐

gas 汇编器支持.align 命令, 它用于在特定的内存边界对准定义的数据元素, 在数据段中.align命令紧贴在数据定义的前面

五,控制流程

无条件跳转:

1, 跳转

jmp location 其中location为要跳转到的内存地址, 在汇编中为定义的标签

2,调用

调用指令分为两个部分:
1, 调用call address 跳转到指定位置
2, 返回指令ret, 它没有参数紧跟在call指令后面的位置

执行call指令时,它把EIP的值放到堆栈中, 然后修改EIP以指向被调用的函数地址, 当被调用函数完成后, 它从堆栈获取过去的EIP的值, 并把控制权返还给原始程序。

3,中断

由硬件设备生成中断。 程序生成软件中断 当一个程序产生中断调用时, 发出调用的程序暂停, 被调用的程序接替它运行, 指令指针被转移到被调用的函数地址, 当调用完成时使用中断返回指令可以返回调原始程序。

条件跳转:

条件跳转按照EFLAGS中的值来判断是否该跳转, 格式为:

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
jxx address, 其中xx是1-3个字符的条件代码, 取值如下:
a 大于时跳转
ae 大于等于
b 小于
be 小于等于
c 进位
cxz 如果CX寄存器为0
ecxz 如果ECS寄存器为0
e 相等
na 不大于
nae 不大于或者等于
nb 不小于
nbe 不小于或等于
nc 无进位
ne 不等于
g 大于(有符号)
ge 大于等于(有符号)
l 小于(有符号)
le 小于等于(有符号)
ng 不大于(有符号)
nge 不大于等于(有符号)
nl 不小于
nle 不小于等于
no 不溢出
np 不奇偶校验
ns 无符号
nz 非零
o 溢出
p 奇偶校验
pe 如果偶校验
po 如果奇校验
s 如果带符号
z 如果为零

条件跳转不支持分段内存模型下的远跳转, 如果在该模式下进行程序设计必须使用程序逻辑确定条件是否存在, 然后实现无条件跳转, 跳转前必须设置EFLAGS寄存器

比较:
cmp operend1, operend2

进位标志修改指令:
CLC 清空进位标志(设置为0)
CMC 对进位标志求反(把它改变为相反的值)
STC 设置进位标志(设置为1)

循环:
loop 循环直到ECX寄存器为0
loope/loopz 循环直到ecx寄存器为0 或者没有设置ZF标志
loopne/loopnz 循环直到ecx为0或者设置了ZF标志

指令格式为: loopxx address 注意循环指令只支持8位偏移地址

六,数字

IA-32平台中存储超过一字节的数都被存储为小尾数的形式但是把数字传递给寄存器时, 寄存器里面保存是按照大尾数的形式存储

把无符号数转换成位数更大的值时, 必须确保所有的高位部分都被设置为零

把有符号数转换成位数更大的数时:
intel 提供了movsx指令它允许扩展带符号数并保留符号, 它与movzx相似, 但是它假设要传送的字节是带符号数形式

浮点数:

fld 指令用于把浮点数字传送入和传送出FPU寄存器, 格式:

1
fld source

其中source可以为32 64或者80位整数值

IA-32使用FLD指令用于把存储在内存中的单精度和双精度浮点值FPU寄存器堆栈中, 为了区分这两种长度GNU汇编器使用FLDS加载单精度浮点数, FLDL加载双精度浮点数

类似FST用于获取FPU寄存器堆栈中顶部的值, 并且把这个值放到内存位置中, 对于单精度使用FSTS, 对于双精度使用FSTL

七,基本数学运算

1, 加法

ADD source, destination 把两个整数相加
其中source可以是立即数内存或者寄存器, destination可以是内存或者寄存器, 但是两者不能同时都是内存位置

ADC 和ADD相似进行加法运算, 但是它把前一个ADD指令的产生进位标志的值包含在其中, 在处理位数大于32(如64)位的整数时, 该指令非常有用

2, 减法

SUB source, destination 把两个整数相减 NEG 它生成值的补码
SBB 指令, 和加法操作一样, 可以使用进位情况帮助执行大的无符号数值的减法运算. SBB在多字节减法操作中利用
进位和溢出标志实现跨数据边界的的借位特性

3,递增和递减

dec destination 递减
inc destination 递增
其中dec和inc指令都不会影响进位标志, 所以递增或递减计数器的值都不会影响程序中涉及进位标志的其他任何运算

4, 乘法

mul source 进行无符号数相乘 它使用隐含的目标操作数, 目标位置总是使用eax的某种形式, 这取决与源操作数的长度, 因此根据源操作数的长度, 目标操作数必须放在AL, AX, EAX中。 此外由于乘法可能产生很大的值, 目标位置必须是源操作数的两倍位置, 源为 8时, 应该是16, 源为16时, 应该为32, 但是当源为16位时intel为了向下兼容, 目标操作数不是存放在eax中, 而 是分别存放在DX:AX中, 结果高位存储在DX中, 地位存储在AX中。对于32位的源, 目标操作数存储在EDX:EAX中, 其中 EDX存储的是高32位, EAX存储的是低32位

imul source 进行有符号数乘法运算, 其中的目标操作数和mul的一样

imul source, destination 也可以执行有符号乘法运算, 但是此时可以把目标放在指定的位置, 使用这种格式的缺陷 在与乘法的操作结果被限制为单一目标寄存器的长度.

imul multiplier, source, destination 其中multiplier是一个立即数, 这种方式允许一个值与给定的源操作数进行快速的乘法运算, 然后把结果存储在通用 寄存器中

5, 除法

div divisor 执行无符号数除法运算 除数的最大值取决与被除数的长度, 对于16位被除数 ,除数只能为8位, 32或64位同上 被除数 被除数长度 商 余数
AX 16位 AL AH
DX:AX 32位 AX DX
EDX:EAX 64位 EAX EDX

idiv divisor 执行有符号数的除法运算, 方式和div一样

6, 移位

左移位:
sal 向左移位
sal destination 把destination向左移动1位
sal %cl, destination 把destination的值向左移动CL寄存器中指定的位数
sal shifter, destination 把destination的值向左移动shifter值指定的位数
向左移位可以对带符号数和无符号数执行向左移位的操作, 移位造成的空位用零填充, 移位造成的超过数据长度的任何位 都被存放在进位标志中, 然后在下一次移位操作中被丢弃

右移位:
shr向右移位
sar向右移位
SHR指令清空移位造成的空位, 所以它只能对无符号数进行移位操作
SAR指令根据整数的符号位, 要么清空, 要么设置移位造成的空位, 对于负数, 空位被设置为1

循环移位:
和移位指令类似, 只不过溢出的位被存放回值的另一端, 而不是丢弃
ROL 向左循环移位
ROR 向右循环移位
RCL 向左循环移位, 并且包含进位标志
RCR 向右循环移位, 并且包含进位标志

7, 逻辑运算

AND OR XOR
这些指令使用相同的格式:
and source, destination
其中source可以是8位 16 位或者32位的立即值 寄存器或内存中的值, destination可以是8位 16 位或者 32位寄存器或内存中的值, 不能同时使用内存值作为源和目标。 布尔逻辑功能对源和目标执行按位操作。 也就是说使用指定的逻辑功能按照顺序对数据的元素的每个位进行单独比较。

NOT指令使用单一操作数, 它即是源值也是目标结果的位置 清空寄存器的最高效方式是使用OR指令对寄存器和它本身进行异或操作.当和本身进行XOR操作时, 每个设置为 1的位就变为0, 每个设置为0的位也变位0。

位测试可以使用以上的逻辑运算指令, 但这些指令会修改destination的值, 因此intel提供了test指令, 它不 会修改目标值而是设置相应的标志

八,字符串处理

1, 传送字符串
1
2
3
4
movs 有三种格式
movsb 传送单一字节
movsw 传送一个字
movsl 传送双字

movs指令使用隐含的源和目的操作数, 隐含的源操作数是ESI, 隐含的目的操作数是EDI, 有两种方式加载内存地址到 ESI和EDI, 第一种是使用标签间接寻址 movl $output, %ESI, 第二种是使用lea指令, lea指令加载对象的地址到指定 的目的操作数如lea output, %esi, 每次执行movs指令后, 数据传送后ESI和EDI寄存器会自动改变,为另一次传送做 准备, ESI和EDI可能随着标志DF的不同自动递增或者自动递减, 如果DF标志为0则movs指令后ESI和EDI会递增, 反之会 递减, 为了设置DF标志, 可以使用一下指令:
CLD 将DF标志清零
STD 设置DF标志

2,rep前缀

REP 指令的特殊之处在与它不执行什么操作, 这条指令用于按照特定次数重复执行字符串指令, 有ECX寄存器控制, 但不需要额外的loop指令, 如rep movsl
rep的其他格式:
repe 等于时重复
repne 不等于时重复
repnz 不为零时重复
repz 为零时重复

3, 存储和加载字符串

LODS 加载字符串, ESI为源, 当一次执行完lods时会递增或递减ESI寄存器, 然后把字符串值存放到EAX中

STOS 使用lods把字符串值加载到EAX后, 可以使用它把EAX中的值存储到内存中去: stos使用EDI作为目的操作数, 执行stos指令后, 会根据DF的值自动递增或者递减EDI中的值

4, 比较字符串

cmps 和其他的操作字符串的指令一样, 隐含的源和目标操作数都为ESI和EDI, 每次执行时都会根据DF的值把 ESI和EDI递增或者递减, cmps指令从目标字符串中减去源字符串, 执行后会设置EFLAGS寄存器的状态.

5,扫描字符串

scas 把EDI作为目标, 它把EDI中的字符串和EAX中的字符串进行比较 ,然后根据DF的值递增或者递减EDI

九,使用函数

GNU汇编语言定义函数的语法:
.type 标签(也就是函数名), @function
ret 返回到调用处

十,linux系统调用

linux系统调用的中断向量为0x80

1, 系统调用标识存放在%eax中
2, 系统调用输入值:

1
2
3
4
5
EBX 第一个参数
ECX 第二个参数
EDX 第三个参数
ESI 第四个参数
EDI 第五个参数

需要输入超过6个输入参数的系统调用, EBX指针用于保存指向输入参数内存位置的指针, 输入参数按照连续的的顺序 存储, 系统调用的返回值存放在EAX中

十一,汇编语言的高级功能

1,gnu内联汇编的语法:

asm__asm__(“汇编代码”); 指令必须包含在引号里 如果包含的指令超过一行 必须使用新行分隔符分隔

使用c全局变量, 不能在内联汇编中使用局部变量, 注意在汇编语言代码中值被用做内存位置, 而不是立即数值

如果不希望优化内联汇编, 则可以volatile修饰符如:__asm__ volatile("code");

2,GCC内联汇编的扩展语法

__asm__("assembly code":output locations:input operands:changed registers); 第一部分是汇编代码 第二部分是输出位置, 包含内联汇编代码的输出值的寄存器和内存位置列表 第三部分是输入操作数,包含内联汇编代码输入值的寄存器和内存位置的列表 第四部分是改动的寄存器, 内联汇编改变的任何其他寄存器的列表 这几个部分可以不全有, 但是没有的还必须使用:分隔

1, 指定输入值和输出值, 输入值和输出值的列表格式为:

“constraint”(variable), 其中variable是程序中声明的c变量, 在扩展asm格式中, 局部和全局变量都可以使用, 使用constrant(约束)定义把变量存放到哪(输入)或从哪里传送变量(输出) 约束使用单一的字符, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
约束 描述
a 使用%eax, %ax, %al寄存器
b 使用%ebx, %bx, %bl寄存器
c 使用%ecx, %cx, %cl寄存器
d 使用%edx, %dx, %dl寄存器
S 使用%esi, %si寄存器
D 使用%edi, %di寄存器
r 使用任何可用的通用寄存器
q 使用%eax, %ebx, %ecx,%edx之一
A 对于64位值使用%eax, %edx寄存器
f 使用浮点寄存器
t 使用第一个(顶部)的浮点寄存器
u 使用第二个浮点寄存器
m 使用变量的内存位置
o 使用偏移内存位置
V 只使用直接内存位置
i 使用立即整数值
n 使用值已知的立即整数值
g 使用任何可用的寄存器和内存位置

除了这些约束之外, 输出值还包含一个约束修饰符:

1
2
3
4
5
输出修饰符 描述
+ 可以读取和写入操作数
= 只能写入操作数
% 如果有必要操作数可以和下一个操作数切换
& 在内联函数完成之前, 可以删除和重新使用操作数

如:
__asm__("assembly code": "=a"(result):"d"(data1),"c"(data2)); 把c变量data1存放在edx寄存器中, 把c变量data2存放到ecx寄存器中, 内联汇编的结果 将存放在eax寄存器中, 然后传送给变量result

在扩展的asm语句块中如果要使用寄存器必须使用两个百分号符号

不一定总要在内联汇编代码中指定输出值, 一些汇编指令假定输入值包含输出值, 如movs指令

其他扩展内联汇编知识:
1, 使用占位符

输入值存放在内联汇编段中声明的特定寄存器中, 并且在汇编指令中专门使用这些寄存器. 虽然这种方式能够很好的处理只有几个输入值的情况, 但对于需要很多输入值的情况, 这 中方式显的有点繁琐. 为了帮助解决这个问题, 扩展asm格式提供了占位符, 可以在内联 汇编代码中使用它引用输入和输出值.

占位符是前面加上百分号的数字, 按照内联汇编中列出的每个输入和输出值在列表中的位置, 每个值被赋予从0开始的地方. 然后就可以在汇编代码中引用占位符来表示值。

如果内联汇编代码中的输入和输出值共享程序中相同的c变量, 则可以指定使用占位符作为 约束值, 如:

1
2
3
__asm__("imull %1, %0"
: "=r"(data2)
: "r"(data1), "0"(data2));

如输入输出值**享相同的变量data2, 而在输入变量中则可以使用标记0作为输入参数的约束

2, 替换占位符

如果处理很多输入和输出值, 数字型的占位符很快就会变的很混乱, 为了使条理清晰 ,GNU汇编 器(从版本3.1开始)允许声明替换的名称作为占位符.替换的名称在声明输入值和输出值的段中 定义, 格式如下:

1
2
3
4
5
%[name]"constraint"(variable)
定义的值name成为内联汇编代码中变量的新的占位符号标识, 如下面的例子:
__asm__("imull %[value1], %[value2]"
: [value2] "=r"(data2)
: [value1] "r"(data1), "0"(data2));
3, 改动寄存器列表

编译器假设输入值和输出值使用的寄存器会被改动, 并且相应的作出处理。程序员不需要在改动的 寄存器列表中包含这些值, 如果这样做了, 就会产生错误消息. 注意改动的寄存器列表中的寄存器 使用完整的寄存器名称, 而不像输入和输出寄存器定义的那样仅仅是单一字母。 在寄存器名称前面 使用百分号符号是可选的。

改动寄存器列表的正确使用方法是, 如果内联汇编代码使用了没有被初始化地声明为输入或者输出 值的其他任何寄存器 , 则要通知编译器。编译器必须知道这些寄存器, 以避免使用他们。如:

1
2
3
4
5
6
7
8
9
10
11
12
int main(void) {
int data1 = 10;
int result = 20;

__asm__("movl %1, %%eax\n\t"
"addl %%eax, %0"
: "=r"(result)
: "r"(data1), "0"(result)
: "%eax");
printf("The result is %d\n", result);
return 0;
}
4, 使用内存位置

虽然在内联汇编代码中使用寄存器比较快, 但是也可以直接使用c变量的内存位置。 约束m用于引用输入值 和输出值中的内存位置。 记住, 对于要求使用寄存器的汇编指令, 仍然必须使用寄存器, 所以不得不定义 保存数据的中间寄存器。如:

1
2
3
4
5
6
7
8
9
10
11
12
int main(void) {
int dividentd = 20;
int divisor = 5;
int result;

__asm__("divb %2\n\t"
"movl %%eax, %0"
: "=m"(result)
: "a"(dividend), "m"(divisor));
printf("The result is %d\n", result);
return 0;
}
5, 处理跳转

内联汇编语言代码也可以包含定义其中位置的标签。 可以实现一般的汇编条件分支和无条件分支, 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void) {
int a = 10;
int b = 20;
int result;

__asm__("cmp %1, %2\n\t"
"jge greater\n\t"
"movl %1, %0\n\t"
"jmp end\n"
"greater:\n\t"
"movl %2, %0\n"
"end:"
:"=r"(result)
:"r"(a), "r"(b));
printf("The larger value is %d\n", result);
return 0;
}

在内联汇编代码中使用标签时有两个限制。 第一个限制是只能跳转到相同的asm段内的标签, 不能从-个asm段跳转到另一个asm段中的标签。第二个限制更加复杂一点。 以上程序使用 标签greater和end。 但是, 这样有个潜在的问题, 查看汇编后的代码清单, 可以发现内联 汇编标签也被编码到了最终汇编后的代码中。 这意味着如果在c代码中还有另一个asm段, 就 不能再次使用相同的标签, 否则会因为标签重复使用而导致错误消息。还有如果试图整合使用 c关键字(比如函数名称或者全局变量)的标签也会导致错误。