程序的机器级表示
时隔一年把 CS:App 再看一遍, 尤其针对栈的运行机制加深理解.
访问信息
16 个通用寄存器
一个 x86-64 CPU 包含一组 16 个存储 64 位值的通用目的寄存器. 虽然是通用寄存器, 但也有一些约定成俗的用法. r8 r9 ... 为 80386 之后扩展的 8 个寄存器
\(rax\), 返回值
\(rbx\), 被调用者保存
\(rcx\), 第 4 个参数
\(rdx\), 第 3 个参数
\(rsi\), 第 2 个参数
\(rdi\), 第 1 个参数
\(rbp\), 被调用者保存
\(rsp\), 栈指针. 最为特殊, 用来指明栈的结束位置(栈顶)
\(r8\), 第 5 个参数
\(r9\), 第 6 个参数
\(r10\), 调用者保存
\(r11\), 调用者保存
\(r12\), 被调用者保存
\(r13\), 被调用者保存
\(r14\), 被调用者保存
\(r15\), 被调用者保存
操作数指令符
大多数指令有一个或多个操作数 (operand) 指示出执行一个操作中要使用的源操作数, 以及放置结果的目的操作数. 根据源数据和目的位置的取值可分为三种类型
立即数(immediate), 用来表示常数值
寄存器(register), 表示某个寄存器的内容
内存引用, 根据计算出来的地址访问某个内存位置
多种寻址模式
在 ATT 格式的汇编代码中, 立即数的写法为 \(\$\) 后跟一个标准 C 表示法的整数
用符号 \(r_a\) 来表示任意寄存器 \(a\), 用引用 \(R[r_a]\) 来表示它的值, 这里将寄存器集合看成一个数组, 用寄存器标识符作为索引
将内存看作一个很大的字节数组, 用符号 \(M_b[Addr]\) 表示对存储在内存中可以地址 \(Addr\) 开始的 \(b\) 个字节值的引用, 下表省略下标 \(b\)
类型 | 格式 | 操作数值 | 名称 |
---|---|---|---|
立即数 | $$Imm $ | \(Imm\) | 立即数寻址 |
寄存器 | \(r_a\) | \(R[r_a]\) | 寄存器寻址 |
存储器 | \(Imm\) | \(M[Imm]\) | 绝对寻址 |
存储器 | \((r_a)\) | \(M[R[r_a]]\) | 间接寻址 |
存储器 | \(Imm(r_b)\) | \(M[Imm + R[r_a]]\) | (基址 + 偏移值)寻址 |
存储器 | \((r_b, r_i)\) | \(M[R[r_b] + R[r_i]]\) | 变址寻址 |
存储器 | \(Imm(r_b, r_i)\) | \(M[Imm + R[r_b] + R[r_i]]\) | 变址寻址 |
存储器 | \((, r_i, s)\) | \(M[R[r_i] \cdot s ]\) | 比例变址寻址 |
存储器 | \(Imm(, r_i, s)\) | \(M[Imm + R[r_i] \cdot s ]\) | 比例变址寻址 |
存储器 | $ (r_b, r_i, s)$ | \(M[R[r_b] + R[r_i] \cdot s ]\) | 比例变址寻址 |
存储器 | $ Imm(r_b, r_i, s)$ | \(M[Imm + R[r_b] + R[r_i] \cdot s ]\) | 比例变址寻址 |
数据传输指令
指令 | 效果 | 描述 |
---|---|---|
\(MOV \quad S, D\) | \(D \leftarrow S\) | 传送 |
\(movabsq \quad I, R\) | \(R \leftarrow I\) | 传送绝对的四字 |
\(MOVZ \quad S, R\) | \(R \leftarrow 零扩展 (S)\) | 以零进行扩展进行转送 |
\(MOVS \quad S, R\) | \(R \leftarrow 符号扩展 (S)\) | 转送符号扩展的字节 |
\(movsbw \quad S, R\) | 将符号扩展的字节传送到字 | |
\(ctlq\) | \(\%rax \leftarrow 符号扩展 (\%eax)\) | 把 %eax 符号扩展到 %rax |
算术和逻辑操作指令
指令 | 效果 | 描述 |
---|---|---|
\(leaq \quad S, D\) | \(D \leftarrow \&S\) | 加载有效地址 |
\(INC \quad D\) | \(D \leftarrow D + 1\) | 加 1 |
\(DEC \quad D\) | \(D \leftarrow D - 1\) | 减 1 |
\(NEG \quad D\) | \(D \leftarrow -D\) | 取负 |
\(NOT \quad D\) | \(D \leftarrow \sim D\) | 取反 |
\(ADD \quad S, D\) | \(D \leftarrow D + S\) | 加 |
\(SUB \quad S, D\) | \(D \leftarrow D - S\) | 减 |
\(IMUL \quad S, D\) | \(D \leftarrow D * S\) | 乘 |
\(XOR \quad S, D\) | \(D \leftarrow D\) ^ \(S\) | 异或 |
\(OR \quad S, D\) | \(D \leftarrow D \mid S\) | 或 |
\(AND \quad S, D\) | \(D \leftarrow D \& S\) | 与 |
\(SAL \quad k, D\) | \(D \leftarrow D << k\) | 左移 |
\(SHL \quad k, D\) | \(D \leftarrow D << k\) | 左移, 等同于 SAL |
\(SAR \quad k, D\) | \(D \leftarrow D >>_A k\) | 算术左移(考虑符号) |
\(SHR \quad k, D\) | \(D \leftarrow D >>_L k\) | 逻辑 |
特殊的算术操作
支持两个 64 位数字的全 128(8 字, oct Word) 位乘积以及整数除法的指令, 可以看到除法是分步对高低 64 位操作的
指令 | 效果 | 描述 |
---|---|---|
\(imulq \quad S\) | \(R[\%rdx]:R[\%rax] \leftarrow S \times R[\%rax]\) | 有符号全乘法 |
\(mulq \quad S\) | \(R[\%rdx]:R[\%rax] \leftarrow S \times R[\%rax]\) | 无符号全乘法 |
\(clto \quad S\) | \(R[\%rdx]:R[\%rax] \leftarrow 符号扩展 R[\%rax]\) | 转换为 8 字 |
\(idivq \quad S\) | \(R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] mod S \\ R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] \div S\) | 有符号除法法 |
\(divq \quad S\) | \(R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] mod S \\ R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] \div S\) | 无符号除法法 |
控制
条件码
\(CF\): 进位标志. 最近的操作使最高位产生了进位. 可用来检查无符号操作的溢出.
\(ZF\): 零标志. 最近的操作得出的结果为 0.
\(SF\): 符号标志. 最近的操作得到的结果为负数
\(OF\): 溢出标志. 最近的操作导致一个补码溢出(正溢出或者负溢出)
条件码会发生改变的操作
算术和逻辑操作指令
比较和测试指令
比较和测试指令
这两个系列指令不修改任何寄存器的值, 只设置条件码
指令 | 效果 | 描述 |
---|---|---|
\(CMP \quad S_1, S_2\) | \(S_2-S_1\) | 比较 |
\(TEST \quad S_1, S_2\) | \(S_1 \& S_2\) | 测试 |
访问条件码
指令 | 同义名 | 效果 | 描述 |
---|---|---|---|
\(sete \quad D\) | \(setz\) | \(D \leftarrow ZF\) | 相等 / 零 |
\(setne \quad D\) | \(setnz\) | \(D \leftarrow \sim ZF\) | 不等 / 非零 |
\(sets \quad D\) | \(D \leftarrow SF\) | 负数 | |
\(setns \quad D\) | \(D \leftarrow \sim SF\) | 负数 | |
\(setg \quad D\) | \(setnle\) | \(D \leftarrow \sim(SF \land OF) \& \sim ZF\) | 大于(有符号 & gt;) |
\(setge \quad D\) | \(setnl\) | \(D \leftarrow \sim(SF \land OF)\) | 大于等于(有符号 >=) |
\(setl \quad D\) | \(setnge\) | \(D \leftarrow SF \land OF\) | 小于(有符号 & lt;) |
\(setle \quad D\) | \(setng\) | \(D \leftarrow \sim(SF \land OF) \mid ZF\) | 小于等于(有符号 <=) |
\(seta \quad D\) | \(setnbe\) | \(D \leftarrow \sim CF \& \sim ZF\) | 大于(无符号 & gt;) |
\(setae \quad D\) | \(setnb\) | \(D \leftarrow \sim CF\) | 大于等于(无符号 & gt;=) |
\(setb \quad D\) | \(setnae\) | \(D \leftarrow CF\) | 小于(无符号 & lt;) |
\(setbe \quad D\) | \(setna\) | \(D \leftarrow CF \mid ZF\) | 小于等于(无符号 & lt;=) |
跳转指令
访问条件码
指令 | 同义名 | 跳转条件 | 描述 |
---|---|---|---|
\(jmp \quad Label\) | 1 | 直接跳转 | |
\(jmp \quad *Operand\) | 1 | 间接跳转 | |
\(je \quad Label\) | \(jz\) | \(ZF\) | 相等 / 零 |
\(jne \quad Label\) | \(jnz\) | \(\sim ZF\) | 不相等 / 非零 |
\(js \quad Label\) | \(SF\) | 负数 | |
\(jns \quad Label\) | \(\sim SF\) | 非负数 | |
\(jg \quad D\) | \(jnle\) | \(D \leftarrow \sim(SF \land OF) \& \sim ZF\) | 大于(有符号 & gt;) |
\(jge \quad D\) | \(jnl\) | \(D \leftarrow \sim(SF \land OF)\) | 大于等于(有符号 >=) |
\(jl \quad D\) | \(jnge\) | \(D \leftarrow SF \land OF\) | 小于(有符号 & lt;) |
\(jle \quad D\) | \(jng\) | \(D \leftarrow \sim(SF \land OF) \mid ZF\) | 小于等于(有符号 <=) |
\(ja \quad D\) | \(jnbe\) | \(D \leftarrow \sim CF \& \sim ZF\) | 大于(无符号 & gt;) |
\(jae \quad D\) | \(jnb\) | \(D \leftarrow \sim CF\) | 大于等于(无符号 & gt;=) |
\(jb \quad D\) | \(jnae\) | \(D \leftarrow CF\) | 小于(无符号 & lt;) |
\(jbe \quad D\) | \(jna\) | \(D \leftarrow CF \mid ZF\) | 小于等于(无符号 & lt;=) |
跳转指令一般将目标指令的地址与紧跟在跳转指令后面那条指令之间的差作为编码
有下 C 代码
- int foo() {
- for (int i = 0; i <3; i++)
- if (i == 1)
- return 1;
- return 0;
- }
反汇编二进制代码
- think@pc$ gcc -O0 -c foo.c
- think@pc$ objdump -S foo.o
- foo.o: file format elf64-x86-64
- Disassembly of section .text:
- 0000000000000000 <foo>:
- 0: 55 push %rbp
- 1: 48 89 e5 mov %rsp,%rbp
- 4: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
- b: eb 11 jmp 1e <foo+0x1e>
- d: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)
- 11: 75 07 jne 1a <foo+0x1a>
- 13: b8 01 00 00 00 mov $0x1,%eax
- 18: eb 0f jmp 29 <foo+0x29>
- 1a: 83 45 fc 01 addl $0x1,-0x4(%rbp)
- 1e: 83 7d fc 02 cmpl $0x2,-0x4(%rbp)
- 22: 7e e9 jle d <foo+0xd>
- 24: b8 00 00 00 00 mov $0x0,%eax
- 29: 5d pop %rbp
- 2a: c3 retq
看第一个跳转指令在地址 b, 跳转的地址为 1e, 其值为 11 + d, 这里比较特殊的是
地址是无符号类型
相对地址为有符号类型
看内存地址为 0x24 的那条 jle 指令, 其跳转地址为 d = 24(unsigned) + e9(-17,signed). 与 PC 计数器 指向下一条执行的指令的现象相符合, 这样就可以比较轻易的完成链接操作.
过程
对于我们一般的认识就是过程可以理解为函数调用.
过程的机器级支持需要处理多种属性
传递控制. 在程序进入过程 \(Q\)时 \(PC\)必须设置为 \(Q\)的代码起始地址, 返回时要把 \(PC\)设置为 \(P\)中调用 \(Q\)后面的那条指令的地址.
传递参数.\(P\)必须能够向 \(Q\)提供一个或者多个参数,\(Q\)必须能够向 \(P\)返回一个值.
分配和释放内存. 在开始时, Q 可能需要为局部变量分配空间, 而在返回前, 又必须释放这些分配的内存.
栈的弹出和压入指令
指令 | 效果 | 描述 |
---|---|---|
\(pushq \quad S\) | \(R[\%rsp] \leftarrow R[\%rsp] - 8 \\ M[R[\%rsp]] \leftarrow S\) | 四字入栈 |
\(popq \quad D\) | \(D \leftarrow M[R[\%rsp]] \\ R[\%rsp] \leftarrow R[\%rsp] + 8\) | 四字出栈 |
一个简单的示例代码
- // c 代码
- long three_n_sum(long a1, long a2, long a3) { return a1 + a2 + a3; }
- long sum(long a1, long a2, long a3, long a4, long a5, long a6, long a7, long a8) {
- long b1 = three_n_sum(a1, a2, a3);
- long b2 = three_n_sum(a4, a5, a6);
- long b3 = three_n_sum(a7, a8, 0);
- long b = b1 + b2 + b3;
- return b;
- }
- int main() { long s = sum(1, 2, 3, 4, 5, 6, 7, 8); }
- // 反汇编的二进制代码
- 0000000000001125 <three_n_sum>:
- 1125: 55 push %rbp
- 1126: 48 89 e5 mov %rsp,%rbp
- 1129: 48 89 7d f8 mov %rdi,-0x8(%rbp)
- 112d: 48 89 75 f0 mov %rsi,-0x10(%rbp)
- 1131: 48 89 55 e8 mov %rdx,-0x18(%rbp)
- 1135: 48 8b 55 f8 mov -0x8(%rbp),%rdx
- 1139: 48 8b 45 f0 mov -0x10(%rbp),%rax
- 113d: 48 01 c2 add %rax,%rdx
- 1140: 48 8b 45 e8 mov -0x18(%rbp),%rax
- 1144: 48 01 d0 add %rdx,%rax
- 1147: 5d pop %rbp
- 1148: c3 retq
- 0000000000001149 <sum>:
- 1149: 55 push %rbp
- 114a: 48 89 e5 mov %rsp,%rbp
- 114d: 48 83 ec 50 sub $0x50,%rsp
- 1151: 48 89 7d d8 mov %rdi,-0x28(%rbp)
- 1155: 48 89 75 d0 mov %rsi,-0x30(%rbp)
- 1159: 48 89 55 c8 mov %rdx,-0x38(%rbp)
- 115d: 48 89 4d c0 mov %rcx,-0x40(%rbp)
- 1161: 4c 89 45 b8 mov %r8,-0x48(%rbp)
- 1165: 4c 89 4d b0 mov %r9,-0x50(%rbp)
- 1169: 48 8b 55 c8 mov -0x38(%rbp),%rdx
- 116d: 48 8b 4d d0 mov -0x30(%rbp),%rcx
- 1171: 48 8b 45 d8 mov -0x28(%rbp),%rax
- 1175: 48 89 ce mov %rcx,%rsi
- 1178: 48 89 c7 mov %rax,%rdi
- 117b: e8 a5 ff ff ff callq 1125 <three_n_sum>
- 1180: 48 89 45 f8 mov %rax,-0x8(%rbp)
- 1184: 48 8b 55 b0 mov -0x50(%rbp),%rdx
- 1188: 48 8b 4d b8 mov -0x48(%rbp),%rcx
- 118c: 48 8b 45 c0 mov -0x40(%rbp),%rax
- 1190: 48 89 ce mov %rcx,%rsi
- 1193: 48 89 c7 mov %rax,%rdi
- 1196: e8 8a ff ff ff callq 1125 <three_n_sum>
- 119b: 48 89 45 f0 mov %rax,-0x10(%rbp)
- 119f: 48 8b 45 18 mov 0x18(%rbp),%rax
- 11a3: ba 00 00 00 00 mov $0x0,%edx
- 11a8: 48 89 c6 mov %rax,%rsi
- 11ab: 48 8b 7d 10 mov 0x10(%rbp),%rdi
- 11af: e8 71 ff ff ff callq 1125 <three_n_sum>
- 11b4: 48 89 45 e8 mov %rax,-0x18(%rbp)
- 11b8: 48 8b 55 f8 mov -0x8(%rbp),%rdx
- 11bc: 48 8b 45 f0 mov -0x10(%rbp),%rax
- 11c0: 48 01 c2 add %rax,%rdx
- 11c3: 48 8b 45 e8 mov -0x18(%rbp),%rax
- 11c7: 48 01 d0 add %rdx,%rax
- 11ca: 48 89 45 e0 mov %rax,-0x20(%rbp)
- 11ce: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 11d2: c9 leaveq
- 11d3: c3 retq
- 00000000000011d4 <main>:
- 11d4: 55 push %rbp
- 11d5: 48 89 e5 mov %rsp,%rbp
- 11d8: 48 83 ec 10 sub $0x10,%rsp
- 11dc: 6a 08 pushq $0x8
- 11de: 6a 07 pushq $0x7
- 11e0: 41 b9 06 00 00 00 mov $0x6,%r9d
- 11e6: 41 b8 05 00 00 00 mov $0x5,%r8d
- 11ec: b9 04 00 00 00 mov $0x4,%ecx
- 11f1: ba 03 00 00 00 mov $0x3,%edx
- 11f6: be 02 00 00 00 mov $0x2,%esi
- 11fb: bf 01 00 00 00 mov $0x1,%edi
- 1200: e8 44 ff ff ff callq 1149 <sum>
- 1205: 48 83 c4 10 add $0x10,%rsp
- 1209: 48 89 45 f8 mov %rax,-0x8(%rbp)
- 120d: b8 00 00 00 00 mov $0x0,%eax
- 1212: c9 leaveq
- 1213: c3 retq
- 1214: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
- 121b: 00 00 00
- 121e: 66 90 xchg %ax,%ax
转移控制
将控制从函数 \(P\)转移到 \(Q\)只需要简单的把 \(PC\)设置为 \(Q\)的代码起始位置.
程序的返回处理器需要记录 \(P\)的执行的代码位置, 这个信息由指令 \(call \, Q\)来记录
call 指令将地址 A 压入栈中, 并将 \(PC\)设置为 \(Q\)的起始地址,
压入的地址 A 是紧跟在 call 指令后面的那条指令的地址
, 被称为返回地址.
运行时栈
一个函数调用栈的变化, 用函数 sum() 做示例
将 \(\%rbx\) 入栈保存上一个栈基地址, 设置新的栈帧的基地址, 分配栈内存(sub $0x50,%rsp)
将寄存器保存在栈中, 设置参数, 最多六个参数保存在寄存器中(参考 main 函数)
将 \(PC\) 设置为 1149, 压入 call 之后的指令地址 1205, 跳转
调用子例程则重复以上动作
返回则执行 leaveq 等价于 movl %ebp %esp popl %ebp; ret 弹出返回地址并且跳转
用一张图来表示栈的变化, 观察汇编代码地址 119f 和 11ab, 在第三次调用 three_n_sum()时参数的取值时存在于 main 函数栈帧中, 而参数都存在于栈顶位置也利于子例程取值.
上图是 CS:App 中的图, 我用 processon 画的, 折腾后面上图 2 中的栈结构真的是麻烦, 书里的图文不符, 只能根据这个汇编代码重画一下.
后面又在 15213 的课件中找到一张图, 在我自己的图上栈帧的大小表示上又和课件的图有出入, 拿 sum() 来看,%ebp 入栈后, 栈指针继续分配栈上内存, 向地址减小 0x50, 课件中的栈帧包含了那个 %ebp, 而我自己做的图单独列出了, 这样看应该是课件中的图准确点, 一个栈帧应该包含当前栈的所有信息(栈上内存 + 栈基地址 + 返回地址), 不过 processon 上的图么有了, 这里就插个眼再传个课件的图吧.
)
参考
Intel® 64 and IA-32 Architectures Software Developer's Manual, Volume 1: Basic Architecture, Intel 开发手册
15213 http://www.cs.cmu.edu/~213/resources.html , 计算机系统导论课程
CS:App 3e http://csapp.cs.cmu.edu/ , 深入理解计算机系统
来源: https://www.cnblogs.com/shuqin/p/11000334.html