寄存器与数据传送
寄存器总览
x86-64 有 16 个通用 64 位寄存器。写入 32 位子寄存器(如 eax)会将完整寄存器的高 32 位零扩展;写入 16/8 位子寄存器则保持高位不变。
; General-purpose 64-bit registers (x86-64)
; rax, rbx, rcx, rdx — main accumulators / scratch
; rsi, rdi — source/destination index (string ops)
; rbp, rsp — base pointer / stack pointer
; r8 .. r15 — extended registers
; 32-bit: eax, 32-bit: ecx, 16-bit: ax, 8-bit: al/ah
mov rax, 42 ; load immediate into 64-bit register
mov eax, 0x1F ; 32-bit move (zero-extends to rax)mov — 数据传送
mov 在寄存器、内存和立即数之间复制数据。不允许内存到内存的传送——需经过寄存器。当操作数大小不明确时,务必指定大小(byte/word/dword/qword),例如 mov dword [rsp], 1。
mov rax, 60 ; immediate -> register
mov rbx, rax ; register -> register
mov qword [rsp-8], 7 ; immediate -> memory (qword = 8 bytes)
mov rcx, [rsp-8] ; memory -> register
; INVALID: mov [rsp-8], [rsp-16] (memory-to-memory not allowed)
; Use two steps: mov rax, [src]; mov [dst], raxlea — 加载有效地址
lea 计算内存操作数的地址但不访问内存。它常被用作快速算术指令(通过比例+索引实现乘以 5/3/9 等),也用于获取栈变量或数据标签的地址。
; Compute address without dereferencing
lea rax, [rbx + rcx*8 + 16] ; rax = rbx + rcx*8 + 16
; Common idiom: fast arithmetic (no memory access)
lea rax, [rax + rax*4] ; rax *= 5 (rax = rax + rax*4)
; Pointer into a buffer
lea rsi, [buffer] ; rsi = address of buffer
lea rdi, [rsp + 32] ; rdi = address of stack slotpush / pop — 栈操作
push 先将 rsp 减 8(64 位模式)再存入值;pop 先加载再将 rsp 加 8。在 System V AMD64 ABI 中,前 6 个整数参数放在 rdi、rsi、rdx、rcx、r8、r9 中——push 主要用于保存被调用者保存的寄存器(rbx、rbp、r12-r15)和临时溢出。
push rax ; rsp -= 8; [rsp] = rax
push qword 42 ; push immediate
pop rbx ; rbx = [rsp]; rsp += 8
; Save/restore callee-saved registers
push rbx
push r12
; ... function body ...
pop r12
pop rbx
ret
; Push arguments in reverse (C calling convention)
push 3
push 2
push 1xchg / xadd — 原子交换
带内存操作数的 xchg 总是原子的(隐式加锁)。xadd 结合了交换和加法。它们与 lock cmpxchg(比较并交换)一起构成无锁同步原语的基础。
; xchg swaps two operands (implicitly LOCKed with memory)
xchg rax, rbx ; swap register/register
xchg [counter], rcx ; swap memory/register (atomic)
; xadd: swap then add (returns old value in src)
; lock xadd [counter], rax ; atomic fetch-and-add
; Spinlock idiom
spin:
xor eax, eax
lock cmpxchg [lock_var], 1 ; if [lock_var]==0, set to 1
jnz spin ; retry if not acquiredmovzx / movsx — 零扩展/符号扩展
movzx 将较小的值零扩展到较大的寄存器(无符号加载)。movsx 进行符号扩展(有符号加载)。用 movsxd 将 32 位有符号值加载到 64 位寄存器;在 64 位模式下 32→64 位的 movsx 编码为 movsxd。
; movzx: zero-extend (unsigned)
movzx rax, byte [rsi] ; load byte, zero-extend to 64-bit
movzx eax, word [rdi] ; load word, zero-extend to 32-bit
; movsx: sign-extend (signed)
movsx rax, byte [rsi] ; sign-extend byte to 64-bit
movsx rax, dword [rdi] ; sign-extend 32-bit to 64-bit (movsxd)算术与逻辑
add / sub — 整数算术
add/sub 设置 CF(无符号进位/借位)、OF(有符号溢出)、SF(符号)、ZF(零)、PF(奇偶)和 AF(辅助进位)。用 jc/jo 检测无符号/有符号溢出。从 rsp 减去值可分配栈空间——按 ABI 要求调用前保持 rsp 16 字节对齐。
add rax, rbx ; rax = rax + rbx
add rax, 10 ; rax = rax + 10
sub rcx, rdx ; rcx = rcx - rdx
sub rsp, 32 ; allocate 32 bytes of stack (align!)
; 64-bit addition with carry check
add rax, rbx
jc .overflow ; jump if unsigned overflow (CF=1)imul / idiv — 乘法与除法
双操作数 imul 是常见形式——只保留低 64 位,编译器对 a*b 就生成它。全宽度乘法用单操作数 mul/imul(结果在 rdx:rax)。idiv 前需用 cqo 符号扩展被除数(32 位用 cdq);无符号 div 则先用 xor rdx,rdx 清零 rdx。
; Two-operand imul (most common): dst = dst * src
imul rax, rbx ; rax = rax * rbx (lower 64 bits)
imul rcx, 10 ; rcx = rcx * 10
; One-operand: rdx:rax = rax * src (full 128-bit)
mul rbx ; unsigned: rdx:rax = rax * rbx
imul rbx ; signed: rdx:rax = rax * rbx
; idiv: signed divide rdx:rax by src
; Must sign-extend rax into rdx:rax first!
cqo ; sign-extend rax -> rdx:rax
idiv rcx ; rax = rdx:rax / rcx, rdx = remainder
; Unsigned: use div (zero rdx first)
xor rdx, rdx
div rcx ; rax = rdx:rax / rcx, rdx = remainderinc / dec / neg
inc 和 dec 不更新进位标志(CF)——这使它们能在依赖 CF 的多精度加减法链中使用。它们会更新 ZF/SF/OF。neg 计算二进制补码取反(0 - dst),且除非操作数为 0 否则设置 CF=1。
inc rax ; rax++ (does NOT affect CF!)
dec rcx ; rcx-- (does NOT affect CF!)
neg rdx ; rdx = -rdx (two's complement negate)
; Common loop pattern
mov rcx, 10
.loop:
; ... loop body ...
dec rcx
jnz .loop ; repeat until rcx == 0and / or / xor / not / test
xor reg,reg 是将寄存器清零的习惯写法(比 mov reg,0 编码更短)。test a,a 等价于 and a,a 但丢弃结果——常用于在不修改操作数的情况下检查符号/零。and/or/xor 会清除 CF 和 OF。
and rax, 0xFF ; mask low byte (rax &= 0xFF)
or rcx, 0x10 ; set bit 4
xor rdx, rdx ; rdx = 0 (idiomatic zeroing)
xor rax, rax ; clear rax (shorter than mov rax,0)
not r8 ; bitwise NOT (one's complement)
test rax, rax ; set flags from rax & rax (checks zero/sign)
test rcx, 0x1 ; test if low bit set (odd/even check)shl / shr / sar — 移位
shl/shr 是逻辑移位(零填充);sar 保留符号位(算术移位)。通过移位实现乘除 2 的幂比 imul/idiv 快得多。移位计数在 32 位操作数时掩码为 5 位,64 位操作数时掩码为 6 位。
shl rax, 4 ; logical left shift (rax *= 16)
shr rax, 3 ; logical right shift (unsigned rax /= 8)
sar rax, 3 ; arithmetic right shift (signed rax /= 8)
; Rotate (carry not involved)
rol rax, 4 ; rotate left
ror rax, 4 ; rotate right
; Shift by CL (only low 5 bits used in 64-bit mode)
mov cl, 4
shl rax, cl ; shift left by 4控制流与分支
cmp / test — 比较
cmp 像减法一样设置标志但不存储结果。有符号比较用 je/jne/jl/jg/jle/jge;无符号用 jb/ja/jbe/jae(below/above)。混淆两者是经典 bug——jl 检查 SF!=OF 而 jb 只检查 CF。
cmp rax, rbx ; compute rax - rbx, set flags (discard result)
cmp rax, 10 ; compare with immediate
; Signed comparisons
cmp rax, rbx
je .equal ; jump if rax == rbx (ZF=1)
jl .less ; jump if rax < rbx signed (SF!=OF)
jg .greater ; jump if rax > rbx signed (ZF=0 and SF==OF)
jle .le ; jump if rax <= rbx signed
jge .ge ; jump if rax >= rbx signed
; Unsigned comparisons (use 'below'/'above' mnemonics)
cmp rax, rbx
jb .below ; rax < rbx unsigned (CF=1)
ja .above ; rax > rbx unsigned (CF=0 and ZF=0)
jbe .be ; rax <= rbx unsigned
jae .ae ; rax >= rbx unsignedjmp — 无条件跳转
jmp 执行无条件跳转。条件跳转(jcc)测试由 cmp/test/算术运算设置的标志。64 位模式下近条件跳转可达 ±2GB,因此「短跳转过远」的问题基本消失。通过寄存器/内存的间接跳转可实现 switch/case 跳转表。
; Direct jump to a label
jmp .end
; Conditional jumps (short/near, 64-bit allows near)
.loop:
dec rcx
jnz .loop ; jump if ZF=0 (rcx != 0)
; Indirect jump through register/memory (jump table)
lea rax, [table]
mov rdi, [rax + rbx*8]
jmp rdi ; jump to address in rdi
; Jump table example (switch statement)
table: dq .case0, .case1, .case2, .case3常用条件跳转
有许多同义词(je==jz,jb==jc==jnae)。有符号(l/g)与无符号(b/a)的区别至关重要:jl/jg 测试 SF 和 OF;jb/ja 测试 CF。比较有符号整数后用有符号形式,比较指针/无符号整数后用无符号形式。
; After cmp/test:
je / jz ; jump if equal / zero (ZF=1)
jne / jnz ; jump if not equal / nonzero (ZF=0)
; Signed
jl / jnge ; less (SF!=OF)
jge / jnl ; greater-or-equal (SF==OF)
jle / jng ; less-or-equal (ZF=1 or SF!=OF)
jg / jnle ; greater (ZF=0 and SF==OF)
; Unsigned
jb / jnae / jc ; below / carry (CF=1)
jae / jnb / jnc ; above-or-equal (CF=0)
jbe / jna ; below-or-equal (CF=1 or ZF=1)
ja / jnbe ; above (CF=0 and ZF=0)
; Special
js ; jump if sign (SF=1)
jns ; jump if not sign (SF=0)
jo ; jump if overflow (OF=1)
jno ; jump if no overflow (OF=0)loop 循环
loop 将 rcx 减 1,若 rcx!=0 则跳转。虽然方便,但在现代 CPU 上比 dec/jnz 对慢,因此编译器生成 dec/jnz。loope/loopz 额外要求 ZF=1;loopne/loopnz 要求 ZF=0。
; loop: dec rcx then jump if rcx != 0
mov rcx, 5
.loop:
; ... body executes 5 times ...
loop .loop
; loope/loopz: loop while equal/zero
; loopne/loopnz: loop while not equal/not zero
; Modern compilers prefer dec + jnz (loop is slower on many CPUs)
mov rcx, 5
.loop:
; ...
dec rcx
jnz .loopcmov — 条件传送
cmovcc 执行条件传送——无分支代码,可避免分支预测失败惩罚。目标必须是寄存器(非内存)。注意 cmov 总是评估两个源操作数,因此当某条路径有副作用或源内存访问可能出错时应避免使用。
; cmovcc: move only if condition is true (branchless)
cmp rax, rbx
cmovl rax, rbx ; if rax < rbx (signed), rax = rbx -> rax = min(a,b)
; max(a, b)
cmp rax, rbx
cmovl rax, rbx ; rax = max(rax, rbx)? No: if rax<rbx, set rax=rbx -> max
; Branchless abs:
; abs(x): mask = x >> 63; result = (x ^ mask) - mask
mov rax, rdi
sar rdi, 63 ; all 1s if negative, 0 if positive
xor rax, rdi
sub rax, rdi ; rax = |original rdi|栈、函数与调用约定
函数序言与结语
标准序言保存 rbp、将 rbp 设为帧指针,并通过从 rsp 减去来分配局部变量。leave 是一字节结语,等价于 mov rsp,rbp; pop rbp。System V AMD64 ABI 要求 rsp 在调用点 16 字节对齐(因此压入返回地址后偏移 8 字节)。
; System V AMD64 calling convention (Linux/macOS)
; Args: rdi, rsi, rdx, rcx, r8, r9 (then stack)
; Return: rax. Callee-saved: rbx, rbp, r12-r15
my_func:
push rbp
mov rbp, rsp ; standard prologue
sub rsp, 32 ; allocate locals (keep 16-byte aligned)
; ... function body ...
mov rsp, rbp ; or: leave
pop rbp ; standard epilogue
ret
; Compact form using leave
my_func2:
push rbp
mov rbp, rsp
sub rsp, 16
; ...
leave ; mov rsp,rbp; pop rbp
retcall / ret — 调用与返回
call 将返回地址(64 位下 8 字节)压入栈并跳转到目标;ret 将其弹回 rip。尾调用用 jmp,这样被调用者直接返回到你的调用者——这保留了栈并启用尾调用优化。
; call pushes return address, then jumps
call my_func
; ret pops return address into rip
my_func:
; ...
ret
; Call with arguments (System V AMD64)
mov rdi, 1 ; 1st arg
mov rsi, 2 ; 2nd arg
mov rdx, 3 ; 3rd arg
call add_three ; result in rax
; Tail call: jmp instead of call+ret
my_wrapper:
jmp target_func ; reuses our return address叶子函数与红区
System V AMD64 ABI 在 rsp 下方保留了 128 字节的「红区」,叶子函数(不调用其他函数的函数)可以无需调整 rsp 就使用它。Windows x64 没有红区。叶子函数通常完全不需要序言。
; Leaf function (no calls) can use the red zone:
; 128 bytes below rsp that won't be clobbered by signals/interrupts
leaf_sqrt_sum:
; rdi, rsi = args, rax = result
mov rax, rdi
add rax, rsi
ret ; no prologue needed!
; Non-leaf functions MUST save rsp properly because
; a call would write into the red zone.
; Red zone is NOT honored on Windows x64.局部变量与栈帧
有帧指针(rbp)时,局部变量通过 [rbp - offset] 访问,传入的栈参数通过 [rbp + offset] 访问。编译器常省略帧指针(-fomit-frame-pointer)并直接从 rsp 引用局部变量,释放 rbp 作为通用寄存器。
my_func:
push rbp
mov rbp, rsp
sub rsp, 32 ; 32 bytes for locals
; Local variables accessed via [rbp - offset]
mov qword [rbp-8], 10 ; local1
mov qword [rbp-16], 20 ; local2
; Read arguments (also via rbp once saved on stack,
; or directly from registers)
mov rax, [rbp-8]
add rax, [rbp-16]
leave
ret保存被调用者保存的寄存器
被调用者保存寄存器(rbx、rbp、r12-r15)必须在函数中保持——入口处 push,出口处逆序 pop。调用者保存寄存器(rax、rcx、rdx、rsi、rdi、r8-r11)可自由使用,但若需要跨调用保存其值则须自行保存。
; rbx, rbp, r12, r13, r14, r15 are callee-saved
; rax, rcx, rdx, rsi, rdi, r8-r11 are caller-saved
my_func:
push rbx ; we want to use rbx
push r12 ; and r12
mov rbx, rdi ; use them
mov r12, rsi
; ... do work, may call other functions ...
; (those calls will preserve rbx/r12 for us)
mov rax, rbx ; prepare return value
pop r12 ; restore in REVERSE order
pop rbx
ret系统调用与 Hello World
syscall — Linux 系统调用
Linux x86-64 上,系统调用用 syscall 指令,编号在 rax 中,最多 6 个参数在 rdi、rsi、rdx、r10、r8、r9 中(注意第 4 个参数是 r10 而非 rcx)。常见编号:write=1、read=0、exit=60、mmap=9。syscall 会破坏 rcx 和 r11。macOS 使用不同编号(如 write=0x2000004)。
; Linux x86-64 syscall convention:
; rax = syscall number
; rdi, rsi, rdx, r10, r8, r9 = args (NOTE: r10 not rcx!)
; return value in rax; clobbers rcx and r11
; write(1, msg, 12)
mov rax, 1 ; syscall: write
mov rdi, 1 ; fd = stdout
lea rsi, [msg] ; buf
mov rdx, 12 ; count
syscall
; exit(0)
mov rax, 60 ; syscall: exit
xor rdi, rdi ; status = 0
syscall
section .data
msg: db "hello world", 10 ; 12 bytes with newline完整 Hello World(NASM, Linux)
独立的 hello world 不链接任何 libc——直接通过 syscall 调用内核。$ - msg 在汇编时计算长度($ 符号是当前地址)。_start 是 ld 的默认入口点。程序以 exit 系统调用结束;从 _start 返回是未定义行为。
; nasm -f elf64 hello.asm && ld hello.o -o hello && ./hello
section .data
msg: db "Hello, World!", 10
msg_len: equ $ - msg ; length computed at assemble time
section .text
global _start
_start:
; write(1, msg, msg_len)
mov rax, 1 ; write
mov rdi, 1 ; stdout
mov rsi, msg
mov rdx, msg_len
syscall
; exit(0)
mov rax, 60
xor rdi, rdi
syscall读取命令行 argc / argv
进入 _start 时(Linux,无 libc),内核将 argc 放在 [rsp],然后是 argv 指针、envp、最后是 NULL。如果链接 libc 并使用 main,C 运行时会为你解析这些——main 改为在 rdi/rsi 中接收 argc 和 argv。
; At _start the stack looks like:
; [rsp] = argc
; [rsp+8] = argv[0]
; [rsp+16] = argv[1]
; ...
; [rsp + 8*(argc+1)] = NULL
_start:
mov rdi, [rsp] ; argc
mov rsi, [rsp+8] ; argv[0] (program name)
mov rdx, [rsp+16] ; argv[1] (first user arg)
; Loop over argv
mov rbx, rsp
add rbx, 8 ; skip argc, point at argv[0]
.loop:
mov rax, [rbx]
test rax, rax
jz .done ; NULL terminator
; rax -> one argv string
add rbx, 8
jmp .loop
.done:
; exit
mov rax, 60
xor rdi, rdi
syscall调用 C 库函数
调用 C 函数时,在 call 之前将 rsp 对齐到 16 字节(call 压入 8 字节,使其偏移 8)。对于 printf 等变参函数,rax 必须存放向量(XMM)寄存器参数的数量——纯整数调用时设为 0。使用 main 作为入口点,以便 C 运行时设置 libc 并在退出时调用析构函数。
; Link with gcc: nasm -f elf64 demo.asm && gcc demo.o -o demo -no-pie
extern printf
section .data
fmt: db "sum = %d", 10, 0
section .text
global main
main:
push rbp
mov rbp, rsp
sub rsp, 16 ; keep stack 16-byte aligned
; printf("sum = %d\n", 42)
; Variadic: rdi=fmt, rsi=arg, rax=#vector regs (0)
lea rdi, [fmt]
mov rsi, 42
xor rax, rax ; 0 floating-point args
call printf
xor rax, rax ; return 0
leave
ret相关 Assembly 代码片段
Copy-paste ready code for common tasks.
通过 Linux 系统调用实现 Hello World
仅使用内核系统调用打印并退出的独立 x86-64 程序 。
函数调用约定 (System V AMD64)
在寄存器中传递参数,保留被调用者保存的寄存器,保持 rsp 16 字节对齐。
循环求和 (1..N)
使用 dec/jnz 的计数循环对 1..N 求和。
strlen — 扫描至 NUL
通过扫描内存直到零字节来计算 C 字符串长度。
memcpy — 用 rep movsb 复制
使用 rep movsb 字符串指令实现紧凑的内存复制。
位操作:popcount、ctz、abs
使用 BMI/ABM 指令实现无分支位操作。
通过系统调用读取文件
仅使用 Linux 系统调用 open/read/write/close 文件。
递归阶乘
用正确的栈帧递归实现 factorial(n)。
这篇内容对您有帮助吗?