Skip to content

CSAPP 笔记(待整理)

Chapter 3

Tools

  • gcc -Og -S test.c
    • -S: compile code into Assembler instructions
    • -c: produce an object file without linking
    • -o: 指定产生的可执行文件
    • -Og: 生成复合原始C代码整体结构的机器代码
    • -O1, -O2: 较高级别的优化
  • objdump -d test.o
    • -f: display file header information
  • GDB调试程序的字节代码
    • 首先通过反汇编器确定该过程的代码长度
    • (gdb) x/14xb multstore

3.2 程序代码

  • 机器级代码:两种抽象
    • ISA: Instruction Set Architecture 指令集体系结构/指令集架构:定义处理器状态、指令格式、指令对状态的影响。程序的行为被描述为好像每条指令顺序执行,而硬件处理远比描述的精细复杂,并发执行许多指令
    • 机器级程序使用虚拟地址:内存模型看起来是一个非常大的字节数组。而存储器系统实际实现是将多个硬件和操作系统软件组合起来
  • C对程序员隐藏的处理器状态:
    • 程序计数器 PC:%rip,下一条指令在内存中的地址
    • 整数寄存器文件:包含16个命名位置,分别存储64位值。可以存储地址(指针)或整数数据,记录程序状态,保存临时数据,如参数、局部变量、返回值
    • 条件码寄存器:最近执行的算术或逻辑指令的状态信息,实现控制或数据流中的条件变化
    • 向量寄存器:一个或多个整数/浮点数
  • 从C到机器语言需要转换的思维:
    • C对各种数据类型对象进行声明和分配内存,而机器代码简单地看作按字节寻址的数组:这意味着不区分任何数据类型,不区分指针和整数
    • 程序内存:程序的机器代码,操作系统所需信息,调用和返回的运行时栈,用户分配内存块(malloc)。操作系统负责翻译虚拟地址为实际处理器中内存物理地址
    • 机器指令仅执行非常基本的操作:寄存器相加、存储器和寄存器之间传送、条件分支到新指令地址。编译器必须产生这些指令的序列,从而实现程序结构
  • 机器代码和反汇编特性:
    • x86-64的指令长度:1-15个字节不等,常用指令和操作数较少的指令所需字节数较少
    • 指令设计格式:某给定位置开始,字节可以唯一解码成机器指令。如只有 pushq %rbx 是以字节值53开头
    • 反汇编器:仅基于机器代码的字节序列确定汇编代码;指令命名规则与GCC有些细微差别,比如GCC省略了很多指令结尾的q,这些后缀是大小指示符,很多情况下可以忽略
    • 一组目标文件运行链接器所做的事:包含了启动和终止程序的代码,和与操作系统交互的代码
    • 链接后的程序进行反编译,会发现左侧列出的地址不同:链接器将地址移到了不同的地址范围中;且链接器填充了 callq 指令调用函数需要使用的地址(这就是链接器的任务之一:找到匹配函数的可执行代码的位置)
    • nop(hex = 90) 指令:对程序没有影响,只是为了让函数代码变为16字节,更好地放置下一个代码块
  • 汇编代码的格式:
    • . 开头的行都是汇编器和链接器的伪指令
    • 书本的表述基于ATT,与GCC、OBJDUMP和一些其他工具相同
    • Intel格式的表述被Microsoft的工具所采用
      • 省略指示大小的后缀如 q
      • 省略寄存器前的 %
      • 描述内存位置的方法不同
      • 列出操作数的顺序与ATT相反,这在两种格式的转换中引起极大困惑
  • 在C中嵌入汇编代码的两种方式:
    • 用汇编代码编写整个函数,在链接阶段合并
    • 利用GCC直接在程序中嵌入代码(内联汇编 inline assembly),使用asm伪指令在C程序中包含简短的汇编代码。这导致代码与机器相关

3.3 数据格式

术语:

  • 字节 byte: 8位

  • 字 word:16位

  • 双字 double words:32位(32位被看作长字 long word)
  • 四字 quad words:64位
C数据类型 Intel数据类型 汇编代码后缀 大小(byte) 大小(bit)
char 字节 b 1 8
short w 2 16
int 双字 l 4 32
long 四字 q 8 64
指针 四字 q 8
float 单精度 s 4
double 双精度 l 8

比如,数据传送指令有:movb, movw, movl, movq

  • 浮点数使用的是一组完全不同的指令和寄存器

3.4 访问信息

Summary: x86-64体系结构

通用目的寄存器:16个存储64位值,用于整数数据和指针,都以 %r 开头,它们的命名是指令集历史演化造成的

  • %rsp %esp %sp %spl: 栈指针最为特别,指明运行时栈的结束位置,有些程序明确读写该寄存器
  • %rax %eax %ax %al: 返回值,比较常用
  • %rbx %ebx %bx %bl: 调用者保存。c,d是,第4、3个参数
  • %rdi %edi %di %dil: 第一个参数。第二个参数是 s

条件码寄存器:

  • PF(parity flag): 奇偶标志。当执行算术或逻辑运算时,如果得到运算结果的低8位中有偶数个1,该位置1,否则置0。在C语言中计算该信息至少需要7次位移、掩码、异或运算。
  • 不同大小级别的操作可以访问寄存器的不同部分
    • 剩下的字节会怎么样有两条规则:1、2字节的指令不改变,4字节指令将高位全部置0,这是从IA32到x86-64扩展而采用的

操作数

  • 数据读出:常数形式、寄存器、内存;结果存放:寄存器、内存

  • 立即数:表示常数值:$0x1F 使用标准C表示法表示整数。不同指令允许的立即数值范围不同

  • 寄存器r_a 表示任意寄存器,R[r_a] 表示寄存器中的值

  • 内存引用: 根据计算出来的地址访问某个内存地址M_b[Addr],表示对存储在内存中从 Addr 开始的 \(b\) 个字节的引用。常省去下标 \(b\)

内存引用总是以四字长寄存器给出,哪怕操作数只是一个字节、一个字或双字

- **寻址模式**: `Imm(r_b, r_i, s) = M[Imm + R[r_b] + R[r_i]*s]`比例变址寻址,这是引用数组和结构元素时通用的形式
- `Imm`: 立即数偏移
- `R[r_b]`: 基址寄存器(64位)
- `R[r_i]`: 变址寄存器(64位)
- `s`: 比例因子,必须是 $1,2,4,8$

例:计算260(%rax,%rdx,4) 的值

指令

在学习指令时,注意每一类命令的源和目的的要求是非常重要的

访问信息

  • 数据传送:它们的源/目的类型不同,或执行的转换不同,或者有一些其他的副作用。我们把相同操作的一系列执行划为一类,它们的操作数大小不同

    • MOV S,D:不做任何变化,数据从S到D

      • movb, movw, movl, movq, movabsq

      • S(源操作数)应当指定一个立即数,存储在寄存器或内存中

      • D(目的操作数)应当指定一个位置,寄存器或者内存地址

      • 两个操作数不能都指向内存: 将一个值从内存复制到另一个内存,必须先载入寄存器,再写入目的位置

    movq %rax, -12(%rbp) \\register--memory 8bytes
    movb (%rdi, %rcx),%al \\memory--register 1bytes
    
    - `movabsq`: 处理64位**立即数数据**,仅以**寄存器**作为目的。常规 `movq` 仅以表示为**32位补码数字**的立即数作为源操作数,然后符号扩展得到64位值(什么是[符号扩展](https://blog.csdn.net/haoyuedangkong_fei/article/details/71043973)?)
    

以下两类数据移动指令将较小的源值复制到较大的目的时使用,它们以寄存器或内存地址为源,寄存器作为目的。第一个后缀指示源大小,第二个指示目的大小

- `MOVZ` 零扩展(zero-extend),剩余位填充0
    - `movzbw, movzbl, movzwl, movzbq, movzwq`
    - 没有 `movzlq` 这可以通过 `movl` 实现
- `MOVS` 符号扩展(sign-extend),源的最高位复制
    - `movsbw, movsbl, movswl, movsbq, movswq, movslq, cltq`
    - `cltq`: 将 `%eax` 符号扩展到 `%rax`,没有操作数。这等价于 `movslq %eax,%rax`

数据传送指令的示例

long exchange(long *xp, long y){
 long x = *xp;
 *xp = y;
 return x;
}
exchange: 
 movq (%rdi), %rax
 movq %rsi, (%rdi)
 ret

值得注意的点:

  • 指针就是地址,间接引用(解引用)就是把指针放在寄存器中,然后在内存引用(间接寻址)时使用这个寄存器
  • 局部变量通常保存在寄存器中,访问速度快得多
  • 如果你需要符号/零扩展(强制类型转换),则必须在拷贝到寄存器时使用扩展的数据移动指令 技能:GCC产生的汇编代码上有后缀,反编译没有。你需要掌握的技能是:通过识别源和目的确定相应的指令后缀
  • 压入和弹出栈数据:栈向下(小的方向)增长,栈顶元素的地址是所有栈中元素地址最低的

    • pushq, popq
    • pushq %rbp 的行为等于以下两条指令:subq $8, %rsp; movq %rbq,(%rsp),先递减栈指针,再将值存储到对应位置
    • 【Question】movq 8(%rsp),%rdx 进行了什么操作?

算术和逻辑操作

分成四组:加载有效地址、一元操作、二元操作、移位

  • leaq
  • INC,DEC,NEG,NOT
  • ADD,SUB,IMUL,XOR,OR,AND
  • SAL,SHL,SAR,SHR

  • 加载有效地址(load effective address):没有引用内存,不是从指定的位置读入数据,只是将有效地址写入*目的操作数(必须是一个寄存器)

    • leaq 7(%rdx,%rdx,4),%rax
    • leaq 有一些灵活用法,计算器常常根本不用它来计算地址。它可以执行加法和有限形式的乘法,在编译简单表达式时很有用处
  • 一元操作:只有一个操作数,既是源又是目的。可以是寄存器或内存地址
  • 二元操作:第二个操作数既是源又是目的,源操作数是第一个。第一个可以是立即数、寄存器、内存位置,第二个可以是寄存器、内存位置
    • 当第二个操作数是内存地址时,处理器必须先读出,执行操作,再写回
  • 移位操作:给出移位量(只能是立即数或存放在单字节%cl中),第二项是要移位的数
    • 一个字节可以编码的移位范围达到了255
    • \(w\) 位的数据值进行操作,移位量是由 %cl 的低 \(\log_2 w\) 位决定的,高位被忽略
    • 例:%cl = 0xFF时,salb 移动 \(7\) 位,salq 移动 \(63\)
    • 左移的两个名字效果是一样的

【讨论】

  • 以上大多数指令可用于无符号运算和补码运算,这是补码成为实现有符号整数运算的比较好的方法的原因
    • 只有右移操作需要区分有无符号