【CSAPP】第三章 程序的机器级表示

1. 数据的编码与存储

  • 数据类型编码

ch3_p119_1

  • x86-64寄存器
image-20220112193901217

Note

  1. 寄存器可以分为4组:

    • 函数参数寄存器:rdi rsi rdx rcx r8 r9。这六个寄存器用于传递函数的参数,如果多于6个参数,需要在栈上申请空间,算在被调用函数的栈帧中

    • 函数返回值寄存器:rax。返回函数的结果,当然也可以将存储结果的空间的指针传递给被调函数,例如:

      400efe:	48 83 ec 28          	sub    $0x28,%rsp
      400f02:	48 89 e6             	mov    %rsp,%rsi
      400f05:	e8 52 05 00 00       	callq  40145c <read_six_numbers>
      

      被解析的字符串的地址放在了rdi寄存器中,调用者在自己的栈帧上开辟了40个字节的空间用于存储被调函数的结果

    • 栈指针寄存器:rsp。指向栈顶的

    • 保留寄存器:rbx rbp r10 r13 r14 r15,也是通用寄存器,但是需要保证在调用函数前后里面的内容不变,也就是函数在使用他们之前要压栈,返回之前要弹栈

  2. 同一个寄存器有1字节 2字节 4字节 8字节,对1 2字节部分操作不会影响其余部分的内容,对4字节操作会将剩余的4字节置零,例如:

2. 汇编指令

2.1 数据传送指令

访存方式

Note

比例变址寻址可以用于数组的访问,

数据传送指令

语句基本格式为MOV src, dest,表示将数据从dest传送到src,地址的格式遵循访存方式。

功能:

  • 相等长度的数据传送。传送的源和目的的位数必须相同,并在mov后面指明为b/w/l/q
image-20220114222826687
  • 短数字-->长数字。传送前必须要做扩展,并指明在mov后面,分为两种情况:

    • 无符号扩展:高位补0
    • 有符号扩展:高位补符号位

    mov后第一个表示扩展0(z)或者符号(s),第二和第三分别表示源和目的操作数的长度

    image-20220114223120529image-20220114223147278

  • 长数字--> 短数字。如果保留低位,直接按低位寄存器操作,比如将rax中的低8位移动到dil,直接mov %al, %dil即可;如果保留高位,先位移,然后按移动低位操作进行

Note

  1. dest不能是立即数
  2. src和dest不能同时为内存地址,即将内存的数据传送到内存中另一个位置必须经过寄存器
  3. 零扩展传送中没有movzlq指令,这是因为操作双字时,会将高4字节置零
  4. cltq等效于movslq %eax, %rax

入栈出栈

image-20220114224117653

  • 对rsp寄存器操作
  • 向下生长
  • 按字节编址

2.2 算术/逻辑指令

分为四组指令:

  1. 取有效地址。相当于C语言中的取地址运算符,可以替代一些算术运算。比如,如果%rax的值为x,%rdx为y,则leaq 7(%rax, %rdx, 5), %rax就是在计算x+5y+7的值
  2. 一元操作指令
  3. 二元算术指令,操作数的顺序与intel的规定相反
  4. 位移。分为算术和逻辑位移

2.3 过程控制指令

控制码

进位:CF

零标志:ZF

负数标志:SF

溢出标志:OF

比较指令

cmp S1, S2:基于S2-S1进行

test S1, S2:基于S1&S2

  • 都有b l w q四种变种
  • 只设置条件码,不改变寄存器

跳转指令

条件设置指令

Note

比较指令配合跳转指令即可实现循环和分支结构

3. 程序设计

3.1 循环

do-while循环

C代码:

long fact_do(long n) {
    long result = 1;
    do {
        result *= n;
        n = n - 1;
    } while(n > 1);
    return result;
}

汇编:

fact_do:
# 初始化
movl	$1, %eax 
.L2:
# 循环体
imulq 	%rdi, %rax

# 边界判断
subq	$1, %rdi
cmpq	1, %rdi
jg		.L2

# 结束
ret

while循环

C代码:

long fact_while(long n) {
    long result = 1;
     while(n > 1){
        result *= n;
        n = n - 1;
    }
    return result;
}
  1. 中间跳转法。将test代码写在最后,在初始化之后直接跳转到测试部分,相比于guarded-do可以少写一段测试的代码
	goto test
loop:
	body-statement
test:
	t = test-expr
	if(t) goto loop
fact_while:
# 初始化
movl	$1, %eax 
jmp		.L5
.L6:
# 循环体
imulq 	%rdi, %rax
subq	$1, %rdi

.L5:
#边界判断
cmpq	1, %rdi
jg		.L6

# 结束
ret
  1. guarded-do。在进入循环之前就测试
t = test-expr
if(!t) goto done
loop:
	body-statement
	t = test-expr
	if(t) goto loop
done:
fact_while:
# 进入前测试
cmpq	$1, %rdi
jle		.L7
# 初始化
movl	$1, %eax 
.L6:
# 循环体
imulq 	%rdi, %rax
subq	$1, %rdi
# 边界判断
cmpq	1, %rdi
jne		.L6
ret
# 直接退出
.L7:
movl	$1, %eax
ret

for循环,类似于while循环的实现

Note

在阅读汇编代码中的循环时,首先根据sub+cmp+jmp辨别出循环代码的部分,sub或者cmp指令中涉及的寄存器就是循环变量,jmp到前面的地址就是循环体的开始部分,循环体之前部分就是初始化;然后根据sub/cmp/jmp的具体类型和分布判断循环的类型;最后分析循环体的功能

3.2 if-else分支

  • goto翻译,与循环类似,先计算test-exp,然后根据结果跳转到不同部分执行

  • 条件传送。将goto中的跳转结构变成了顺序指令,保证了流水线的稳定

    • step1: 计算test-exp
    • step2:计算if分支
    • step3:计算else分支
    • step4:cmovge if, else

    例如:

    long asbdiff(long x, long y) {
        long result;
        if(x < y) {
            result = y - x;
        } else {
            retult = x - y;
        }
        return result;
    }
    

    翻译成:

    # x in %rdi, y in %rsi
    absdiff:
    movq	%rsi, %rax
    subq	%rdi, %rax # y - x
    movq	%rdi, %rdx
    subq	%rsi, %rdx # x - y
    cmpq	%rsi, %rdi
    cmovge	%rdx, %rax	x >= y, 传送x - y, 否则传送y - x
    ret
    

3.3 switch分支

采用了跳转表+间接跳转

image-20220115003952631image-20220115004319387

3.4 函数调用

运行时栈

  • 存放着传递控制(pc指针,即返回地址)和数据(函数参数等信息)、内存分配的信息(寄存器的值或者栈上分配的变量空间)
  • 不是所有的函数都需要栈帧

调用的实现

step1: 传递参数,将参数放到rdi rsi rdx rcx r8 r9寄存器以及栈上

step2:执行call指令,进入被调函数,运行

step3:将操作结果放入rax或者指定位置,恢复保留寄存器的值,返回

以拆炸弹实验代码为例:

# 后面要用到rbp rbx寄存器,先压栈 
  400efc:	55                   	push   %rbp
  400efd:	53                   	push   %rbx

  # 可以看出栈指针被当做第二个参数传递给了read_six_numbers函数,此处的28为十六进制数,
  400efe:	48 83 ec 28          	sub    $0x28,%rsp
  400f02:	48 89 e6             	mov    %rsp,%rsi 					# 传递参数
  400f05:	e8 52 05 00 00       	callq  40145c <read_six_numbers> 	# 调用函数,运行

3.5 递归

主要是保护好调用者保留寄存器

4. 工具使用

4.1 gcc

编译过程:

  • 预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删掉,这里并不会检查语法;
  • 编译:检查语法,将预处理后的文件编译成汇编文件;
  • 汇编: 将汇编文件生成目标文件(二进制文件);
  • 链接: C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到可执行程序中去。

命令:

步骤 命令
预处理 gcc -E hello.c -o hello.i
编译 gcc -S hello.i -o hello.s
汇编 gcc -c hello.s -o hello.o
链接 gcc hello.o -o hello_elf
  • 如果要一步到位直接生成可执行文件,命令为gcc hello.c -o hello

  • 可以在gcc后指定 -Og/-O1/-O2设定编译优化级别

以一下代码为例:

//main.c
#include<stdio.h>
#include<stdlib.h>
void multstore(double, double, double*);
int main(){
	double d;
	multstore(2, 3, &d);
	printf("2 * 3-->%ld\n", d);
	system("pause");
	return 0;
}
double mult2(double a, double b){
	double s = a * b;
	return s;
}
//mstore.c
double mult2(double, double);
void multstore(double x, double y, double *dest){
	double t = mult2(x,y);
	*dest = t;
}

预处理:

gcc main.c -o main.i
gcc mstore.c -o mstore.i

编译:

gcc -S main.i -o main.s
gcc -S mstore.i -o mstore.s

汇编:

gcc -c main.s -o main.o
gcc -c mstore.s -o mstore.o

链接:

gcc main.o mstore.o //不指定名称,默认为a.exe

一步:

gcc main.c mstore.c -o test//指定名称为test.exe

4.2 gdb&objdump

机器代码反汇编到汇编:

objdump -d target.o

如:

汇编到C?

4.3 makefile

makefile的格式

target ... : prerequisites ...
        command
        ...
        ...

target:输出文件的名称

prerequisites:输入的文件名称

command:一系列的gcc命令

test : main.o mstore.o
	gcc main.o mstore.o -o test
	
main.o : main.s
	gcc main.s -o main.o
mstore.o : mstore.s
	gcc mstore.s - mstore.o
	
main.s : main.c
	gcc main.c -o main.s
mstore.s : mstore.c
	gcc mstore.c -o mstore.s

make的高级特性

  1. 自动推导:make可以识别一个.o文件,自动将对应的.c文件加在依赖关系中。并且也会自动推导出相关的编译命令。因此上述的makefile文件可以只包含前两行

  2. 使用变量

    objs = main.o mstore.o
    test : $(objs)
    	gcc $(objs) -o test
    
posted @ 2020-08-09 20:09  十三w~w  阅读(228)  评论(0编辑  收藏  举报