计算系统安全速成之汇编基础【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 标签直接对应循环体入口,先执行后跳转,进一步印证了这一点。

相关推荐
white-persist16 小时前
【攻防世界】reverse | IgniteMe 详细题解 WP
c语言·汇编·数据结构·c++·python·算法·网络安全
小邓   ༽19 小时前
50道C++编程练习题及解答-C编程例题
c语言·汇编·c++·编程练习·c语言练习题
资料,小偿2 天前
4.1.2基于51单片机汇编语言出租车计价器proteus仿真出租车计价器,汇编语言51单片机
汇编·51单片机·proteus
ol木子李lo5 天前
Visual studio 2022高亮汇编(ASM)语法方法
汇编·ide·windows·visual studio
资料,小偿6 天前
4.1.1基于51单片机汇编语言出租车计价器可切换白天黑夜,可修改价格
汇编·51单片机·proteus
embrace996 天前
【C语言学习】数据在内存中存储
java·c语言·开发语言·汇编·c++·学习·算法
T.Ree.12 天前
汇编_mov指令
汇编
CC-NX13 天前
32位汇编:实验12动态链接库
汇编
资料,小偿13 天前
4.101基于8086国旗图案proteus8.9,8086彩灯图案流水灯图案,国期图案仿真,四个开关四种模式。近期本人原创
汇编·proteus