目录
一、概述
CSAPP第三章笔记,备忘。
二、x86_64 (AT&T)
2.1 数据格式
这样使用后缀指示数的大小
char | b | byte |
short | w | word |
int | l | long word / doubleword |
long | q | quad word |
char * | q | quad word |
2.2 寄存器
2.2.1格式
2.2.2 调用规范
%rax | 返回值 |
%rdi, %rsi, %rdx, %rcx, %r8, %r9 |
前6个参数 |
%rbx, %rbp, %r12, %r13, %r14, %r15 | 被调用着保存 |
%r10, %r11 | 调用者保存 |
%rsp | 栈指针 |
%rip | PC |
2.3 寻址
operand操作数分三种:
- 立即数,使用$, 如$-577
- 寄存器,使用%,如%rax
- 内存
2.4 指令
2.4.1 数据传送指令
- mov S, D S->D
常用的有movb,movw, movl, movq, movabsq, 源操作数S可以为立即数,寄存器,或者内存,目的操作数可以时寄存器或者内存
- 两个操作数不能同时是内存,如立即数和内存。
- 寄存器大小一定要与指令后缀(b/w/l/q)匹配,且两个操作数都是寄存器时宽要匹配
- mov指令只会填充指令后缀指示大小的内存和寄存器,例外是movl 以寄存器作为目的操作数时,会将高4字节置0
- movabsq处理64位立即数,而常规的movq只能将32bit补码表示的立即数做源操作数,再符号扩展成64bit。movabsq能以64bit立即数做源操作数,只能以寄存器做目的操作数
- MOVZ S, R 零扩展S->R
常用的指令movzbw,movzbl, movzbq, movzwl, movzwq
- 目的操作数一定是寄存器,源操作数是内存或者寄存器
- 有符号
- 并没有movzlq指令,可以使用目的操作数是寄存器的movl指令实现,会把高4字节置0
- MOVS S, R 符号扩展S->R
常用的指令movsbw, movsbl, movsbq, movswl, movswq, movslq, cltq
- 目的操作数一定是寄存器,源操作数是内存或者寄存器
- 用于补码
- cltq 是 符号扩展(%eax)->%rax
2.4.2 栈操作
满递减的,栈顶时出口,在这种情况下,在小地址。
- pushq S
rsp = rsp - 8,S-> (rsp)
- popq D
(rsp)->D, rsp = rsp + 8
- 每条指令占一个字节
- %rsp总是指向栈顶
2.4.3 算术&逻辑操作
- leaq S, D &S -> D 加载有效地址
- INC D
- DEC D
- NEG D
- NOT D
- ADD S, D
- SUB S, D
- IMUL S, D
- XOR S, D
- OR S, D
- AND S, D
- SAL k, D
- SHL k,D
- SAR k, D 算术右移,有符号
- SHR k, D 逻辑右移,无符号
- leaq 目的操作数必须时寄存器,实际上根本没有访问内存,只是把其地址取出,如leaq 7(%rdx, %rdx, 4), %rax,那么%rax = 5* %rdx,leaq能执行加法和有限形式的乘法,在编译简单的算术表达式时非常有用
- 一元操作数可以时地址也可以时寄存器
- 二元操作数中的第二个操作数既是源也是目的操作数,第一个操作数可以是立即数,寄存器和内存,第二个操作数只能是寄存器和内存,当其为内存时,既要从内存读取数,也要将最终的结果写回内存
- 除了leaq外,其余指令的指令后缀形式和上面相同
- 移位操作的移位量可以时立即数也可以放在%cl中
2.4.4 乘法&除法
- imulq S 有符号全乘法, S * %rax -> %rdx:%rax ,乘数分别时操作数和%rax,高位在%rdx, 低位在%rax
- mulq S 无符号全乘法,操作同上
- clto 转换为8字, 将%rax 符号扩展到%rdx:%rax
- idivq S 有符号除法被除数在%rdx:%rax中, 除数为S,商在 %rax中,余数在%rdx中
- divq S 无符号除法,操作同上
- 16字称 oct word
- imulq也可以用双操作数
- clto除法时用,用于扩展被除数
2.4.5 控制——比较指令
CPU中提供“测试数据值,然后根据测试结果来改变控制流或数据流”的底层机制。CPU中有单bit的条件码(condition code)寄存器,描述了最近算术或者逻辑的结果属性,通过检测这些寄存器来执行条件分支指令,如下:。
- CF: Carry Flag,进位标志,最近操作产生进位,用来检查无符号操作溢出
- ZF: Zero Flag,最近操作产生的结果为0
- SF: Sign Flag, 最近操作产生的结果为负数
- OF: Overflow Flag,最近的操作导致一个补码溢出(正溢出或负溢出)
t = a + b的情况:
- CMP S1, S2
比较指令,基于S2 - S1改变condition code,相关的指令有cmpb, cmpw, cmpl, cmpq
- TEST S1, S2
测试指令,基于 S1 & S2改变conditon code, 相关的指令有testb,testw,testl,testq,通常用法两个操作数都是自身,检测正数、负数、0(test %rax, %rax)
上述的指令描述了“根据condition code的某种组合,将某一数设置成0或1”
2.4.6 控制——跳转
- jmp Lable 直接跳转,跳转由标号指出,标号是跳转编码的一部分。
- jmp *%rxx/*(%rxx) 间接跳转,跳转由寄存器或者内存中读出
- 有条件跳转只能是直接跳转
2.4.6.1 跳转指令的编码:
- PC relative,将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差作为编码,执行时,PC的值是跳转指令的下一条指令,而不是跳转本身的地址,这种惯例可以追述到早期计算机系统,当时处理器会将更新PC作为执行这条指令的第一步。在链接后,跳转的编码是不会改变的,因此程序可以放在内存中的不同的位置,这样的代码是地址无关的。jmp相对值+下一条指令的地址就是跳转的最终地址
- 绝对地址,用4个字节直接指定目标
2.4.6.2 C语言与汇编的对应关系
条件控制实现条件分支:
if (test-expr) {
then-statement
}else {
else-statement
}
//汇编指令通常会描述成如下c-style
t = test-expr
if (!t) {
goto false;
}
then-statement
goto done;
false:
else-statemnet
done:
- 汇编器会为then和else语句产生各自的代码块,插入条件和无条件分支,保证正确执行。
- 可以看到就是产生原条件相反的值,然后插入无条件分支
条件传送实现分支跳转
条件指令会引起branch prediction miss,惩罚时间较长,会冲洗流水线,条件传送实现了一种无须分支指令实现跳转的方法,它需要先计算出不同的结果,根据条件选择结果。当然 ,条件传送也有其局限性,如需要计算每个分支的结果,在某些时候,可能很大的开销。特别的,有时候不能对两个分支同时求值,可能会引起错误
2.4.7 控制——循环
do-while
do {
body-statement
} while (test-expr)
//对应的汇编很容易理解(c-style)
loop:
body-statement
t = test-expr
if (t)
goto loop
在分析汇编循环代码时,看看能不能找到寄存器和局部变量的对应关系,另外“看看循环前如何初始化寄存器,循环中如何更新和测试寄存器,循环结束时又如何使用寄存器”
while循环
while (test-expr) {
body-statement
}
//第一种 jump to middle
goto test
loop:
body-statement
test:
t = test-expr
if (t)
goto loop
//第二种 guarded-do
t = test-expr
if (!t)
goto done
loop:
body-statement
t = test-expr
if (!t)
goto loop
done:
- 第一种相当于在do-while基础上先使用无条件跳转直接先进行一次条件测试。
- 第二种先进行条件判断,如果成立就执行do-while流程,使用较高等级(如O1)会用这种方式,
for 循环
for循环实际上可以和while循环互换
for (init-expr, test-expr, update-expr) {
body-statement
}
//等价的while
init-expr
while (test-expr) {
body-statement
update-expr
}
按照上面有两种方法,就不展开了。
switch 语句
switch语句一般通过跳转表实现,如果case 的情况可以集中在小范围内,可以通过跳转表组成的索引来定位,实现上如下图所示:
2.4.8 过程
函数调用规范,需要考虑如下几个方面:
- 传递数据:参数传递和返回值,参数传递一般会通过寄存器传递,参数较多时利用栈传递,在C中是由右向左压栈。返回值保存在固定的寄存器中。
- 传递控制:控制PC指针,调用时确定过程地址,调用完成时指向调用时的下一条指令。
- 分配/释放空间:除了参数,还需对局部变量,返回地址,被调方可能用到的寄存器分配和释放空间。
- callq Lable 直接跳转
- callq *operand 间接跳转
- retq
- call相当于执行 push 下一条指令地址,jmp Lable
- ret相当于pop %rip
函数调用时栈的示意:
- 栈的内存布局顺序要记牢
- 当寄存器不足以存放数据时,需要使用栈上的空间,如参数,局部变量,有时候需要引用局部变量的地址或者处理数组结构体等变量时,都要在栈上分配空间
函数调用时栈的特性是:调用时分配,完成后释放,这为函数链式调用,递归调用等提供了机制
2.4.9 数组的表示
- T A[N] &A[n] = A + n = A + n * sizeof(T)
- A[n - 8] / *(A + n + 8) movq (8 * sizeof(T))(A, n, sizeof(T)), %reg
- &A[n - 8] / A + n - 8 leaq -(8 * sizeof(T)(A, n, sizeof(T)), %reg addr = A + sizeof(T) * (8 - n)
- 注意一下指针的运算
- T A[R][C] A[r][c] = *(A + r) + c = A + r *sizeof(T) * C + c * sizeof(T)
- 使用伸缩和加法特性得到值
2.4.10 异构——结构体和联合
typedef struct node {
void *value;
struct node *next;
}node_t;
node_t *node;
//假设node存储在%rdi中
node = node->next;
movq 8(%rdi), %rdi
&node = node->next;
leaq 8(%rdi), %rdi
程序说明了在进行node = node->next,这样的操作时取的时node_t 中next域的值,node是指针变量
- 结构体使用指针+偏移的形式
- 对齐原则:任何K字节的基本对象的地址必须是K的倍数