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


本文为《深入理解计算机系统》第三章 程序的机器级表示的笔记,主要介绍x86平台下的机器代码以及人类可读的表示——汇编语言。通过阅读汇编代码,我们能理解编译器的优化能力,并分析代码中隐含的低效率。

计算器执行机器代码,用字节序列编码低级的操作(包括处理数据、管理内存、读写存储设备上的数据,以及利用网络通信):

  • GCC C语言编译器以汇编代码的形式产生输出(汇编代码是机器代码的文本表示,给出程序中的每一条指令)
  • GCC调用汇编器和链接器,根据代码生成可执行的机器代码。

程序的机器级表示

一、程序编码

代码执行流程:

  • 1.C预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏
  • 2.编译器产生两个源文件的汇编代码,后缀为s
  • 3.汇编器将汇编代码转换成二进制目标代码,后缀为o(目标代码是机器代码的一种形式,包含所有指令的二进制表示,只是还没有填入全局值的地址)
  • 4.链接器将两个目标代码与实现库函数(例如printf)的代码合并,并产生最终的可执行代码文件

机器级代码

计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节:

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

x86的机器代码中,通常对C语言程序员隐藏的处理器状态都是可见的:

  • 程序计数器(通常称为“PC”,在86中用%rip表示)给出将要执行的下一条指令在内存中的地址。
  • 整数寄存器文件 包含16个命名的位置,分别存储64位的值,可以存储地址(指针)或整数数据。有的寄存器被用来记录重要的程序状态。
  • 条件码寄存器 保存着最近执行的算术或逻辑指令的状态信息。用来实现控制或数据流中的条件语句。
  • 向量寄存器 可以存放一个或多个整数或浮点数值。

虽然C语言可以在内存中声明和分配各种数据类型的对象,但是机器代码只是简单的将内存看成一个很大的、按字节寻址的数组

程序内存包含

  • 程序的可执行机器代码
  • 操作系统需要的一些信息
  • 用来管理过程调用和返回的运行时栈
  • 以及用户分配的内存块。

程序内存用虚拟地址来寻址。在任意给定的时刻,只有有限的一部分虚拟地址被认为是合法的。

操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器内存中的物理地址。

一条机器指令只执行一个非常基本的操作(例如将存放在寄存器中的两个数字相加,在寄存器和寄存器之间传送数据,或是条件分支转移到新的指令地址),编译器必须产生这些指令的序列。

二、数据格式

汇编代码指令都有一个字符的后缀,表明操作数的大小。
例如,数据传送mov指令有四个变种:movb(传送字节)、movw(传送字)、movl(传送双字)和movq(传送四字)
在这里插入图片描述

三、访问信息

一个x86的中央处理单元(CPU)包含一组16个存储64位值的通用目的寄存器,用来存储整数数据和指针。这些寄存器都以%r开头(不同命名规则是由于指令集历史演化造成的)
在这里插入图片描述
指令可以对这16个寄存器的低位字节中存放的不同大小的数据进行操作: 字节级操作可以访问最低的字节;16位操作可以访问最低的2个字节;32位操作可以访问最低的4个字节;而64位操作可以访问整个寄存器。

对于小于8字节结果的指令,寄存器剩下的字节如何处理?

  • 生成1字节和2字节数字的指令会保持剩下的字节不变
  • 生成4字节的数字的指令会把高位4个字节置为0

栈指针%rsp,用来指明运行时栈的结束位置。

1.操作数指示符

操作数(Operand)指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。

操作数的类型:

  • 立即数(immediate):用来表示常数值。格式为$整数,例如:%-577代表常数-577
  • 寄存器(register):用来表示某个寄存器的内容,用 ra 来表示任意寄存器a,用引用 R[ ra ] 来表示它的值(将寄存器看成一个数组R,用寄存器标识符作为索引)
  • 内存引用:根据计算出来的地址访问某个内存位置。用Mb[ Addr ] 表示对存储在内存中从地址Addr开始b个字节值的引用。

寻址模式:
有多种不同的寻址模式,允许不同形式的内存引用。
如最常用的 Imm(rb,ri,s),由四个部分组成:

  • 一个立即数偏移Imm
  • 一个基址寄存器rb(必须是64位寄存器)
  • 一个变址寄存器ri(必须是64位寄存器)
  • 一个比例因子s(这里s必须是1、2、4或8)

有效地址被计算为:

Imm + R[ rb ] + R[ ri ] * s

在这里插入图片描述

2.数据传送指令

数据传送指令(Move):把数据从源位置复制到目的位置,不做任何变化。

  • 源操作数是一个立即数,存储在寄存器中或内存中。
  • 目的操作数指定一个位置,寄存器或者内存地址。

在这里插入图片描述
movabsq指令能够以任意64位立即数值作为源操作数,并且只能以寄存器作为目的。

x86限制:传送指令的两个操作数不能都指向内存位置。
因此从内存复制到内存需要两条指令:

  1. 将源值加载到寄存器
  2. 将寄存器值写入目的位置

零扩展填充(movz): 把目的中剩余的字节填充为0.
在这里插入图片描述

符号扩展填充(movs): 把源操作数的最高位复制,进行填充
在这里插入图片描述

3.数据传送实例

在这里插入图片描述
由这段代码可以看出:

  • “指针”其实就是地址
  • 间接引用指针就是将该指针放在一个寄存器中,然后在内存引用中使用这个寄存器
  • 局部变量通常保存在寄存器中,而不是内存中。访问寄存器比访问内存要快的多

4.压入和弹出栈数据

栈操作遵循“后进先出”原则。通过push操作把数据压入栈;通过pop操作删除数据。

  • 总是从一端插入和删除数据,这端被称为栈顶。
  • 栈向下增长,栈顶元素的地址是所以栈中元素地址中最低的。
  • 栈指针%rsp保存着栈顶元素的地址
    在这里插入图片描述

压入栈步骤:

  1. 将栈指针减8
  2. 将值写到新的栈顶地址

弹出栈步骤:

  1. 从栈顶读出数据
  2. 将栈指针加8 在这里插入图片描述

四、算术和逻辑操作

在这里插入图片描述

1.加载有效地址

加载有效地址(load effective address) 指令leaq用于将有效地址写入到目的位置。指令形式是从内存读数据(地址)到寄存器

leaq指令能执行加法和有限形式的乘法:
在这里插入图片描述

2.一元和元操作

一元操作: 只有一个操作数,既是源又是目的。如自增指令: incq (%rsp)
二元指令: 第二个数既是源又是目的。如减法指令:subq %rax,%rdx

3.移位操作

移位:先给出移位量,第二项给出要移位的数。移位量可以是一个立即数,或者放在单字节寄存器%cl中。

左移有两个名字:SAL和SHL,效果是一样的。
右移:

  • 算术右移(SAR):执行算术移位(填上符号位)
  • 逻辑右移(SHR):执行逻辑移位(填上0)

右移操作要求区分有符号和无符号数。这个特性使得补码运算成为实现有符号整数运算的较好方式。

4.特殊的移位操作(扩展)

两个64位的整数相乘需要128位来表示。
128位(16字节)的数称为八字(oct word)

128位乘积以及整数除法的指令:
在这里插入图片描述
单操作数乘法指令:一个参数必须在寄存器%rax中,另一个作为指令的源操作数给出。乘积存放在寄存器%rdx(高64位)和%rax(低64位)中。

  • 无符号数乘法:mulq
  • 补码乘法:imulq (汇编器能通过计算操作数的数目分辨出要用哪条指令)

存储乘积需要两个movq指令:一个存储低8个字节,一个存储高8个字节。

除法:

在这里插入图片描述

五、控制

机器代码提供两种基本的低级机制来实现有条件的行为:

  1. 测试数据值
  2. 根据测试的结果来改变控制流或数据流

jump指令可以可以改变指令的执行顺序,指定控制应该被传递到程序的某个其他部分

1.条件码

除整数寄存器外,CPU还维护着一组单个位的条件码寄存器,描述了最近的算术或逻辑操作的属性。可以检测这些寄存器来执行条件分支指令。

常用的条件码:

  1. CF:进位标志。最近的操作使最高位产生了进位。(可用来检查无符号操作的溢出)
  2. ZF:零标志。最近的操作得出的结果为0。
  3. SF:符号标志。最近的操作得到的结果为负数。
  4. OF:溢出标志。最近的操作导致一个补码溢出(正溢出或负溢出)

图3-10中的指令会设置条件码
而leaq指令不改变任何条件码,因为它是用来进行地址计算的。

只设置条件码而不改变任何其他寄存器的指令:

  1. CMP指令根据两个操作数之差来设置条件码。与SUB指令的行为是一样的。
  2. TEST指令的行为与AND指令一样,只是TEST只设置条件码而不改变寄存器的值。(如 testq %rax,%rax 用来检查%rax是负数、0,还是正数)

在这里插入图片描述

2.访问条件码

访问条件码的方法:

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

SET指令: 根据条件码的某种组合,将一个字节设置为0或1
不同后缀的set指令指明了所考虑的条件码组合(指令的后缀表示不同的条件而非操作数大小)。

在这里插入图片描述

一条set指令的目的操作数是一个低位单字节寄存器,或一个字节的内存位置,指令会将这个字节设置成0或1

3.跳转指令

跳转指令会导致执行切换到程序中一个全新的位置,跳转的目的地通常用一个标号表明。
jmp指令是无条件跳转,可以是直接跳转或间接跳转:

  • 直接跳转:跳转目标是作为指令的一部分编码的(给出一个标号作为跳转目的,如 jmp .L1
  • 间接跳转:跳转目标是从寄存器或内存位置中读出的(格式为 * 后面跟一个操作数指示符,如指令jmp *rax 用寄存器%rax的值作为跳转目标;而指令jmp *(%rax) 以%rax中的值作为读地址,从内存中读出跳转目标)

其他跳转指令都是有条件的,根据条件码的某种组合,或者跳转,或者继续执行代码序列中下一条指令:
在这里插入图片描述

4.跳转指令的编码

这节内容结合例子好理解些:
*加粗样式

5.用条件控制来实现条件分支

C语言中的if—else语句的通用形式:
在这里插入图片描述
对应的汇编形式:
在这里插入图片描述

求两数之差绝对值的C程序以及对应的汇编程序(C语言里的goto语句类似于汇编的无条件跳转,所以为方便转译汇编增加了goto语句的C代码):
在这里插入图片描述

6.用条件传送来实现条件分支

使用数据的条件转移:计算一个条件操作的两种结果,再根据条件是否满足从中选取一个。

在这里插入图片描述
该表达式用条件控制转移来编译会得到如下形式:
在这里插入图片描述
这段代码包含两个代码序列:一个对then-expr求值,另一个对else-expr求值。条件跳转和无条件跳转结合起来使用是为了保证只有一个序列执行。
基于条件传送的代码,会对then-exprelse-expr都求值,最终值的选择基于对test-expr的求值。可以用下面的抽象代码描述:
在这里插入图片描述

为什么基于条件数据传送的代码会比基于条件控制转移的代码性能要好:
处理器通过使用流水线来获得高性能,在流水线中,一条指令的处理要经过一系列的阶段,每个阶段执行所需操作的一小部分(例如,从内存取指令、确定指令类型、从内存读数据、执行算术运算、向内存写数据,以及更新程序计数器)。这种方法通过重叠连续指令的步骤来获得高性能(也就是说需要并行执行这些操作),要做到这一点,要求能事先确定要执行的指令序列,这样才能保持流水线中充满了待执行的指令。当机器遇到分支时,只有当分支条件求值完成之后,才能决定分支往哪边走。处理器采用非常精密的分支预测逻辑来猜测每条跳转指令是否会执行。
同条件跳转不同,处理器无需预测测试的结果就可以执行条件传送(处理器只是读源值,检查条件码,然后要么更新目的寄存器,要么保持不变)。

条件传送指令:

  • 每条指令都有两个操作数:源寄存器或者内存地址S,和目的寄存器R。
  • 指令的结果取决于条件码的值。
  • 源值可以从内存或源寄存器中读取,但是只有在指定的条件满足时,才会被复制到目的寄存器中。
  • 源和目的的值可以是16位、32位或64位,不支持单字节的条件传送。

在这里插入图片描述
总的来说,条件数据传送提供了一种用条件控制转移来实现条件操作的替代策略。它们只能用于非常受限制的情况,但这些情况还是相当常见的,而且与现代处理器的运行方式更契合。

7.循环

汇编中通过使用条件测试和跳转组合起来实现循环的效果。
do-while循环:
在这里插入图片描述
在这里插入图片描述
while循环:
在这里插入图片描述
有多种方法将while循环翻译成机器代码:

  • 跳转到中间(jump to middle) :它执行一个无条件跳转跳到循环结尾处的测试,以此来执行初始的测试。在这里插入图片描述

  • guarded-do:首先使用条件分支,如果初始条件不成立就跳过循环,把代码变换为do-while循环。在这里插入图片描述
    for循环:
    在这里插入图片描述
    for循环的行为与while循环的代码的行为一样:
    在这里插入图片描述

8.Switch语句

Switch语句根据一个整数索引值进行多重分支。通过使用跳转表(jump table)这种数据结构使得实现更高效(在于执行开关语句的时间与开关情况的数量无关)。
跳转表: 跳转表是一个数组,表项 i 是一个代码段的地址,这个代码段实现当开关索引值等于 i 时程序应该采取的动作。
程序代码用开关索引值来执行一个跳转表内的数组引用,确定跳转指令的目标。

六、过程(函数)

过程(函数)必须拥有的机制(假设过程P调用过程Q,Q 执行后返回P):

  • 传递控制 在进入过程Q时,程序计数器必须被设置为Q的代码的起始地址;在返回时,要把程序计数器设置为P中调用Q后面的那条指令的地址。
  • 传递数据 P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
  • 分配和释放内存 在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些内存。

1.运行时栈

C语言的过程调用使用了栈数据结构提供的后进先出的内存管理原则。
程序用栈来管理它的过程所需要的存储空间,栈和程序计数器存放着传递控制和数据、分配内存所需要的信息。

在P调用Q的例子中,当Q在执行时,P以及所有在向上追溯到P的调用链中的过程,都是暂时被挂起的。当Q运行时,它只需要为局部变量分配新的存储空间,或者设置到另一个过程的调用。当Q返回时,任何它所分配的局部存储空间都可以被释放。
当P调用Q时,控制和数据信息添加到栈尾。当P返回时,这些信息会释放掉。

  • x86-64的栈向低地址方向增长,而栈指针%rsp指向栈顶元素。

  • 可以用pushq和popq指令将数据存入栈中或是从栈中取出。

  • 将栈指针减小一个适当的量可以为没有指定初始值的数据在栈上分配空间。可以通过增加栈指针来释放空间

  • 当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧。
    在这里插入图片描述

  • 当过程P调用Q时,会把返回地址压入栈中,指明当Q返回时,要从P程序的哪个位置继续执行。Q的代码会扩展当前栈的边界,分配它的栈帧所需的空间。在这个空间中,它可以保存寄存器的值,分配局部变量空间,为它调用的过程设置参数。

  • 通过寄存器,过程P可以传递最多6个整数值(指针或者整数),如果Q需要更多参数,P可以在调用Q之前在自己的栈帧里存储好这些参数。

2.转移控制

将控制从函数P转换到函数Q:把程序计数器(PC)设置为Q代码的起始位置。

Q返回时,处理器必须记录好它需要继续执行P的代码的位置:

  • x86里用指令call Q来记录该信息,首先将地址A压入栈中,并将PC设置为Q的起始地址。
  • 压入的地址被称为起始地址,是紧跟在call指令后面那条指令的地址。对应的指令ret会从栈中弹出地址A,并把PC设置为A。

call和ret指令的一般形式:
在这里插入图片描述

call指令用来指明被调用过程起始的指令地址。有直接调用和间接调用:

  • 直接调用的目标是一个标号
  • 间接调用是 * 后面跟一个操作数指示符

3.数据传送

过程调用还可能包括把数据作为参数传递,而从过程返回还有可能包括返回一个值。
x86中,大部分过程间的数据传送是通过寄存器实现的。通过寄存器最多可以传递6个整型参数。
寄存器的使用是有特殊顺序的。寄存器使用的名字取决于要传递的数据类型的大小。
如下图,会根据参数在参数列表中的顺序为它们分配寄存器。通过64位寄存器适当的部分访问小于64位的参数。(如果第一个参数是32位的,那么可以通过%edi来访问它)
在这里插入图片描述
对不止有6个参数的函数来说,超出的参数就要通过栈来传递(假设P调用Q,有n个整型整数,且n>6。那P的代码分配的栈帧必须要能容纳7到n号参数的存储空间。要把参数1 ~ 6复制到对应的寄存器,把参数7 ~ n放到栈上,而参数7位于栈顶)。
通过栈传递参数时,所以的数据大小都向8的倍数对齐。

4.栈上的局部存储

局部数据必须存放在内存中(而非寄存器)的常见情况:

  • 寄存器不够存放所有的本地数据
  • 对一个局部变量使用地址运算符‘&’,因因此必须能够为它产生一个地址
  • 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到

过程通过减小栈指针在栈上分配空间。分配的结果作为栈帧的一部分,标号为“局部变量”。
运行时栈提供了一种简单的、在需要时分配、函数完成时释放局部存储的机制。

5.寄存器中的局部存储空间

寄存器是唯一被所有过程共享的资源。 因此必须确保当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器值。
为此x86采用了一组统一的寄存器使用惯例,所有的过程(包括程序库)都必须遵守:

  • 寄存器%rbx、%rbp和%r12 ~ %r15被划分为被调用者保存寄存器
  • 所有其他的寄存器,除了栈指针%rsp,都分类为调用者保存寄存器,任何函数都能修改它们。

递归过程: 递归调用一个函数本身与调用其他函数是一样的。
栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置和被调用者保存寄存器的值)存储空间。如果需要,它还可以提供局部变量的存储。栈分配和释放的规则很自然地就与函数调用----返回的顺序匹配,这种实现函数调用和返回的方法甚至对更复杂的情况也适用,包括相互递归调用。

七、数组分配和访问

1.基本原则

对于数据类型T和整型常数N的数组:T A[N];

  • 它在内存中分配了一个L*N字节的连续区域(L是数据类型T的大小,单位为字节)
  • 它引入了标识符A,可以用A来作为执行数组开头的指针,指针的值为Xa
    可以用0 ~ N-1的整数索引来访问该数组元素。数组元素会被存放在地址为Xa + L*i的地方。

x86的内存引用指令可以用来简化数组访问: 例如对于一个int型的数组E,地址存放在寄存器%rdx中,i存放在寄存器%rcx中
指令movl(%rdx,%rcx,4),%eax 会执行地址计算 XE +4i,读这个内存位置的值,并将结果存放到寄存器%eax

2.指针运算

对指针进行运算,计算出来的值会根据该指针引用的数据类型的大小进行伸缩: 例如,p是一个指向类型为T的数据的指针,p的值为Xp ,那么表达式p+i的值为Xp + L*i,这里L是数据类型T的大小。

单操作数操作符‘&’和‘*’可以产生指针和间接引用指针:

  • 对于表示某个对象的表达式Expr,&Expr是给出该对象地址的一个指针。
  • 对于一个表示地址的表达式AExpr,*AExpr给出该地址处的值。

数组引用A[i]等同于表达式*(A+i)。它计算第i个数组元素的地址,然后访问这个内存位置。

假设整型数组E的起始地址和整数索引i分别存放在寄存器%rdx和%rcx中。结果存放在寄存器%eax(如果是数据)或寄存器%rax(如果是指针)中。与E有关的表达式以及汇编代码:在这里插入图片描述

3.嵌套的数组

二维数组在内存中按照“行优先”的顺序排列
访问多维数组,编译器会以数组起始为基地址,(可能需要经过伸缩的)偏移量为索引,产生计算期望的元素的偏移量,然后使用某种MOV指令。
对数组:T D[R][C];

它的数组元素D [i][j]的内存地址为:&D[i][j] = xD+L(C*i+j)

4.变长数组

变长数组:允许数组的维度是表达式,在数组被分配时才计算出来。
变量n必须在数组 A [n][n]之前,这样函数就可以在遇到这个数组的时候计算出数组的维度。
变长数组与定长数组的不同在于:多态的版本必须用乘法指令对i伸缩n倍,而不能用一系列的移位和加法。

八、异质的数据结构

1.结构

struct将可能不同类型的对象聚合到一个对象中。用名字来引用结构的各个组成部分。

  • 结构的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址。
  • 编译器维护关于每个结构类型的信息,指示每个字段的字节偏移。它以这些偏移作为内存引用指令中的位移,从而产生对结构元素的引用。

在这里插入图片描述
在这里插入图片描述
结构的各个字段的选取完全是在编译时处理的。机器代码不包含关于字段声明或字段名字的信息。

2.联合

union允许以多种数据类型来引用一个对象。一个联合的总的大小等于它最大字段的大小。
在这里插入图片描述

3.数据对齐

数据对齐:限制某种类型对象地址必须是某个值K(通常是2、4或8)的倍数。 这种对齐限制简化了处理器和内存系统之间接口的硬件设计。

如果我们能保证将所有的double类型数据的地址对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

对齐原则是任何K字节的基本对象的地址必须是K的倍数。
在这里插入图片描述

  • 对于包含结构的代码,编译器可能需要在字段的分配中插入间隙,以保证每个结构元素都满足它的对齐要求。而结构本身对它的起始地址也有一些对齐要求。
    在这里插入图片描述
  • 编译器结构的末尾也可能需要一些填充,以使结构数据中的每个元素都会满足它的对齐要求。

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

1.理解指针

指针和指针映射到机器代码的关键原则:

  • 每个指针都对应一个类型。这个类型表明该指针指向的是哪一类对象。
  • 每个指针都有一个值。这个值是某个指定类型的对象的地址。特殊的NULL(0)值表示该指针没有指向任何地方。
  • 指针用‘&’运算符创建(直接获取地址)。这个运算符可以用到任何lvalue类的C表达式上,lvalue意指可以出现在赋值语句左边的表达式(如变量、结构、联合或者数组)
  • *操作符用于间接引用指针(间接通过地址取里面的值) 。结果是一个值,类型与该指针的类型一致。间接引用是用内存引用来实现的,要么是存储到一个指定的地址,要么是从指定的地址读取。
  • 数组与指针紧密联系。一个数组的名字可以像一个指针变量一样引用(但是不能修改)。数组引用(如a[3])与指针运算和间接引用(例如*(a+3))有一样的效果。数组引用和指针运算都需要用对象大小对偏移量进行伸缩。(如表达式p+i得到的地址计算为p+L*i ,L是p的数据类型的大小)
  • 将指针强转类型后只改变它的类型,而不改变它的值。 强制类型转换的一个效果是改变指针运算的伸缩。(例如char *类型的指针p,值为P,那么表达式(int *)p+7计算为P+28,而(int *)(p+7)计算为P+7。)
  • 指针也可以指向函数。这提供了一个很强大的存储和向新代码传递引用的功能,这些引用可以被程序的某个其他部分调用。函数指针的值是该函数机器代码表示中第一条指令的地址。

2.内存越界引用和缓冲区溢出

C对于数组引用不进行任何边界检查,而且局部变量和状态信息(例如保存的寄存器值和返回地址)都存放在栈中。 对越界的数组元素的写操作会破坏存储在栈中的状态信息。当程序使用这个被破坏的状态,试图重新加载寄存器或执行ret指令时,会出现很严重的错误。

缓冲区溢出(buffer overflow): 通常在栈中分配某个字符数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间。

缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的计算机网络攻击系统安全的方法。通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码,另外,还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,执行ret指令的效果就是跳转到攻击代码。
在一种攻击形式中,攻击代码会使用系统调用启动一个shell程序,给攻击者提供一组操作系统函数。在另一种攻击形式中,攻击代码会执行一些未授权的任务,修复对栈的破坏,然后第二次执行ret指令,(表面上)正常返回到调用者。

在这里插入图片描述

3.对抗缓冲区溢出攻击

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.支持变长栈帧

一些函数需要的局部存储是变长的。例如当函数调用alloca时。alloca是一个标准库函数,可以在栈上分配任意字节数量的存储。当函数声明一个局部变长数组时,也会发生这种情况。
在这里插入图片描述

十、浮点代码

浮点数在机器级程序中使用另外一套寄存器和指令,其余均与之前一致。

浮点数寄存器:
在这里插入图片描述

浮点传送和转换操作:
在这里插入图片描述
在这里插入图片描述

定义和使用浮点常数:
在这里插入图片描述

浮点运算操作:
在这里插入图片描述

浮点代码中使用位级操作:
在这里插入图片描述

浮点比较操作:
在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值