计算系统安全速成之汇编基础【1】

文章目录

  • 说明
  • [一 汇编基础知识](#一 汇编基础知识)
    • [1.1 IA32](#1.1 IA32)
    • [1.2 程序员可见状态](#1.2 程序员可见状态)
    • [1.3 汇编与机器代码定义](#1.3 汇编与机器代码定义)
    • [1.4 X86-64整数寄存器](#1.4 X86-64整数寄存器)
    • [1.5 数据移动指令(movq)[重点]](#1.5 数据移动指令(movq)[重点])
    • [1.6 简单的内存寻址模式[重点]](#1.6 简单的内存寻址模式[重点])
    • [1.7 swap函数示例分析[重点]](#1.7 swap函数示例分析[重点])
    • [1.8 完整内存寻址模型[重点]](#1.8 完整内存寻址模型[重点])
  • [二 算术与逻辑运算](#二 算术与逻辑运算)
    • [2.1 地址计算指令leaq](#2.1 地址计算指令leaq)
    • [2.2 单、双操作数指令](#2.2 单、双操作数指令)
    • [2.3 算术运算案例arith](#2.3 算术运算案例arith)
  • [三 汇编语言和机器码](#三 汇编语言和机器码)
    • [3.1 C代码到目标代码的转换过程](#3.1 C代码到目标代码的转换过程)
    • [3.2 汇编语言特性:数据类型](#3.2 汇编语言特性:数据类型)
    • [3.3 汇编语言特性:操作类型](#3.3 汇编语言特性:操作类型)
    • [3.4 处理器状态与条件码](#3.4 处理器状态与条件码)
    • [3.5 jx跳转指令](#3.5 jx跳转指令)
    • [3.6 while循环编译](#3.6 while循环编译)

说明

  • 庄老师的课堂,如春风拂面,启迪心智。然学生愚钝,于课上未能尽领其妙,心中常怀惭愧。
  • 幸有课件为引,得以于课后静心求索。勤能补拙,笨鸟先飞,一番沉浸钻研,方窥见知识殿堂之幽深与壮美,竟觉趣味盎然。
  • 今将此间心得与笔记整理成篇,公之于众,权作抛砖引玉。诚盼诸位学友不吝赐教,一同切磋琢磨,于学海中结伴同行。
  • 资料地址:computing-system-security

一 汇编基础知识

  • 复杂指令集计算机 (CISC):Complex Instruction Set Computer
  • 精简指令集计算机 (RISC):Reduced Instruction Set Computer

1.1 IA32

  • IA32是Intel于1985年推出的32位x86处理器架构,也称为i386x86-32。它是从早期16位8086处理器发展而来的第一个32位版本,支持"平坦寻址"模式,能够运行Unix等现代操作系统。

    • IA32是传统x86架构的32位阶段,引入了32位寄存器(如%eax、%ebx)和32位地址空间。
    • 它具备复杂的指令集(CISC),但在Linux程序中仅使用其中一小部分。
    • 2004年,Intel推出了EM64T技术(几乎与AMD的x86-64相同),将IA32扩展为支持64位操作。
  • 简言之,IA32是一种32位x86指令集架构,曾广泛用于PC和服务器,现已被x86-64取代,但仍兼容运行大量32位代码

1.2 程序员可见状态

  • 程序计数器(PC):指向下一条指令地址,x86-64中称为RIP。
  • 寄存器文件:存放频繁使用的数据。
  • 状态码:记录最近算术或逻辑操作的状态,用于条件跳转。
  • 内存:字节可寻址数组,包含代码、用户数据和栈 (支持过程调用)。

1.3 汇编与机器代码定义

  • 架构(ISA):编写正确机器/汇编代码所需理解的部分,如指令集、寄存器机器码:处理器执行的字节级程序。
  • 汇编代码:机器码的文本表示形式。
  • 微架构:架构的具体实现,如缓存大小、核心频率。
  • 常见ISA包括Intel的x86、IA32、Itanium、x86-64,ARM (移动设备主流),以及开源RISC-V。

1.4 X86-64整数寄存器

  • x86-64 架构下的整数寄存器(CPU 内部用于临时存储整数、地址等数据的高速空间)
  • 可以从寄存器的位宽、分类、特殊作用三个角度理解:
  1. 寄存器的"位宽"与"命名规则" :x86-64 是 64 位 CPU 架构,这些寄存器既可以存 64 位数据,也能兼容 32 位程序(访问"低 32 位")。
    • 左列:64 位寄存器的名称,格式为 %rxx(如 %rax%r8)。
    • 右列:对应寄存器的 32 位版本(仅访问"低 32 位")
  • 格式分两种:
    • 传统寄存器(如 %rax)的 32 位版本是 %eax("e"表示 32 位时代的命名延续);
    • 新增寄存器(如 %r8)的 32 位版本是 %r8d("d"表示 32 位双字,x86-64 新增寄存器的 32 位命名规则)。

  1. 寄存器的"分类":寄存器分为传统继承型和x86-64 新增型。
    • 传统继承型(前 8 个:rax、rbx、rcx、rdx、rsi、rdi、rsp、rbp)。这些寄存器在 32 位 x86 时代(如老式 Windows XP、32 位 Linux)就存在,当时是 32 位寄存器(如 %eax%ebx)。x86-64 时代,它们被扩展为 64 位,名字前加 r(如 %rax%eax 的 64 位扩展)。
    • x86-64 新增型(r8 ~ r15):64 位架构新增的 8 个通用寄存器,32 位时代没有它们,因此 64 位命名为 %r8~%r15,32 位命名为 %r8d~%r15d
  2. %rsp%esp :这两个寄存器背景标红,因为它们是栈指针寄存器,负责管理程序的栈空间(函数调用时存储返回地址、局部变量等临时数据的区域):
    • %rsp 是 64 位栈指针,64 位程序中用它指向"栈顶";
    • %esp 是 32 位栈指针,32 位程序中用它指向"栈顶"。

"压栈(push)"或"弹栈(pop)"指令会自动修改栈指针,让数据有序地存入/取出栈。


  • 🧩 X86-64通用寄存器速查表
寄存器 全称 核心作用 位宽兼容(低32/16/8位) 调用约定规则
RAX Accumulator Register 函数返回值存储,算术运算(乘除指令默认使用) EAX / AX / AL/AH 无需保存(调用者保存)
RBX Base Register 基址寻址,数据存储(被调用者需备份) EBX / BX / BL/BH 需保存(被调用者保存)
RCX Counter Register 循环计数(如LOOP指令),函数第4参数 ECX / CX / CL/CH 无需保存(调用者保存)
RDX Data Register I/O操作,乘除结果扩展,函数第3参数 EDX / DX / DL/DH 无需保存(调用者保存)
RSI Source Index 字符串操作源地址,函数第2参数 ESI / SI / SIL 无需保存(调用者保存)
RDI Destination Index 字符串操作目的地址,函数第1参数 EDI / DI / DIL 无需保存(调用者保存)
RBP Base Pointer 栈帧基址指针,访问局部变量 EBP / BP / BPL 需保存(被调用者保存)
RSP Stack Pointer 栈顶指针,管理函数调用栈 ESP / SP / SPL 需保存(被调用者保存)
R8-R15 - 新增通用寄存器,函数第5-6参数(R8/R9) R8D-R15D / R8W-R15W / R8B-R15B R12-R15需保存,R8-R11无需保存
  • x86-64架构前6个参数寄存器顺序为rdi、rsi、rdx、rcx、r8、r9,超过6个的参数从右向左入栈

补充说明:表格中"调用约定规则"指函数调用时寄存器值是否需要被调用者备份,违反可能导致数据错误。


  • x86-64寄存器通过命名后缀区分访问位宽(64/32/16/8位),仅支持低位分层访问 ,高位不可独立操作。如 不可直接访问高位 (如%rax的高32位)。
  • 传统IA32寄存器(如%ah%bh)支持高8位访问(如%ah%ax的高8位),但x86-64中此类设计已被淘汰,仅保留低位的分层访问。

  • IA32寄存器结构与分类:IA32(32位x86架构)寄存器分为通用寄存器和专用寄存器,并保留了对16位和8位模式的向后兼容性。
  • 后缀含义
    • 无后缀 (如%rax):64位完整寄存器。
    • d (如%r15d%eax):低32位(double word)。
    • w (如%ax):低16位(word)。
    • b (如%al):低8位(byte)。
  • 命名层级示例%rax(64位)→ %eax(低32位)→ %ax(低16位)→ %ah(高8位)[IA32支持 x86-64不支持]→ %al(低8位)。
  • 注:%r15等新寄存器无%ah类高8位设计。

  1. 通用寄存器(General Purpose) :共8个,每个32位寄存器可拆分为16位和8位子寄存器(部分),右侧标注了其历史起源(多已过时):
32位寄存器 16位子寄存器 8位高字节 8位低字节 历史起源(Origin)
%eax %ax %ah %al 累加器(accumulate)
%ecx %cx %ch %cl 计数器(counter)
%edx %dx %dh %dl 数据寄存器(data)
%ebx %bx %bh %bl 基址寄存器(base)
%esi %si - - 源变址寄存器(source index)
%edi %di - - 目的变址寄存器(destination index)

  1. 专用寄存器(Special Purpose) :2个,用于栈操作,整体标红突出。
32位寄存器 16位子寄存器 8位子寄存器 功能
%esp %sp - 栈指针(stack pointer)
%ebp %bp - 基址指针(base pointer)

  1. 兼容性说明
  • 16位虚拟寄存器:32位寄存器的低16位(如%eax%ax),用于兼容16位模式。
  • 8位子寄存器:仅%eax/%ax%ecx/%cx%edx/%dx%ebx/%bx支持拆分(如%ax拆分为%ah%al),其他寄存器无8位子寄存器。

  • IA32寄存器设计兼顾了向后兼容性(支持16位/8位操作),但现代编程中更常用32位寄存器(如%eax%ebx等)。
  • %esp%ebp为栈操作核心,%esp指向栈顶,%ebp常用于函数调用时保存栈帧基址。

1.5 数据移动指令(movq)[重点]

  • movq基础语法:movq Source, Dest
  • 操作类型可以分为立即数、寄存器、内存。
    • 立即数:通常为整数数据,以$开头,如$Ox400$-520,通常为1、2、4字节编码。
    • 寄存器:通常为16个整数寄存器之一,如%rax%r13,但%rsp为栈指针寄存器,不可操作。
    • 内存:由寄存器给出地址的8个连续字节,如 (%rax)。
  • Intel文档中 mov 指令为 dest, src, 而AT&T语法为 src, dest
  • movq请使用的的两大禁忌:不允许内存到内存的直接传输,目标不可以是立即数。

1.6 简单的内存寻址模式[重点]

  • 直接寄存器寻址®:内存地址计算为Mem[Reg[R]],先从寄存器 R 中读取值(Reg[R]),再把这个值作为内存地址,最终访问该地址对应的内存单元(Mem[地址])。如movq (%rcx),%rax实现C语言中的指针解引用。

    bash 复制代码
    movq (%rcx), %rax
    • 把寄存器 rcx 中的值作为内存地址,读取该地址处的8字节数据(movq 是"传送四字",对应 64 位),并将数据写入寄存器 rax。
  • 位移寻址 D®:内存地址计算Mem[Reg[R]+D],R为基址寄存器,D为常量偏移量。含义:先从寄存器 R 中读取值(Reg[R],可理解为"基地址"),再加上一个常数位移量 D,最终把 Reg[R]+D 作为内存地址,访问对应内存单元。

    bash 复制代码
    movq 8(%rbp), %rdx
    • 把寄存器 rbp 中的值(基地址)加上 8(位移量 D),得到最终内存地址,读取该地址处的8字节数据,并写入寄存器 rdx。

1.7 swap函数示例分析[重点]

  • x86-64架构前6个参数寄存器顺序为rdi、rsi、rdx、rcx、r8、r9,超过6个的参数从右向左入栈。



1.8 完整内存寻址模型[重点]

  • 通用形式D(Rb,Ri,S): M e m [ R e g [ R b ] + S ∗ R e g [ R i ] + D ] Mem[Reg[Rb] + S*Reg[Ri] + D] Mem[Reg[Rb]+S∗Reg[Ri]+D]
    • D:常量位移 (1、2 或 4 字节)
    • Rb:基址寄存器 (任意 16个整数寄存器之一)
    • Ri:索引|寄存器(不能是%rsp)
    • S:比例因子(1、2、4、8),用于数组访问优化
  • 特殊情况:
    • (Rb,Ri)= M e m [ R e g [ R b ] + R e g [ R i ] ] Mem[Reg[Rb] + Reg[Ri]] Mem[Reg[Rb]+Reg[Ri]]
    • D(Rb,Ri)= M e m [ R e g [ R b ] + R e g [ R i ] + D ] Mem[Reg[Rb] + Reg[Ri] + D] Mem[Reg[Rb]+Reg[Ri]+D]
    • (Rb,Ri,S)= M e m [ R e g [ R b ] + S ∗ R e g [ R i ] ] Mem[Reg[Rb] + S*Reg[Ri]] Mem[Reg[Rb]+S∗Reg[Ri]]

二 算术与逻辑运算

2.1 地址计算指令leaq

  • leaq Src,Dst:将Src表达式的地址加载到Dst寄存器。
  • leaq指令不进行内存访问,仅计算地址,常用于实现p=&x[i]类型的指针操作。可以高效计算 x + k × y ( k = 1 , 2 , 4 , 8 ) x+k \times y(k=1,2,4,8) x+k×y(k=1,2,4,8)的算术表达式。
  • 例如:将一个数乘以12,编译后使用leaq (%rdi,%rdi,2),%rax 实现 × + 2*× = 3x,再通过salg $2,%rax左移两位 (乘以 4)得到12x。
c 复制代码
long multiple12(long x)
{
  return x*12;
}
c 复制代码
leaq (%rdi,%rdi,2), %rax  # t = x+2*x
salq $2, %rax             # return t<<2

2.2 单、双操作数指令

  • 操作数指令
指令格式 操作描述 备注
addq Src,Dest Dest = Dest + Src 加法运算
subq Src,Dest Dest = Dest - Src 减法运算
imulq Src,Dest Dest = Dest * Src 有符号乘法
shlq/salq Src,Dest Dest = Dest << Src 逻辑左移/算术左移(等价)
sarq Src,Dest Dest = Dest >> Src 算术右移(符号位填充)
shrq Src,Dest Dest = Dest >> Src 逻辑右移(零填充)
xorq Src,Dest Dest = Dest ^ Src 按位异或
andq Src,Dest Dest = Dest & Src 按位与
orq Src,Dest `Dest = Dest Src`
incg Dest Dest= Dest+ 1 自增
decg Dest: Dest= Dest - 1 自减
negq Dest Dest =-Dest 取负值
notg Dest Dest =~Dest 按位取反
  1. shlqsalq 在 x86-64 中功能完全相同,均表示左移。
  2. sarqshrq 的区别在于右移时的填充方式(算术右移保留符号位,逻辑右移补零)。
  3. 所有指令均以 q 后缀表示 64 位操作数。
  4. 无符号与有符号区别:整数运算不区分有符号与无符号类型,标志位和后续条件跳转决定语义差异。

2.3 算术运算案例arith

c 复制代码
// C 源码 
long arith(long x, long y, long z) {
    long t1 = x + y;           // t1 = x + y
    long t2 = z + t1;          // t2 = z + (x + y)
    long t3 = x + 4;           // t3 = x + 4
    long t4 = y * 48;          // t4 = y * 48
    long t5 = t3 + t4;         // t5 = (x + 4) + (y * 48)
    long rval = t2 * t5;       // 返回值 = t2 * t5
    return rval;
}
c 复制代码
x in %rdi, y in %rsi, z in %rdx
arith:
    leaq    (%rdi,%rsi), %rax   # t1 <- x + y 
    addq    %rdx, %rax          # t2 <- z + t1 = z + x + y
    leaq    (%rsi,%rsi,2), %rdx # tmp <- y + y*2 = 3y
    salq    $4, %rdx            # t4 <- 3y << 4 = 3y * 16 = y * 48
    leaq    4(%rdi,%rdx), %rcx  # t5 <- 4 + x + t4 = x + 4 + y*48
    imulq   %rcx, %rax          # rval <- t2 * t5
    ret

参数 寄存器
x %rdi
y %rsi
z %rdx

返回值通过 %rax 返回。


  • 以"数据流"为主线,逐步追踪变量是如何被计算和复用的。

✅ 第一步:计算 t1 = x + yt2 = z + t1

c 复制代码
long t1 = x+y;
long t2 = z+t1;

对应汇编:

asm 复制代码
leaq    (%rdi,%rsi), %rax   # t1 -> %rax
addq    %rdx, %rax          # t2 = z + t1 -> %rax
  • leaq (A,B) 是取地址形式,但常用于高效计算 A + B
  • leaq (%rdi,%rsi), %rax 等价于 %rax = %rdi + %rsi → 即 x + y
  • 接着 addq %rdx, %raxz 加到 %rax 上 → 得到 t2 = x + y + z

📌 注意:t1 并没有单独保存,而是直接合并进 t2 的计算中,这是编译器的常见优化(消除中间临时变量)。


✅ 第二步:计算 t4 = y * 48

c 复制代码
long t4 = y * 48;

对应汇编:

asm 复制代码
leaq    (%rsi,%rsi,2), %rdx   # %rdx = y + 2*y = 3y
salq    $4, %rdx              # %rdx <<= 4 → 3y * 16 = 48y 
  • 乘法 *48 被分解为:48 = 3 × 16 = (1 + 2) × 2^4
  • 所以,先用 leaq (%rsi, %rsi, 2) 计算 y + 2*y = 3y(利用寻址模式实现加法),再左移 4 位:salq $4, %rdx → 相当于 ×16,最终得到 3y × 16 = 48y

⚡ 为何不用 imulq $48, %rsi, %rdx

因为 leaq + shift 通常比直接乘法更快,尤其是当乘数是"可分解为移位+加法"的常数时,编译器会自动选择更优路径。


✅ 第三步:计算 t5 = t3 + t4 = (x + 4) + (y * 48)

c 复制代码
long t3 = x + 4;
long t5 = t3 + t4;

对应汇编:

asm 复制代码
leaq    4(%rdi,%rdx), %rcx   # %rcx = 4 + x + t4

这是一条极其高效的复合寻址指令:

  • leaq 4(%rdi,%rdx), %rcx 表示:%rcx = 4 + %rdi + %rdx
  • 此时:%rdi = x%rdx = t4 = y * 48
  • 所以结果就是:t5 = x + 4 + y*48

💡 编译器再次合并了两个操作(x+4+t4)为一条 leaq 指令 ------ 这正是 x86 中 lea 指令的强大之处:可用于任意形如 a + b + c 的算术运算。


✅ 第四步:最终结果 rval = t2 * t5

c 复制代码
long rval = t2 * t5;
return rval;

对应汇编:

asm 复制代码
imulq   %rcx, %rax     # %rax = %rax * %rcx
ret

此时:

  • %rax 中仍保存着 t2 = x + y + z
  • %rcx 刚刚计算出 t5 = x + 4 + 48y
  • imulq %rcx, %rax 表示:%rax *= %rcx → 结果自动留在 %rax(即返回值寄存器)

最后 ret 返回。


🔹 手动模拟执行示例

假设输入:x=1, y=2, z=3

步骤 含义
t1 = x + y 1 + 2 3
t2 = z + t1 3 + 3 6
t4 = y * 48 2 * 48 96
t5 = x + 4 + t4 1 + 4 + 96 101
rval = t2 * t5 6 * 101 606

验证汇编结果是否一致?

asm 复制代码
%rdi=1, %rsi=2, %rdx=3 
leaq (%rdi,%rsi), %rax  → %rax = 1+2 = 3
addq %rdx, %rax         → %rax = 3+3 = 6
leaq (%rsi,%rsi,2), %rdx → %rdx = 2 + 4 = 6
salq $4, %rdx           → %rdx = 6 << 4 = 96
leaq 4(%rdi,%rdx), %rcx → %rcx = 4 + 1 + 96 = 101
imulq %rcx, %rax        → %rax = 6 * 101 = 606
ret                     → 返回 606 ✅

✅ 完全吻合!


🔹 为什么 leaq 能做算术运算?

  • leaq(Load Effective Address Quad-word)本来是用来计算内存地址的,但它不访问内存,只做地址计算,因此可以安全用于一般整数加法运算。例如:
asm 复制代码
leaq (%rax, %rbx, 4), %rcx   # rcx = rax + rbx*4

这种结构广泛用于实现指针运算、数组索引、以及上述的快速乘法/加法合成。

因为它不修改标志位(不影响 CF/ZF 等),且通常比 add/imul 更快或可与其他指令并行执行。

三 汇编语言和机器码

3.1 C代码到目标代码的转换过程

  • 源文件 p1.c p2.c 经 gcc -Og 编译生成可执行文件 p。
  • 编译流程:C 程序 → 汇编程序(.s)→ 目标程序(.o)→ 可执行程序(p)。
    • 编译器:gcc -Og -S
    • 汇编器:gcc 或 as
    • 链接器:gcc 或 ld
    • 静态库(.a 文件)参与链接

3.2 汇编语言特性:数据类型

  • 整形数据包含1、2、4、8字节类型,可以表示值或地址(无类型指针)。
  • SIMD向量是指单指令多数据(Single Instruction, Multiple Data)的向量数据类型,其大小为8、16、32或64字节。它允许处理器通过一条指令同时对多个数据元素执行相同的操作,常用于加速多媒体处理、科学计算和机器学习等领域的并行计算任务。
  • 代码:编码一系列指令的字节序列。
  • 无聚合类型如数据、结构体,仅仅表现为连续的字节块。
  • "无聚合类型"指的是在汇编语言中不直接支持数组或结构体这样的聚合数据类型,所有的数据都被视为连续分配的字节序列。换句话说,像数组或结构体这类复杂的数据组织形式在汇编层面并不存在,而是通过内存中连续的字节来间接实现。

3.3 汇编语言特性:操作类型

  • 数据传输:支持内存和寄存器之间移动数据,从内存加载到寄存器(load),从寄存器存储到内存(store)。
  • 算术运算:在寄存器上火内存数据上执行算术函数。

3.4 处理器状态与条件码

  • 核心组件包括通用寄存器、栈指针、指令指针、条件码标志位。
  • 栈指针:%rsp指向当前栈顶。
  • 指令指针:%rip指向下一条指令地址。
  • 条件码标志位:CF、ZF、SF、OF。
    • CF(进位标志):无符号运算溢出时置位。
    • ZF(零标志):结果为0时置位。
    • SF(符号标志):结果为负(最高位为1)时置位。
    • OF(溢出标志):有符号运算溢出时置位。
  • 注意:leaq不影响条件码

  • 显式设置:比较指令(cmpq) cmpq Src2,Src1→类似计算 Src1- Src2 但不保存结果
    • CF:若减法需借位则置位(用于无符号比较)。
    • ZF:当Src1 ==Src2 时置位
    • SF:当(Src1-Src2)<0时置位。
    • OF:检测有符号减法溢出。
  • 显式设置:测试指令 (testq),testq Src2,Src1→执行 Src1& Src2但不保存结果
    • ZF:当按位与结果为0时置位
    • SF:当按位与结果为负时置位
    • 常见用法:testq%rax,%rax→检查%rax是否为0或负数。

  • setX Dest:根据条件码的组合将目标Dest的低阶字节设置为0或1,不会更改 Dest 的其余 7 个字节。

  • Setx指令特性:根据条件码组合设置单个字节,目标必须是可寻址的字节寄存器。不修改其余字节内容,通常配合movzbl指令使用以清零高位,32位操作自动将高位设为0。
  • 基础知识(回顾)
  1. 寄存器(Register):CPU 内部的高速存储单元,比内存快得多。图中涉及的是 x86-64 架构的寄存器(64位系统):
  • %rdi%rsi:函数参数寄存器(x86-64 调用约定规定,前几个参数通过寄存器传递,而非栈)。
  • %rax:返回值寄存器(函数的返回值必须放在这里)。
  • %al%rax 的低 8 位(8位寄存器),%eax%rax 的低 32 位(32位寄存器)。

  1. 函数调用约定:C 语言函数在 x86-64 系统中遵循 System V AMD64 ABI 约定------前 6 个参数依次用寄存器 %rdi%rsi%rdx%rcx%r8%r9 传递;返回值放在 %rax 中。 图中函数 gt(long x, long y) 的参数 x%rdi 传递,y%rsi 传递,符合这个规则。

  • 解释代码
c 复制代码
int gt(long x, long y) { 
    return x > y;  // 返回 1(真)或 0(假) 
} 

目标:比较 xy,如果 x > y 返回 1,否则返回 0。

汇编代码详解

  1. 第一行汇编代码
c 复制代码
cmpq %rsi, %rdi  # Compare x:y
  • 指令含义:比较两个 64 位整数(q 表示 quadword,64位)。
  • 操作数:%rsiy%rdixcmp 指令格式是 cmp 源, 目的,实际计算 目的 - 源(即 x - y),但不保存结果,只影响标志寄存器(如 ZF 零标志、SF 符号标志、OF 溢出标志等)。
  • 作用:通过 x - y 的结果设置标志位,后续指令根据标志位判断 x > y 是否成立。
  1. 第二行汇编代码
c 复制代码
setg %al  # Set when >
  • 指令含义:setg 是"Set if Greater"的缩写,即当 x > y 时,将目标寄存器设为 1,否则设为 0。
  • 目标寄存器:%al%rax 的低 8 位)。因为 set 类指令只能操作 8 位寄存器(如 %al%bl 等),不能直接操作 %rax
  • 判断依据:setg 通过标志寄存器判断 x > y(具体是根据 SF、OF、ZF 标志位的组合,无需深究细节,记住 setg 对应 > 即可)。
  • 举例:若 x=5, y=3,则 %al 被设为 1;若 x=2, y=5%al 被设为 0
  1. 第三行汇编代码
c 复制代码
movzbl %al, %eax  # Zero rest of %rax
  • 指令含义:movzbl 是"Move Zero-Extended Byte to Long"的缩写,即把 8 位的 %al 扩展为 32 位的 %eax,高位用 0 填充(Zero-Extended)。
  • 为什么需要这一步?:
    • %al 是 8 位,而函数返回值需要放在 64 位的 %rax 中。如果直接用 %al 的值作为返回值,%rax 的高 56 位可能残留垃圾数据(但实际 %eax%rax 的低 32 位,x86-64 中写入 %eax 会自动将 %rax 的高 32 位清零)。
    • 执行 movzbl %al, %eax 后,%eax 的低 8 位是 %al 的值(0 或 1),高 24 位被清零;而 %rax 的高 32 位也会因写入 %eax 而清零,最终 %rax 整体是 64 位的 0 或 1,符合 C 语言 int 类型的返回值要求(int 通常是 32 位或 64 位,这里通过扩展确保结果正确)。
  1. ret指令含义:函数返回,将控制权交还给调用者。此时 %rax 中存放的就是返回值(0 或 1)。

整体流程总结

  1. 比较 x%rdi)和 y%rsi),通过 cmpq 设置标志位;
  2. setg 根据标志位判断 x > y,结果存到 %al(0 或 1);
  3. movzbl%al 扩展为 32 位的 %eax,间接确保 %rax 整体为 0 或 1;
  4. ret 返回,%rax 中的值作为函数返回值。

3.5 jx跳转指令

jX 指令:依据条件码跳转到代码的不同区段,隐式读取条件码。

jX 指令 条件(结果为1) 描述
jmp 1 无条件
je ZF 相等 / 零
jne ~ZF 不相等 / 非零
js SF 负数
jns ~SF 非负数
jg ~(SF^OF) & ~ZF 大于(有符号)
jge ~(SF^OF) 大于等于(有符号)
jl SF^OF 小于(有符号)
jle (SF^OF) | ZF 小于等于(有符号)
ja ~CF & ~ZF 高于(无符号)
jb CF 低于(无符号)

| >- 说明:ZF(零标志)、SF(符号标志)、OF(溢出标志)、CF(进位标志)为 x86 架构下的条件码(状态标志位);^ 表示异或、& 表示逻辑与、| 表示逻辑或、~ 表示取反。 |
|--------------------------------------------------------------------------------------------------------------|


3.6 while循环编译

  • C语言源代码
c 复制代码
long pcount_do
  (unsigned long x) {
  long result = 0;
  do {
    result += x & 0x1;
    x >>= 1;
  } while (x);
  return result;
}
  • goto编译的低级代码版本
c 复制代码
long pcount_goto
  (unsigned long x) {
  long result = 0;
 loop:
  result += x & 0x1;
  x >>= 1;
  if(x) goto loop;
  return result;
}
  • 寄存器结构

汇编代码对应C语言逻辑,以下是每条指令的功能和执行流程:

汇编指令 解释 对应C代码逻辑
movl $0, %eax %eaxresult)初始化为0(movl 是32位传送,高位自动清0)。 long result = 0;
.L2: 循环体入口标签(对应C代码中的 loop:)。 loop:
movq %rdi, %rdx %rdix)的值复制到 %rdx(临时寄存器)。 t = x;(临时变量 t 用于计算最低位)
andl $1, %edx %edxt)与 1 按位与(t & 0x1),结果存回 %edx t = x & 0x1;
addq %rdx, %rax %rdxt)的值加到 %raxresult)中。 result += t;
shrq %rdi %rdix)无符号右移1位(x >>= 1,高位补0)。 x >>= 1;
jne .L2 %rdix)不为0(x != 0),则跳回 .L2(循环体入口)。 if(x) goto loop;
rep; ret 函数返回(ret 指令弹出栈顶值作为返回地址,rep 是冗余前缀,不影响功能)。 return result;

四、执行流程总结

  1. 初始化:%raxresult)置0,%rdi 保存输入参数 x
  2. 循环体(.L2):
    • 提取 x 的最低位(x & 1),通过 %rdx 临时存储并累加到 result
    • x 右移1位,丢弃已处理的最低位。
  3. 循环条件判断:jne .L2 检查 x 是否为0。若 x != 0,继续循环;否则退出循环,返回 result%rax)。

关键逻辑明

  • 无符号右移:shrq %rdi 确保 x 右移后高位补0(无符号数特性),避免符号位影响循环次数。

  • 条件跳转:jne .L2jump if not equal)依赖 shrq 指令对标志位的影响:右移后若 x 不为0,零标志位(ZF)为0,触发跳转;若 x 为0,ZF为1,不跳转,退出循环。

  • 寄存器复用:通过 %rdx 临时存储中间结果(x & 1),避免直接修改 %rdix)或 %raxresult)。

  • C代码中,循环体先执行(result += x&1x >>=1),再判断条件(if(x) goto loop),符合 do-while 循环"先执行后判断" 的特性(循环体至少执行一次)。汇编中 .L2 标签直接对应循环体入口,先执行后跳转,进一步印证了这一点。

相关推荐
我在人间贩卖青春14 天前
汇编之伪指令
汇编·伪指令
我在人间贩卖青春15 天前
汇编之伪操作
汇编·伪操作
济61715 天前
FreeRTOS基础--堆栈概念与汇编指令实战解析
汇编·嵌入式·freertos
myloveasuka15 天前
汇编TEST指令
汇编
我在人间贩卖青春15 天前
汇编编程驱动LED
汇编·点亮led
我在人间贩卖青春15 天前
汇编和C编程相互调用
汇编·混合编程
myloveasuka15 天前
寻址方式笔记
汇编·笔记·计算机组成原理
请输入蚊子15 天前
《操作系统真象还原》 第六章 完善内核
linux·汇编·操作系统·bochs·操作系统真像还原
myloveasuka16 天前
指令格式举例
汇编·笔记·计算机组成原理
我在人间贩卖青春16 天前
汇编之分支跳转指令
汇编·arm·分支跳转