设为首页 加入收藏

TOP

深入理解计算机系统-第3章程序的机器级表示(一)
2023-07-23 13:35:51 】 浏览:36
Tags:解计算 章程序

计算机执行机器代码,用字节序列编码低级的操作,包括处理数据、管理内存、读写存储设备上的数据,以及利用网络通信。编译器基于编程语言的规则、目标机器的指令集和操作系统遵循的惯例,经过一系列阶段生成机器代码。

在本章中,我们将详细学习一种特别的汇编语言,了解如何将 C 程序编译成这种形式的机器代码。阅读编译器产生的汇编代码,需要具备的技能不同于手工编写汇编代码,我们必须了解典型的编译器在将 C 程序结构变换成机器代码时所做的转换。相对于 C 代码表示的计算操作,优化编译器能够重新排列执行顺序,消除不必要的计算,用快速操作替换慢速操作,甚至将递归计算变换成迭代计算。但是源代码与对应的汇编代码的对应关系通常不太容易理解,因为这是一种逆向工程(reverse engineering)-通过研究系统和逆向工作。

本章内容会涉及到 x86-64 汇编级指令代码。

1,历史观点

Intel 处理器系列俗称 x86,经历了一个长期的、不断进化的发展过程。

2,程序编码

Linux 系统默认的编译器时 GCC C 编译器。编译器选项 -Og 会指示编译器使用会生成符合原始 C 代码整体结构的机器代码的优化等级,通常使用 -O1-O2 选项。

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

  • 程序计数器(PC,在 x86-64 中用 %rip 表示)给出将要执行的下一条指令在内存中的地址。
  • 整数寄存器文件包含 16 个命名的位置,分别存储 64 位的值。这些寄存器可以存储地址(对应于C 语言的指针)或整数数据。有的寄存器被用来记录默写重要的程序状态,有的寄存器保存临时数据,如过程的参数和局部变量,以及函数的返回值。
  • 条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化,如用来实现 if 和 else 语句。
  • 一组向量寄存器可以存放一个或多个整数或浮点数值。

3,数据格式

C 语言数据类型在 x86-64 中的大小。

image

浮点数主要有两种形式:单精度(4 字节)值,对应于 C 语言数据类型 float,双精度(8 字节)值,对应于 C 语言数据类型 double。如上图所示,大多数 GCC 生成的汇编代码指令都有一个字符的后缀,表明操作数的大小,例如,数据传送指令有四个变种: movb(传送字节)、movw(传送字)、movl(传送双字)和movq(传送四字)。

4,访问信息指令

一个 x86-64 的中央处理单元(CPU)包含一组 16 个存储 64 位值的通用目的寄存器,这些寄存器用来存储整数数据和指针。下图显示了这 16 个寄存器,它们的名字都以 %r 开头,后面还跟着一些不同的命名规则的名字。最初的 8086 中有 8 个 16 位的寄存器,即图 3-2 中的 %ax 到 %bp,每个寄存器都有特殊的用途,它们的名字就反映了这些不同的用途。拓展到 IA32 架构时,这些寄存器也拓展成 32 位寄存器,标号从 %eax 到 %ebp。拓展到 x86-64 后,用来的 8 个寄存器拓展成 64 位,标号从 %rax%rbp,除此之外,还增加了 8 个寄存器,标号从 %r8%r15

image

4.1,操作数指示符

大多数指令有一个或多个操作数(operand),指示出执行一个操作中要使用的源数据值,以及防止结果的目的位置。不同操作数被分为三种类型。

  • 立即数(immediate),用来表示常数值。
  • 寄存器(register
  • 内存引用: 它会根据计算出来的地址访问某个内存位置。

如图 3-3 所示,有多重不同的寻址模式,允许不同形式的内存引用。

image

4.2,数据传送指令

最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。图 3-4 列出的最简单形式的数据传送指令-MOV 类: 把数据从源位置复制到目的位置,不做任何变化

image

简单的数据传送指令示例代码如下。(记住,第一个是源操作数,第二个是目的操作数)

image

4.3,数据传送示例

image

4.4,压入和弹出数据

栈和队列都是一种"操作受限"的线性表(逻辑结构),只允许在一端插入和删除数据;栈的特性是先进后出,队列是先进先出在处理函数调用过程中很重要,通过 push 指令把数据压入栈中,通过 pop 指令删除数据。

栈可以可以通过数组实现,总是从数组的一端插入和删除元素,这一端称为栈顶,栈顶元素的地址是所有栈中元素地址中最低的,栈指针 %rsp 保存着站定元素的地址。入栈和出栈汇编指令描述如下,栈操作指令都只有一个操作数-压入的数据源和弹出的栈顶数据。

image

将一个四字值压入栈中,分为两步,首先先将栈指针减 8,然后将值写到新的栈顶地址,因此 pushq 指令等价于下面两条指令:

subq $8,%rsp Decrement stack pointer
moq %rbp,(%rsp)

image

5,算术和逻辑操作指令

图 3-10 列出了 x86-64 的一些整数和逻辑操作。和访问信息指令一样,算术和逻辑操作指令类也有各种带不同大小操作数的变种。例如,指令类 ADD 由四条加法指令组成: addb、addw、addl 和 addq,分别是字节加法、字加法、双字加法和四字加法。算术和逻辑操作指令分为四组:加载有效地址、一元操作、二元操作和移位。二元操作数有两个操作数,而一元操作数有一个操作数。

image

5.1,加载有效地址

加载有效地址(load effective address)指令 leaq 实际上是 movq 指令的变形。leaq 指令可以简洁地描述普通的算术操作,如果寄存器 %rdx 的值为 x,那么指令 leaq 7(%rdx, %rdx, 4),%rax 将设置寄存器 %rax 的值为 5x+7。目的操作数必须是一个寄存器。

image

5.2,一元和二元操作

对于二元操作指令,第一个操作数可以是立即数、寄存器或内存位置,第二个操作可以是寄存器或内存位置。

5.3,移位操作

移位操作指令,先给出移位量,第二项给出要移位的数,移位量可以是立即数,或者在单字节寄存器 %cl 中,移位量是由 %c1 寄存器的低 m 位决定的。例如当寄存器 %c1 的十六进制位 0xFF 时,指令 salb 会移 7 位,salw 会移 15 位,sall 会移 31 位,salq 会移 63 位。

5.4,总结

image

6,控制指令

前面的两类指令都是直线代码行为,也就是指令一条接着一条顺序地执行。C 语言中的某些结构,比如条件语句、循环语句和分支语句,要求有条件的执行,根据数据测试的结果来决定操作执行的顺序。

6.1,条件码

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

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

除了图 3-10 的整数算术操作指令会设置条件码,还有两类指令 CMPTEST但它们只设置条件码而不改变任何其他寄存器。如下图 3-13 所示,CMP 指令根据两个操作数之差来设置条件码。

image

6.2,访问条件码

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

  1. 可以根据条件码的某种组合,将一个字节设置为 0 或者1。
  2. 可以有条件跳转到程序的某个其他的部分。
  3. 可以有条件地传送数据。
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇crontab使用说明【一文搞懂Linux.. 下一篇LNMP简介

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目