csapp实践(二):程序的机器级表示

csapp第三章中,对汇编指令的要求比较高
这里把常见的指令整理一下

1
g++ -Og -S -masm=intel xxx.cpp

得到反汇编文件查阅

程序编码

代码示例和解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

long mult2(long a, long b) {
return a*b;
}

void multstore(long x, long y, long* des) {
long t = mult2(x, y);
*des = t;
}

int main() {
long a = 100, b = 150;
long dest;

multstore(a, b, &dest);
cout << dest << endl;
}

反汇编代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__Z9multstorellPl:              ## @_Z9multstorellPl
.cfi_startproc
## %bb.0: ## rdx第三个参数,rsi第2个参数,rdi第一个参数
push rbp ## rbp为基指针寄存器(base pointer),存取调用堆栈中的数据
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp ## rsp为堆栈指针(stack pointer)寄存器,只可访问堆栈顶
.cfi_def_cfa_register rbp
push rbx ## rbx操作数寄存器,用来存放运算结果
push rax ## 用来临时存放函数mult2()的返回值,rax一般都是来存放返回值的
.cfi_offset rbx, -24
mov rbx, rdx ## rdx是第三个参数,copy dest to rbx, 最后*dest = mult2(x, y), 要先把dest存储在寄存器中,稍后访问
call __Z5mult2ll
mov qword ptr [rbx], rax ## 运行的结果,将mult2(x, y)存入[rbx]即*dest中
add rsp, 8
pop rbx ## 可以这么理解: rbx = rax, rbx may be the left value, rax may be the right value, 一般来说, rax是返回值
pop rbp ## restore rbp
ret
.cfi_endproc
## -- End function
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main

数据传输

寄存器
第一个参数:rdi, 第二个参数:rsi, 第三个参数:rdx, 第四个参数:rcx

汇编器常见的错误

1
2
movl %rax, %(rsp)	## wrong, it is moveq %rax, %(rsp)
movl %eax, %rdx ## 原来应该是movzlq,但是并没有这样的指令,rdx应该对应movq

数据传送示例

1
2
3
4
5
6
7
char -> int

char类型4字,转int需要符号扩展,4字用movl,这里需要符号扩展
用movsbl

movsbl (%rdi), %eax
movl %eax, (%rsi)
1
2
3
4
5
6
char -> unsigned

同样4字对4字,需要符号扩展movsbl

movsbl (%rdi), %eax
movl %eax, (%rsi)
1
2
3
4
5
6
7
unsigned char -> long

0扩展,转成8字存储,操作的是unsigned char,本来用的是movl, 需要0扩展
用movzbl

movzbl (%rdi), %eax;
movq %rax, (%rsi)
1
2
3
4
5
6
int -> char

操作数是int, 直接用movl即可, int是4字,存放到eax中
movl (%rdi), %eax
movb %al, (%rsi)
char只需要低位的,所以用movb %al, (%rsi)
1
2
3
4
5
unsigned -> unsigned char

操作数unsigned,4字,movl存放在eax中
movl (%rdi), %eax
movb %al, (%rsi)
1
2
3
4
5
6
7
char -> short

需要做符号扩展,并且char要截断,取低位,存放在ax中,本来用movw
这里涉及到符号扩展,用movsbw

movsbw (%rdi), %ax
movw %ax, (%rsi)

移位操作
移位量是由%cl寄存器的低m位决定的,高位会被忽略

1
2
3
4
5
6
7
8
9
10
11
long shift_left4_rightn(long x, long n) {
x <<= 4;
x >>= n;
return x;
}

shift_left4_rightn:
movq %rdi, %rax
salq $4, %rax
movl %esi, %ecx ## get n (4 bytes)
sarq %cl, %rax ## 本来应该是sarq %ecx, %rax, 因为移位操作取的是低位%cl

特殊的算术操作
乘法,乘积低位放在%rax中,高位放在%rdx中
除法,需要用到%rdx寄存器来存放参数,商放在%rax中,余数放在%rdx中

控制

条件控制实现条件分支

1
setnle  D <-- ~(SF ^ OF) & ~ZF

溢出的处理,用xor,看作是不进位的加法
如果有符号溢出,相应的值要变化,比如
SF: t < 0
溢出的话,要xor OF
实际上SF 0->1

注意跳转指令前面有符号,f8表示符号位是-1,所以是0xd-0x8

1
2
3
4
5
6
0xffffff73所代表的跳转值,首先最高位为负
值为8位16进制数
所以最高位为-16^7

-16^7 + 0xfffff73 = -141
即为跳转偏移量

跳转指令翻译

1
2
3
4
5
6
7
8
9
cmpq xxx1, xxx2
jge .L2
code....

means:
if(xxx2 < xxx1)
code...
else
.L2

用条件传送来实现条件分支

1
2
3
4
5
6
7
8
testq 	%rdi, %rdi
cmovns %rdi, %rax

## 实现方法:cmovns表示非负数
## if( >= 0) rax <-- rdi
## 条件中的值为testq %rdi的值,即

if(x >= 0) v = x;

用循环来实现条件分支

for循环

1
2
3
4
5
6
7
8
9
10
11
fun_a:
movl $0, %eax
jmp .L5
.L6:
xorq %rdi, %rax
shrq %rdi
.L5:
testq %rdi, %rdi
jne .L6
andl $1, %eax
ret

转换成c语言代码,可以发现L5是循环的主体

1
2
3
4
5
6
7
8
while(%rdi != 0) {
.L6
}
return %eax & 0x1

.L6的实现如下
val ^= x
x >>= 1

该实现如下:

1
2
3
4
5
6
7
8
long fun_a(unsigned long x) {
long val = 0;
while(x) {
val ^= x;
x >>= 1;
}
return val & 0x1;
}

代码功能:奇数个1, 每一位取出来xor,其值还是1
偶数个1,每一位取出来,其值是0
如果有奇数个1,返回1,偶数个1,返回0

1
2
3
4
5
6
7
8
9
10
11
12
fun_b:
movl $64, %edx
movl $0, %eax
.L10:
movq $rdi, %rcx
andl $1, %ecx
addq %rax, %rax
orq %rcx, %rax
shrq %rdi
subq $1, %rdx
jne .L10
rep; ret

for循环主体

1
2
3
4
5
6
7
8
9
10
11
12
13
subq	$1, %rdx

eax = val = 0;
for(edx = 64; edx != 0; edx--) {
.L10
}

.L10:
val in rax

tmp = x; tmp &= 1; (tmp in rcx)
val << 1; val |= tmp
x >> = 1

综上:

1
2
3
4
5
6
7
8
9
long fun_b(unsigned long x) {
long val = 0;
long i;
for(i = 64; i != 0; i--) {
val = (x & 0x1) | (val << 1)
x >>= 1;
}
return val;
}

这个代码的作用很有意思
如图所示,创造x的镜像

switch语句

1
2
3
4
5
switch2:
addq $1, %rdi
cmpq $8, %rdi
ja .L2
jmp *.L4(, %rdi, 8)

start: rdi+1

$ x+1 = 0 $
$ x+1 > 8 \quad (default) $
$ x+1 < 8 $
$ x = -1, 0, 1, 2, 3, 4, 5, 6, 7 $

函数调用过程

转移控制

函数调用的具体分析如下

stack上的局部存储


寄存器中的局部存储空间

数组的分配和访问

定长数组

异质的数据结构,联合,结构体

联合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef union {
struct {
long u;
short v;
char w;
} t1;

struct {
int a[2];
char* p;
} t2;
} u_type;

void get(u_type *up, type* dest) {
*dest = expr;
}

// up in %rdi, dest in %rsi

具体的内存分配如下:

数据对齐

数据对齐的时候,start位置必须是类型的整数倍
如果不满足,则会填充

这里又一个优化技巧
结构体对数据类型进行降序排列

1
2
3
4
5
6
7
8
struct P {
char* x1;
long x2;
float x3;
int x4;
short x5;
...
};

指针与缓冲区溢出

对抗缓冲区溢出攻击

支持变长栈帧

1
2
3
long vframe(long n, long idx, long *q) {
long *p[n];
}