内容:简单总结 x86 汇编基础和函数调用的过程,但只涉及可用指令和汇编指令的一小部分,但非常有用。主要采用 GNU 汇编器(GAS) 的 AT&T 语法进行说明。
补充的资料:
x86 处理器有 8 个通用寄存器,如下图,其中 EAX 过去被称为累加器,因为它被许多算术运算使用;ECX 被称为计数器,因为它被用来保存循环索引,然而现在基本上失去了其专有目的,成为通用寄存器。但是 EBP 通常用于栈基指针,ESP 用于栈顶指针。
EAX、EBX、ECX、EDX 还可以分别访问其低地址的 16 位,和其中的高低字节。如下图中所示。
EAX、ECX、EDX,被调用者 callee-saved 需要保存的寄存器有 EBP、EBX、EDI、ESI。x86-64 处理器扩展了上述通用寄存器到 64 位,并增加了一些新的寄存器,如 R8~R15,所以有 16 个 64 位寄存器。但是,为了向后兼容,32 位寄存器仍然可以使用。

R 前缀访问,如 RAX、RBX、RCX、RDX 如上图。SIMD: MMX, SSE, AVX, AVX-512
此外,还提供了 16 个 SSE 寄存器(xmm0~xmm15),每个寄存器宽度 128 位,以及 8 个 x87 指令浮点寄存器(st(0)~st(7)),每个寄存器宽度 80 位(主要用于早期的浮点计算, 以及 MMX 指令会共享该寄存器)。
Intel AVX(高级向量扩展)提供了 16 个 256 位的 YMM 寄存器(ymm0~ymm15),其低 128 位即对应的 128 位 SSE 寄存器(别名);AVX-512 扩展提供了 32 个 512 位的 ZMM 寄存器(zmm0~zmm31),其低 256 位对应于 256 位 YMM 寄存器,低 128 位对于 128 位 XMM 寄存器。(因此 xmm16 - %xmm31 / ymm16 - ymm31 只在 AVX-512 扩展中有效)
我们将使用这些寄存器和 SIMD 指令进行浮点运算:
https://sourceware.org/binutils/docs/as/index.html
GNU 汇编快速入门
这里主要使用 AT&T 语法,它是 GNU 汇编器(GAS) 的默认语法。AT&T 语法的特点是源操作数在前,目的操作数在后,操作数之间用逗号分隔。如下所示:
pushq %rbp
movq %rsp, %rbp
movl %edi, -20(%rbp)
movl %esi, -24(%rbp)
movl %edx, -28(%rbp)
movl -20(%rbp), %eax
movl %eax, -4(%rbp)
......
popq %rbp
ret
与 Intel 汇编语法的主要区别有:全面的差异
| 差异 | AT&T 语法 | 说明 | Intel 语法 | 说明 |
|---|---|---|---|---|
| 操作数顺序 | movq %rsp, %rbp |
源操作数在前,目的操作数在后 | mov rbp, rsp |
目的操作数在前,源操作数在后 |
| 指令后缀 | movq %rsp, %rbp |
指令后缀表示操作数大小,如 q 表示四字操作 |
mov rbp, rsp |
不用后缀,通过寄存器操作数推断大小;内存寻址大小歧义时用 BYTE PTR 等显示标注 |
| 寄存器 | %rsp |
寄存器名称前加 % |
rsp |
寄存器名称不加 % |
| 操作数中的立即数 | movq $0x2, %rax |
操作数中的立即数前加 $ |
mov rax, 0x2 |
不加 $ |
| 内存引用 | movq 8(%rsp), %rax |
立即数在 () 外面 |
mov rax, [rsp+8] |
立即数在 [] 里面 |
| 跳转/call 操作数 | je *%rax, callq *%rax |
操作数前加 * |
je rax, call rax |
不加 * |
| 注释 | # comment |
注释符号为 # |
; comment |
注释符号为 ; |
多数指令使用后缀来表示操作数的大小,如:
b:byte 表示字节操作,如 movb、addb。w:word(2 bytes) 表示字操作,如 movw、addw。l:long/doubleword(4 bytes) 表示双字操作,如 movl、addl。q:quadword(8 bytes) 表示四字操作,如 movq、addq。如果可以从操作数中推断出操作数的大小,可省略后缀。如 movq %rsp, %rbp 可以写成 mov %rsp, %rbp,操作数 %rsp 暗示 q,%eax 暗示 l,以此类推。
少数指令例如 movs(符号扩展)、movz(零填充)有两个后缀,第一个表示源操作数位宽,第二个表示目的操作数位宽,例如 movzbl 移动一个字节长度源操作数到一个双字长度目的操作数,高位用 0 填充。
Note: 当目标寄存器是子寄存器时,只有子寄存器范围的字节会被写入更新,但有一个例外:32 位的指令写入时,会将目标寄存器的高 32 位清零,例如 mov $ebx, %ebx 这种看似冗余的指令会进行将 rbx 的高 32 位清零。这里等效 movzlq %ebx, %rbx。
指令中访问内存的一般形式是:
displacement:立即数,表示偏移量。base:基址寄存器,表示基址。index:索引寄存器,表示索引。scale:比例因子,表示索引寄存器的倍数。例如,。
以写立即数 1 为例说明多种寻址模式:
movl $1, 0x604892 # 直接寻址,地址 0x604892
movl $1, (%rax) # 间接寻址,地址为 rax 寄存器的值
movl $1, -24(%rbp) # 基址+偏移,地址为 rbp 寄存器的值减去 24
movl $1, 8(%rsp, %rdi, 4) # 基址+索引*比例+偏移,地址为 rsp + rdi * 4 + 8
movl $1, 0x8(, $rdx, 4) # 索引*比例+偏移,地址为 rdx * 4 + 8
movl $1, 0x4(%rax, %rcx) # 比例假定为1,地址为 rax + rcx + 4
常用的指令和寻址方式可以参考:Common instructions and Addressing modes - cheatsheet
主要有数据移动、算术运算/逻辑运算、控制流等。
数据移动
mov:数据传送指令,如 movl %eax, %ebx。movz: 0 填充(小位宽赋值到大位宽),如 movzbl (%rdi), %eax, 其中后缀 b 表示字节操作,l 表示双字(双指令后缀)。movs: 符号扩展(小位宽赋值到大位宽),如 movsbl (%rdi), %eax, 其中后缀 b 表示字节操作,l 表示双字。cltq: movs 指令对 rax 寄存器特化指令,将其低 32 位符号扩展到 64 位。push:将数据压入栈,如 pushq %rbp。具体操作:栈顶指针减去 8,然后将数据写入栈顶。pop:将数据弹出栈,如 popq %rbp。具体操作:读取栈顶数据,然后栈顶指针加 8。lea: 取操作数的有效地址,不进行地址解引用,如 lea 8(%rsp), %rax。算术运算/逻辑运算
许多算术指令同时适用于有符号和无符号类型。例如 add、sub 等。通过操作后设置的标志位可以检测不同类型的溢出。
add:加法,如 addl %eax, %ebx。sub:减法,如 subl %eax, %ebx。inc/dec: 自增/自减,如 incl %eax。neg:取负数,如 negl %eax。imul:整型有符号乘法,如 imull %eax, %ebx。结果保存在 [EDX:EAX] 中,高 32 位在 EDX 中,低 32 位在 EAX 中。idiv:整型有符号除法,如 idivl %eax。使用 idiv 之前,需要将被除数放在 EDX:EAX 中。结果的商保存在 EAX 中,余数保存在 EDX 中。mulss/divss: 标量单精度浮点数乘法/除法 scalar single-precision,如 mulss %xmm1, %xmm2。mulsd/divsd: 标量多精度浮点数乘法/除法 scalar double-precision,如 mulsd %xmm1, %xmm2。and/or/xor:按位与/或/异或,如 andl %eax, %ebx。not:按位取反,如 notl %eax。shl/shr:逻辑左移/右移,省略源操作数则默认为 1,如 shll $2, %eax。sal/sar:算术左移/右移,省略源操作数则默认为 1,如 sarl $2, %eax。控制流指令
eflags 标志寄存器用于存储条件码,条件码是由上一条指令设置的,并且大多数算数指令会更新这些标志。条件码用于控制条件跳转指令。常用的标志位有:ZF(零标志位)、SF(符号标志位)、OF(溢出标志位)、CF(进位标志位)。
OF:溢出标志位,针对有符号数,当结果超出有符号数的表示范围时设置。包括正溢出和负溢出。CF:进位/借位标志位,针对无符号数,当结果超出无符号数的表示范围时设置。包括无符号加法进位和无符号减法借位。条件跳转指令和使用的标志位对应如下:

AND 指令类似,但是不保存结果,只设置标志寄存器,如 testl %eax, %eax。SUB 指令类似,但是不保存结果,只设置标志寄存器,如 cmpl %eax, %ebx。与分支指令配合使用。jmp:无条件跳转,如 jmp label。call func, 其操作是:将下一条指令的地址压入栈,然后跳转到函数的入口地址。ret。其操作是:将弹出的返回地址加载到指令指针寄存器(RIP)中,从而跳转到函数调用后的下一条指令,继续执行。leave。其操作是:将栈帧指针 rbp 的值赋给栈顶指针 rsp(清空当前栈帧),然后弹出栈帧指针 rbp(恢复原始 rbp, 还原上一个栈帧)。和标志寄存器相关的指令:有两种常见指令可以读取/响应当前标志寄存器的值
setx:x 是条件占位符,根据条件(x)设置一个字节寄存器为 0 或 1,如 sete %al。cmovx:条件移动指令,x 是条件占位符,根据条件(x)将源寄存器复制到目的寄存器,如 cmovle %eax, %ebx。x 是条件占位符,值及其含义与上图中的条件跳转指令相同。为了允许共享代码并简化子程序的使用,程序员通常采用一种通用的调用约定。调用约定是一种关于如何调用和返回例程的协议。例如,给定一组调用约定规则,程序员不需要检查子程序的定义来确定如何将参数传递给该子程序。此外,给定一组调用约定规则,高级语言编译器可以按照这些规则进行编译,从而允许手动编写的汇编语言例程和高级语言例程相互调用。
MacOSX 和 Linux 的 x86-64 调用协议都遵循 SystemV ABI (见参考资料)。
ABI 将参数/返回值定义了多种类别,依据寄存器的种类定义为:INTEGER(整型,能够适应于通用寄存器 8B)、SSE(能够适应于单个向量寄存器 8B)、SSEUP(继续利用上次使用的向量寄存器中的高字节部分>8B)、MEMORY(通过栈传递的类型)和其他类型。
函数调用的前 6 个(整型,包括指针)参数通过寄存器传递,传递顺序为 %rdi,%rsi,%rdx,%rcx,%r8,%r9(如图所示 arg1~arg6)。超出部分的参数通过函数栈帧传递。
如果函数有返回值(整型),%rax 用作第一个函数返回值,%rdx 用作第二个函数返回值。浮点数使用 xmm0 作为返回值。
其他一些规则:
%rdi 隐式传递该内存指针作为第一个参数。被调用函数对该内存赋值后,直接返回该指针。每个函数在运行时堆栈上都有一个帧。函数栈帧从高地址往低地址方向增长,System V ABI 使用两个寄存器访问函数栈帧:帧指针 %rbp 和栈指针 %rsp。 帧指针 %rbp 指向当前函数栈帧基址(栈底),栈指针 %rsp 指向当前函数栈帧栈顶。
函数调用的栈帧结构图如下所示:

%rbp 用来存取函数栈帧上的数据,例如传递进来的函数参数,或者函数的本地局部变量。 System V ABI 要求最后一个压入栈的函数参数(argument 0)地址需要 16 字节边界对齐,如果有 __m256 类型的参数使用栈传递,则需要 32 字节边界对齐。%rbp+16, 中间隔着 8 字节的返回地址。在 32 位系统中,第一个参数的位置在 %ebp+8。(只是 32 位系统在参数传递和寄存器保存上有所不同)栈红区(red zone)优化:红区是栈上 rsp 指向位置之后的 128 字节区域。该区域在函数调用时被认为是保留的,期间不被信号处理程序和中断处理程序修改,从而保护了函数栈的完整性。所以函数可以安全地使用这个区域来存储临时数据。
可用于优化函数调用:叶函数(即不调用其他函数的函数)可以利用红区作为整个栈帧的一部分,而不必在函数入口/序言和退出/结尾时调整栈指针(直接使用 rsp 寻址,并且不显示地调用 subq $N, %rsp 分配空间,直接使用栈红区作为整个栈帧)。这可以减少栈操作的开销,提高函数调用的性能。
函数调用协议分为 caller 端和 callee 端,每端各有两个重要步骤:
1)caller 调用者:
call 指令。(先压入返回地址,再跳转执行)2)callee 被调用者,函数序言:
pushq %rbp 压入 rbp 寄存器,用来保存前一个栈帧基址;movq %rsp, %rbp 初始化 rbp 寄存器,用来指向当前栈帧基址;(新的 rbp 常用于寻址参数/局部变量)subq $N, %rsp;3) callee 被调用者,函数尾声:
函数体执行完毕后,需要执行以下步骤:
rax 中(浮点数置入 xmm0);popq 倒序从栈帧中恢复寄存器 】;movq %rbp %rsp 先回退栈顶指针 %rsp,popq %rbp 再恢复原基址指针 %rbp。(两个操作,合并等效为 leave 指令);ret 指令,弹出返回地址,执行流回到 caller 。4)caller 调用者:
popq 倒序从栈帧中恢复,释放这些空间】;至此,一个完整的函数调用过程完成。函数执行的结果一般位于寄存器 rax 中,如果是浮点数,位于在 xmm0 中。
说明:
xmm0~xmm7 传递参数,返回值使用 xmm0~xmm1。MIPS(Microprocessor without Interlocked Pipeline Stages)是一种精简指令集计算(RISC)架构,广泛应用于嵌入式系统、网络设备以及教育领域。它的设计强调简洁性和高效性,所有指令的长度固定为 32 位(部分版本支持 16 位压缩指令)。
MIPS 有32个通用寄存器:

函数调用:
$a0-$a3(MIPS32)或 $a0-$a7(MIPS64)传递前 4 或 8 个参数,更多参数通过栈传递。$v0 和 $v1 返回函数结果。$t0-$t9$s0-$s7mips assembly lecture - by nju
RISC-V 是一种基于精简指令集(RISC)的开源指令集架构(ISA),其设计具有模块化、灵活性和跨平台支持的特点。基础指令集(如 RV32I、RV64I)只提供最基本的操作,其他功能通过扩展模块实现,例如:
RV32I 有32个通用寄存器:

t0-t6s0-s11a0-a7阅读
本文链接: 汇编基础和函数调用ABI
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
发布日期: 2024-07-23
最新构建: 2025-08-11
欢迎任何与文章内容相关并保持尊重的评论😊 !