
Essay2026 / 03 / 15
初入汇编-编译过程与基础汇编指令
本文以C语言与x86-64为基底,开始讲述程序底层的汇编表示。内容包括,编译器工作流程,一个实际的例子,基本的汇编代码知识与汇编指令
1 编译器工作
我们通过输入命令 gcc -o hello.exe hello.c,让gcc编译器将我们的hello.c转变为hello.exe.这其中,gcc实际上需要其他工具来完成这一个过程。该过程一共有四步,每一步都有相应地程序执行。
预编译 处理你的#include等指令,做一些文本替换的活。
编译 把预编译过来的代码文件翻译成汇编代码。此时你的代码依然为文本形式。
汇编 将汇编代码文件翻译为二进制代码。
链接 将二进制代码链接,最终形成可执行文件.
其中在上述的gcc编译命令中,-O指令可指定优化级别,不同优化级别产生的汇编代码是有一定不同的。
2 开始我们的尝试
现在,打开你的IDE,输入下面这段C代码,文件名为 mstore.c
long mult2(long ,long);
void mulstore(long x,long y,long *dest){
long t = mult2(x ,y);
*dest = t;
}
然后,执行gcc -Og -S mstore.c
-Og指定编译器的优化级别,使得得到的汇编代码适合我们学习
-S指定gcc执完编译该步后输出汇编文件mstore.s。它是文本文件,你可以直接打开它。
该汇编文件一般有下面这几行代码,每一行都代表一条机器指令。
mulstore:
pushq %rbx
movq %r8, %rbx
call mult2
movl %eax, (%rbx)
addq $32, %rsp
popq %rbx
ret
其中如果让编译器继续往下执行到汇编这步,输入 gcc -Og -c mstore.c,则会生成目标文件mstore.o。其内部就存储着机器执行的二进制码。其中核心的是下面这个14字节的序列
53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3
但它是机器读的,不是人类读的。我们需要反汇编器(disassembler) 来将二进制码翻译回汇编代码。
执行 objdump -d mstore.o,将会输出以下内容
Disassembly of section .text:
0000000000000000 <mulstore>:
0: 53 push %rbx
1: 48 83 ec 20 sub $0x20,%rsp
5: 4c 89 c3 mov %r8,%rbx
8: e8 00 00 00 00 call d <mulstore+0xd>
d: 89 03 mov %eax,(%rbx)
f: 48 83 c4 20 add $0x20,%rsp
13: 5b pop %rbx
现在你应该关注到一个点,就是 机器执行的指令是不定长的。从1到15字节不等。其中常用简单的指令会短,不常用复杂的指令会长
3 读懂我们的汇编
刚才我们最接近系统底层且能读懂的内容就是汇编代码。 那么打开我们刚刚的mstore.s。忽略掉那些以.开头的行,得到的汇编代码如下
注解:以 . 开头的都是伪指令,他们是起指导汇编器与编译器的作用
mulstore:
pushq %rbx
movq %r8, %rbx
call mult2
movl %eax, (%rbx)
addq $32, %rsp
popq %rbx
ret
其中每一行代表一条机器指令。我们接下来将学习汇编代码基础知识。最后读懂汇编代码与找到相应的C语言代码之间的映射
3.1 数据格式
在刚才的汇编代码中,push,mov,add等指令都带有一个字母的后缀。这个后缀表示数据的格式。其中不同后缀对应的数据格式如下
| C声明 | Intel数据类型 | 汇编代码后缀 | 大小(字节) |
|---|---|---|---|
| char | 字节 | b(yte) | 1 |
| short | 字 | w(ord) | 2 |
| int | 双字 | l(ong) | 4 |
| long | 四字 | q(uad) | 8 |
| char* | 四字 | q | 8 |
| float | 单精度 | s | 4 |
| double | 双精度 | l | 8 |
注解:虽然int与double采用同一套后缀l,但是执行他们的指令与寄存器是完全不同的
3.2 寄存器命名
一个x86-64CPU提供一组16个存储64位值的通用目的寄存器。在汇编代码中,如%rbx,%r8,%eax都是在表示相应的寄存器。寄存器是存在与CPU内的高级其中命名对应的寄存器关系详见CSAPP:120页
3.3 操作数
函数要参数,指令就要操作数
操作数有三种
立即数(immediate) 存常量,格式为$数值
寄存器(register) 寄存器的值,格式如 %rax或rax
内存值 内存的值,通过寻址与指定长度选取相应的值。 格式为 立即值 (寄存器1,寄存器2,2的幂次) 如12(%rax,%rdx,4)表示的内存地址为 12 + %rax + 4*%rdx.
3.4 传送数据指令
现在我们解释完了数据格式的表示,寄存器,指令要的操作数。终于能够讲讲指令了。
完成数据移动操作的指令为 MOV指令MOV S,D 将S的数值复制到D中。
例子:
movl $0x4050,%eax 将立即值0x4050传入寄存器eax 4字节
movw %hp,%sp 将寄存器hp值传入寄存器sp 2字节
movb (%rdi,%rcx),$al 将内存(%rdi,%rcx)值传入寄存器al 1字节
movq $114514,(%rsp) 将立即值114514传入内存(%rsp) 8字节
movq %rax,-12(%rbp) 将寄存器rax值传入内存-12(%rbp) 8字节
其中,对于小数据(char,short)传到大数据(int,long)的过程,还有两种指令
MOVZ指令MOVZ S,R和MOVS指令MOVS S,R
MOVZ类指令遵循零扩展原则,较小得值传到较大值时,剩余位用0填充
MOVS类指令遵顼符号填充原则,较小值传到较大值时,剩余位用源的符号位进行填充
这两类指令后面跟两个数据长度标识。
例子
movzbw 0xFF,%dx byte --> word 将0xFF复制到寄存器dx 1字节到2字节
movswl %dx,%eax word --> long将寄存器dx的值复制到eax中 2字节到4字节
3.5 算术逻辑运算指令
x86-64的算数逻辑运算指令表如下
这里按照分类分为 加载有效地址,一元运算符,二元运算符,移位
3.5.1 加载有效地址
Load Effective Address(lea) 格式为lea S D S为内存地址,D为寄存器。起作用为将左侧内存地址的值赋给右侧寄存器,实际上就是一种特化的mov指令。在64位系统中,地址长度8字节,所以lea指令为leaq。在32位系统中,地址长度为4字节,所以lea指令为leal。
这条命令有两个作用,一个是将内存地址加载到寄存器,还有就是实现简单的算数运算
例子:
设x在%rdi,y在%rsi,z在%rdx
leaq 0x123456789ABCDEF,%rax 左侧立即值直接赋给rax
leaq (%rdx,%rdx,4),%rdx 实现z = z + 4z
leaq (%rdi,%rsi,4),%rdx 实现z = x + 4y
再比如说下面这段C代码,编译后由三条leaq指令实现
long scale(long x,long y,long z){
long t = x + 4*y + 12*z;
return t;
}
设x --> %rdi y --> %rsi z --> %rdx t --> %rax
scale:
leaq (%rdi,%rsi,4),%rax t = x + 4y
leaq (%rdx.%rdx,2),%rdx z = z + 2z
leaq (%rax,%rdx,4),%rax t = t + 4z
ret
3.5.2 一元二元操作
一元操作符的操作数即是源又是目的 二元操作符由两个操作数,第一个是源,第二个即使源也是目的 例子:
设 x -- > rax ,y --> rcx , z --> (%rax)
addq %rcx,%rax y = x + y
subq %rcx,(%rax) z = z - y
incq %rax x = x + 1
decq %rcx y = y - 1
3.5.3 移位操作
移位操作的格式为 移位指令 移动位数,操作数
其中,移位指令有 SAL算术左移,SHL逻辑左移,SAR算术右移,SHR逻辑右移
3.6 控制
接下来我们将要理解C的控制语句if,while,for的汇编实现
3.6.1 条件判断
汇编层面的核心指令为 CMP指令,TEST指令,SET指令
要理解这三个指令,需要了解一个预备知识
条件码(condition code) CPU内部还维护着一组单个位的条件码寄存器。它们记录着最近的算术或者逻辑操作的属性。常用的有CF(进位标志),ZF(零标志),SF(符号标志),OF(溢出标志)。所以在上述的算术逻辑运算指令执行后,也会相应地更新这些寄存器。如果要进行条件判断记录,就要在其寄存器内容还未改变前读取寄存器数据。
具体例子如 执行addq %rax %rex后,加法发生溢出,那么OF就会被设置为1.此时如果使用能够读取条件码的指令,我们就可以判断刚才的加法有没有发生溢出
CMP指令 CMP指令接受两个操作数,然后更新寄存器。其操作就是让两个数相减,但是不输出减法结果,只修改条件码寄存器。
TEST指令 TEST指令接受两个操作数,然后更新寄存器,其操作就是让两个数进行位级与运算(A & B),但是不输出结果,只修改条件码寄存器。其中test指令用的最多的用途如下
设x --- > rdi
testq %rdi,%rdi TEST同一寄存器
sete %al 判断x == 0
setg %al 判断x > 0
setle %al 判断x <= 0
SET指令 SET指令根据标志位寄存器的值,进行不同的运算,将返回值存储在寄存器的低八位上。
3.6.2 跳转指令
跳转的核心指令为 JMP类指令
为了方便讲解,直接上代码
无条件判断的jmp指令有直接跳转与间接跳转两种模式
jmp L1 直接跳转,跳转至L1位置
jmp *%rax 间接跳转,从寄存器或者内存读取到跳转目标
jmp *(%rex) 间接跳转的写法就是*跟一个操作数指示符
然后,jmp类指令还存在有条件判断的。其种类与set类指令相同,通过读取条件码寄存器并运算,通过运算结果检验条件是否成立,在条件成立时发生跳转。 下面为例子
设 int x ---> %rax , int y ---> %rex
cmpl y,x 比较x,y
je L1 在x == y情况下,跳转至L1
jl L2 在x > y的情况下,跳转至L2
L1:
movq %rax %rex
L2:
movq %rex %rax
3.6.3 条件传送
条件传送指令为 CMOV指令