汇编语言
>> CNAME=<name>
# 源代码编译成汇编
## -fno-omit-frame-pointer : 不使用ebp
>> gcc -O2 -m32 -S -masm=intel $CNAME.c -o $CNAME.S
# 汇编代码至二进制模块
## -gstabs/ -gstabs+ : 以 stabs 格式声称调试信息
## --32 : 生成32位obj文件
>> as -o $CNAME.o $CNAME.S
# 链接二进制模块成可执行文件
## -m elf_i386: 生成i386架构的ELF文件
## 这一步如果有错误,则安装一下库
## sudo apt-get install g++-multilib
>> ld -o $CNAME $CNAME.o
计算机体系基本结构
指令集
- CISC
- 不定长指令
- 一个字(word)= 2 Byte = 16 bits
- x86
- RISC
- 大部分位定长指令, 只能通过
LOAD
与STORE
访问内存 - 一个字(word)= 4 Byte = 32 bits
- MIPS, ARM
- 大部分位定长指令, 只能通过
机器字长
硬件架构 | 寻址空间 |
---|---|
32位 | 4 GB |
64位 | ~ |
x86-64 | 48位宽,256TB |
逻辑运算符 | 符号 | 英文 | 名称 | 含义 | | :-: | :-: | :-: | :-: | | \& | and | 与 | 都真则真 | | | | or | 或 | 有真则真 | | ~ | not | 非 | 取反 | | \^ | xor | 异或 | 不同则真 相同则假 |
寄存器
通用寄存器
栈寄存器
指令寄存器
C语言与汇编
从C代码生成汇编代码
# 使用gcc生成 32位的Intel风格汇编代码
>> gcc -O2 -m32 -fno-omit-frame-pointer -S -masm=intel code.c -o code.S
汇编语言数据格式

第一条汇编指令
C代码:
int add(int x, int y) {
int t = x + y;
return t;
}
汇编代码:
# AT&T
addl 8(%ebp) %eax
# Intel
add eax, [ebp+8]

内存寻址模式
间接寻址 Mem[Reg[R]]
# 寄存器R指向的内存地址
## AT&T
MOVL (%ecx) %eax
## Intel
MOV eax, [ecx]
基址+偏移量寻址 Mem[Reg[R]+D]
# 寄存器R指向内存起始地址, 常数D指定偏移量
## AT&T
MOVL 8(%ebp) %edx
## Intel
MOV edx, [ebp+8]
变址寻址 D(Rb,Ri,S), Mem[Reg[Rb]+S*Reg[Ri]+D]
# D: 常量, 即地址偏移量
# Rb: 基址寄存器: 8个寄存器都可以
# Ri: 索引寄存器: 除esp外都可以(通常不会使用ebp)
# S: 比例因子 1,2,4,8

1. 数据传送指令 MOV
将一个双字从Source移动到Dest
# AT&T
MOVL <源操作数> <目的操作数>
# Intel
MOV <目的操作数>, <源操作数>
允许的操作数类型:
- 立即数
- AT&T: $0x400, $-533
- Intel: 0xff
- 可以被 1/2/4 字节表示
- 寄存器
- eax, edx, ecx, ebx, esi, edi, esp, ebp
- 存储器(两个操作数不能同时位内存地址)
- 四个连续字节, 支持多种寻址模式
变种

地址计算指令 LEA
将计算出来的地址赋值给Dest, 与MOV最大的区别是,它不会访问内存
常用于地址计算, 可以用于表达式x+k*y(k=1,2,4,8)
的计算
// 地址计算
int *p = &x[i];
// 特定模式的整数计算
/*
MOV eax, [ebp+8]
MOV edx, [ebp+12]
*/
int f(int x, int y, int z) {
// LEA ECX, [0 + EAX*4]
int n1 = x + y;
int n2 = z + n1;
}
常用算数指令
双操作数指令

格式 | 计算 | 说明 |
---|---|---|
ADD dst, src | dst=dst + src | |
SUB dst, src | dst=dst - src | |
IMUL dst, src | dst=dst * src | |
SAL dst, src | dst=dst « src | 逻辑左移 |
SHL dst, src | dst=dst « src | 逻辑左移 |
SAR dst, src | dst=dst » src | 算数右移 |
SHR dst, src | dst=dst » src | 逻辑右移 |
XOR dst, src | dst=dst ^ src | |
AND dst, src | dst=dst & src | |
OR dst, src | dst=dst | src |
单操作数指令

格式 | 计算 | 说明 |
---|---|---|
INC dst | dst=dst + 1 | 自增 |
DEC dst | dst=dst - 1 | 自减 |
NEG dst | dst=0 - dst | 取反 |
NOT dst | dst= ~ dst | 取非 |
控制流
条件码
条件码 | 英文名 | 含义 |
---|---|---|
CF | Carry Flag | 无符号整数溢出, 则为1 |
SF | Sign Flag | 如果运算结果<0, 则为1 |
ZF | Zero Flag | 如果运算结果=0, 则为1 |
OF | Overflow Flag | 带符号整数运算溢出, 则为1 |
比较指令 CMP
# 计算SRC2-SRC1, 并且将计算结果存储到条件码中, 不会改变目的操作数
CMP SRC1, SRC2
测试指令 TEST
# 计算SRC1&SRC2, 并设置相应条件码, 但是不改变目的操作数
# CF与OF为0
TEST SRC1, SRC2
读取条件码的指令 SET*

####
栈调用过程
压栈指令: PUSH
栈顶增长4字节(esp=esp-4), 然后将操作数放在栈顶
PUSH Src
# 等效于
MOV edx, src
SUB esp, 4
MOV [esp], edx
出栈指令: POP
将栈顶内容存入目的操作数中,然后栈顶减小4字节(esp=esp+4)
POP Dst
# 等效于
MOV dst, [esp]
ADD esp, 4
过程调用指令: CALL
将返回地址压入栈, 并且跳转到label
CALL Label
# 等效于
PUSH retaddr
JMP label
eax寄存器通常用于存放函数返回值
过程返回指令: RET
调用ret指令时, 栈顶指向返回地址, ret会跳转至栈顶指向的返回地址
RET
# 等效于
POP edx
JMP edx
栈帧的创建与释放
- 存储内容
- 局部变量
- 返回地址
- 零时空间
- 栈帧的分配与释放
- 进入过程先分配栈帧空间
- “Set-Up” Code
- 过程返回时释放栈帧空间
- “Finish” Code
- 进入过程先分配栈帧空间
当前栈帧中存储的内容
- 子过程的参数
- 局部变量(因为寄存器数目有限)
- 父过程的寄存器值
- 父过程栈帧的基地址(old ebp)

Set-Up过程
PUSH ebp
将父栈的基址压入栈MOV ebp, esp
将ebp设置为栈顶地址PUSH ebx
存储子过程会修改的寄存器值
Finish过程
MOV ebx, [ebp-4]
恢复父过程的ebx值MOV esp, ebp
让esp指向当前栈的栈底POP ebp
将esp指向的old-ebp存入ebp中,并让栈顶指向ret addrRET
将eip寄存器设置为esp指向的地址

寄存器使用惯例

- 栈寄存器
- ebp: 栈底地址寄存器(栈基址寄存器)
- esp: 栈顶地址寄存器
- 被调用者(callee)保存寄存器
- ebx, esi, edi
- 调用者(caller)保存寄存器
- eax, edx, ecx
- eax: 用于存放函数返回值
####
C语言运行时内存结构

汇编指示
数据段 .section
.rodata
- 只读数据段


.data
- 声明带有初始值的数据内容
.bss
- 声明无需初始化的内容
.comm
声明未初始化的全局内存区域.lcomm
声明未初始化的本地内存区域- eg:
# 声明1000字节长度的内存,外部模块无法访问 .section .bss .lcomm buffer, 1000
.text
- 代码段,存放汇编代码

参考
文档信息
- 本文作者:Birkhoff
- 本文链接:https://blogs.maikebuke.com/2021/12/20/Assembly/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)