这篇文章是csapp第三章前五节的阅读笔记。
0x00 程序编码
对于两个C程序,可以通过如下的方式编译:
gcc -Og -o p p1.c p2.c
其中-Og
选项告诉编译器不进行优化,直接根据C代码生成对应的汇编代码。
给出下面的C代码:
long mult2(long, long);
void multstore(long x, long y, long *dest) {
long t = mult2(x, y);
*dest = t;
}
为了查看生成的汇编代码,在编译的时候可以加上-S
选项:
gcc -Og -S mstore.c
结果如下:
.file "mstore.c"
.text
.globl multstore
.type multstore, @function
multstore:
.LFB0:
.cfi_startproc
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE0:
.size multstore, .-multstore
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-5)"
.section .note.GNU-stack,"",@progbits
其中以.
开头的都是为了方便汇编器和链接器的内容。
如果使用-c
选项的话,就可以生成.o
和.s
文件了:
gcc -Og -c mstore.c
如果只有.o
文件,那么也可以通过工具来生产对应的.s
文件:
objdump -d mstore.o
结果:
mstore.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 :
0: 53 push %rbx
1: 48 89 d3 mov %rdx,%rbx
4: e8 00 00 00 00 callq 9
9: 48 89 03 mov %rax,(%rbx)
c: 5b pop %rbx
d: c3 retq
生成一个可执行文件需要对.o
文件进行链接,其中需要有一个main
函数。比如下面的main.c:
#include <stdio.h>
void multstore(long, long, long *);
int main() {
long d;
multstore(2, 3, &d);
printf("2 * 3 --> %ld\n", d);
return 0;
}
long mult2(long a, long b) {
long s = a * b;
return s;
}
编译可执行文件:
gcc -Og -o prog main.c mstore.c
就会生成一个名为prog
的可执行文件。
可以对这个文件进行反编译:
objdump -d prog
其中就有我们之前看到的mstore.s的汇编代码:
0000000000400570 :
400570: 53 push %rbx
400571: 48 89 d3 mov %rdx,%rbx
400574: e8 ed ff ff ff callq 400566
400579: 48 89 03 mov %rax,(%rbx)
40057c: 5b pop %rbx
40057d: c3 retq
40057e: 66 90 xchg %ax,%ax
不同在于,在retq
执行令之后还有一个指令,这是为了补齐,使得程序刚好是16字节的整数倍。
0x01 数据格式
不同的数据类型有不同的字节数,在汇编语言中,使用一个字母的后缀来表示对应指令操作的数据类型:
C declaration | Intel data type | Assembly-code suffix | Size (bytes) |
---|---|---|---|
char |
Byte | b |
1 |
short |
Word | w |
2 |
int |
Double word | l |
4 |
long |
Quad word | q |
8 |
char* |
Quad word | q |
8 |
float |
Single precision | s |
4 |
double |
Double precison | l |
8 |
其中l
对应两个类型:short
和double
。
0x02 寄存器
x86-64的处理器有16个通用寄存器,每个寄存器都可以用来存储数字和指针:
这16个寄存器每一个还都可以分成小的来使用,比如%rax是8字节,%eax是4字节,%ax是2字节,%al是一个字节。
有一些寄存器有特殊的用途:
- %rsp:栈指针;
- %rax:返回值;
- %rdi:第一个参数;
- %rsi:第二个参数;
- %rdx:第三个参数;
- %rcx:第四个参数;
- %r8:第五个参数;
- %r9:第六个参数。
0x03 操作数指示符
寄存器是指令存放数据的地方,为了能够顺利执行指令,还需要指定对应的操作数。
操作数有三种来源:
- 常量;
- 来自寄存器;
- 来自内存。
所以获取操作数的方式也有三大类:
Type | Form | Operand Value | Name |
---|---|---|---|
Immediate | $Imm |
Imm |
Immediate |
Register | ra |
R[ra] |
Register |
Memory | Imm |
M[Imm] |
Absolute |
Memory | (ra) |
M[R[ra]] |
Indirect |
Memory | Imm(rb) |
M[Imm+R[rb]] |
Base+displacement |
Memory | (rb,ri) |
M[R[rb]+R[ri]] |
Indexed |
Memory | Imm(rb,ri) |
M[Imm+R[rb]+R[ri]] |
Indexed |
Memory | (,ri,s) |
M[R[ri]·s] |
Scaled indexed |
Memory | Imm(,rj,s) |
M[Imm+R[rj]·s] |
Scaled indexed |
Memory | (rb,ri,s) |
M[R[rb]+R[ri]·s] |
Scaled indexed |
Memory | Imm(rb,ri,s) |
M[Imm+R[rb]+R[ri]·s] |
Scaled indexed |
通过一个例子就能理解了。
这里是一些寄存器和内存中的数据:
Address | Value | Register | Value |
---|---|---|---|
0x100 | 0xFF | %rax | 0x100 |
0x104 | 0xAB | %rcx | 0x1 |
0x108 | 0x13 | %rdx | 0x3 |
0x10c | 0x11 |
下面给出获取操作数的过程:
Operand | Type | Value | Note |
---|---|---|---|
%rax |
Register | 0x100 |
|
0x104 |
Memory | 0xAB |
M[Imm] |
$0x108 |
Immediate | 0x108 |
|
(%rax) |
Memory | 0xFF |
M[R[ra]] |
4(%rax) |
Memory | 0xAB |
M[Imm+R[rb]] |
9(%rax,%rdx) |
Memory | 0x11 |
M[Imm+R[rb]+R[ri]] |
260(%rcx,%rdx) |
Memory | 0x13 |
M[Imm+R[rb]+R[ri]] |
0xFC(,%rcx,4) |
Memory | 0xFF |
M[Imm+R[rj]·s] |
(%rax,%rdx,4) |
Memory | 0x11 |
M[R[rb]+R[ri]·s] |
0x04 数据传送指令
数据传输指令是使用非常频繁的指令,这些指令将数据从一个地方传送到另一个地方。
其中数据源可以是常量、寄存器和内存,目的地是寄存器和内存,不能是常量。
同时,还有一个规则,数据源和目的地不能都是内存。
Instruction | Effect | Description |
---|---|---|
MOV S, D |
D <- S |
Move |
movb |
Move byte | |
movw |
Move word | |
movl |
Move double word | |
movq |
Move quad word | |
movabsq I,R |
R <- I |
Move absolute quad word |
最后一个指令movabsq
是为了处理64位常数的,指令movq
的数据源如果是常数的话只能是32位补码数,将这个数放入寄存器后,高位会使用补码的最高位填充。
而指令movabsq
可以将任意64位的数放入寄存器中。
如果目的地是寄存器的话,大多数指令只是根据后缀标识的大小来覆盖相应寄存器中的数据,比如movb $FF %al
只是将%rax中最后的一个字节置成0xFF
。
唯一的例外是movl
,这个指令会将高位置成0:
movabsq $0x0011223344556677, %rax ; %rax = 0011223344556677
movb $-1, %al ; %rax = 00112233445566FF
movw $-1, %ax ; %rax = 001122334455FFFF
movl $-1, %eax ; $rax = 00000000FFFFFFFF
movq $-1, %rax ; $rax = FFFFFFFFFFFFFFFF
当数据源的位数小于目的地时,对于高位有两种处理方式,补零,或者使用源数据的最高位填充。
补零:
Instruction | Effect | Description |
---|---|---|
MOVZ S, R |
R <- ZeroExtend(S) |
Move with zero extension |
movzbw |
Move zero-extended byte to word | |
movzbl |
Move zero-extended byte to double word | |
movzbq |
Move zero-extended byte to quad word | |
movzwl |
Move zero-extended word to double word | |
movzwq |
Move zero-extended word to quad word |
其中没有从double word到quad word的movzlq
指令,因为movl
指令默认就是补零。
使用最高位填充:
Instruction | Effect | Description |
---|---|---|
MOVS S, R |
R <- SignExtend(S) |
Move with sign extension |
movsbw |
Move sign-extended byte to word | |
movsbl |
Move sign-extended byte to double word | |
movsbq |
Move sign-extended byte to quad word | |
movswl |
Move sign-extended word to double word | |
movswq |
Move sign-extended word to quad word | |
movslq |
Move sign-extended double to quad word | |
cltq |
%rax <- SignExtend(%eax) |
Sign-extended %eax to %rax |
其中最后一个指令cltq
没有操作数,这个指令和movslq %eax, %rax
效果一样,不过更加精简。
例子:
movabsq $0011223344556677, %rax ; %rax = 0011223344556677
movb $0xAA, %dl ; %dl = AA
movb %dl, %al ; %rax = 00112233445566AA
movsbq %dl, %rax ; %rax = FFFFFFFFFFFFFFAA
movzbq %dl, %rax ; %rax = 00000000000000AA
0x05 数据传送示例
比如下面的C代码:
long exchange(long* xp, long y) {
long x = *xp;
*xp = y;
return x;
}
编译后的汇编代码如下:
exchange:
.cfi_startproc
movq (%rdi), %rax
movq %rsi, (%rdi)
ret
首先,函数的两个参数xp
和y
分别存放在寄存器%rdi和%rsi中。
编译后一共两个指令,第一个指令是将寄存器%rdi地址中的数放入寄存器%rax中。
数据源是从内存中取数据,对应指针xp
的解引用;
而寄存器%rax用于返回值,刚好对应return x
。
然后第二个指令将寄存器%rsi中的数放入内存中,地址在%rdi中。
对应语句*xp = y
。
从这里就可以看出,C语言中的指针仅仅是内存的地址;
指针的解引用就是将这个地址复制到寄存器中,然后使用那个寄存器来获取内存中的数据;
同样,函数的局部变量通常放在寄存器中,而不是放在内存里。
0x06 入栈和出栈
16个寄存器中有一个专门指栈地址的寄存器%rsp。
下面这两个指令就是和栈有关:入栈和出栈。
Instruction | Effect | Description |
---|---|---|
pushq S |
R[%rsp] <- R[%rsp]-8;M[R[%rsp]] <- S |
Push quad word |
popq D |
D <- M[R[%rsp]];R[%rsp] <- R[%rsp]+8 |
Pop quad word |
比如下面的例子:
0x07 算术和逻辑操作
下面列出了一些x86-64的整数和逻辑运算符:
Instruction | Effect | Description |
---|---|---|
leaq S, D |
D <- &S |
Load effective address |
INC D |
D <- D+1 |
Increment |
DEC D |
D <- D-1 |
Decrement |
NEG D |
D <- -D |
Negate |
NOT D |
D <- ~D |
Complement |
Add S, D |
D <- D+S |
Add |
SUB S, D |
D <- D-S |
Subtract |
IMUL S, D |
D <- D*S |
Multiply |
XOR S, D |
D <- D^S |
Exclusive-or |
OR S, D |
`D <- D | S` |
AND S, D |
D <- D&S |
And |
SAL k, D |
D <- D<<k |
Left shift |
SHL k, D |
D <- D<<k |
Left shift (same as SAL) |
SAR k, D |
D <- D>>_A k |
Arithmetic right shift |
SHR k, D |
D <- D>>_L k |
Logical right shift |
先来看看leaq
指令。
这个指令其实是movq
的变形,但是它将有效的地址复制到对应的寄存器中。
这个指令还可以用来进行算术运算。比如如果%rax
中存放值x
,那么leaq 7(%rdx,%rdx,4), %rax
将计算5x+7
。
这个指令的目标操作数必须是一个寄存器。
下面是更多的例子(其中%rax中是x,%rcx中是y):
Instruction | Result |
---|---|
leaq 6(%rax), %rdx |
6+x |
leaq (%rax,%rcx), %rdx |
x+y |
leaq (%rax,%rcx,4), %rdx |
x+4y |
leaq 7(%rax,%rax,8), %rdx |
7+9x |
leaq 0xA(,%rcx,4), %rdx |
10+4y |
leaq 9(%rax,%rcx,2), %rdx |
9+x+2y |
在上面的指令中,有的指令需要一个操作数,有的需要两个。
对于INC D
指令来说,可以实现C语言中的++运算符。
对于ADD S, D
指令来说,可以使用C语言中+=运算符。
0x08 移位操作
上面指令中的最后四个是关于移位操作的。
第一个操作数用来指定移动的位数,既可以是一个常数,也可以从寄存器%cl中读取。
寄存器%cl有一个字节,那么可以表示最大到255,而第二个操作数可以有不同的字节。
那么怎么来用%cl中的数来表示移动的位数而不会超过规定的最大位移数呢?
比如quad word是64位,那么就读取%cl中的最低6位就可以了;
对于double word的32位,读取%cl中的最低5位就可以了;
对于word的16位,读取%cl中的最低4位就可以了;
对于byte的8位,读取%cl中的最低3位就可以了。
0x09 特殊的算术操作
下面列出了一些特殊的算术操作:
Instruction | Effect | Description |
---|---|---|
imulq S |
R[%rdx]:R[%rax] <- S * R[%rax] |
Signed full multiply |
mulq S |
R[%rdx]:R[%rax] <- S * R[%rax] |
Unsigned full multiply |
cqto |
R[%rdx] <- SignExtend(R[%rax]) |
Convert to oct word |
idivq S |
R[%rdx] <- R[%rdx]:R[%rax] mod S;R[%rax] <- R[%rdx]:R[%rax] / S |
Signed divide |
divq S |
R[%rdx] <- R[%rdx]:R[%rax] mod S;R[%rax] <- R[%rdx]:R[%rax] / S |
Unsigned divide |
其中oct word是16个字节。
需要注意的是,对于有符号和无符号的乘法和除法来说,指令下的操作方式是一样的。
这是因为不管是有符号还是无符号数,底层的运算规则都是一样的。
那么处理器就无法仅仅通过操作来判断是有符号还是无符号了,只能通过不同的指令名字来指明。
这就是编译过程中把C语言中的类型翻译成不同的指令来完成的。
后面还有一些这样的例子。