第三章 程序的机器级表示
1. 程序编码
1.1 gcc编译C程序
编译过程:
- 预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删掉,这里并不会检查语法;
- 编译:检查语法,将预处理后的文件编译成汇编文件;
- 汇编: 将汇编文件生成目标文件(二进制文件);
- 链接: 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
", 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
1.2 Cmake使用
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的高级特性
-
自动推导:make可以识别一个
.o
文件,自动将对应的.c
文件加在依赖关系中。并且也会自动推导出相关的编译命令。因此上述的makefile文件可以只包含前两行 -
使用变量
objs = main.o mstore.o test : $(objs) gcc $(objs) -o test
1.3 反汇编
机器代码反汇编到汇编:
objdump -d target.o
如:
汇编到C?
2. 数据表示
- 针对不同大小,数据传送指令分为movb(字节), movw(字), movl(双字), movq(四字)
- 后缀 l 同时表示4字节整数和8字节双精度浮点数,根据指令和寄存器进行区分
3. 数据访问
3.1 整数寄存器
x86-64处理器包含了16个64bit的通用目的寄存器,存储整数和指针
明明规则:
- 前8组以16bit寄存器为主,64 32和8bit分别带r e 和l的前缀或者后缀;
- 后8组以64bit寄存器为主,命名为r8~r15,32 16 8bit分别带d w b后缀
功能:
- 函数返回值:ax
- 函数参数:di si dx cx r8 r9共9个,分别保存1到6个函数参数,超出6个的参数被放在被调函数的栈帧上
- 被调用者保存寄存器:剩余的都是,不管哪个程序都可以使用,但是在使用前需要保存寄存器原始数据,使用完要恢复(在函数调用时,保存现场和恢复现场由被调函数完成)
3.2 寻址
共三类:
- 立即数:只能是整数,浮点数没有立即数
- 寄存器:16个寄存器的低位1 2 4 8字节
- 内存引用:M[Addr]
寻址方式:有效地址=Imm + R[rb] + R[ri]*s,表示为Imm(rb, ri, s)
3.3 数据传送指令
简单传送指令
- movabsq的目的地之只能是64位寄存器
- movl会将寄存器高32位置0
扩充传送(将少位数的扩充后传送到多位数的)
3.4 出栈入栈
- 对rsp寄存器操作
- 向下生长
- 按字节编址
3.5 算逻运算
-
结果都存储在第二个寄存器处
-
除了leaq外,其余指令都有 q l w b四个变种
-
特殊算数运算:针对8字(128位)的运算
rax存乘数、乘积低位、商,rdx存乘积高位、余数
3.6 控制
控制码
进位:CF
零标志:ZF
负数标志:SF
溢出标志:OF
比较和测试
cmp S1, S2:基于S2-S1进行
test S1, S2:基于S1&S2
- 都有b l w q四种变种
- 只设置条件码,不改变寄存器
访问条件码
set指令
总结:需要区分有符号数和无符号数的指令有移位、特殊除法、set指令
跳转指令
- 直接跳转:jump Label
- 间接跳转:jump *Adrr
if语句实现
C语言模板:
if(test-expr){
then-statement
}else{
else-statement
}
汇编控制流:
t = test-expr
if(!t)
goto false;
then-statement
goto done
false:
else-statement
done:
用条件传送实现分支
-
实现:计算出两种操作的结果,然后根据条件选择传送一种
例如对于条件赋值表达式:
v = test-expr ? then-expr : else-expr
条件传送的控制流为:
v = then-expr ve = else-expr t = test-expr if(!t)v = ve
-
副作用:需要对then-expr和else-expr都求值,一方面增加了计算量,另一方面可能导致非法行为(非法地址访问)
循环实现
do-while循环
loop:
body-staement
t = test-expr
if(t) goto loop
while循环
- 中间跳转法
goto test
loop:
body-statement
test:
t = test-expr
if(t) goto loop
- guarded-do
t = test-expr
if(!t) goto done
loop:
body-statement
t = test-expr
if(t) goto loop
done:
for循环,类似于while循环的实现
switch实现
跳转表+间接跳转
3.7 函数调用
函数调用的实现机制(P调用Q):
- 控制转移:将Q的入口地址读到rsp中,将P的断点存到栈中
- 传递数据:P将Q需要的参数传入,优先传到rdi rsi rdx rcx r8 r9
- 分配和释放内存:Q的局部变量