跳过正文

X86_64 AT&T 汇编 - 个人参考手册

·7629 字
Gnu Asm As Gcc Manual
目录

AT&T 汇编
#

GNU Linux 使用是 AT&T 汇编语法(GAS),而 Windows、Intel 手册使用 Intel 语法:

特性 AT&T 语法 Intel 语法
操作数顺序 源在左,目标在右 源在右,目标在左
立即数 带 $ 前缀 无前缀
寄存器 带 % 前缀 无前缀
内存寻址 disp(base,index,scale) [base + index*scale + disp]
操作数大小 指令带后缀 (l,w,b) 用关键字 (BYTE PTR 等)

示例:

# AT&T 语法
movl $42, %eax               # 立即数到寄存器
movl $42, -4(%rbp)          # 立即数到内存
movl -4(%rbp), %eax         # 内存到寄存器
movl (%rbx,%rcx,4), %eax    # 复杂寻址

数据定义:

# GAS 数据定义
.byte   ; 定义字节
.word   ; 定义字
.long   ; 定义双字
.quad   ; 定义四字

# GAS 段定义
.section .data
.section .text

# 举例
.section .data
message: .string "Hello"

.section .text
.global _start
_start:
    movq $1, %rax       # 源操作数在左,目标在右
    movq $1, %rdi
    movq $message, %rsi
    movq $5, %rdx
    syscall

gdb、objdump 和 linux kenel 默认都使用 AT&T 汇编语法,但是也可以通过配置参数来指定 Intel 语法风格。

gdb:

# 默认 AT&T 语法
(gdb) disas main
Dump of assembler code for function main:
   0x0000555555555129 <+0>:     push   %rbp
   0x000055555555512a <+1>:     mov    %rsp,%rbp
   0x000055555555512d <+4>:     sub    $0x10,%rsp

# 切换到 Intel 语法
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
   0x0000555555555129 <+0>:     push    rbp
   0x000055555555512a <+1>:     mov     rbp,rsp
   0x000055555555512d <+4>:     sub     rsp,0x10

objdump:

# 默认 AT&T 语法
$ objdump -d program

0000000000001129 <main>:
    1129:       55                      push   %rbp
    112a:       48 89 e5                mov    %rsp,%rbp
    112d:       48 83 ec 10             sub    $0x10,%rsp
    1131:       89 7d fc                mov    %edi,-0x4(%rbp)

# 切换到 Intel 语法
$ objdump -M intel -d program
0000000000001129 <main>:
    1129:       55                      push    rbp
    112a:       48 89 e5                mov     rbp,rsp
    112d:       48 83 ec 10             sub     rsp,0x10
    1131:       89 7d fc                mov     DWORD PTR [rbp-0x4],edi

寄存器
#

x86_64 提供了 16 个通用寄存器,每个寄存器为 64 位。

  • 64位: %rax, %rbx, %rcx, %rdx, %rsi, %rdi, %rbp, %rsp, %r8-%r15
  • 32位: %eax, %ebx, %ecx, %edx, %esi, %edi, %ebp, %esp, %r8d-%r15d
  • 16位: %ax, %bx, %cx, %dx, %si, %di, %bp, %sp, %r8w-%r15w
  • 8位: %al, %bl, %cl, %dl, %sil, %dil, %bpl, %spl, %r8b-%r15b

特殊用途寄存器:

  • RSP:栈指针,指向当前栈顶。
  • RBP:基址指针,用于栈帧基址。
  • RIP:指令指针,指向下一条将执行的指令。
  • RFLAGS:状态标志寄存器,存储算术运算结果和 CPU 状态。

段寄存器:分为代码段、数据段、栈段等,段寄存器(如 CS、DS)已在现代架构中弱化。

  • %cs - 代码段
  • %ds - 数据段
  • %ss - 栈段
  • %es - 扩展段
  • %fs - 额外段
  • %gs - 额外段

数据传送指令
#

字节序:x86_64 使用小端序,低字节存储在低地址。

操作数大小:支持 8 位(字节)、16 位(字)、32 位(双字)、64 位(四字)。

# mov 指令
movq $60, %rax        # 立即数到寄存器
movq %rax, %rbx       # 寄存器到寄存器
movq (%rax), %rbx     # 内存到寄存器
movq %rax, (%rbx)     # 寄存器到内存

# 零扩展传送
movzbq %al, %rax          # 字节到四字,零扩展
movzwq %ax, %rax          # 字到四字,零扩展

# 符号扩展传送
movsbq %al, %rax          # 字节到四字,符号扩展
movswq %ax, %rax          # 字到四字,符号扩展

内存寻址
#

基本寻址格式:

AT&T 语法格式:

offset(base, index, scale)

等价于计算公式:

最终地址 = offset + base + (index * scale)

其中:

  • offset: 偏移量(可选),常数
  • base: 基址寄存器(可选)
  • index: 索引寄存器(可选)
  • scale: 比例因子(可选),必须是 1、2、4 或 8

直接寻址:

# 直接访问指定内存地址
movq value, %rax      # value 是一个标签
movq $0x123456, %rax  # 直接地址

寄存器间接寻址:

# 通过寄存器中的地址访问内存
movq (%rax), %rbx     # 使用 rax 中的地址
movq (%rsp), %rax     # 访问栈顶元素

基址寻址:

# base + offset
movq 8(%rbp), %rax    # rbp + 8
movq -8(%rbp), %rax   # rbp - 8
movq 16(%rsp), %rax   # rsp + 16

变址寻址:

# (index * scale) + offset
movq array(, %rcx, 8), %rax   # array + rcx * 8
movq (%rcx, %rdx, 4), %rax    # rcx + rdx * 4

基址变址寻址:

# base + (index * scale) + offset
movq 8(%rbx, %rcx, 4), %rax   # rbx + rcx * 4 + 8
movq data(%rip, %rcx, 8), %rax # RIP相对寻址

示例:

数组访问:

# int array[10];
# 访问 array[i],假设 i 在 %rcx 中

    # 方式1:基址变址寻址
    leaq array(%rip), %rax      # 获取数组基址
    movl (%rax, %rcx, 4), %edx  # 访问 array[i]

    # 方式2:直接使用标签
    movl array(, %rcx, 4), %edx # 直接访问 array[i]

结构体访问:

# struct Point {
#     int x;   // offset 0
#     int y;   // offset 4
#     long z;  // offset 8
# };

    # 假设结构体指针在 %rax
    movl (%rax), %edx          # 访问 p->x
    movl 4(%rax), %edx         # 访问 p->y
    movq 8(%rax), %rdx         # 访问 p->z

多维数组访问:

# int matrix[3][4];
# 访问 matrix[i][j]
# i 在 %rcx,j 在 %rdx

    # 计算偏移:i * 4 * 4 + j * 4
    movq %rcx, %rax
    imulq $16, %rax           # i * 16 (4 * 4)
    leaq (%rax, %rdx, 4), %rax
    movl matrix(%rax), %edx

栈帧访问:

# 访问局部变量和参数
    pushq %rbp
    movq %rsp, %rbp
    subq $16, %rsp           # 分配栈空间

    # 访问局部变量
    movq $1, -8(%rbp)       # 第一个局部变量
    movq $2, -16(%rbp)      # 第二个局部变量

    # 访问参数(假设是通过栈传递)
    movq 16(%rbp), %rax     # 第一个栈参数
    movq 24(%rbp), %rdx     # 第二个栈参数

RIP和基址寻址
#

RIP 相对寻址是特殊的寻址模式:

# 格式:label(%rip)
# 计算方式:目标地址 = 下一条指令地址 + 偏移量

# 位置无关代码中常用
leaq message(%rip), %rdi    # 加载字符串地址
movq value(%rip), %rax      # 加载全局变量

基址寻址:

# 格式:offset(base_register)
# 计算方式:目标地址 = 基址寄存器内容 + 偏移量

movq 8(%rbp), %rax         # 访问栈帧参数
movq -8(%rbp), %rax        # 访问局部变量
movq 16(%rsp), %rax        # 访问栈上数据

# 多重间接访问
    movq (%rax), %rbx           # 一级间接
    movq (%rbx), %rcx           # 二级间接

# 复杂偏移计算
    movq 8(%rax, %rcx, 8), %rdx # base + index * scale + offset

RIP 相对寻址:

  1. 用于位置无关代码 (PIC)
  2. 基于当前指令位置
  3. 适合访问全局数据
  4. 支持 ASLR
  5. 偏移量在编译时确定

基址寻址:

  1. 用于局部数据访问
  2. 基于寄存器内容
  3. 适合访问栈数据
  4. 运行时动态计算
  5. 偏移量是固定的

内存布局示例:

  1. RIP 相对寻址
代码段:
0x1000: leaq message(%rip), %rdi
0x1007: ...                     # 下一条指令

数据段:
0x2000: message: "Hello"

计算: 0x1007 + (0x2000 - 0x1007) = 0x2000
  1. 基址寻址
栈:
高地址   参数 2        [%rbp + 24]
        参数 1        [%rbp + 16]
        返回地址      [%rbp + 8]
%rbp ->  旧 %rbp      [%rbp]
        局部变量 1    [%rbp - 8]
        局部变量 2    [%rbp - 16]
%rsp ->  ...
低地址

使用场景对比:

  1. 适合 RIP 相对寻址的场景
# 1. 访问全局变量
movq global_var(%rip), %rax

# 2. 加载字符串常量
leaq string_const(%rip), %rdi

# 3. 访问静态数组
leaq static_array(%rip), %rsi

# 4. 跳转表
leaq jump_table(%rip), %rax
  1. 适合基址寻址的场景
# 1. 函数参数访问
movq 16(%rbp), %rax    # 第一个栈参数

# 2. 局部变量访问
movq -8(%rbp), %rax    # 局部变量

# 3. 数组索引
movq (%rbx,%rcx,8), %rax  # 访问数组元素

# 4. 结构体成员访问
movq 8(%rdi), %rax     # 访问结构体字段

示例:

# RIP 相对寻址:全局数据访问
.section .data
global_var:
    .quad 123

.section .text
func:
    movq global_var(%rip), %rax   # 访问全局变量

# 基址寻址:局部变量访问
func:
    pushq %rbp
    movq %rsp, %rbp
    subq $16, %rsp

    movq $1, -8(%rbp)      # 访问局部变量
    movq 16(%rbp), %rax    # 访问参数

LEAQ
#

leaq(Load Effective Address Quadword) 指令计算地址但不访问内存,常用于:

  • 地址计算
  • 简单算术运算
  • 指针操作

例子:

# 基本用法
leaq (%rdi), %rax           # 相当于 move %rdi, %rax
leaq 8(%rdi), %rax         # rax = rdi + 8
leaq (%rdi,%rsi), %rax     # rax = rdi + rsi
leaq (%rdi,%rsi,4), %rax   # rax = rdi + rsi * 4

# 用于算术运算
leaq (%rdi,%rdi,2), %rax   # rax = rdi * 3
leaq (%rdi,%rdi,4), %rax   # rax = rdi * 5

# 数组寻址
leaq array(,%rdi,4), %rax  # rax = &array[rdi]

# RIP相对寻址
leaq message(%rip), %rdi   # 加载字符串地址

算术运算指令
#

# 加法
addq $1, %rax         # rax = rax + 1
addq %rbx, %rax       # rax = rax + rbx

# 减法
subq $1, %rax         # rax = rax - 1
subq %rbx, %rax       # rax = rax - rbx

# 乘法
imulq $2, %rax        # rax = rax * 2
imulq %rbx, %rax      # rax = rax * rbx

# 除法
idivq %rbx            # rdx:rax / rbx,商在rax,余数在rdx

PUSH/POP 栈指令
#

# 栈操作
pushq %rax                # 压栈
popq %rbx                 # 出栈

# 多寄存器操作
pushq %rbp
pushq %rbx
# ...
popq %rbx
popq %rbp

另外,函数调用的 callleaveret 也对栈进行操作:

  • call: 将当前 EIP 寄存器值压栈,然后跳转到被调用函数地址执行;
  • leave: 相当于 movq %rbp, %rsp;popq %rbp;
  • ret: 从栈弹出值保存到 EIP ,然后调整到对应函数地址处执行。

逻辑运算指令
#

# 与运算
andq $0xF, %rax       # rax = rax & 0xF

# 或运算
orq $0xF, %rax        # rax = rax | 0xF

# 异或运算
xorq %rax, %rax       # 清零rax

# 移位
shlq $1, %rax         # 左移1位
shrq $1, %rax         # 右移1位

比较和跳转指令
#

# 比较
cmpq $10, %rax        # 比较rax与10
cmpq %rbx, %rax       # 比较rax与rbx

testq %rax, %rax         # 测试 rax(常用于检查零)

# 设置条件码
setl %al                 # 如果小于则设置
setg %al                 # 如果大于则设置
sete %al                 # 如果相等则设置

# 无条件跳转
jmp label             # 跳转到label

# 条件跳转
je  label             # 相等时跳转
jne label             # 不相等时跳转
jg  label             # 大于时跳转
jge label             # 大于等于时跳转
jl  label             # 小于时跳转
jle label             # 小于等于时跳转

位运算
#

# 与运算
andq %rbx, %rax          # rax &= rbx

# 或运算
orq %rbx, %rax           # rax |= rbx

# 异或运算
xorq %rax, %rax          # 清零 rax
xorq %rbx, %rax          # rax ^= rbx

# 移位
shlq $1, %rax            # 左移1位 (乘2)
shrq $1, %rax            # 右移1位 (除2)
sarq $1, %rax            # 算术右移

CALL/RET 函数调用和返回
#

CALL 指令执行两个主要操作:

  1. 将下一条指令的地址(返回地址)压入栈
  2. 跳转到目标函数的地址
执行前:
%rip -> 当前指令(call)
%rsp -> 栈顶

执行后:
%rip -> 目标函数地址
%rsp -> 栈顶 - 8
[%rsp] = 返回地址
调用前:
                   |              |
                   |--------------|
            %rsp ->|              |
                   |--------------|

调用后:
                   |              |
                   |--------------|
                   |  返回地址    |  <-- 压入函数返回地址,及 call 指令下一条指令地址
            %rsp ->|              |
                   |--------------|

CALL 指令的类型:

# 1. 直接调用(使用标签)
call function_name

# 2. 间接调用(通过寄存器)
call *%rax

# 3. 间接调用(通过内存地址)
call *(%rax)

RET 指令执行两个主要操作:

  1. 从栈中弹出返回地址
  2. 跳转到返回地址
执行前:
%rip -> ret指令
%rsp -> 返回地址

执行后:
%rip -> 返回地址
%rsp -> 栈顶 + 8
返回前:
                   |              |
                   |--------------|
            %rsp ->|  返回地址    |
                   |--------------|
                   |              |

返回后:
                   |              |
                   |--------------|
            %rsp ->|              |  <-- 弹出返回地址
                   |--------------|
                   |              |

示例:

# 函数调用
call function          # 调用函数,将当前 EIP 压栈
ret                    # 返回,从栈弹出压栈返回地址到 EIP

# 调用示例
call *%rax             # 间接调用
call *(%rax)           # 通过内存中的地址调用

示例:计算斐波那契数列
#

.section .text
.globl fib
fib:
    pushq %rbp
    movq %rsp, %rbp

    cmpq $2, %rdi           # 检查n是否小于2
    jge .L2
    movq %rdi, %rax         # 返回n
    jmp .L3

.L2:
    pushq %rbx              # 保存被调用者保存的寄存器

    movq %rdi, %rbx        # 保存n
    subq $1, %rdi          # 计算fib(n-1)
    call fib

    movq %rax, %rsi        # 保存fib(n-1)的结果
    movq %rbx, %rdi
    subq $2, %rdi          # 计算fib(n-2)
    call fib

    addq %rsi, %rax        # fib(n-1) + fib(n-2)

    popq %rbx              # 恢复寄存器

.L3:
    movq %rbp, %rsp
    popq %rbp
    ret

系统调用
#

参考:

  1. https://akaedu.github.io/book/ch19s01.html
  2. https://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_Calling_Conventions

Linux x86_64 的系统调用号和参数传递约定:

  • 系统调用号放在 %rax
  • 参数依次放在 %rdi, %rsi, %rdx, %r10, %r8, %r9
  • 使用 syscall 指令进行系统调用

常用系统调用:

sys_read    = 0
sys_write   = 1
sys_open    = 2
sys_close   = 3
sys_exit    = 60

示例:

.section .data
msg:
    .ascii "Hello, World!\n"
    len = . - msg

.section .text
.globl _start
_start:
    # write(1, msg, len)
    movq $1, %rax            # sys_write
    movq $1, %rdi            # stdout
    movq $msg, %rsi          # message
    movq $len, %rdx          # length
    syscall

    # exit(0)
    movq $60, %rax          # sys_exit
    xorq %rdi, %rdi         # status = 0
    syscall

函数调用和栈帧
#

栈帧(Stack Frame)是为每个函数调用分配的一块连续内存区域,用于存储:

  • 局部变量
  • 保存的寄存器值
  • 函数参数
  • 返回地址

栈帧涉及两个寄存器:

  • %rsp: 始终指向栈顶
  • %rbp: 指向当前栈帧的基址,用于定位局部变量和参数

Intel 是小端模式,对于 32 位值,低地址保存低部分值。

栈帧布局
#

调用者栈帧: %rbp 寄存器指向的内存地址及以上的地址。

当前栈帧:%rsp 寄存器指向的内存地址到 %rbp 指向的内存内置。

高地址
                   |                                |
                   |--------------------------------|
                   |      栈参数 (第7个及以后)      |
                   |--------------------------------|
                   |      函数返回地址              |
                   |--------------------------------|
                   |          保存的 %rbp           |  <-- %rbp
                   |--------------------------------|
                   |          局部变量              |
                   |          ...                   |
                   |--------------------------------|
                   |       临时数据/spill区域       |
                   |--------------------------------|
                   |     被调用者保存的寄存器       |  <-- %rsp
低地址

栈帧创建过程
#

# 函数序言(Function Prologue)
pushq %rbp           # 保存栈基址
movq %rsp, %rbp      # 设置新的栈基址
subq $16, %rsp       # 分配栈空间,保存函数内变量

# ... 函数体 ...

# 函数结尾(Function Epilogue)
movq %rbp, %rsp      # 恢复栈指针
popq %rbp            # 恢复栈基址
ret                  # 返回

详细过程:

1. 初始状态:
   %rbp -> |  旧帧指针  |
           |    ...     |
   %rsp -> |           |

2. pushq %rbp 后:

   %rbp -> |  旧帧指针  |
           |    ...     |
           |  旧 %rbp   | <-- %rsp

3. movq %rsp, %rbp 后:

           |  旧帧指针  |
           |    ...     |
   %rbp -> |  旧 %rbp   | <-- %rsp

4. subq $16, %rsp 后:

           |  旧帧指针  |
           |    ...     |
   %rbp -> |  旧 %rbp   |
           |  局部变量  |
           |  局部变量  |
   %rsp -> |           |

调用者职责
#

调用者在调用函数前需要:

pushq %r10              # 保存调用者保存的寄存器
pushq %r11

设置参数,有两种方式 …

  1. 通过寄存器传参
  2. 寄存器+压栈传参:当 6 个寄存器用完时,从第 7 个参数开始使用压栈传参。

调用函数:

call function

函数返回后:

addq $16, %rsp         # 可选:清理压栈传递的参数,$16 的值取决于压栈传参的大小
popq %r11              # 可选:恢复调用者保存的寄存器, 如 r11 和 r10
popq %r10

被调用者职责
#

function:
    # 函数序言
    pushq %rbp
    movq %rsp, %rbp

    # 保存被调用者保存的寄存器
    pushq %rbx
    pushq %r12
    pushq %r13
    pushq %r14
    pushq %r15

    # ... 函数体 ...

    # 恢复寄存器和返回
    popq %r15
    popq %r14
    popq %r13
    popq %r12
    popq %rbx

    # 函数尾声
    movq %rbp, %rsp
    popq %rbp
    ret

寄存器传参
#

System V AMD64 ABI 调用约定,参数按顺序使用以下寄存器:

第1个参数:%rdi
第2个参数:%rsi
第3个参数:%rdx
第4个参数:%rcx
第5个参数:%r8
第6个参数:%r9
更多参数:通过栈传递(从右向左压栈)

浮点数参数:按照顺序使用 XMM 寄存器:

第1个参数:%xmm0
第2个参数:%xmm1
第3个参数:%xmm2
第4个参数:%xmm3
第5个参数:%xmm4
第6个参数:%xmm5
第7个参数:%xmm6
第8个参数:%xmm7
额外参数:通过栈传递

返回值存放在 %rax

调用者保存: %rcx, %rdx, %rsi, %rdi, %r8-r11

被调用者保存: %rbx, %rbp, %r12-r15

示例:

# 调用函数 func(1, 2, 3, 4, 5, 6, 7, 8)
movq $1, %rdi        # 第1个参数
movq $2, %rsi        # 第2个参数
movq $3, %rdx        # 第3个参数
movq $4, %rcx        # 第4个参数
movq $5, %r8         # 第5个参数
movq $6, %r9         # 第6个参数
pushq $8             # 第8个参数(先压栈,压栈的顺序是从右至左)
pushq $7             # 第7个参数
call func            # 调用函数
addq $16, %rsp       # 清理压栈传递的参数

# 另一个例子:
# void func(int a, double b, long c, float d)
# a in %rdi
# b in %xmm0
# c in %rdx
# d in %xmm1

.globl func
func:
    pushq %rbp
    movq %rsp, %rbp

    # 使用参数
    movq %rdi, -8(%rbp)    # 保存整数参数 a
    movsd %xmm0, -16(%rbp) # 保存双精度浮点数 b
    movq %rdx, -24(%rbp)   # 保存长整型 c
    movss %xmm1, -28(%rbp) # 保存单精度浮点数 d

结构体传递示例:

# struct Point { int x; int y; };
# void func(struct Point p)

func:
    pushq %rbp
    movq %rsp, %rbp
    subq $16, %rsp

    # 结构体通过寄存器传递(如果大小<=16字节)
    movq %rdi, -16(%rbp)  # 保存整个结构体

    # 访问结构体成员
    movl -16(%rbp), %eax  # 访问 x
    movl -12(%rbp), %edx  # 访问 y

参数访问:一般通过相对于 %rbp 的偏移量来访问(基址寻址):

  • (%rbp): 保存旧 %rbp 寄存器值;
  • 8(%rbp): 保存函数返回地址
# 访问第一个参数
movq 16(%rbp), %rax   # 从 rbp+16 的位置读取

# 访问第二个参数
movq 24(%rbp), %rax   # 从 rbp+24 的位置读取

可变参数:

# int sum(int count, ...)
sum:
    pushq %rbp
    movq %rsp, %rbp

    # 可变参数通过寄存器和栈传递
    # 需要保存 %al 中的 XMM 寄存器数量

    # 访问可变参数
    movq %rsi, -8(%rbp)   # 第一个可变参数
    movq %rdx, -16(%rbp)  # 第二个可变参数
    # ...

Red Zone:

  • 函数可以使用返回地址之下的128字节而无需调整栈指针
  • 不适用于信号处理程序
func:
    # 可以直接使用 red zone
    movq %rdi, -8(%rsp)   # 安全
    movq %rsi, -16(%rsp)  # 安全
    # ... 最多使用到 -128(%rsp)

局部变量
#

参考:https://akaedu.github.io/book/ch19s03.html

局部变量在栈上分配,使用 %rbp 相对寻址:

局部变量分配示例:

.globl function
function:
    pushq %rbp
    movq %rsp, %rbp

    # 分配局部变量空间
    subq $32, %rsp        # 分配32字节的局部变量空间

    # 初始化局部变量
    movq $0, -8(%rbp)     # 第1个8字节变量
    movq $0, -16(%rbp)    # 第2个8字节变量
    movl $0, -20(%rbp)    # 第3个4字节变量
    movb $0, -21(%rbp)    # 第4个1字节变量

    # 对齐到16字节
    andq $-16, %rsp

一般使用相对于 %rbp 寄存器的偏移来访问:

# 访问第一个局部变量(假设是8字节)
movq -8(%rbp), %rax   # 从 rbp-8 的位置读取

# 访问第二个局部变量
movq -16(%rbp), %rax  # 从 rbp-16 的位置读取

返回值
#

返回值约定:

  1. 整数/指针返回值:%rax
  2. 浮点数返回值: %xmm0
  3. 大型结构体(超过 16 Bytes): 通过栈或指针返回

示例:

# 返回两个值的函数
.globl get_two_values
get_two_values:
    movq $1, %rax        # 第一个返回值
    movq $2, %rdx        # 第二个返回值(约定)
    ret

小于等于16字节的结构体,返回值通过 rax:rdx 传递:

# struct SmallStruct { long a; long b; };
func:
    movq $1, %rax    # a
    movq $2, %rdx    # b
    ret

大于16字节的结构体,通过隐藏参数(指针)返回,caller 分配空间,地址作为第一个参数传入

# long calc(int a, int b)
.globl calc
calc:
    pushq %rbp
    movq %rsp, %rbp

    # 计算 a + b
    movl %edi, %eax   # 第一个参数
    addl %esi, %eax   # 加上第二个参数

    # 返回值已在 %eax 中
    movq %rbp, %rsp
    popq %rbp
    ret

# 返回结构体示例
# struct Point { int x, y; } get_point()
get_point:
    pushq %rbp
    movq %rsp, %rbp

    # 返回 {1, 2}
    movq $0, %rax     # 清零 rax
    movl $1, %eax     # 设置 x
    movl $2, %edx     # 设置 y

    movq %rbp, %rsp
    popq %rbp
    ret

示例
#

.globl example_function
example_function:
    # 函数序言
    pushq %rbp              # 保存旧的帧指针
    movq  %rsp, %rbp        # 设置新的帧指针
    subq  $16, %rsp         # 分配16字节的局部变量空间(不含后续保存的寄存器)

    # 保存被调用者保存的寄存器
    pushq %rbx
    pushq %r12

    # 函数主体
    # ... 函数代码 ...

    # 恢复寄存器
    popq %r12
    popq %rbx

    # 函数尾声
    movq %rbp, %rsp         # 恢复栈指针
    popq %rbp               # 恢复帧指针
    ret                     # 返回

递归函数示例(计算斐波那契数):

.globl fib
fib:
    # 函数序言
    pushq %rbp
    movq %rsp, %rbp
    pushq %rbx            # 保存被调用者保存的寄存器

    # 检查基础情况
    cmpq $2, %rdi
    jge .L2

    # n < 2 的情况,直接返回n
    movq %rdi, %rax
    jmp .L3

.L2:
    # 保存n
    movq %rdi, %rbx

    # 计算fib(n-1)
    decq %rdi
    call fib
    movq %rax, %r12      # 保存fib(n-1)的结果

    # 计算fib(n-2)
    movq %rbx, %rdi
    subq $2, %rdi
    call fib

    # 计算结果
    addq %r12, %rax

.L3:
    # 函数尾声
    popq %rbx
    movq %rbp, %rsp
    popq %rbp
    ret

带局部变量的函数示例:

.globl calculate
calculate:
    # 函数序言
    pushq %rbp
    movq %rsp, %rbp
    subq $16, %rsp        # 分配两个局部变量的栈空间

    # 保存通过寄存器传递的参数到栈局部变量
    movq %rdi, -8(%rbp)   # 第一个局部变量
    movq %rsi, -16(%rbp)  # 第二个局部变量

    # 计算过程
    movq -8(%rbp), %rax
    addq -16(%rbp), %rax  # 函数返回值保存在 rax 中

    # 函数尾声
    movq %rbp, %rsp
    popq %rbp
    ret # 此时 %rsp 指向的地址保存有函数返回地址

简单函数调用:

.section .text
.globl main
main:
    # 调用前的准备
    pushq %rbp          # 保存旧的帧指针
    movq %rsp, %rbp     # 设置新的帧指针

    call function       # 调用函数,此时返回地址被压入栈

    # 函数返回后继续执行
    movq %rbp, %rsp
    popq %rbp
    ret

function:
    pushq %rbp
    movq %rsp, %rbp

    # 函数体

    movq %rbp, %rsp
    popq %rbp
    ret                # 返回到调用点

带参数的函数调用:

# 函数调用示例:func(1, 2)
    movq $1, %rdi       # 第一个参数,前六个参数通过寄存器传参
    movq $2, %rsi       # 第二个参数
    call func           # 调用函数
    # 返回值在 %rax 中

func:
    pushq %rbp
    movq %rsp, %rbp

    # 使用 %rdi 和 %rsi 中的参数

    movq %rbp, %rsp
    popq %rbp
    ret

多层函数调用示例:

调用链:main -> func1 -> func2

栈的状态:
高地址
                   |     main参数     |
                   |------------------|
                   |   main返回地址   |
                   |     保存的rbp    |
                   |------------------|
                   |   func1返回地址  |
                   |     保存的rbp    |
                   |------------------|
                   |   func2返回地址  |
                   |     保存的rbp    |
            %rsp ->|                  |
低地址

多函数调用示例:

# 定义结构体 (仅作参考)
# struct LargeStruct {
#     long a[8];      # 64字节
#     int b;          # 4字节
# };
#
# struct Point {
#     int x, y;       # 8字节
# };
#
# 函数原型:
# int main();
# int func1(int a, char b, const char* str);
# long func2(int a, struct LargeStruct big, struct Point* p,
#           char c, double d, const char* str);

.section .data
str1:
    .string "Hello"
str2:
    .string "World"

.section .text
.globl main
main:
    # 函数序言
    pushq %rbp
    movq %rsp, %rbp
    subq $144, %rsp           # 分配栈空间,包括对齐和调用 func2 时压栈传递的参数

    # 保存被调用者保存的寄存器
    pushq %rbx
    pushq %r12
    pushq %r13

    # 为 func1 准备参数
    movl $42, %edi          # int a = 42
    movb $'A', %sil         # char b = 'A'
    leaq str1(%rip), %rdx   # const char* str = "Hello"

    # 调用 func1
    call func1
    movl %eax, %r12d        # 保存 func1 的返回值

    # 为 func2 准备参数
    # 首先在栈上构建 LargeStruct
    movq $1, -72(%rbp)      # big.a[0]
    movq $2, -64(%rbp)      # big.a[1]
    movq $3, -56(%rbp)      # big.a[2]
    movq $4, -48(%rbp)      # big.a[3]
    movq $5, -40(%rbp)      # big.a[4]
    movq $6, -32(%rbp)      # big.a[5]
    movq $7, -24(%rbp)      # big.a[6]
    movq $8, -16(%rbp)      # big.a[7]
    movl $9, -12(%rbp)      # big.b

    # 构建 Point 结构体
    subq $16, %rsp          # 为 Point 分配栈空间
    movl $10, (%rsp)        # p->x = 10
    movl $20, 4(%rsp)       # p->y = 20
    movq %rsp, %r13         # 保存 Point 指针

    # 准备 func2 的参数
    movl $100, %edi         # int a = 100

    # LargeStruct 通过引用传递(隐式)
    leaq -72(%rbp), %rsi    # struct LargeStruct*

    movq %r13, %rdx         # struct Point* p
    movb $'B', %cl          # char c = 'B'
    movsd .LC0(%rip), %xmm0 # double d = 3.14
    leaq str2(%rip), %r9    # const char* str = "World"

    # 调用 func2
    call func2

    # 恢复栈和寄存器
    addq $16, %rsp          # 清理 Point 结构体空间
    popq %r13
    popq %r12
    popq %rbx

    # 函数尾声
    movq %rbp, %rsp
    popq %rbp
    ret

# 第一个函数
.globl func1
func1:
    pushq %rbp
    movq %rsp, %rbp

    # 函数体 (简单示例)
    movl %edi, %eax        # 返回第一个参数

    movq %rbp, %rsp
    popq %rbp
    ret

# 第二个函数
.globl func2
func2:
    pushq %rbp
    movq %rsp, %rbp
    subq $16, %rsp         # 分配本地变量空间

    # 访问参数示例
    # %edi 已经包含 int a
    # %rsi 包含 LargeStruct 的指针
    # %rdx 包含 Point 的指针
    # %cl 包含 char c
    # %xmm0 包含 double d
    # %r9 包含 string 指针

    # 函数体 (简单示例)
    movq (%rdx), %rax      # 返回 Point 的 x 值

    movq %rbp, %rsp
    popq %rbp
    ret

.section .rodata
.LC0:
    .double 3.14159        # 双精度浮点常量

详细说明:

  1. 参数传递说明

    • func1 使用常规寄存器传递
    • func2 的参数较复杂:
      • int a 通过 %edi 传递
      • LargeStruct 通过引用传递(指针在 %rsi)
      • Point* 通过 %rdx 传递
      • char c 通过 %cl 传递
      • double d 通过 %xmm0 传递
      • const char* 通过 %r9 传递
  2. 结构体处理

    • LargeStruct 因为超过 64 字节,所以隐式通过引用传递
    • Point 结构体指针直接通过寄存器传递
  3. 栈帧管理

    • main 函数分配足够空间用于本地变量和临时结构体
    • 保持16字节对齐
    • 正确保存和恢复被调用者保存的寄存器
  4. 内存布局

    • 字符串常量在 .data 段
    • 浮点常量在 .rodata 段
    • 临时结构体在栈上构建
  5. 寄存器使用

    • 遵循 System V AMD64 ABI 调用约定
    • 正确处理参数传递
    • 适当保存和恢复寄存器
  6. 安全性考虑

    • 正确的栈对齐
    • 适当的栈空间分配
    • 寄存器的保存和恢复

调用过程图解:

  1. 初始状态(程序刚启动)
高地址
                   |-------------------|
                   |    环境变量等     |
                   |-------------------|
                   |    命令行参数     |
                   |-------------------|
                   |    返回地址       |
            %rsp ->|-------------------|
低地址
  1. main 函数开始
高地址
                   |-------------------|
                   |    返回地址       |
                   |-------------------|
                   |    旧 %rbp        |  <-- %rbp
                   |-------------------|
                   |     %rbx          |
                   |     %r12          |
                   |     %r13          |
                   |-------------------|
                   |                   |
                   |   LargeStruct     |
                   |    (72字节)       |
                   |                   |
                   |-------------------|
                   |     对齐填充      |
            %rsp ->|-------------------|
低地址
  1. 调用 func1 前的准备
                   |-------------------|
                   |    main栈帧       |
                   |-------------------|
                   |  参数1: %edi=42   |
                   |  参数2: %sil='A'  |
                   |  参数3: %rdx=str1 |
            %rsp ->|-------------------|
  1. func1 执行时
高地址
                   |-------------------|
                   |    main栈帧       |  # 包含通过 stack 为 fun1 传递的参数
                   |-------------------|
                   |    返回地址       |
                   |-------------------|
                   |    旧 %rbp        |  <-- %rbp
            %rsp ->|-------------------|
低地址

寄存器状态:
%edi = 42
%sil = 'A'
%rdx = str1的地址
  1. func1 返回后,准备调用 func2
高地址
                   |-------------------|
                   |    main栈帧      |
                   |-------------------|
                   |   LargeStruct    |
                   |    a[0] = 1      |
                   |    a[1] = 2      |
                   |    a[2] = 3      |
                   |    a[3] = 4      |
                   |    a[4] = 5      |
                   |    a[5] = 6      |
                   |    a[6] = 7      |
                   |    a[7] = 8      |
                   |    b = 9         |
                   |------------------|
                   |    Point struct  |
                   |    x = 10        |
                   |    y = 20        |
            %rsp ->|------------------|
低地址

寄存器状态:
%edi = 100                 # int a
%rsi = LargeStruct指针     # struct LargeStruct
%rdx = Point指针           # struct Point*
%cl = 'B'                 # char c
%xmm0 = 3.14159          # double d
%r9 = str2的地址          # const char*
  1. func2 执行时
高地址
                   |-------------------|
                   |    main栈帧       |
                   |-------------------|
                   |    返回地址       |
                   |-------------------|
                   |    旧 %rbp        |  <-- %rbp
                   |-------------------|
                   |   本地变量空间    |
            %rsp ->|   (16字节)        |
                   |-------------------|
低地址
  1. 完整的内存布局
高地址
                   |-------------------|
                   |    环境变量等     |
                   |-------------------|
                   |    命令行参数     |
                   |-------------------|
                   |  main返回地址     |
                   |-------------------|
                   |  main旧%rbp       |
                   |-------------------|
                   |    保存的寄存器   |
                   |     %rbx          |
                   |     %r12          |
                   |     %r13          |
                   |-------------------|
                   |   LargeStruct     |
                   |    (72字节)       |
                   |-------------------|
                   |   Point struct    |
                   |    (8字节)        |
                   |-------------------|
                   | func2返回地址     |
                   |-------------------|
                   | func2旧%rbp       |
                   |-------------------|
                   | func2本地变量     |
            %rsp ->|-------------------|
低地址

.section .data:
str1: "Hello\0"
str2: "World\0"

.section .rodata:
LC0: 3.14159 (双精度浮点数)
  1. 数据流图
main()
   |
   |---> func1(42, 'A', "Hello")
   |      返回值在 %eax
   |
   |---> func2(100, largeStruct, &point, 'B', 3.14, "World")
         返回值在 %rax

参数传递流程:
main -> func1:
%edi <-- 42
%sil <-- 'A'
%rdx <-- str1地址

main -> func2:
%edi <-- 100
%rsi <-- LargeStruct指针
%rdx <-- Point指针
%cl  <-- 'B'
%xmm0 <-- 3.14159
%r9  <-- str2地址

参考
#

  1. Linux Assembly HOWTO
  2. Programming from the Ground Up
  3. GNU Assembler Examples
  4. Architecture 1001: x86-64 Assembly

相关文章

GCC 编译器 - 个人参考手册
·8152 字
Gnu Cpp Gcc Manual
gcc 编译器个人参考手册。
链接器 ld
··6277 字
Gnu Gcc Ld
GCC 交叉编译工具链
·6210 字
Cgo Go Compile Gcc
交叉编译是指编译器能生成和它执行环境不同的 CPU 架构的二进制,例如在 arm64 机器上编译出在 x86_64 机器上运行的二进制。 本文分别以常用的 ubuntu aarch64 和 fedora 40 x86_64 编译环境为例,介绍这两个问题的解决方案。
使用 musl 交叉编译和静态链接
··2322 字
Cgo Go Compile Gcc Musl
本文介绍了使用轻量化 libc 库 musl 进行交叉编译生成多架构二进制的方案,最终实现在 x86_64 编译机器上能同时构建出静态链接的 x86_64 和 aarch64 二进制的目标,大大简化了多套构建脚本的开发和维护成本。