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位值。可以存储地址(指针)或整数数据,记录程序状态,保存临时数据,如参数、局部变量、返回值
- 条件码寄存器:最近执行的算术或逻辑指令的状态信息,实现控制或数据流中的条件变化
- 向量寄存器:一个或多个整数/浮点数
- 程序计数器 PC:
- 从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(目的操作数)应当指定一个位置,寄存器或者内存地址
-
两个操作数不能都指向内存: 将一个值从内存复制到另一个内存,必须先载入寄存器,再写入目的位置
-
- `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`
数据传送指令的示例
值得注意的点:
- 指针就是地址,间接引用(解引用)就是把指针放在寄存器中,然后在内存引用(间接寻址)时使用这个寄存器
- 局部变量通常保存在寄存器中,访问速度快得多
- 如果你需要符号/零扩展(强制类型转换),则必须在拷贝到寄存器时使用扩展的数据移动指令 技能: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\) 位 - 左移的两个名字效果是一样的
【讨论】
- 以上大多数指令可用于无符号运算和补码运算,这是补码成为实现有符号整数运算的比较好的方法的原因
- 只有右移操作需要区分有无符号