kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

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 汇编的语法及编译原理和程序控制流程, 具体的指令细节就不那么重要了。