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

20179202杨晓桐 20179202杨晓桐     2022-10-13     419

关键词:

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 “混用”的例子,什么时候该用什么时候不该用呢?

读书笔记《深入理解计算机系统》(第三版)概述

  《深入理解计算机系统》第三版刚出来不到一周,便买下了这本书;之所以阅读本书,一方面源于网友推荐以及豆瓣不错的评分、评价;另一方面是针对本人非科班出身,计算机系统相关的知识相对比较薄弱,很多情况下此... 查看详情

《深入理解计算机系统》(第三版)读书疑问

问题:第一章helloworld是怎样工作的?预处理器、编译器、汇编器、链接器是怎样把.c的源程序分别修改为.i、.s.、.o的程序的?第二章反码和补码在作用上有什么区别?第三章直到型循环和当行循环有什么异同?第四章Y86指令集... 查看详情

《深入理解计算机系统(第三版)》第二章信息的表示和处理

《深入理解计算机系统(第三版)》第二章信息的表示和处理??计算机本身是由处理器和存储器子通过系统组成。在核心部分,我们需要方法来表示基本数据类型,比如整数和实数运算的近似值。然后,我们考虑机器级指令如何... 查看详情

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

1.知识总结(主要对新知识)(1)计算机提供不同层次的抽象表示,来隐藏实际实现的复杂性文件是对I/O设备的抽象表示虚拟存储器是对主存和磁盘I/O设备的抽象表示进程是对处理器、主存和I/O设备的抽象表示(2)程序员必须... 查看详情

《深入理解计算机系统》(第三版)第一章疑问思考

...过学习,弄清楚了操作系统的中断机制:是操作系统获得计算机控制权的根本保证。其基本原理是:设备在完成自己的任务后向CPU发出终端,CPU判断优先级,然后确定是否响应。如果响应,则执行中断服务程序,并在中断服务程... 查看详情

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

下载地址:网盘下载 内容简介  · · · · · ·和第2版相比,本版内容上*大的变化是,从以IA32和x86-64为基础转变为完全以x86-64为基础。主要更新如下:基于x86-64,大量地重写代码,首次介绍对处理浮... 查看详情

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

4.1Y86-64指令集体系结构1.指令体系结构:处理器支持的指令和指令的字节级编码。2.与X86-64相比,Y86-64指令集的数据类型、指令和寻址方式要少一些,字节级编码也比较简单,机器代码没有Y86-64紧凑,虽简单但足够完整。3.定义一... 查看详情

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

...为地址,所有可能地址的集合称为虚拟地址空间。2.每台计算机都有一个字长,指明指针数据的标称大小。32位程序和64位程序区别在于该程序如何编译,而不是其运行的机器类型。C语言各种数据类型分配的字节数如下:int32_t和i... 查看详情

速读《深入理解计算机系统(第三版)》问题及解决

第一章计算机漫游P13:用户栈和运行时堆有什么区别?数据结构中经常说堆栈,这里的堆和栈一样吗?和操作系统的堆、栈有什么区别?参考:堆和栈的区别(内存和数据结构)操作系统:栈:由操作系统自动分配释放,存放函数的... 查看详情

《深入理解计算机系统(原书第三版)》pdf+python经典书籍两本

神书一样的存在,其中很多知识面试常考百度网盘链接:https://pan.baidu.com/s/1jTVO_KF-U4zJ_2RByuFmmw提取码:jpky  内容简介  · · · · · ·和第2版相比,本版内容上*大 查看详情

求《深入理解计算机系统(第三版)》的pdf中文版

第三版更新了最新到X86-64位,各个章节更加容易阅读和理解,中文第三版可以参考:https://zhidao.baidu.com/question/139032425214224925.html?fr=iks&word=%CE%D2%C2%F2%C1%CB%D2%BB%B1%BE%D6%BD%D6%CA%B5%C4%A1%B6%C9%EE%C8%EB%C0%ED%BD%E2%BC%C6%CB%E3%BB%FA%CF%B5%CD%B3%A1%B7%B5%D... 查看详情

《深入理解java虚拟机-jvm高级特性与最佳实践(第三版)》阅读笔记

《深入理解Java虚拟机》阅读笔记本repository为《深入理解Java虚拟机-Jvm高级特性与最佳实践(第三版)》阅读笔记,因为第一章主要讲的是Java的发展历史,这里就不做笔记,直接从第2章的"Java内存区域与内... 查看详情

《深入理解计算机系统》第三章学习笔记

通过本周的学习,总结出一下知识内容机器级代码计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。对于机器级编程来说,其中两种抽象尤为重要:1、指令集体系结构(InstructionsetarchitectureISA)... 查看详情

深入理解计算机系统第三章大略和第五章大略

这2章总结的很少,主要是觉得没那么重要。1.2个操作数的指令,第二个操作数通常是目的操作数:movbab,moveatob,而addab,b+=a,指令分为指令类,如mov类:movb,movw,movl,b指一个字节,w表示2个字节,l表示4个字节         ... 查看详情

深入理解计算机系统(代码片段)

深入理解计算机系统卡内基·梅隆一门棵。原书第3版资料.第三版源码.原书第2版资料.计算机系统漫游源文件到目标文件的翻译过程可分为四个阶段,这四个阶段的程序被称为预处理器,编译器,汇编器和链接器,它们一起构成... 查看详情

深入理解计算机系统第三章程序的机器级表示part1

 如题所示,这一章讲解了程序在机器中是怎样表示的,主要讲汇编语言与机器语言。 学习什么,为什么学,以及学了之后有什么用我们不用学习如何创建机器级的代码,但是我们要能够阅读和理解机器级的代码。虽然现... 查看详情

深入理解java虚拟机(第三版)-14.线程安全与锁优化

14.线程安全与锁优化1.什么是线程安全?当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替进行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可... 查看详情

《深入理解java虚拟机》第三版第七章要点总结(代码片段)

本文仅作为复习清单使用类生命周期加载验证准备解析初始化使用卸载常量优化常量传播常量折叠类的加载通过全限定名获取二进制字节流将静态存储结构转化为方法区的运行时数据结构生成Class对象验证过程(可关闭)文件格式... 查看详情