1 Star 0 Fork 11

coder_lw / wiki

forked from deepinwiki / wiki 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
汇编.md 13.02 KB
一键复制 编辑 原始数据 按行查看 历史
htqx 提交于 2023-03-15 01:30 . 添加寄存器用途

汇编

前言

汇编语言对应机器语言,是程序语言最底层的,且人类可读的形式。汇编在 c 函数和调试的时候都可能涉及到。一些底层的系统调用,需要通过汇编来书写。因此,就算很少使用,也应该对其有一些了解。

调试器

常用的是 gcc 项目的 gdb 调试器,和 llvm 项目的 lldb 调试器。lldb 命令兼容 gcc。

zig cc main.c # 编译
lldb main # 调试 mian 程序,然后进入调试的交互模式,q 退出
r # 运行
file a.out # 加载 a.out 程序
r # 运行
b mian.c:18 # 在对应的源文件 18 行设置断点
b main # 对 mian 函数设置断点
r # 停在断点
br list # 断点的列表b
br del 1 # 删除断点(序号)

w s v a # 监视 a 的值发生变化
w l # 监视列表
w del 1 # 删除监视项

bt # 显示调用栈
f 1 # 选择调用栈序号 1,即上一个调用者
v # 显示局部变量
re r # 显示寄存器
p a # 计算表达式,打印 a 的值
d # 反汇编
f # 源码

s # 执行下一步(会进入下一个层级)
si # 执行下一条指令
n # 跳到下一步
ni # 跳到下一步(指令级)
c # 下一个断点

kill # 关闭当前运行的线程
help n # 显示 n 指令的帮助文档,了解更多参数使用方法
q # 退出交互模式

脚本:

target stop-hook add # 暂停时执行以下脚本
re r rbp rsp # 显示寄存器
d -p # 显示反汇编
DONE # 结束

指令

寄存器

  1. ax : 16 位(实模式),还能分成高8位和低8位(ah,al)
    1. eax : 32 位
    2. rax : 64 位(现状)
  2. rax,rbx,rcx,rdx :返回值,数据指针,循环计数,io指针
  3. rdi,rsi :目标指针,源指针
  4. rbp,rip,rsp :栈帧指针,指令指针,栈指针
  5. rfalgs :符号
  6. r8,r9,r10,r11,r12,r13,r14,r15 :新增
  7. cs,ds,es,ss,fs,gs :16位段寄存器(64 位不常用)

寄存器用途

callee saved 表示由被调用函数负责保存和恢复的寄存器,即函数的上下文。即函数调用后,必须保证 rbx,rsp,rbp,r12,r13,r14,r15 不变。

段落

程序按习惯分为多种段落

  1. 代码段
  2. 数据段
  3. 堆(空洞,即剩余空间)
  4. 栈(逆向生长)
  5. 环境变量

栈的方向是从高到低,比如栈底是3,下一个地址就是2,1,0。堆就是夹在中间的内存可用地址,一般需要借助系统 api 动态申请。重点是对栈的理解,因为函数调用使用的是栈空间。

栈是一种后进先出的结构,一般而言,栈基(栈底)是高地址,栈顶是低地址。入栈表示将数据添加到栈顶,栈顶 - 1,出栈表示栈顶 + 1,表示删除栈顶位置的数据。刚开始栈顶指向占底,随着数据增多,栈顶地址变小,又随着数据减少,栈顶变大。就是这么一个动态变化。

栈的总体大小是预先分配的。栈顶最大不能超出(栈底 - 大小),最小不能超出栈底。

函数和栈的关系非常密切,所以需要深入的理解汇编是如何实现函数调用过程的。

函数调用实际上有多种协议(约定),c 语言约定是:

  1. 入栈:参数(c 函数是从右到左顺序入栈)
    1. 先使用六个寄存器:rdi,rsi,rdx,rcx,r8,r9
  2. call 函数
    1. 入栈:返回地址 push %rip
    2. 跳转到目标函数 (jmp)
    3. 设置当前栈帧
      1. push rbp
      2. rsp -> rbp
    4. 入栈:局部变量(执行函数逻辑)
    5. 设置返回值:rax
    6. 出栈:局部变量
      1. rbp -> rsp
      2. pop %rbp
    7. 返回:pop %rip(ret)
  3. 返回调用处
    1. 出栈:参数 add $num, %rsp

简而言之,调用者首先入栈参数(少于6个时使用寄存器)然后跳转到函数(该指令功能上等于入栈当前指令指针加 jmp)。

然后就到了函数内部,首先保存调用者的栈帧指针 rbp,再设置 rbp 等于当前 rsp 栈顶。因为 rsp 总是指向栈顶,它是一个不断变化的指针,所以需要 rbp 栈帧指针作为一个相对定位的基地址指针。

可以理解为每个函数,都有自己的基地址,然后编译器就可以相对定位参数和内部变量的位置,从而逻辑上解耦了调用者和自身的关系。所以可以从任意地方调用该函数,只要按约定提供参数即可。这个相对独立的函数空间,被命名为栈帧(栈片段)。

然后是给函数分配局部变量的空间(局部变量都是可以提前计算总大小和定位相对位置的,编译器可以一次性用 sub $num, %rsp 立即数来分配完整的空间),运行完毕设置返回值,一般设置给 rax。出栈局部空间(只需要用 rbp->rsp即可),然后弹出之前保存的 rbp,用来恢复调用者的栈帧指针。最后是跳转调用处(这条指令相当于弹出 rip,然后 jmp 到该位置)。

因为 c 语言是调用者预先准备参数的,所以回到调用处,调用者就应该自己清理这些参数,将其出栈(add 参数占用, %rsp)。

仔细理顺这一个过程(可以先看完其余部分再回来跟踪这一个过程),作为程序员来说,可以不了解多少汇编指令,因为很少人需要用汇编来写复杂的程序(调试跟踪或作为关键部分少量使用),但是对函数调用这一个过程是需要认知清晰的。

堆栈示意图

以上是 32 位堆栈的情况,在 64 位第一个入栈参数是 16(%rbp),第二个是 24(%rbp) 依此类推。

指令种类

  1. 操作数 2
    1. 操作数 1
    2. 操作数 0
    3. 其他
  2. 寄存器 + 寄存器 (源,目的地)
    1. 立即数 + 寄存器
    2. 寄存器 + 内存
    3. 立即数 + 内存
    4. 内存 + 寄存器
  3. 位宽,普通指令后加上位宽后缀
    1. b: 字节
    2. w:字(双字节)
    3. l: 四字节
    4. q: 八字节
  4. mov: 拷贝
    1. lea: 拷贝地址
  5. xchg: 交换(原子操作)
  6. %rflags: 符号寄存器,很多指令依赖符号
    1. CF: 进位,最高位进位(检测无符号溢出)
    2. OF: 溢出,次高位进位不等于 CF 时(检测有符号溢出)
    3. SF: 符号
    4. ZF: 是否为零
    5. DF: 方向(递增或递减)
    6. PF: 奇偶
  7. cmp: 比较,类似 sub 但只改变符号寄存器
    1. test: 类似 and 但只改变符号寄存器
  8. je: 相等时跳到目标(修改 rip 指令指针指向下一个位置)
    1. jg / jgz / jl / jle: 大于、大于等于、小于、小于等于(有符号)
    2. ja / jae / jb / jbe: 大于、大于等于、小于、小于等于(无符号)
    3. jc / jo / jp / js / jz: 对应符号位:CF、OF、PF、SF、ZF
    4. jmp: 无条件跳转
    5. jcxz: %cx = 0 计数器等 0 时
      1. jecxz: %rcx = 0
    6. loop: %rcx 减 1 ,当不等于 0 时
      1. loope: 上述或相等
      2. loopne: 或不等
    7. rep movsb: 类似浓缩的 loop,重复执行 movsb 指令
      1. repe: 上述且相等
  9. add: 累加
    1. inc: 加 1
  10. sub: 减,目的 - 源 --> 目的
    1. dnc: 减 1
    2. neg: 负(补码:取反 + 1)
  11. mul: 无符号乘(x * rax = rdx:rax)
    1. imul: 有符号乘,OF=CF=1 无效(双操作数),或 rdx 非 0(单操作数)
    2. sal / shl: 左移,乘 2^N, 可用 %cl 指定位数
  12. div: 无符号除(rdx:rax / x = rax...rdx)
    1. idiv:有符号除
    2. sar: 算术右移(最高位补符号)
    3. shr: 逻辑右移(最高位补0)
  13. 位操作
    1. not: 取反
    2. and: 与
    3. or:或
    4. xor:异或
  14. syscall : 系统调用,%rax = id,参数(rdi,rsi,rdx,r10,r8,r9),返回值 rax,rdx
    1. /usr/include/asm/unistd_64.h :系统调用编号
  15. 字符串(指针:rsi -> rdi)
    1. movsb: 从源指针复制数据到目的指针,并累加指针,每次 1 字节
    2. rep movsb: 递减 %rcx ,非 0 重复 movsb
    3. std / cld: 递减 / 递增 指针(设置 rflags 方向标志 df)
    4. lodsq: mov (%rsi),%rax,然后递增 %rsi
    5. stosq: mov %rax,(%rdi),然后递增 %rdi
    6. cmps: 比较源指针(%rsi)和目标指针(%rdi),然后递增两指针
    7. scas: 用 %rax 查找 (%rdi),然后递增指针
  16. 函数操作
    1. call : 调用函数(类似下面)
      1. push %rip + x (下一个指令的位置)
      2. jmp 函数
    2. 参数传入
      1. 寄存器(c语言前6个:rdi,rsi,rdx,rcx,r8,r9)
      2. 堆栈(还包括函数局部变量)
        1. push :入栈(c语言从右到左入栈)
        2. pop:出栈
          1. add $16, %rsp (可直接改栈顶)
      3. 全局变量
    3. ret: 函数返回
      1. pop %rip 类似(但该指令无效)
    4. 调用函数应该注意保存后续要用到的寄存器(避免被调用函数修改)

汇编符号

汇编除了指令,还有一些元信息,他们给汇编器提供构造信息。比如分段,相对地址和符号的绑定等。

汇编语言和 cpu 架构有关,因为不同架构指令不同。也和汇编器使用的语法有关。比如 intel 汇编格式和 AT&T 格式虽然生成的指令一样,但写法有一些差异。这里主要使用 AT&T 汇编(linux 常见格式)。

  1. .global main : 全局符号 main
  2. main: : 符号 main 是一个起始地址,由汇编器计算实际地址
  3. .section .data : 数据段
    1. .rodata : 只读数据段
      1. .fill 100 : 100 字节空间(值为0)
    2. .bss : 未初始化数据段
      1. .lcomm buffer,1000 : 定义1000字节空间
  4. .section .text : 代码段
  5. .equ Steam,0x80: 定义常量 Steam
  6. .string : 字符串 \0 结尾
    1. length = . - buffer : 点代表当前地址, $length 计算上一个变量长度
    2. .double :浮点 8B
    3. .float : 单精度浮点 4B
    4. .int : 整数(4B)
    5. .byte .short .quad .octa : 字节,2B,8B,16B
    6. 大于一个字节时有两种编码
      1. 大端:内部字节序从高到低,$0x06ff -> $0x06,$0xff
      2. 小端(x86):内部字节序从低到高,$0x06ff -> $0xff,$0x06
  7. $1 : 立即数 1
    1. $buffer : 取 buffer 的地址。$ 实际上是计算后续地址表达式,而不对其取指针。
    2. *%rax : 取地址,而不是读写指针,如 jmp *%rax 读取 rax 中的值,而非将 rax 视为指针,执行取指针操作
  8. %rbx : 寄存器 rbx
  9. "字符串"
  10. (%rbx) : 读写指针
    1. -4(%rbp) : 偏移 -4 的指针
    2. -4(%rbp, 2, 4) : 基址(偏移,下标,步进),即:-4 + %rbp + 2*4
  11. .type 函数名,@function : 定义函数

对比 intel 汇编

特征 AT&T Intel
方向 源,目的 目的,源
立即数 $1 1
进制 $0xf2 f2h
寄存器 %rax rax
指针 -4(%rbp) [rbp - 4]

编译(例子)

# ata.S
.section .data
output:
    .string "hello asm!\n"
    length = .-output
.section .text
.global _start
_start:
    mov $1, %rdi
    mov $output, %rsi
    mov $length, %rdx
    mov $1, %rax
    syscall

    mov $0, %rdi
    mov $60, %rax
    syscall
# 使用 zig cc 编译器(需要安装 zig)
zig cc ata.S -o ata -nolibc # 编译且不要链接 c 标准库

内联汇编

高级语言很多支持内联汇编,从而让高级语言可以在高级语言代码中临时使用,这可能是程序员使用汇编比较常见的方式。

gcc 内联汇编语法(zig llvm):

// 基本语法
asm volatile("汇编文本"); // volatile 别优化,inline 内联, goto 支持标签

// 扩展语法
asm volatile(
    "模板"
    : // 输出 (请查看后续详细说明)
    : // 输入
    : // 破坏影响
);

// zig 编程语言使用的内联样式
var result = asm volatile(
"call printf" // 调用 c 函数 printf
: [ret]"+{rax}"(->i32) // 返回值,类型 i32
: [x]"{rdi}" ("print number=%d \n"),  // 第一个参数
    [num]"{rsi}" (123), [zero] "{rax}" (0) // 第二个参数, printf 要求 rax = 0
: "rcx" // 笔者估计 printf 内部会使用的寄存器
); // 模板并没有使用变量,所以 x num 之类的标签只是为了满足语法格式要求

模板:

  1. 元字符: %, %=, {, | ,}
  2. {} : 可选
  3. {a|b}: 两组(为了支持汇编中的多种语法格式,如intel 和 at&t)
  4. %0 %1 %2 : 引用匿名变量(根据出现的位置)
  5. %[符号]: 引用命名变量

输入输出:

  1. 语法格式: [符号] 约束 (变量 或 表达式)
  2. %0 %1 %2 : 绑定匿名变量(没有指定符号)
  3. 约束
    1. "=" 或 "+" : 输出前缀、输入和输出(无:输入)
    2. "r": 寄存器
    3. "m": 内存
    4. "0" : 指定和匿名变量位置相同(因为同名不代表代码一致)
    5. "[标签]":指定和命名变量位置相同

破坏影响: 内联汇编除了输出外还影响到的其他地方,需要提供给编译器做优化。

  1. "cc" : 标志寄存器
  2. "memory" : 内存

参考

  1. 【AT&T风格汇编语言】2019天津大学智算学部汇编语言程序设计-李罡:https://www.bilibili.com/video/BV1Pb411L73
  2. 内联汇编 gcc 语法: https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C
1
https://gitee.com/coder_lw/wiki.git
git@gitee.com:coder_lw/wiki.git
coder_lw
wiki
wiki
master

搜索帮助