第3章--程序的机器级表示

本文详细介绍了x86-64架构下的程序编码,包括机器级代码和汇编代码,强调了CPU的寄存器和内存模型。内容涵盖数据传送、算术和逻辑操作,以及控制流如条件码、跳转指令和循环实现。此外,文章还讨论了过程调用、栈管理、数组访问、结构与联合,以及浮点数处理。重点指出缓冲区溢出的安全问题及其防御策略。
摘要由CSDN通过智能技术生成

一、程序编码

1. 机器级代码

对于机器级编程来说,其中两种抽象尤为重要。
第一种是由指令集体系结构或指令集架构来定义机器级程序的格式和行为,它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
第二种抽象是,机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。

x86-64的机器代码和原始的C代码差别非常大。一些通常对C语言程序员隐藏的处理器状态都是可见的:

  • 程序计数器:通常称为PC,给出将要指向的下一条指令在内存中的地址。
  • 整数寄存器:包含16个命名的位置,分别存储64位的值。有的用来记录程序状态,有的用来保存临时数据。
  • 条件码寄存器:保存着最近执行的算术或逻辑指令的状态信息。
  • 一组向量寄存器:可以存放一个或多个整数或浮点数值。

程序内存包括:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的内存块。
一条机器指令只执行一个非常基本的操作。编译器必须产生这些指令的序列,从而实现程序结构。

2. 汇编代码

要查看机器代码文件的内容,有一类称为反汇编器的程序可以根据机器代码产生一种类似于汇编代码的格式。
在Linux系统中,带-d命令行标志的程序OBJDUMP可以充当这个角色。

二、访问信息

一个x86-64的中央处理单元(CPU)包括一组16个存储64位值的通用目的寄存器。
这些寄存器用来存储整数数据和指针。图3-2显示了这16个寄存器。
就像图中右边的解释说明的那样,在常见的程序里不同的寄存器扮演不同的角色。

1. 操作数指示符

大多数指令有一个或多个操作数,指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。
源操作值可以以常数形式给出,或是从寄存器或内存中读出。结果可以存放在寄存器或内存中。
因此,各种不同的操作数的可能性被分为三种类型:

  • 立即数:用来表示常数值。
  • 寄存器:表示某个寄存器的内容。
  • 内存引用:根据计算出来的地址访问某个内存位置。

2. 数据传送指令

图3-4列出的是最简单形式的数据传送指令–MOV类。这些指令把数据从源位置复制到目的位置,不做任何变化。
MOV类由四条指令组成:movb、movw、movl和movq。
这些指令都执行同样的操作,主要区别在于他们的数据大小不同,分别是1、2、4和8字节。
所有这些指令都把数据从源(在寄存器或内存中)复制到目的寄存器。

MOVZ类中的指令把目的中剩余的字节填充为0,而MOVS类中的指令通过符号扩展来填充,把源操作的最高位进行复制。
每条指令名字的最后两个字符都是大小指示符,第一个字符指定源的大小,而第二个指明目的的大小。

3. 数据传送示例

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

4. 压入和弹出数据

通过push操作把数据压入栈中,通过pop操作删除数据;
它具有一个属性:弹出的至永远是最近被压入而且仍然在栈中的值。

三、算术和逻辑操作

1. 加载有效地址

加载有效地址指令leaq实际上是movq指令的变形。
他的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。
类似于C语言的地址操作符&s,这条指令可以为后面的内存引用产生指针。另外,它还可以简洁的描述普通的算术操作。

2. 一元和二元操作

一元操作只有一个操作数,既是源又是目的。这个操作数可以是一个寄存器,也可以是一个内存位置。
二元操作中的第二个操作数既是源又是目的。
第一个操作数可以是立即数、寄存器或是内存位置。第二个操作数可以是寄存器或是内存位置。

3. 移位操作

最后一组是移位操作,先给出移位量,然后第二项给出的是要移位的数。可以进行算数和逻辑右移。
移位量可以是一个立即数,或者放在单字节寄存器%cl中。
移位操作的目的操作数可以是一个寄存器或是一个内存位置。

四、控制

1. 条件码

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

  • CF:进位标志。最近的操作时最高为产生了进位。
  • ZF:零标志。最近的操作得出的结果为0。
  • SF:符号标志。最近的操作得到的结果为负数。
  • OF:溢出标志。最近的操作导致一个补码溢出。

leaq指令不改变任何条件码,因为它是用来进行地址计算的。除此之外,图3-10中列出的所有指令都会设置条件码。
CMP类和TEST类指令只设置条件码而不改变任何其他寄存器。

2. 访问条件码

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

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

3. 跳转指令

正常执行的情况下,指令按照他们出现的顺序一条一条的执行。
跳转指令会导致执行切换到程序中一个全新的位置。在汇编代码中,这些跳转的目的地通常用一个标号指明。
图3-15列举了不同的跳转指令。jmp指令是无条件跳转。

4. 跳转指令的编码

跳转指令有几种不同的编码,但是最常用都是PC相对的。
也就是,他们会将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差作为编码。
这些地址偏移量可以编码为1、2或4个字节。第二种编码方法是给出绝对地址,用4个字节直接指定目标。

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

将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。
当条件满足时,程序沿着一条执行路径执行,而当条件不满足时,就走另一条路径。
这种机制简单而通用,但是在现代处理器上,他可能会非常低效。

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

这种方法计算一个条件操作的两种结果,然后再根据条件是否满足从中选取一个。
只有在一些受限制的情况中,这种策略才可行,可以用一条简单的条件传送指令来实现它。
条件传送指令更符合现代处理器的性能特性。
图3-18列举了x86-64上一些可用的条件传送指令。
源值可以从内存或者源寄存器中读取,但是只有在指定的条件满足时,才会被复制到目的寄存器中。

7. 循环

① do-while循环
这种通用形式可以被翻译成如下所示的条件和goto语句:
也就是说,每次循环,程序会执行循环体里的语句,然后执行测试表达式。
如果测试为真,就回去再执行一次循环。
② while循环
第一种翻译方法,称为跳转到中间,它执行一个无条件跳转跳到循环结尾处的测试,以此来执行初始的测试。
第二种翻译方法,称为guarded-do,首先用条件分支,如果初始条件不成立就跳过循环,把代码变换为do-while循环。
当使用较高优化等级编译时,例如使用命令行选项-O1,GCC会采用第二种策略。
③ for循环
GCC为for循环产生的代码是while循环的两种翻译之一,这取决于优化的等级。

8. switch语句

switch语句可以根据一个整数索引值进行多重分支。在处理具有多种可能结果的测试时,这种语句特别有用。
它们不仅提高了C代码的可读性,而且通过使用跳转表这种数据结构使得实现更加高效。
跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当开关索引值等于i时程序应该采取的动作。

五、过程

过程是软件中一种很重要的抽象。
他提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。

1. 运行时栈

C语言过程调用机制的一个关键特性在于使用了栈数据结构提供的后进先出的内存管理原则。
程序可以用栈来管理它的过程所需要的存储空间,栈和程序寄存器存放着传递控制和数据、分配内存所需要的信息。
当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧。

2. 转移控制

当控制从函数P转移到函数Q时,处理器必须记录好他需要继续P的执行的代码位置。
在x86-64机器中,这个信息是用指令call Q来记录的。该指令会把地址A压入栈中,并将PC设置为Q的起始地址。
压入的地址A被称为返回地址,是紧跟在call指令后面的那条指令的地址。对应的指令ret会从栈中弹出地址A,并把PC设置为A。

3. 数据传送

过程调用还可能包括把数据作为参数传送,而从过程返回还有可能包括返回一个值。
x86-64中,大部分过程间的数据传送是通过寄存器实现的。

4. 栈上的局部存储

大部分过程都不需要超出寄存器大小的本地存储区域。不过有些时候,局部数据必须存放在内存中,常见的情况包括:

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

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

寄存器组是唯一被所有过程共享的资源。
虽然在给定时刻只有一个过程是活动的,我们仍然必须确保当一个过程调用另一个过程时,被调用者不会覆盖调用者稍后会使用的寄存器值。
为此,x86-64将寄存器分为了被调用者保存寄存器和调用者保存寄存器。

6. 递归过程

前面描述的寄存器和栈的惯例使得x86-64过程能够调用他们自身。
每个过程调用在栈中都有他自己的私有空间,因此多个未完成调用的局部变量不会互相影响。
此外,栈的原则很自然的就提供了适当的策略,当过程被调用分配局部变量,当返回时释放存储。

六、数组分配和访问

1. 指针运算

C语言允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型的大小进行伸缩。
单操作数操作符&和*可以产生指针和间接引用指针。可以对数组和指针应用数组下标操作。
数组引用A[i]等同于表达式*(A+i)。他计算第i个数组元素的地址,然后访问这个内存位置。

七、异质的数据结构

1. 结构

C语言的struct声明创建一个数据类型,将可能不同类型的对象聚合到一个对象中。用名字来引用结构的各个组成部分。
类似于数组的实现,结构的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址。

2. 联合

联合提供了一种方式,能够规避C语言的类型系统,允许以多种类型来引用一个对象。
联合声明的语法与结构的语法一样,只不过语义相差比较大。他们使用不同的字段来引用相同的内存块。
联合还可以用来访问不同数据类型的位模式。

3. 数据对齐

许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2、4或8)的倍数。
这种对其限制简化了形成处理器和内存系统之间接口的硬件设计。
对于包含结构的代码,编译器可能需要在字段的分配中插入间隙,以保证每个结构元素都满足他的对齐要求。
而结构本身对她的起始地址也有一些对齐要求。

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

1. 理解指针

指针是C语言的一个核心特色。他们以一种统一方式,对不同数据结构中的元素产生引用。
在此,我们重点介绍一些指针和他们映射到机器代码的关键原则。

  • 每个指针都对应一个类型。
  • 每个指针都有一个值。
  • 指针用&运算符创建。
  • *操作符用于间接引用指针。
  • 数组与指针紧密联系。
  • 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值。
  • 指针也可以指向函数。

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

C对于数组引用不进行任何边界检查,而且局部变量和状态信息(例如保存的寄存器值和返回地址)都存放在栈中。
这两种情况结合到一起就能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息。
当程序使用这个被破坏的状态,试图重新加载寄存器或执行ret指令时,就会出现很严重的错误。
一种特别常见的状态破坏称为缓冲区溢出。
通常,在栈中分配某个字符数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间。
缓冲区溢出的一个更加致命的使用就是让程序执行他本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法。

3. 对抗缓冲区溢出攻击

① 栈随机化
栈随机化的思想使得栈的位置在程序每次运行时都有变化。
因此,即使许多机器上都运行同样的代码,它们的栈地址都是不同的。
② 栈破坏检测
最近的GCC版本在产生的代码中加入了一种栈保护者机制,来检测缓冲区越界。
其思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值(随机值)。
在恢复寄存器状态和从函数返回之前,程序检查这个值是否被改变,如果是,那么程序异常终止。
③ 限制可执行代码区域
最后一招是消除攻击者向系统中插入可执行代码的能力。
一种方法是限制哪些内存区域能够存放可执行代码。
在典型的程序中,只有保存编译器产生的代码的那部分内存才需要是可执行的。其它部分可以被限制为只允许读和写。

九、浮点代码

1. 浮点传送和转换操作

图3-46给出了一组在内存和XMM寄存器之间以及从一个XMM寄存器到另一个不作任何转换的传送浮点数的指令。
引用内存的指令是标量指令,意味着他们只对单个而不是一组封装好的数据值进行操作。

图3-47和图3-48给出了在浮点数和整数数据类型之间以及不同浮点格式之间进行转换的指令集和。
这些都是对单个数据值进行操作的标量指令。

2. 过程中的浮点代码

在x86-64中,XMM寄存器用来向函数传递浮点参数,以及从函数返回浮点值。
如图3-45所示,可以看到如下规则:

  • XMM寄存器%xmm0~%xmm7最多可以传递8个浮点参数。按照参数列出的顺序使用这些寄存器。可以通过栈传递额外的浮点参数。
  • 函数使用寄存器%xmm0来返回浮点值。
  • 所有的XMM寄存器都是调用者保存的。

3. 浮点运算操作

图3-49描述了一组执行算术运算的标量AVX2浮点指令。每条指令有一个或两个源操作数,和一个目的操作数。
第一个源操作数可以是一个XMM寄存器或一个内存位置。第二个源操作数和目的操作数都必须是XMM寄存器。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值