0. 警告
不要试图通过这篇意识流笔记自学。
右转睿站九曲阑干,可以帮你快速建立基本概念。
1. 基本的汇编语法
I. 数据格式
三种数据类型:
立即数:常数,一般用十进制表示,如果要使用十六进制表示,在前面加上$
寄存器:寄存器
内存:把内存抽象成一个大数组 ,使用 M[i] 的形式来理解 i 地址指向的数据
\(Imm(r_b,r_i,s)\) 语法:
\(Imm\) 是一个立即数,\(r_b\) 表示基址寄存器,\(r_i\) 是一个变址寄存器,\(s\) 是比例因子,只能是 1,2,4,8 。
返回值为 \(M[s*r_i+r_b+Imm]\)
\(Imm\) 可省略,如果括号中的参数不满三个则优先取前面的,如果想要仅使用 \(r_i\),\(s\) 需要使用括号占位
e.g
\(4(\%rax,,)\) , \((,r_i,4)\)
简单记忆:视作一个关于变址的一次函数: \(b_2(b_1,x,a)\) 返回结果为 \(M[ax+b]\)。
寄存器指针化:
设寄存器 %rax 中存放的数为 x,地址 0x108 存放 0x13 则有
操作 | 值 |
---|---|
%rax | x |
(%rax) | M[x] |
0x108 | 0x13 |
$0x108|\(264=108_{16}\) |
常见寄存器及用途(64bit)
%rax 返回值
%rsp 栈指针
%rdi,%rsi,%rdx,%rcx,%r8,%r9 函数传参的前六个参数
%rbx,%rbp,%r12,%r13-15 被调用者保存
%r10,%r11 调用者保存
深入理解寄存器
%rax 64位,%eax 32位,%ax 16位,%al 8位
%r8 64位,%r8d 32位,%r8w 16位,%r8b 8位
其它格式大体上差不多,具体参阅原书 120 页表格。
特别注意:
- rax eax ax al 都是一个寄存器,不过 eax 选的是前 32 位,ax 选的是前 16 位以此类推。
- 如果用寄存器保存地址,在 64 位机上应该使用 64 位寄存器,在 32 位机上使用 32 位寄存器,否则会报错。
II. 数据传送指令
基本格式:MOV A,B
把 A 的数据传送到 B。
MOV 含有 movb,movw,movl,movq 四条指令,根据要传递的字节数做区分,movb 8bit,movw 16bit,movl 32bit,movq 64bit。
A 可以是立即数,寄存器或地址,B可以是寄存器或地址,但 A 和 B 不能均为内存地址,如果想实现内存地址间的拷贝需要借助寄存器中转。
使用限制
- A 和 B 不能均为内存地址。
- b/w/l/q 的值要与字节数匹配,如果 A 与 B 的字节数不同且是,请采用下边的扩展数据传送指令。例外:A 是 32bit,B 是 64bit,不存在movzlq 指令。但是,X86-64规定,使用
movl %eax %rax
(注意要是同一个寄存器)会自动进行零扩展高位补零。
扩展数据传送指令:
MOVZ,MOVS,将较小的源复制到较大的目标时使用,这两个目标都要是寄存器。
MOVZ:零扩展,高位直接补 0
MOZS:符号扩展,高位补符号位(根据补码的知识,不改变数值)
指令格式:mov(z/s)(S_typ)(T_typ)
例:movzbw 表示将byte传送到word,使用零扩展
特殊的,没有movzlq的指令,可以直接用 movl,原因上面
III. 压栈/弹出栈
程序栈向下增长:栈底元素的地址是最大的,栈顶元素地址是最小的。
栈顶指针 %rsp 保存栈顶元素的地址。
压栈和弹栈的指令:
popq D 表示把栈顶元素扔进 D 并且弹栈
pushq S 表示把 S 压进栈。
用 subq 和 movq 来模拟 pushq 的操作:懒得写。
IV. 算术和逻辑操作
1. 加载有效地址
指令: leaq S D
功能:将 S 的值赋给 D,注意与 movq 指令不同的是, S 不一定要是左值(真实存在的地址)
常用来简化计算:例如 leaq (%rdi,%rsi,4) %rax 表示把 %rax 的值赋为 4y+x
2A. 一元操作符
指令 | 效果 |
---|---|
INC D | D++ |
DEC D | D-- |
NEG D | D 取负 |
NOT D | D 取反 |
实际应用时,根据字节数加上b/w/l/q
2B. 二元操作符
指令格式: ADD S D 表示 D+=S
ADD,SUB,IMUL,OR,XOR,AND 类似
实际应用时,根据字节数加上b/w/l/q
注意没有除法,以及有符号乘法是IMUL,不是MUL(不要跟下面混淆了)
2C. 移位指令
指令 | 效果 |
---|---|
SAL/SHL k D | D<<=k |
SAR k D | D>>=k 补符号位 |
SHR k D | D>>=k 补零 |
实际应用时,根据字节数加上b/w/l/q
其中,k是一个立即数或存储在特殊的寄存器 %cl 里边,不可以存储在别的寄存器里。
小知识:xorq %rdx %rdx 常用来表示赋 0 操作,相比 leaq 指令所需长度更短一些。
2D. 拼数的算数操作
需要借用 %rax 和 %rdx 寄存器
%rdx 作高位,%rax 作低位拼成一个 128 位大整数,为了方便,记这个拼合而成的大整数所在寄存器为 R
指令 | 效果 |
---|---|
imulq S | \(R=S*\%rax\) 补码乘法 |
mulq S | \(R=S*\%rax\) 无符号乘法 |
idivq S | \(\%rdx=R mod S\) , \(\%rax=R/S\) 补码除法 |
divq S | \(\%rdx=R mod S\) , \(\%rax=R/S\) 有符号除法 |
cqto | 将 %rax 带符号地扩展到 R |
IV.控制
1. 条件码
CF: 最高位是否进位,用来检查无符号整数的溢出
ZF: 最近的操作得到结果为 0
OF: 最近的操作导致补码溢出或负溢出
SF: 最近的操作得到结果为负数
除了 leaq 以外的运算指令在执行完后会根据结果设置条件码。
如果不需赋值,只需要判断,可以使用以下指令:
cmpq S1,S2 根据 S2-S1 的值设置条件码,没有赋值操作
testq S1,S2 根据 S1&S2 的值设置条件码,没有赋值操作
2. 条件码的应用
SET(P137) 系列指令根据条件码之间的运算来指定一个寄存器的值。
e.g. sete %al 表示把寄存器 %al 的值设为 ZF
注意,有符号数和无符号数比较大小的算数运算是不同的,具体见下。
如何比较 \(a<b\) (有符号)?
计算 \(SF \wedge OF\)。解释:不妨假设 \(a\),\(b\) 的值都在 int32 内 ,(假设负溢出)溢出后的值先覆盖正数再覆盖负数,|a-b| 的实际值 \(\leq 2^{32}\),所以不会覆盖到负数部分。
如何比较 \(a<b\) (无符号)?
w 位的无符号数 a 和 b , \(a-b\) 在模 \(2^w\) 意义下等于 \(a+(2^w-b)\),所以无符号加法 a+b 的实现就是 \(a + b^{'}\),\(b^{'}\) 是 \(b\) 的各位取反再加上 \(1\)。
所以可以用 CF 判断 a 是否小于 b
3. 跳转
无条件跳转指令:jmp (+Label/Address)
有条件跳转指令:je/jne/jg/ja…… (+Label/Address)
根据条件码的值来决定是否跳转,详见 P139 表格。
对于二进制码,使用相对地址记录,相对地址为该指令的结尾。
例如 P140 程序 jmp .L2 (L2 是下面的一个 Label),编译机器码为 eb 03
,03 是指令的参数,表示要跳到该命令地址 +3 的位置。
反汇编器输出的是绝对地址。
上面的程序,链接前和链接后反汇编输出的结果是不同的。
细节:编辑器的偏移量为 1,2,或 4 个字节,以补码表示,所以当机器码为 74 f4 时,实际跳转参数为 -12(补码f4的源码为-12)
细节二:观察以下的机器码和对应的源码
400543:77 02 ja 400547
400545:5d pop %rbp
ja 跳转到的地址 +02 是从 ja 一行的 end:400545 开始的。
2. 程序的逻辑结构
1. if 语句的实现
将一般的 if-else 语句改写成 goto 形式:
if(test_expr) statement1;
else statement2;
改写后
if(!test_expr) goto case2;
statement1;goto case_fin;
case2:statement2;
case_fin:
翻译成汇编(略)
2. 用数据传送来实现条件分支
基于控制的条件转移:在 if 条件满足时执行一条语句线,否则执行另一条。
if(x<y) ans=y-x;
else ans=x-y;
基于数据的条件转移:if 内只有赋值操作
int v1=x-y,v2=y-x;
ans=v1;
if(x<y) ans=v2;
可以使用带条件的 mov 指令实现:如 cmovge(注意它不用显式指定数据类型)
为什么基于数据的条件转移会更快?
现代处理器使用分支预测来猜测条件跳转指令是否执行,猜测错会进行大幅重构,造成性能上的浪费。条件传送不依赖于分支预测,所以不会有分支预测错的情况,可以跑得更快。更多知识请看流水线一章。
不适合使用数据传送实现条件分支的情况:
x=(i>n?a[n]:a[i]);
如果需要使用数据传送实现条件分支,则需要先算出 a[n] 和 a[i],喜提数组越界。
比较聪明的编译器会根据不同开销情况择优决定使用控制或数据传送。
3. 循环
使用 if-goto 语句改写循环,略。
4. Switch 语句
当情况比较多,值的跨度比较小的时候,编译器使用跳转表来维护。
switch(n)
{
case 100:
case 102:
case 104:
case 106:
}
编译器先将n减去100,这样就把100-106映射到了0-6上面,开一个指针数组,a[0],a[2],a[4],a[6] 指向对应的分支,a[1],a[3],a[5],a[7] 指向 Switch 结束。
3. 函数调用
1.运行时栈
栈从高地址(栈底)到低地址(栈顶)
栈指针 %rsp 指向栈顶元素。
函数传送:前六个参数通过寄存器传递,后面在栈上开空间。
2. 函数调用
函数 P 调用 Q,将返回地址压入栈,指示 Q 返回时从 P 的哪个位置开始执行。再跳转到 Q 的开始。
P167 示例,注意 %rdi 和 %rax
3. 函数传参
参数 \(\leq 6\) ,使用寄存器传递: %rdi,%rsi,%rdx,%rcx,%r8,%r9
如果是 32 位,使用 %edi,%esi……传递
超过 6 的部分使用程序栈传递,栈顶/小地址存放参数列表里边放在后面的,栈底/大地址存放放在前面的。
注意参数的地址空间一般是从 %rsp+8(64位机)开始的,因为 %rsp 需要用来存放返回跳转的地址。
4.局部变量
如果局部变量足够少/他不需要地址,可以直接使用寄存器存储。
如果一个变量需要被引用/有指针指向它,则它需要在内存中有地址,不能仅用寄存器表示。
在栈上分配空间: %rsp-
空间对齐:看P172 Fig.3.33
5. 保存寄存器
调用者保存寄存器和被调用者保存寄存器
约定:P 调用 Q
被调用者保存寄存器: %rbp,%rbx,%12~%r15 (Q 在执行前备份,执行后复原)
调用者保存寄存器:其它寄存器(除%rsp) 指不保证 Q 调用过程中会不会发生改变,如果 P 之后用不到它其实可以不备份
指令:pushq/popq %rbx
注:P173 3.34 代码的第 4 行 subq 并不是为了分配空间。而是因为,call 指令要求执行前栈是 16 字节对齐的。注意不要忽略指针,所以栈里其实有三个8—Byte变量要存。
4. 数组,结构体
数组——指针(略)
二维数组:行优先顺序存储,对于数组 D[R][C],有 \(\&D[i][j]=x_D+i*C+j\)
结构体内的数据按照顺序存储,类似数组。
数据对齐
无论数据是否对齐,x86-64硬件都能正常工作。
基本原则:任何 K 字节的基本对象的地址都要是 K 的倍数
对于包含结构体的代码,编译器会在字段的分配间插入空隙,参考P190
- 任何内存分配函数生成的块的起始地址必须是 16 的倍数
- 大部分函数栈帧的边界是 16 字节的倍数
5. 综合,其它
1. 指针
函数指针 int (*fp)(int,int *)
表示 fp 是一个指向 int (int,int *) 类函数的指针。
2. gdb
配环境配不出来,vscode又不是不能用,先不管了
3. 内存越界引用与缓冲区溢出
栈上的局部变量:破坏存储在栈中的局部空间。
一种基本的攻击方法:喂给程序一段攻击代码,并利用缓冲区溢出修改返回指针,使指针指向攻击代码。P196 炼习 3.46 可直观理解。
3B. 对抗缓冲区溢出攻击
1. 栈随机化
在程序开始前,在栈上分配一块大小随机的空间,使每次执行时栈的位置发生变化。P196 的 gets 字符串攻击只能注入固定的返回值地址,所以这里就不管用了。
2. 栈破坏检测
在返回地址前加入一个随机值,每次返回函数前检测这个随机值是否被改变,被改变说明爆栈了。
为了更好的防护栈顶,编译优化会把数组放在栈顶,局部变量放在靠近栈底的位置。
3. 限制可执行代码区域
字面意思。
4. 变长栈帧
void f()
{
int n;scanf("%d",&n);
int a[n];
}
编译器无法在程序开始之前得知这个函数的栈帧要给多大.
回顾一般的情况:函数开始时 %rsp 先下移 x 个 Byte,结束时上移 x 个即完成栈上空间释放。
变长栈帧:函数开始时把开头(也就是之后%rsp要返回的地方用%rbp)存储。之后正常分配空间。
对应的指令:
pushq %rbp
movq %rsp %rbp
...
movq %rbp %rsp
popq %rbp
最后两行可以用 leave 指令替代。
6. 浮点
咕。
标签:CSAPP,跳转,地址,指令,寄存器,C3,rsp,rax From: https://www.cnblogs.com/ss80194/p/17989541