深入理解计算系统第三章程序的机器级表达总结

1.

2.

3.

4.

5.

6.

7.

8.

程序编码

2.数据格式 字表示16位数据类型

C声明 Intel数据类型 汇编代码后缀 大小(字节) char 字节 b 1 short 字 w 2 int 双字 l 4 long 四字 q 8 char * 四字 q 8 flota 单精度 s 4 double 双精度 l 8

大多数GCC生成的汇编会有一个字符的后缀表示操作数的大小 例如 movb(传送字节),movw(传送字),movl(传送双字),movq(传送四字)

2.1访问信息(寄存器) 2.2操作数指示符 有三种操作数类型

    立即数 在ATT书写方式是 $后面跟个整数 寄存器 他表示某个寄存器的内容我们用符号ra代表寄存器a 用R[ra]表示他的值 ** ** 它会根据计算出的地址访问内存位置 因为将内存看成一个很大的字节数组,我们用符号Mb[Addr]表示存储在内存中从地址addr开始的b个字节值的引用,为了简便通常省去b

2.3数据传送指令 将一个数据从一个位置复制到另一个位置的指令 最简单的数据传送指令 ------ mov类 把源位置数据复制到目的位置

指令 效果 描述 mov S,D D <- S 传送 movb 传送字节 movw l传送字 movl 传送双字 movq 传送四字 movabsq R<-I 传送绝对四字

限制:传送指令的两个操作数不能都指向内存 将一个值从一个内存位置复制到另一个内存位置需要两条指令 第一条先加载到寄存器 第二条从寄存器写到目的位置

将较小的源复制到较大的目的时 有两种扩展操作

指令 效果 描述 movz S,R R<-零扩展(s) 以零扩展进行传送 movzbw 将做了零扩展的字节传送到字 movzbl 将做了零扩展的字节传送到双字 movzwl 将做了零扩展的字传送到字 movzbq 将做了零扩展的字节传送到四字 movzwq R<-I 将做了零扩展的字传送到四字
指令 效果 描述 movs S,R R<-符号扩展S 传送符号扩展的字节 movsbw 将做了符号扩展的字节传送到字 movsbl 将做了符号扩展的字节传送到双字 movswl 将做了符号扩展的字传送到字 movsbq 将做了符号扩展的字节传送到四字 movswq 将做了符号扩展的字传送到四字 movslq 将做了符号扩展的双传送到四字 cltq %rax<-符号扩展(%eax) 把%eax符号扩展到%rax

cltq //只作用于%eax 和 %rax 2.4压入和弹出栈数据 栈属性是后进先出 从栈顶进行插入和删除元素 栈顶向下增长 栈顶元素地址最低往栈底逐渐增大

指令 效果 描述 pushq S R[%rsp]<-R[%rsp]-8; M[R[%rsp]] 将四字压入栈 popq D D<-M[R[%rsp]]; R[%rsp]+8 将四字弹出栈

3.算术和逻辑操作

指令 效果 描述 leaq S,D D<-&S 加载有效地址 INC D D<-D + 1 加1 DEC D D<-D - 1 减1 NEG D D<- -D 取负 NOT D D<- ~D 取补 ADD S,D D<-D + S 加 SUB S,D D<-D -S 减 IMUL S,D D<-D *S 乘 XOR S,D D<-D ^ S 异或 OR S,D D<-D or S 或 AND S,D D<-D & S 与 SAL S,D D<-D << k 左移 SHL S,D D<-D << k 左移(等同于SAL SAR S,D D<- D>> k 算数右移 SHR S,D D<- D >> k 逻辑右移

3.1加载有效地址 leaq它的指令形式是从内存读数据到寄存器 但是它实际上根本没有引用内存,第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数 例如leaq可以当算术操作 leaq 7(%rdx,%rdx,4) ,% rax 设置寄存器%rax的值位5R[%rdx] + 7 3.2一元操作和二元操作 一元操作: 只有一个操作数既是源又是母的 所以可以是一个寄存器也可以是一个内存地址 例如: incq(%rsp)会让栈顶的八字节元素加一 二元操作: 第二个操作数既是源又是目的,不过源操作数是第一个,木点操作数是第二 例如 subq %rax,%rdx 从%rdx减去%rax ,第一个操作数可以是立即数,寄存器或者地址。第二个操作数可以是寄存器或者地址 ,注意:当第二个操作数为内存地址时,寄存器必须从内存读出值,执行操作,再把结果写回内存 3.3移位操作 先给出移位量再给出要移位的数。移位量可以是一个立即数,或者放在单字节寄存器%cl(这些指令很特别,因为只允许这个特定的寄存器作为操作数)

3.4特殊的算术操作

指令 效果 描述 imulq S R[%rdx]: R[%rax]<- S * R[%rax] 有符号全乘法 mulq S R[%rdx]: R[%rax]<- S * R[%rax] 无符号全乘法 clto R[%rdx]: R[%rax]<- 符号扩展R[%rax] 转换为八字 idivq S R[%rdx]: R[%rdx]<- R[%rax] mod s; R[%rdx]: R[%rdx]<- R[%rax] / s 有符号除法 divq S R[%rdx]: R[%rdx]<-R[%rax] % s; R[%rdx]: R[%rdx]<- R[%rax] / s 无符号除法

给出个例子说明如何 x86-64如何实现除法

void remdiv(long x,long y,long *qp,long *rp){
          
   
	long q = x / y;
	long r = x % y;
	*qp = q;
	*rp = r
}
//汇编代码如下
void remdiv(long x,long y,long *qp,long *rp)
x in %rdi,y in %rsi,qp in %rdx, rp in %rcx
rediv:
	movq %rdx,%r8  复制qp
	movq %rdi,%rax 传给%rax
	cqto //隐含读出%rax的符号位 并且复制到 %rdx所有位
	idviq %rsi 
	movq %rax (%r8) 将商保存在 qp
	movq %rdx,(%rcx) 将余数保存在rp
	ret

4.控制

4.1条件码寄存器 除了整数寄存器外 CPU还维护者一组但各位的条件码寄存器 他们教书了最近的算术与逻辑操作的属性可以检测这些寄存器来检测这些寄存器执行条件分支指令。最常用的条件码有 : 1.CF:进位标志。最近的操作使最高位产生了仅为,可以用来检查无符号的操作的移除。 2.ZF:零标志。最近的操作得出的结果为0。3.SF:符号标志。最近的操作的得到的结果为负数。4.OF:溢出标志。最近的操作导致一个补码溢出----正溢出或者负溢出。 下面有一些指令能够设置条件码但是不会改变其他任何寄存器

指令 效果 描述 CMP S1,S2 S2 - S1 比较 cmpb 比较字节 cmpw 比较字 cmpl 比较双字 cmpq 比较四字
指令 效果 描述 TEST S1,S2 S2 & S1 测试 testb 测试字节 testw 测试字 testl 测试双字 testq 测试四字

4. 2访问条件码 条件码读取常用的三种方法:1.可以根据条件码的某种组合将一个字节设置为0或者1 2.可以条件跳转到程序的某个其他部分 3.可以有条件的传送数据

指令 同名 效果 描述 sete D setz S2 - S1 相等/零 setne D setnz D<-ZF 不等/非等 sets D D<- ~ZF 负数 setns D D<-SF 非负数 setg D setnle D<- ~(SF^OF)& ~ZF 大于(有符号>) setge D setnl D<- ~(SF^OF) 大于等于(有符号>=) setl D setnge D<- SF^OF 小于(有符号<) setle D setng D<-(SF^OF)orZF 小于等于(有符号<=) seta D setnbe D<- ~CF& ~ZF 超过(无符号>) setae D setnb D<- ~CF 超过或等于(无符号>=) setb D setnae D<- CF 低于(无符号) setbe D setna D<- CF or ZF 低于或都相等(无符号<=)

4.3. 跳转指令

jmp Label是直接跳转,即跳转目标作为指令的一部分编码的 jmp *后面跟一个操作数指示符 是间接跳转 举个例子 指令 jmp *%rax 用寄存器%rax中的值作为跳转目标,而指令以%rax中的值作为都地址,从内存中读出跳转目标 4.4跳转指令的编码 1.最常用的是PC相对的,也就是说它们会将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差作为编码。 2.绝对地址,用4个字节直接指定目标,汇编器和连接器会选择适当的跳转目的编码 4.5用条件控制来实现条件分支 使用goto 4.6使用条件传送来实现条件分支

指令 同名 效果 描述 cmove S,R cmovz S2 - S1 相等/零 cmovne S,R cmovnz D<-ZF 不等/非等 cmovs S,R D<- ~ZF 负数 cmovns S,R D<-SF 非负数 cmovg S,R cmovnle D<- ~(SF^OF)& ~ZF 大于(有符号>) cmovge S,R cmovnl D<- ~(SF^OF) 大于等于(有符号>=) cmovl S,R cmovnge D<- SF^OF 小于(有符号<) cmovle S,R cmovng D<-(SF^OF)orZF 小于等于(有符号<=) cmova S,R cmovnbe D<- ~CF& ~ZF 超过(无符号>) cmovae S,R cmovnb D<- ~CF 超过或等于(无符号>=) cmovb S,R cmovnae D<- CF 低于(无符号) cmovbe S,R cmovna D<- CF or ZF 低于或都相等(无符号<=)

4.7switch语句 5.过程 过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。设计良好的软件用过程作为抽象机制,隐藏某个行为的具体实现,同时又提供清晰简洁的接口定义,说明要计算的是哪些值,过程会对程序状态产生什么样的影响。不同编程语言中,过程的形式多样:函数(function)、方法(method)、子例程(subroutine)、处理函数(handler)等等,但是它们有一些共有的特性。 要提供对过程的机器级支持,必须要处理许多不同的属性。 传递控制。在进入过程Q的时候,程序计数器必须被设置为ρ的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的地址 传递数据。P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。 分配和释放内存。在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间 人们花了大量的力气来尽量减少过程调用的开销。所以,它遵循了被认为是最低要求策略的方法,只实现上述机制中每个过程所必需的那些。 5.1运行时栈 当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间,这个部分称为过程的栈帧 P调用了Q之后要把P之前的状态保存并且Q的下一条指令返回地址放进栈里面 这才算一个完整的帧 5.2转移控制 将控制从函数P转为函数Q只需要把PC设置为Q的代码的起始位置。不过从Q返回后我们需要记录继续P的执行代码位置。在X86-64机器中,这个信息是用指令CALL Q调用过程Q来记录的。该指令会把地址A压人栈中,并将PC设置为Q的起始地址。压人的地址A被称为返回地址,是紧跟在ca11指令后面的那条指令的地址。对应的指令re会从栈中弹出地址A,并把PC设置为A。

指令 描述 call Label 过程调用 call .Operand 过程调用 ret 从过程调用中返回

5.3数据传送 x86-64中,大部分过程间的数据传送是通过寄存器实现的。 x86-64中,可以通过寄存器最多传递6个整型(例如整数和指针)参数。寄存器的使用是有特殊顺序的,寄存器使用的名字取决于要传递的数据类型的大小,如图3-28所示。 会根据参数在参数列表中的顺序为它们分配寄存器。可以通过64位寄存器适当的部分访问小于64位的参数。例如,如果第一个参数是32位的,那么可以用edi来访问它 5.4栈上的局部存储 有些时候,局部数据必须存放在内存中,常见的情况包括:

    寄存器不足够存放所有的本地数据。 对一个局部变量使用地址运算符‘&’,因此必须能够为它产生一个地址。 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。在描述数组和结构分配时,我们会讨论这个问题 一般来说,过程通过减小栈指针在栈上分配空间。分配的结果作为栈帧的一部分,标号为“局部变量” 运行时栈提供了一种简单的、在需要时分配、函数完成时释放局部存储的机制。 5.5寄存器中的局部存储空间 寄存器组是唯一被所有过程共享的资源。 虽然在给定时刻只有一个过程是活动的,我们仍然必须确保当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器值。为此,x86-64采用了一组统一的寄存器使用惯例,所有的过程(包括程序库)都必须遵循 根据惯例,寄存器%rbx、%rbp和%r12~%r15被划分为被调用者保存寄存器。当过程P调用过程Q时,Q必须保存这些寄存器的值,保证它们的值在Q返回到P时与Q被调用时是一样的。 过程Q保存一个寄存器的值不变,要么就根本就不去改变他要么就把原始值压入栈中,改变寄存器的值,然后在返回前从栈中弹出旧值。压入寄存器的那一部分值在栈帧中创建标号为"保存的寄存器“的一部分。 所有其他的寄存器,除了栈指针各%rsp,都分类为调用者保存寄存器。这就意味着任何函数都能修改它们。可以这样来理解“调用者保存”这个名字:过程P在某个此类寄存器中有局部数据,然后调用过程Q。因为Q可以随意修改这个寄存器,所以在调用之前首先保存好这个数据是P(调用者)的责任 5.6递归过程 前面已经描述的寄存器和栈的惯例使得x86-64过程能够递归地调用它们自身。每个过程调用在栈中都有它自己的私有空间,因此多个未完成调用的局部变量不会相互影响此外,栈的原则很自然地就提供了适当的策略,当过程被调用时分配局部存储,当返回时释放存储。
long rfact(long n){
          
   
	long result;
	if(n <= 1) result = 1;
	else result = n * rfact(n - 1);
	return result;
}

long rfact(long n)
n in %rdi
1 rfact:
2	pushq %rbx
3	movq %rdi,%rbx
4	movl $1,%eax
5	cmpq $1,%rdi
6	jle .L35
7	leaq -1(%rdi),%rdi
8	call rfact
9	imulq % rbx,%rax
10.L35:
11	popq %rbx
12	ret

可以从汇编代码看出来使用寄存器%rbx保存参数n,把已有的参数保存在栈上(第二行)随后在返回前恢复该值(第11行)。根据栈的使用特性和寄存器保存规则,可以保证当递归调用rfact(n - 1)返回时(第9行),(1)该次调用的结果会保存在寄存器%rax中,(2)参数n的值任然保存在寄存器%rbx中,把这两个值相乘就能得到期望的结果。 从这个例子我们可以看到,递归调用一个函数本身与调用其他函数是一样的。栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置和被调用者保存寄存器的值)存储空间。如果需要,它还可以提供局部变量的存储。栈分配和释放的规则很自然地就与函数调用返回的顺序匹配。这种实现函数调用和返回的方法甚至对更复杂的情况也适用,包括相互递归调用(例如,过程P调用Q,Q再调用P)。

6.异质的结构 6.1.联合 允许多种类型引用一个对象

union u3{
          
   
	char c;
	int i[2];
	double v;
};
C,i,v偏移量都是为0 u3大小为8字节

6.2数据对齐 许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是 某个值K(通常是2,4或8)的倍数。这种对齐限制简化了形成处理器和存储器系统之间的接口的硬件 设计。例如,假设一个处理器总是从存储器中取出8个字节,则地址必须为8的倍数。如果我们能保 证将所有的double类型数据的地址对齐成8的倍数,那么就可以用一个存储器操作来读或者写值了。 否则,我们可能需要执行两次存储器访问,因为对象可能被分放在两个8字节存储块中。 强制对齐:任何内存分配函数(alloca,malloc,calloc,readlloc)生成的快的起始地址都是16的倍数 大多数栈帧的边界都必须是16的倍数

7.在机器及程序中将控制与数据结合起来 7.1内存越界引用和缓冲区溢出 C对于数组引用不进行任何边界检查 而且局部变量和状态信息都会存在栈中 所以两种结合可能会导致严重的程序错误 可能会覆盖掉存储的返回地址值 这样的话 ret会跳到不知到哪里去 7.2对抗缓冲区溢出攻击 地址空间布局随机化 简称ASLR,包括程序代码,库代码,栈,全局变量和堆数据,都会被加载到内存的不同区域。 不过有些攻击者会采用空操作雪橇,意思就是插入nop操作使得滑过这个序列 如果建立一个256字节的nop sled 那么枚举2 ^15个起始地址就能破解 n = 2^23的随机化 7.3栈破坏检测 在C/C++语言中,由于代码书写人员能够直接通过指针来操作内存的内容,在通常的时候没有可靠的方法来防止对数组的越界访问读写操作。但是,我们可以在发生了越界访问的时候,在没有造成任何有害结果之前,尝试检测到他。栈保护机制是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的“金丝雀值”,也称为“哨兵值”。 7.4支持变长栈帧

long vframe(long n,long idx,long *p){
          
   
	long i;
	long *p[n];
	p[0] = &i;
	for(i = 1; i < n; i++)
		p[i] = q;
	return *p[idx];

计算机不确定分配多少空间。在执行中必须能够访问局部变量 i 和数组p中的元素,返回时该函数必须释放这个栈帧,并将栈指针设置为存储返回地址的位置。为了管理变成栈帧 x86-64使用寄存器 %rbp作为帧指针 %rsp作为栈指针 首先对于从s1开始 因为栈起始点需要从16的倍数开始 所以在汇编里 我们首先就是先找一段对于当前位置偏移量第一个大于8n并且是16的倍数的位置 然后再从这个位置找到第一个对于当前来说倍数为16的位置 所以 才有了 e1 和 e2两段空出来的值 8.浮点代码 8.1浮点传送和转换操作

8.2过程中的浮点代码 8.3浮点运算操作 8.4定义和使用浮点常数 8.5在浮点代码中使用位级操作


8.6对浮点代码的观察结论

经验分享 程序员 微信小程序 职场和发展