内容:简单总结 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-$s7
mips assembly lecture - by nju
RISC-V 是一种基于精简指令集(RISC)的开源指令集架构(ISA),其设计具有模块化、灵活性和跨平台支持的特点。基础指令集(如 RV32I、RV64I)只提供最基本的操作,其他功能通过扩展模块实现,例如:
RV32I 有32个通用寄存器:
t0-t6
s0-s11
a0-a7
阅读
本文链接: 汇编基础和函数调用ABI
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
发布日期: 2024-07-23
最新构建: 2024-12-26
欢迎任何与文章内容相关并保持尊重的评论😊 !