《深入理解计算机系统(第三版)》第三章

3.1 程序编码

1.计算机系统使用了多种不同形式的抽象,对于机器级编程来说,两种抽象尤为重要:

  • 指令集体系结构(ISA):定义了处理器状态、指令的格式,以及每条指令对状态的影响
  • 机器级程序使用的存储器地址是虚拟地址:提供的存储器模型看上去是一个非常大的字节数组

2.反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有区别。反汇编省略了指令结尾的q,给call和ret指令添加了q后缀。

3.可执行程序反汇编和对.c反汇编产生的代码有差别。对于可执行文件的反汇编,链接器将代码的地址移到了一段不同的地址范围,链接器的任务之一就是为函数调用找到匹配的函数的可执行代码的位置。

3.2 数据格式

GCC生成的汇编代码指令都有一个字符的后缀,表明操作数的大小、。后缀l可以表示4字节整数和8字节双精度浮点数,但是并没有歧义,因为浮点数使用的是一组完全不同的指令和寄存器。

3.3 访问信息

1.x86-64的CPU包含一组16个存储64位值的通用目的寄存器,用来存储整数数据和指针。

2.不同操作数可能被分为三种类型,分别为立即数(表示常数)、寄存器(表示某个寄存器的内容)、内存引用(根据计算出来的地址访问某个内存位置)。

3.传送指令两个操作数不能都指向内存位置。MOV指令只会更新目的操作数指定的那些寄存器字节或内存位置。唯一的例外是movl指令以寄存器作为目的时,会把寄存器的高位4字节设置为0。movq指令只能以表示为32位补码数字的立即数作为源操作数,然后把这个值符号扩展得到64位的值,放到目的位置。

4.MOVZ类中的指令把目的中剩余的字节填充为0,MOVS类中的指令通过符号扩展来填充,把源操作的最高位进行复制。它们均以寄存器或内存地址作为源,以寄存器作为目的。

把4字节源值零扩展到8字节逻辑上应该是movzlq,但并没有这样的指令。可以使用movl来实现(movl指令会把寄存器的高位4字节设置为0)。

3.4 算术和逻辑操作:

如果寄存器%eax的值为x,那么指令leal 3(%edx, %edx, 2),%eax将设置%eax的值为2x+3。

移位量可以是一个立即数,或者放在单字节寄存器%cl中。左移指令有SAL和SHL,两者效果一样,都是将右边填上0,而右移指令不同,SAR执行算术移位(填上符号位),而SHR执行逻辑移位(填上0)。

无符号数乘法(mulq)和补码乘法(imulq)要求一个参数必须在%rax中,另一个作为指令的源操作数给出。乘积存放在%rdx(高64位)和%rax(低64位);有符号除法idivl 将寄存器 %rdx(高32位)和 %rax(低32位)中的64位数作为被除数,而除数作为指令的操作数给出。指令将商存储在%rax中,将余数存储于%rdx中。

3.5 控制

1.条件码寄存器描述了最近算术或逻辑运算的属性,可以检测这些寄存器来执行条件分支指令:

  • CF:进位标志。可用来检查无符号操作的溢出。如:(unsigned)t < (unsigned)
  • ZF:零标志。如:(t == 0)
  • SF:符号标志。如:(t < 0)
  • OF:溢出标志,最近的操作导致了补码溢出。如:(a<0==b<0)&&(t<0!=a<0)

2.leaq 指令不会设置条件码,除过前面提到的指令外,CMP(和SUB行为一样)和TEST(和ADD行为一样)指令会设置条件码,但不改变任何其他寄存器。testq %rax %rax用来检查 %rax 是零、正数还是负数。

3.条件码通常不会直接读取,通常使用的方法有三种:

  • 可以根据条件码的某种组合,将一个字节设置为0或者1。
  • 可以条件跳转到程序的某个其他部分。
  • 可以有条件的传送数据

SET指令时条件码的组合,执行比较指令,根据计算t=a-b设置条件码。有符号比较测试基于SF、OF和ZF的组合,无符号比较测试基于CF和ZF。

4.jump 指令有三种跳转方式:直接跳转、间接跳转(‘*’后跟一个操作数指示符)、其他条件跳转(根据条件码的某个组合,或者跳转,或者继续执行代码序列中的下一条指令)。

常用的PC相对的对于跳转指令的编码会将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差作为编码。

5.汇编中没有do-while、while和for相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。大多数汇编器中都要先将其他形式的循环转换成do-while格式。

do-while的通用形式可以翻译成如下所示的条件和goto语句:

loop:
    body-statement
    t=test-expr;
    if(t)
        goto loop;

while循环第一种翻译方式跳转到中间:

    goto test;
loop:
    body-statement
test:
    t=test-expr;
    if(t)
        goto loop;

第二种翻译方式为首先用条件分支,如果初始条件不成立就跳过循环,转化为do-while循环:

t=test-expr;
if(!t)
    goto done;
loop:
    body-statement
    t=test-expr;
    if(t)
        goto loop;
done:

for循环可以很容易转换成while循环,进而转换成do-while形式:

    init-expr;
    t=test-expr;
    if(!t)
        goto done;
loop:
    body-statement
    update-expr;
    t=test-expr;
    if(t)
        goto loop;
done:

switch语句的跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当开关索引值等于i时程序应该采取的动作。

3.6 过程

1.过程提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。过程机制的构建需要实现传递控制、传递数据、分配和释放内存。

2.过程P可以传递最多6个整数值,如果Q需要更多参数,P可以在调用Q前在自己的栈帧里存储好这些参数。寄存器最多传输6个小于等于64位的数据,并通过%rax返回数据。如果一个函数有大于6个整型参数,超出6个的部分就通过保存在调用者的栈帧来传递。

3.%rbx、%rbp和%r12~%15被调用者保存,在使用前被调用者要把这里面的值保存好,保证其值在返回时和调用时是一样的(这里就像有我有一辆豪车,可以把车子借给朋友使用,但是一定要把钥匙保存好,用完了之后还回来),这让我想到了之前看过的汇编代码在被调用函数的第一步都是 push %ebp.

4.所有其他寄存器,除了%rsp为调用者保存,意味着任何函数都能修改它们,则在调用前首先保存好这个数据是调用者的责任。(这里的调用者就像很有票子的王健林一样,儿子王思聪可以无偿的使用王健林的票子)

参考《深入理解计算机系统》| 程序的机器级表示

5.递归的调用其实与其他函数的调用是一样的,因为每个过程调用在栈中都有私有的空间,多个未完成调用的局部变量不会相互影响。

3.7 数据分配和访问

1.设 xA 表示起始位置,则访问数组元素 A[i] 的位置在 xA+ L*i,L为数据类型的大小(单位为字节)。数组元素的访问一般借助存储器引用指令。如计算 int 型的 E[i]: E 的地址存放在 %rdx 中,而 i 存放在 %rcx 中。movl (%rdx,%rcx,4),%eax 表示计算地址 xE+4i,并读取这个存储器位置的值,将结果放到 %eax 中。

2.如果 P 是一个执行类型 T 的数据的指针,P 的值为 xP,那么表达式 P+i 的值为 xP + L*i,L 是数据类型T的大小。假设整型数组 E 的起始地址和整数索引 i 分别存放在 %rdx 和 %rcx 中,下面是一些与 E 有关的表达式,可以明显看出 leal 和 movl 的区别(前者产生地址,后者引用内存):

3.数组的嵌套,也就是数组的数组。对于数组 int A[5][3],可以将 A 看成是一个有 5 个元素的数组,而每个元素都是 3 个 int 类型的数组。计算D[R][C](int 型)的地址:

 &D[i][j] = xD + L(C * i + j) 

由于每组有 C 个数据,所以跳过一组就要乘以C,跳过I组就 C*i 个,再加上偏移的 j 就是所求地址。

3.8 异质的数据结构

1.结构:所有的组成部分在存储器中连续存放,指向结构的指针指向结构的第一个字节。

2.联合:允许以多种类型来引用一个对象,总大小等于它最大字段的大小,而指向一个联合的指针,引用的是数据结构的起始位置。

3.x86-64系统对齐要求为:对于任何需要K字节的标量数据类型的起始地址必须是K的倍数。汇编中.align 8要求后面的数据起始位置是8的倍数。结构体的对齐除了要满足每个字段的对齐要求,还需要考虑整体的结构满足怎样的对齐要求。

如:

struct test {
    int i;
    int j;
    char c;
}; 

我们能保证起始地址4字节对齐要求,但struct s2 d[4]就不能满足 d 的每个元素的对齐要求,因为这些元素的地址分别为xd,xd+9,xd+18和xd+27,所以为s2分配12个字节。

3.9 在机器级程序中将控制与数据结合起来

1.void * 表示通用指针,malloc函数返回一个通用指针,然后转换成一个有类型的指针。

2.指针从一个类型转为另外一个类型,只是伸缩因子变化,不改变它的值。如 p 是一个 char * 类型的指针,值为p,(int * )p + 7计算为 p+28 ,而(int * )(p + 7)计算为 p+7。

3.C对数组引用不做边界检查,同时局部变量和状态信息(寄存器值和返回指针等)都存放在栈中,这使得越界的数组写操作会破坏存储在栈中的状态信息。常见的状态破坏称为缓冲区溢出。

栈是向低地址增长的,数组缓冲区是向高地址增长的。故上图所示 buf[8] 在输入超过 8 个时就会覆盖栈上存储的某些信息。如果破坏了存储的返回地址,那么ret指令会使程序跳转到完全意想不到的地方(如跳转到攻击代码)。使用gets或strcpy、strcat、sprintf等能导致存储溢出的函数(不需要告诉它们目标缓冲区的大小就产生一个字节序列),都不是好的编程习惯。

4 对抗缓冲区溢出攻击的方法:

  • 栈随机化:使得栈的位置在程序每次运行时都有变化。实现的方式是程序开始时,在栈上分配一段0--n字节之间的随机大小空间
  • 栈破坏检测:在栈中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值。这个金丝雀值是在程序每次运行时随机产生的,因此,攻击者没有简单的办法知道它是什么。在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者函数调用的某个操作改变了。如果是,那么程序异常终止
  • 限制可执行代码区域:限制那些能够存放可执行代码的存储器区域

3.10 浮点代码

浮点数操作和整数操作很类似,指令命名上有区别,故此部分简述。

1.AVX浮点体系结构允许数据存储在16个YMM寄存器中,每个YMM寄存器是256位。对标量数据(单个数据)操作时,寄存器只保存浮点数,而且只使用低32位(float)或64位(double)。

2.浮点传送指令:

3.浮点转换指令:

4.%xmm0~%xmm7最多可以传递8个浮点参数,额外参数通过栈传递。%xmm0返回浮点数。XMM寄存器都是调用者保存,被吊用着不用保存就覆盖这些寄存器中的任意一个。当函数包含指针、整数和浮点数混合的参数时,指针和整数通过通用寄存器传递,浮点值通过XMM寄存器传递。

5.浮点运算操作(第一个源操作数S1可以是XMM寄存器或内存位置,第二个源操作数和目的操作数必须是XMM寄存器):

6.AVX浮点操作不能以立即数值作为操作数,编译器需要为所有常量分配和初始化存储空间(从标号为 .LC2 的内存位置读出 1.8,从标号为 .LC3 的内存位置读出 32.0):

7.位级操作:

8.浮点比较操作(S2必须在XMM寄存器中):

浮点比较指令会设置ZF、CF、奇偶标志位PF(当浮点操作数中任一个时NaN会设置该位):

3.11 问题及解决

B中最大为long,所以以8字节对齐,我想当然地将 i 后填充4,c和d后填充7,总共为32字节。看了答案是16字节后,我意识到 i、c、d “拼”一起依然小于8字节,所以应该是在它们后填充2字节,总共就为16字节。如果像我那样做的话就太浪费存储空间了。

E中8字节对齐,P3结构体数组中第二、三个元素c[2]、c[3] 2字节还能和P2结构体的i、c、d “拼”,为什么答案 t 的起始位置为24了,像是没把它们拼一起,直接在c[3]后扩充6字节?最后想了想结构体填充的规则,如果拼一起 t 的起始位置为 18,不是8的倍数。

另外有一问题未解决,习题3.9中在一片movq、salq、sarq中出现了movl,感觉有点奇怪,虽然只有最低位的字节指示着移位量的解释能接受,那在这里使用 movq 和 movl 有什么区别?是效率上的区别吗?书中还有很多地方出现 q、l、b、w “混用”的例子,什么时候该用什么时候不该用呢?

原文地址:https://www.cnblogs.com/Jspo/p/8410156.html

时间: 2024-11-13 14:55:53

《深入理解计算机系统(第三版)》第三章的相关文章

汇编语言第三章知识梳理及思考

第三章 内存访问的角度学习寄存器 3.1内存中字的存储 0号单元是低地址单元,1号单元是高地址单元. 问题: (1)0地址单元(字节单元)中存放的字节型数据是多少?20H (2)0地址字单元中存放的字型数据是多少?4E20 (3)2地址字单元中存放的字节型数据是多少?12H (4)2地址单元中存放的字型数据是多少?0012H (5)1地址字单元中存放的字型数据是多少?124EH 结论:任何两个连续的内存单元,N号单元和N+1号单元,可以将它们看成一个地址为N的字单元中的高位字节单元N+1和低位字

汇编语言第三章总结

第三章中,我们从访问内存的角度了解学习几个寄存器 下面将知识点总结如下: 一.16位寄存器存储一个字,就要用高8位存放高位字节,低8位存放低位字节 0为低地址单元,1为高地址单元,表示数据4E20H(20000),起始地址是0,即是0地址字单元 存储字用两个单元,逆序存放,而存储字节只用一个单元 二.mov al,[0]  表示将内存单元内容存入寄存器,[]中的0表示偏移地址,DS寄存器,存放访问数据的段地址 用mov指令只能通过如 mov bx,1000 mov ds,bx 为DS寄存器赋值,

汇编语言第三章小结

第三章 内存访问 字数据在内存中的存储 1.      内存以字节为单位,划分为若干个单元 2.      字数据的存-取原则:高-高  低-低(小端法) 即: ① 字数据的低位字节存放在低地址内存单元 字数据的高位字节存放在高地址内存单元 ② 取低地址内存单元地址作为字数据地址 例1: 字数据124EH的地址是( 1 ) 字数据3020H存入内存后地址为4,则4存放( 30H ),3存放( 20H ) 例2: 从地址单元1取出一个 (1)     字节数据为:  4EH (2)     字数据

汇编语言第三章

一.知识点(博主自认为关键的几点) 1.把值存入段寄存器ds:需通过先给数据寄存器值(ax, bx, cx, dx),再送入ds中 如: mov ax,2000 mov ds,ax 2.字数据的存取原则: 字数据的低位字节存放在低地址内存单元:高位则放于高位地址:(小端法) 如:124EH在内存中的存放位置如下: (且地址为[1]) 3.关于mov指令值得注意的一点,当作为第二个操作数的常数最高位为a-f时,前面要加0 4.SS:SP,与ds相同,ss也不能直接送值 二.小测试 栈的使用主要需要

汇编语言:第三章 寄存器(内存访问)

3.1内存中字的存储 CPU中寄存器是16位的,可以用高低字节存储一个字,但是每个内存单元是8位的,只能存储一个字节, 所以内存中用相邻2个内存单元存储一个字的高低字节 如:20000数值(4E20H)在地址0的内存单元数值为20H,在地址1的内存单元数值为4EH 两个内存单元存储一个字型数据叫做一个字单元, 字单元的起始地址为N就叫N地址字单元,表示一个字的低字节在地址N的内存单元,高字节在地址N+1的内存单元 任意连续的2个内存单元都可以组成一个字单元 3.2 DS和 [address] C

汇编语言第三章——寄存器(内存访问)

1.内存中字的存储 高位字节存放在高地址单元中,地位字节存放在低地址单元中. 将起始地址为N的字单元简称为N地址字单元. 2.DS和[address] 8086CPU自动取DS中的数据为内存单元的段地址. 8086CPU不支持将数据直接送入段寄存器的操作. 3.字的传送 mov ax,[0]:字型数据传送 mov al,[0]:字节数据传送 原因:高位字节存放在高地址单元中,地位字节存放在低地址单元中. 4.mov,add,sub指令 mov指令的几种形式: mov 寄存器,数据 mov 内存单

汇编语言 第三章 寄存器

3.1 内存中字的存储: 在内存存储时,由于内存单元是字节单元(一个单元存放一个字节),所以一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节则会存放在高地址的单元中. 字单元,即存放一个字型数据的内存单元,由两个地址连续的内存单元组成.高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节. 3.2 Ds和address: CPU想要读取一个内存单元时,必须先给出这个内存单元的地址.8086中有一个DS寄存器,通常用来存放要用的数据的段

汇编语言 第三章 寄存器(内存访问)

1.字数据在内存中的存储 (1)内存以字节为单位划分为若干个单元. (2)字在储存时要用两个地址连续的内存单元来存放. (3)字数据的存-取原则:高-高,低-低.即小端法,低位字节存放在低地址内存单元,高位字节存放在高地址内存单元,并且取低地址内存单元地址作为字数据地址. (4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器.低地址单元和低8为寄存器相对应. 2.数据在内存单元和CPU之间的传送 (1)执行命令时,默认情况下,8086自动取寄存器DS中的值为内存单元的段地址. 1)将

汇编语言 第三章

3.1 内存中字的存储:CPU中,用16位寄存起来存储一个字.高8位存放高位字节,低8位存放低位字节.在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中. 字单元的概念:字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成. 3.2 DS和[address]:8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址. 指令执行时,8086CPU自动

汇编语言程序设计第三章

第三章 寄存器(内存访问) 1 内存中字的存储 1个字=2个字节,如从0开始存放20000(4E20H),20.4E分别表示1个字节,内存中字的存储如下图所示: 任何两个地址连续的内存单元(0.1.2......),N号单元和N+1号单元,可以看成两个内存单元.比如说上图中的0内存单元(字节单元),存放的是字节型数据,就是20H(十进制的32):0地址字单元,存放字型数据,就是4E20H. 任何两个地址连续的内存单元,N号单元和N+1号单元,也可以看成一个地址为N的字单元中的高位字节单元和低位字