程序的机器级表示

  • 历史观点
  • 程序编码
    • 1. 机器级代码
    • 2. 代码示例
  • 数据格式
  • 访问信息
    • 1. 操作数指示符
    • 2. 数据传送指令
  • 算术与逻辑操作
    • 1. 加载有效地址
    • 2. 一元操作和二元操作
    • 3. 移位操作
    • 例子
    • 特殊的算术操作
  • 控制
    • 1. 条件码
    • 2. 访问条件码
    • 3. 跳转指令及编码
    • 4. 翻译条件分支
    • 5. 循环
    • 6. 条件传送指令
    • 7. switch 语句
  • 过程
    • 1. 栈帧结构
    • 2. 转移控制
    • 3. 寄存器使用惯例
    • 4. 过程示例
    • 5. 递归过程
  • 数组分配和访问
    • 1.基本原则
    • 2. 指针运算
    • 3. 嵌套数组
    • 4. 定长数组
    • 5. 变长数组
  • 异质的数据结构
    • 1. 结构体
    • 2. 联合
    • 3. 数据对齐
  • 综合:理解指针
  • 应用: 使用GDB调试
  • 存储器的越界引用和缓冲区溢出
  • x86-64: 将IA32扩展到64位
    • 1. X84-64 的历史和动因
    • 2. 简介
    • 3. 访问信息
    • 4. 控制
    • 5. 过程
    • 5. 数据结构
  • 浮点程序的机器级表示(简述)

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

GCC C语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。然后GCC调用汇编器和链接器,从而根据汇编代码生成可执行的机器代码。在本章中,我们会近距离地观察机器代码,以及人类可读的表示-——汇编代码。

高级语言编程,机器会屏蔽程序实现的细节, 且我们使用起来不容易出错,为什么我们还需要花时间学习机器代码

  • 对于严谨的程序员来说,能够阅读和理解汇编代码仍是一项很重要的技能。以适当的命令行选项调用编译器,编译器就会产生一个以汇编代码形式表示的输出文件。通过阅读这些汇编代码,我们能够理解编译器的优化能力,并分析代码中隐含的低效率。试图最大化一段关键代码的性能的程序员,通常会尝试源代码的各种形式,每次编译并检查产生的汇编代码,从而了解程序将要运行的效率如何。
  • 高级语言提供的抽象层会隐藏我们想要了解的有关程序运行时行为的信息。例如,当用线程包写并发程序时,知道存储器保存不同的程序变量的区域是很重要的。这些信息在汇编代码级是可见的。
  • 另外再举一个例子,程序遭受攻击(使得蠕虫和病毒能够侵扰系统)的许多方式中,都涉及程序存储运行时控制信息方式的细节。许多攻击利用了系统程序中的漏洞重写信息,从而获得系统的控制权。了解这些漏洞是如何出现的以及如何防御它们,需要具备程序机器级表示的知识。

程序员学习汇编代码的需求随着时间的推移也发生了变化,开始时只要求程序员能直接用汇编语言编写程序,现在则是要求他们能够阅读和理解编译器产生的代码。

我们将详细学习两种特别的汇编语言: 了解如何将 C程序编译成这些形式的机器代码。阅读编译器产生的汇编代码,需要具备的技能不同于手工编写汇编代码。我们必须了解典型的编译器在将 C程序结构变换成机器代码时所做的转换。相对于C代码表示的计算操作,优化编译器能够重新排列执行顺序,消除不必要的计算,用快速操作替换慢速操作,甚至将递归计算变换成迭代计算。源代码与对应的汇编码的关系通常不太容易理解——就像要拼出的拼图与盒子上图片的设计不太一样。

学习建议: 精通细节是理解更深合更基本概念的先决条件。 “理解一般规则,而不愿意劳神学习细节”实际上是自欺欺人。

本文讲解事项点

  1. 基于两种机器语言 InterIA32 和 X86-64。前者是计算机主导语言,后者是64位机器上运行的扩展。
  2. 先浏览C语言,汇编语言和机器代码的关系,然后介绍IA32的细节,从数据的表示和处理以及控制的实现开始, 之后会讲到过程的实现,如果维护运行栈来支持过程间数据和控制的传递,以及局部空间的存储。紧接着会考虑在机器级别实现数组,结构和联合这样的数据结构,结尾,我们将给出GDB调试器检查机器级程序运行时行为的技巧。

历史观点

Linux使用了平坦寻址方式将整个存储空间看成一个大的字节数组。

程序编码


编译选项 -01 告诉编译器使用第一级别的优化, 提高优化级别会使最终程序运行得更快,但编译时间更长,从得到的程序性能方面考虑,第二级别的优化 -02 是被认为较好的选择。

实际上gcc命令调用了一系列程序,使得源代码转化为可执行的代码。

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

1. 机器级代码

机器级编程,其中两种抽象尤为重要。

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

在整个编译过程中,编译器会完成大部分的工作,将把用C语言提供的相对比较抽象的执行模型表示的程序转化成处理器执行的非常基本的指令。汇编代码有一个的主要特点,即它用可读性更好的文本格式来表示。能够理解汇编代码以及它与原始C代码的联系,是理解计算机如何执行程序的关键一步。

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

  • 程序计数器(在IA32中,通常称为“PC”,用 %eip表示)指示将要执行的下一条指令在存储器中的地址。
  • 整数寄存器文件包含8个命名的位置,分别存储32位的值。这些寄存器可以存储地址(对应于C语言的指针)或整数数据。有的寄存器被用来记录某些重要的程序状态,而其他的寄存器则用来保存临时数据,例如过程的局部变量和函数的返回值。
  • 条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化,比如说用来实现if和while语句。
  • 一组浮点寄存器存放浮点数据。

2. 代码示例

int accum = 0;int sum(int x, int y) {int t = x + y;accum += t;return t;
}

命令上使用 -S 能得到C语言编译器产生的汇编代码 code.s

gcc -O1 -S code.c


以上代码中每个缩进去的行都对应一条机器指令。比如,pushl指令表示应该将寄存器 % ebp 的内容压入程序栈。这段代码中已经除去了所有关于局部变量名或数据类型的信息。我们还看到了一个对全局变量 accum 的引用,这是因为编译器还不能确定这个变量会放在存储器中的哪个位置。

使用 -c 命令行选项, GCC会编译并汇编该代码 生成 code.o

gcc -O1 -c code.c

如何找到程序的字节表示? 可以利用反汇编器,根据目标代码生成类似汇编代码的格式。 在Linux中 带 -d 命令行标志的程序
OBJDUMP可以充当这个角色。


我们看到按照前面的字节顺序排列的17个十六进制字节值,它们分成了几组,每组有1~6个字节。每组都是一条指令,右边是等价的汇编语言。
其中一些关于机器代码和它的反汇编表示的特性值得注意:

  • IA32指令长度从1到 15个字节不等。常用的指令以及操作数较少的指令所需的字节数少,而那些不太常用或操作数较多的指令所需字节数较多。
·
  • 设计指令格式的方式是,从某个给定位置开始,可以将字节唯一地解码成机器指令。例如,只有指令 pushl %ebp是以字节值55开头的。
  • 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码。它不需要访问程序的源代码或汇编代码。
  • 反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有些细微的差别。在我们的示例中,它省略了很多指令结尾的’i’。这些后缀是大小指示符,在大多数情况下可以忽略。

生成实际可执行的代码需要对一组目标代码文件运行链接器, 这一组目标代码文件必须有一个main函数, 定义一个main.c文件,里面有这样的函数:

int main() {return sum(1, 3);
}gcc -O1 -o prog code.o main.c

文件prog变成了9123个字节,因为它不仅包含两个过程的代码,还包含了用来启动和终止程序的信息,以及用来与操作系统交互的信息。我们也可以反汇编prog文件∶

这段代码与code.c反汇编产生的代码几乎完全一样。

  • 其中一个主要的区别是左边列出的地址不同——链接器将代码的地址移到一段不同的地址范围中。
  • 第二个不同之处在于链接器确定了存储全局变量 accum的地址。在code.o反汇编代码的第6行,accum的地址还是0。在prog 的反汇编代码中,地址就设成了0x804a018。这可以从指令的汇编代码格式中看到。还可以从指令的最后4个字节中看出来,从最低位到最高位列出就是18 a0 04 08。

数据格式

由于是从16位体系结构扩展成32 位的,Intel用术语"字"(word)表示16 位数据类型。因此,称32位数为"双字"double words),称64位数为"四字"(quad words)。我们后面遇到的大多数指令都是对字节或双字操作的。

如图所示,大多数GCC生成的汇编代码指令都有一个字符后缀,表明操作数的大小。例如,数据传送指令有三个变种; movb(传送字节)、movw(传送字)和movl(传送双字)。后缀’l’用来表示双字,因为将32位数看成是"长字"(long word),这是由于沿用了16 位字为标准那个时代的习惯。

注意的是:汇编代码也使用后缀’l’来表示4字节整数和8字节双精度浮点数。这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。

访问信息

一个IA32中央处理单元(CPU)包含一组8个存储32位值的寄存器。这些寄存器用来存储整数数据和指针。

图3-2显示了这8个寄存器。它们的名字都以%e开头。在大多数情况,前6 个寄存器都可以看成通用寄存器,对它们的使用没有限制。我们说“在大多数情况”,是因为有些指令以固定的寄存器作为源寄存器和/或目的寄存器。另外,在过程处理中,对前3个寄存器(%eax、%ecx和 %edx)的保存和恢复惯例不同于接下来的三个寄存器(%ebx、%edi 和 %esi)。我们后续对此加以讨论。最后两个寄存器(%ebp和%esp)保存着指向程序栈中重要位置的指针。只有根据栈管理的标准惯例才能修改这两个寄存器中的值。

如图3-2所示,字节操作指令可以独立地读或者写前4个寄存器的2个低位字节。8086中提供这样的特性是为了后向兼容8008和 8080-—两款可以追溯到1974年的微处理器。

  • 当一条字节指令更新这些单字节"寄存器元素"中的一个时,该寄存器余下的3个字节不会改变。
  • 类似地,字操作指令可以读或者写每个寄存器的低16位。这个特性源自IA32从16 位微处理器演化而来的这个传统,当对大小指示符为 short 的整数进行运算时,也会用到这些特性。

1. 操作数指示符

大多数指令有一个或多个操作数(operand),指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置。

IA32支持多种操作数格式(参见图3-3)。源数据值可以以常数形式给出,或是从寄存器或存储器中读出。结果可以存放在寄存器或存储器中。因此,各种不同的操作数的可能性被分为三种类型。

  • 第一种类型是立即数(immediate),也就是常数值。在 ATT格式的汇编代码中,立即数的书写方式是 s 后面跟一个用标准C表示法表示的整数,比如,'$-577’或$0xlF。任何能放进一个32位的字里的数值都可以用做立即数,不过汇编器在可能时会使用一个或两个字节的编码。
  • 第二种类型是寄存器(register),它表示某个寄存器的内容,对双字操作来说,可以是8个32位寄存器中的一个(例如,%eax),对字操作来说,可以是8个16 位寄存器中的一个(例如,%ax),或者对字节操作来说,可以是8个单字节寄存器元素中的一个(如%al)。在图3-3中,我们用符号Ea 来表示任意寄存器a,用引用R[ Ea ]来表示它的值,这是将寄存器集合看成一个数组 R,用寄存器标识符作为索引。
  • 第三类操作数是存储器(memory)引用,它会根据计算出来的地址(通常称为有效地址)访问某个存储器位置。因为将存储器看成一个很大的字节数组,我们用符号Mb【Addr】表示对存储在存储器中从地址 Addr开始的b个字节值的引用。为了简便,我们通常省去下方的b。

如图所示,有多种不同的寻址模式,允许不同形式的存储器引用。表中底部用语法Imm(Eb, Ei, s)表示的是最常用的形式这样的引用有四个组成部分: 一个立即数偏移 Imm,一个基址寄存器Eb,一个变址寄存器Ei 和一个比例因子s,这里s必须是1、2、4或者8。然后,有效地址被计算为Imm + R【Eb】+ R【Ei】·s。引用数组元素时,会用到这种通用形式。其他形式都是这种通用形式的特殊情况,只是省略了某些部分。正如我们将看到的,当引用数组和结构元素时,比较复杂的寻址模式是很有用的。

2. 数据传送指令

将数据从一个位置复制到另一个位置的指令是最频繁使用的指令。
操作数表示的通用性使得一条简单的数据传送指令能够完成在许多机器中要好几条指令才能完成的功能。

图3-4列出的是一些重要的数据传送指令。把许多不同的指令分成了指令类,一类中的指令执行一样的操作,只不过操作数的大小不同。

MOV类中的指令将源操作数的值复制到目的操作数中:

  • 源操作数指定的值是一个立即数,存储在寄存器中或者存储器中。目的操作数指定一个位置,要么是一个寄存器,要么是一个存储器地址。
  • IA32加了一条限制,传送指令的两个操作数不能都指向存储器位置将一个值从一个存储器位置复制到另一个存储器位置需要两条指令——第一条指令将源值加载到寄存器中,第二条将该寄存器值写入目的位置
  • 对movl 来说, 这些指令的寄存器操作数,可以是8个32位寄存器($eax~%ebp)中的任意一个,对movw来说,可以是8 个16位寄存器(%ax ~%bp)中的任意一个,而对于movb来说,可以是单字节寄存器元素(%ah ~%bh,号al~%bl)中的任意一个。下面的 MOV指令示例给出了源类型和目的类型的五种可能的组合。记住,第一个是源操作数,第二个是目的操作数。
  • MOVS和 MOVZ指令类都是将一个较小的源数据复制到一个较大的数据位置,**高位用符号位扩展(MOVS)或者零扩展(MOVZ)进行填充。用符号位扩展,目的位置的所有高位用源值的最高位数值进行填充。用零扩展,所有高位都用零填充。**正如看到的那样,这两个类中每个都有三条指令,包括了所有的源大小为1个和2个字节、目的大小为2个和4个的情况(当然,省略了冗余的组合movsww 和 movzww)。

注意点

pushl 和 popl 这两个数据传送操作会将数据压入程序栈中, 以及从程序栈中弹出数据。

图3-5的第三栏说明的是在执行完pushl后立即执行指令 popl %edx 的效果。先从存储器中读出值0x123,再写到寄存器号edx中,然后,寄存器号esp 的值将增加回到0x108。

  • pushl 指令的功能是将数据压入栈上,而popl指令是弹出数据,都仅有一个操作数–压入的数据源和弹出的数据目的。
  • pushl %ebp 的行为: 将一个双字值压入栈中,先将栈指针减4,然后将值写入新的栈顶地址。

与下面两条指令等价,但在目标代码中pushl指令编码为1字节,下面两条需要6个字节。

subl $4, %esp
movl %ebp, (%esp)
  • popl %eax 的行为: 弹出一个双字的操作包括从栈顶位置读出数据,将栈指针加4
movl (%esp), %eax
addl $4, %esp

注意的是: 值0x123仍然会保持在存储器位置0x104 中,直到被覆盖(例如被另一条入栈操作覆盖)。无论如何,esp指向的地址总是栈顶。任何存储在栈顶之外的数据都被认为是无效的。

算术与逻辑操作

图3-7列出了一些整数和逻辑操作。大多数操作都分成了指令类,这些指令类有各种带不同大小操作数的变种。(只有leal没有其他大小的变种。)这些操作被分为四组∶加载有效地址、一元操作、二元操作和移位。二元操作有两个操作数,而一元操作有一个操作数。

1. 加载有效地址

加载有效地址(load effective address)指令leal实际上是movl指令的变形。它的指令形式是从存储器读数据到寄存器,但实际上它根本就没有引用存储器。它的第一个操作数看上去是一个存储器引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。

在图3-7中我们用C语言的地址操作符&S 说明这种计算: 这条指令可以为后面的存储器引用产生指针。另外,它还可以简洁地描述普通的算术操作。例如,如果寄存器%edx 的值为x,那么指令leal 7(%edx,%edx,4),%eax将设置寄存器%eax 的值为5x+7。编译器经常发现一些 leal 的灵活用法,根本与有效地址计算无关。目的操作数必须是一个寄存器。

2. 一元操作和二元操作

第二组中的操作是一元操作,它只有一个操作数,既是源又是目的。这个操作数可以是一个寄存器,也可以是一个存储器位置。比如说,指令incl(%esp)会使栈顶的4字节元素加1。这种语法让人想起C语言中的加1运算符(++)和减1运算法(–))。

第三组是二元操作,其中,第二个操作数既是源又是目的。这种语法让人想起C语言中的赋值运算符,例如 x+=y。不过,要注意,源操作数是第一个,目的操作数是第二个,对于不可交换操作来说,这看上去很奇特。例如,指令 subl %eax,%edx 使寄存器%edx的值减去号eax 中的值。(将指令解读成"从 %edx 中减去%eax"会有所帮助。)第一个操作数可以是立即数、寄存器或是存储器位置。第二个操作数可以是寄存器或是存储器位置。不过,同movl 指令一样,两个操作数不能同时是存储器位置。

3. 移位操作

移位操作:

  • 先给出移位量,然后第二项给出的是要移位的位数。它可以进行算术和逻辑右移。
  • 移位量用单个字节编码,因为只允许进行0到31位的移位(只考虑移位量的低5 位)。移位量可以是一个立即数,或者放在单字节寄存器元素% cl中。(这些指令很特别,因为只允许以这个特定的寄存器作为操作数。)
  • 左移指令有两个名字∶SAL 和 SHL。两者的效果是一样的,都是将右边填上0。
  • 右移指令不同,SAR执行算术移位(填上符号位),而 SHIR执行逻辑移位(填上0)。

移位操作的目的操作数可以是一个寄存器或是一个存储器位置。

例子

特殊的算术操作


图3-7中列出的imull 指令称为"双操作数"乘法指令。它从两个32位操作数产生一个32位乘积。回想一下,将乘积截取为32位时,无符号乘和补码乘的位级行为是一样的。

IA32还提供了两个不同的"单操作数"乘法指令,以计算两个32位值的全64位乘积——一个是无符号数乘法(mull),而另一个是补码乘法(imull)

  • 这两条指令都要求一个参数必须在寄存器 %eax 中,而另一个作为指令的源操作数给出。
  • 然后乘积存放在寄存器%edx(高32位)和%eax(低32位)中。虽然imull 这个名字可以用于两个不同的乘法操作,但是汇编器能够通过计算操作数的数目,分辨出想用哪条指令。

有符号除法指令idivl 将寄存器 %edx(高32位)和 %eax(低32 位)中的64位数作为被除数,而除数作为指令的操作数给出。指令将商存储在寄存器%eax 中,将余数存储在寄存器%edx中。


第1行的传送指令和第3行的算术移位指令联合起来的效果,就是根据x的符号将寄存器%edx 设置为全零或者全一;而第2行的传送指令将x复制到%eax。因此,我们有了将寄存器%edx和eax联合起来存放x的64位符号扩展的版本。在 idivl指令之后,商和余数被复制到栈顶的两个位置(指令5和6)。

控制

机器代码提供两种基本的低级机制来实现有条件的行为∶测试数据值,然后根据测试的结果来改变控制流或者数据流。

数据相关的控制流是实现有条件行为的更通用和更常见的方法,所以我们先来介绍它。通常,C语言中的语句和机器代码中的指令都是按照它们在程序中出现的次序,顺序执行的。用jump指令可以改变一组机器代码指令的执行顺序,jump指令指定控制应该被传递到程序的哪个其他部分,可能是依赖于某个测试的结果。编译器必须产生指令序列,这些指令序列构建在这种实现C语言控制结构的低级机制之上。
本文会先涉及机器级机制,然后说明如何用它们来实现C语言的各种控制结构。之后,我们会回来介绍使用有条件的数据传输来实现与数据相关的行为。

1. 条件码

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

  • leal指令不改变任何条件码,因为它是用来进行地址计算的。
  • 除此之外,图3-7中列出的所有指令都会设置条件码。对于逻辑操作,例如XOR,进位标志和溢出标志会设置成0。对于移位操作,进位标志将设置为最后一个被移出的位,而溢出标志设置为0。INC和DEC指令会设置溢出和零标志,但是不会改变进位标志。
  • 有两类指令(有8、16和32位形式),它们只设置条件码而不改变任何其他寄存器;
  1. CMP指令根据它们的两个操作数之差来设置条件码。除了只设置条件码而不更新目标寄存器之外,CMP指令与SUB指令的行为是一样的。如果两个操作数相等,这些指令会将零标志设置为1,而其他的标志可以用来确定两个操作数之间的大小关系。
  2. TEST指令的行为与AND指令一样,除了它们只设置条件码而改变目的寄存器的值。典型的用法是,两个操作数是一样的(例如,testl %eax,seax用来检查告eax是负数、零,还是正数),或其中的一个操作数是一个掩码,用来指示哪些位应该被测试。

2. 访问条件码

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

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

对于第一种情况,图3-11中描述的指令根据条件码的某个组合,将一个字节设置为0 或者1。我们将这一整类指令称为SET指令;

  • 它们之间的区别就在于他们考虑的条件码的组合是什么,这些指令名字的不同后缀指明了它们所考虑的条件码的组合。

  • 这些指令的后缀表示不同的条件而不是操作数大小,知道这一点很重要。例如,指令setl和setb表示“小于时设置”(set less)和“低于时设置”(set below),而不是“设置长字”(set long word)和“设置字节”(set byte)。

  • 一条 SET指令的目的操作数是8个单字节寄存器元素之一,或是存储一个字节的存储器位置,将这个字节设置成0或者1。为了得到一个32位结果,我们必须对最高的24位清零, 下面是一个 a < b的典型指令序列。

  • 各个SET命令的描述都适用的情况是∶执行比较指令,根据计算t=a-b设置条件码。更具体地说,假设a、b和t分别是变量a、b和t的补码形式表示的整数,因此t=a-'b,这里w取决于a和b的大小。

例子:

  • 来看sete的情况,即“当相等时设置”(set when equal)指令。当a=b时,会得到t=0,因此零标志置位就表示相等。
  • 考虑用setl,即“当小于时设置”(set when less)指令,测试一个有符号比较。如果没有发生溢出(OF设置为0就表明无溢出),我们有当a-b<0时a<b,将SF设置为1即指明这一点,而当a一b≥0时a≥b,由SF设置为0指明。另一方面,如果发生溢出,我们有当a一b>0(负溢出)时a<b,而当a-b<0(正溢出)时a>b。当a=b时,不会有溢出。因此,当OF被设置为1时,我们有当且仅当SF被设置为0,有a <b。将这些情况结合起来,溢出和符号位的EXCLUSIVE-OR提供了a<b是否为真的测试。其他的有符号比较测试基于SF^OF和ZF的其他组合

对于无符号比较的测试,现在设a和b是变量a和b的无符号形式表示的整数。在执行计算t=a-b中,当a-b<0时,CMP指令会设置进位标志,因而无符号比较使用的是进位标志和零标志的组合。有些情况需要用不同的指令来处理有符号和无符号操作,例如,使用不同版本的右移、除法和乘法指令,以及不同的条件码组合。

3. 跳转指令及编码

正常执行的情况下,指令按照它们出现的顺序一条一条地执行。跳转(jump)指令会导致执行切换到程序中一个全新的位置。在汇编代码中,这些跳转的目的地通常用一个标号(label)指明。考虑下面的汇编代码序列∶


指令jmp.L1会导致程序跳过movl指令,从popl指令开始继续执行。在产生目标代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。

图3-12列举了不同的跳转指令

  • jmp指令是无条件跳转,它可以是直接跳转,即跳转目标是作为指令的一部分编码的;也可以是间接跳转,即跳转目标是从寄存器或存储器位置中读出的。 汇编语言中,直接跳转是给出一个标号作为跳转目标的,例如上面所示代码中的标号“.L1”。间接跳转的写法是‘*’后面跟一个操作数指示符,可以使用3.4.1节中描述的格式中的一种。

举个例子,指令:

jmp *%eax 是用寄存器 %eax 中的值作为跳转目标,而指令
jmp *(%eax) 是以 %eax 中的值读地址,然后再从存储器读出跳转目标
  • 表中所示的其他跳转指令都是有条件的——它们根据条件码的某个组合,或者跳转,或者继续执行代码序列中下一条指令。这些指令的名字和它们的跳转条件与SET指令是相匹配的(参加图3-11)。条件跳转只能是直接跳转。

跳转指令的编码

常用的都是PC相关的(PC-relative,译者注∶PC=Program Counter,程序计数器)。它们会将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差作为编码这些地址偏移量可以编码为1、2或4个字节。第二种编码方法是给出“绝对”地址,用4个字节直接指定目标。汇编器和链接器会选择适当的跳转目的编码。


右边反汇编器产生的注释中,第1行跳转指令的跳转目标指明为0x17,第7行跳转指令的跳转目标是0xa。不过,观察指令的字节编码,会看到第一条跳转指令的目标编码(在第二个字节中)为0xd(十进制13)。把它加上0xa(十进制10),也就是下一条指令的地址,就得到跳转目标地址0x17(十进制23),也就是第8行指令的地址。

类似地,第二个跳转指令的目标用单字节、补码表示编码为0xf3(十进制-13)。将这个数加上0x17(十进制23),即第8行指令的地址,我们得到0xa(十进制10),即第2行指令的地址。

这些例子说明,当执行与PC相关的寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。这种惯例可以追溯到早期实现,当时的处理器会将更新程序计数器作为执行一条指令的第一步。

4. 翻译条件分支

将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。


C语言中的if-else语句的通用形式模板是这样的∶


这里test-expr是一个整数表达式,它的取值为0(解释为“假”)或者为非0值(解释为“真”)。两个分支语句中(then-statement或else-statement)只会执行一个。

对于这种通用形式,汇编实现通常会使用下面这种形式,这里,我们用C语法来描述控制流,如图, 也就是,汇编器为then-statement和else-statement产生各自的代码块。它会插入条件和无条件分支,以保证能执行正确的代码块

5. 循环

C语言提供了多种循环结构,即 do-while、while和for。汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。大多数汇编器根据一个循环的 do-while 形式来产生循环代码,即使在实际程序中这种形式用的相对较少。其他的循环会首先转换成do-while形式,然后再编译成机器代码

  • do-while 循环的效果就是重复执行body-statement,对test-expr求值,如果求值的结果为非零,则继续循环。可以看到,body-statement至少会执行一次。


  • while 循环

与do-while不同的是,它对test-expr求值,在第一次执行body-statement之前,循环就可能中止。

将while循环翻译成机器代码有很多种方法。一种常见的方法,也是GCC采用的方法,是使用条件分支,在需要时省略循环体的第一次执行,从而将代码转换成do-while循环。



使用这种实现策略,编译器常常会优化最开始的测试,比如说认为总是满足测试条件。

  • for 循环

转换为 do-while形式



6. 条件传送指令

实现条件操作的传统方法是利用控制的条件转移。当条件满足时,程序沿着一条执行路径进行,而当条件不满足时,就走另一条路径。这种机制简单而通用,但是在现代处理器上可能会非常的低效率。

数据的条件转移是一种替代的策略:这种方法先计算一个条件操作的两种结果,然后再根据条件是否满足从而选取一个。只有在一些受限制的情况下,这种策略才可行,但是如果可行,就可以用一条简单的条件传送指令来实现它。条件传送指令更好地匹配了现代处理器的性能特性。

为什么条件传送指令的代码会比基于条件控制转移的代码性能好

处理器通过使用流水线(pipelining)来获得高性能。在流水线中,一条指令的处理要经过一系列的阶段,每个阶段执行所需操
作的一小部分(例如,从存储器中取指令,确定指令的类型,从存储器中读数据,执行算术运算,向存储器中写数据,以及更新
程序计数器)。这种方法通过重叠连续指令的步骤来获得高性能,例如,在取一条指令的时候,执行它前面一条指令的算术运
算。要做到这一点,要求能够事先确定要执行指令的序列,这样才能保持流水线中充满了待执行的指令。当机器遇到条件跳转(也称为“分支”)时,它常常还不能够确定是否会进行跳转。处理器采用非常精密的分支预测逻辑试图猜测每条跳转指令是否会
执行。只要它的猜测还比较可靠(现代微处理器设计试图达到90%以上的成功率),指令流水线中就会充满着指令。另一方面,错误预测一个跳转要求处理器丢掉它为该跳转指令后所有指令已经做了的工作,
然后再开始用从正确位置处起始的指令去填充流水线。正如我们会看到的,这样一个错误预测会招致很严重的惩罚。
大约20~40个时钟周期的浪费,导致程序性能的严重下降。

条件传送指令列举

实例

为了理解如何通过条件数据传输来实现条件操作,考虑下面的条件表达式和赋值的通用形式∶

编译器产生的代码具有一下抽象代码所示的形式:


这个序列中的最后一条语句是用条件传送实现的,只有当测试条件t满足时,vt 的值才会被复制到v中。

注意:

不是所有的条件表达式都可以用条件传送来编译。最重要的是,我们给出的抽象代码会对then-expr和else-expr都求值,无论测试结果如何。如果这两个表达式中的任意一个可能产生错误条件或者副作用(比如解引用空指针,或者有改变全局变量的操作),就会导致非法的行为。

7. switch 语句

switch(开关)语句可以根据一个整数索引值进行多重分支(multi-way branching)。处理具有多种可能结果的测试时,这种语句不仅提高了C代码的可读性,而且通过使用跳转表(jump table)这种数据结构使得实现更加高效。

跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当开关索引值等于i时程序应该采取的动作。程序代码用开关索引值来执行一个跳转表内的数组引用,确定跳转指令的目标。和使用一组很长的if-else语句相比,使用跳转表的优点是执行开关语句的时间与开关情况的数量无关。

GCC根据开关情况的数量和开关情况值的稀少程度(sparsity)来翻译开关语句:当开关情况数量比较多(例如4 个以上),并且值的范围跨度比较小时,就会使用跳转表。


  • 跳转表对重复情况的处理就是简单地对表项4和6用同样的代码标号(loc_D)
  • 对于缺失情况的处理就是对表项1和5使用默认情况的标号(loc_def)

过程

过程调用

  • 概念: 数据(过程参数与返回值)与控制从代码的一部分传递到另一部分。
  • 规则: 在进入时为过程的局部变量分配空间,退出时释放这些空间。
  • 现代处理器的支持: 仅仅包含转移控制到过程,和从过程转移出控制这种简单的指令。 而数据传递,局部变量的分配和释放需要通过操纵程序栈来实现。

1. 栈帧结构

IA32程序用程序栈来支持过程调用。

栈帧(stack frame):机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储,为单个过程分配的那部分栈)

图3-21描绘了栈帧的通用结构: 栈帧的最顶端以两个指针界定,寄存器 %ebp 为帧指针,而寄存器 %esp为栈指针。当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。

这里简单叙述一下:假设过程P(调用者)调用过程Q(被调用者),则Q的参数放在P的栈帧中。另外,当P 调用Q时,P中的返回地址被压人栈中,形成P的栈帧的末尾。返回地址就是当程序从Q返回时应该继续执行的地方。Q的栈帧从保存的帧指针的值(例如 %ebp)开始,后面是保存的其他寄存器的值。

2. 转移控制


当执行 P call Q时产生了如下代码:


3. 寄存器使用惯例

寄存器是被所有的过程(或者说函数)共享的,只不过实际上一次只有一个过程(函数)可以使用它的资源。

这实际上就会引发一个问题:如果寄存器A存储了P的一些信息,当P调用Q时,如果Q也使用A那么就会覆盖掉A存储的P的信息(这样实际上P的信息就丢失了)。

所以必须有一个原则,callee不得覆盖caller之后还会用到的寄存器的信息。(实际上,限制了callee的访问权限)
为了解决这个问题,IA32机器对寄存器加入了一些“限制”,规定了哪些寄存器的状态被caller或者callee保存。

  • %eax,%edx,%ecx :caller-saved寄存器。当P调用Q时,Q可以使用这些寄存器而不用担心破坏P的信息。
  • %ebx,%esi,%edi :callee-saved寄存器。当Q需要覆盖这些寄存器的信息的时候,必须先将其copy到内存中,因为调用Q的caller可能会在今后的计算中用到这些数据。
  • %esp,%ebp:必须要保持状态,改变时要copy出一个副本以便恢复。

4. 过程示例

caller的栈帧包括局部变量arg1和arg2的存储,其位置相对于帧指针是-4和-8。这些变量必须存在栈中,因为我们必须为它们生成地址。

接下来的这段汇编代码来自caller编译过的编译版本,说明它如何调用swap_add∶

  • 保存 %ebp 的副本,并设置 %ebp 为栈帧的开始位置, 2-3 行
  • 栈分配24字节,4行
  • 蒋arg1和arg2分配到栈的内存上,并取出他们的有效地址存储到栈上,形成 swap_add 的参数
  • 调用swap_add

swap_add编译过的代码有三个部分∶

  • "建立"部分: 调用者过程蒋返回地址压入栈中
  • 初始化栈帧: 栈帧指向过程的开始,并存储一个旧栈帧的指针地址。
  • 主体"部分,执行过程的实际计算;
  • "结束"部分,恢复栈的状态,以及过程返回。

5. 递归过程

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

理解: 只要递归,就(保存的返回位置、栈指针和被调用者保存寄存器的值),很多次递归调用就会形成一种链表,每个节点都是一个栈帧,ret返回时就尾删法。

示例


数组分配和访问

1.基本原则

声明

分配

  • 它在存储器中分配一个L·N字节的连续区域;这里L是数据类型T的大小(单位为字节),用Xa 来表示起始位置。
  • 它引入了标识符A;可以用A作为指向数组开头的指针,这个指针的值就是Xa。可以用从0到N-1之间的整数索引来访问数组元素。数组元素i会被存放在地址为Xa+L i 的地方*。

声明和分配的例子


访问

IA32的存储器引用指令可以用来简化数组访问。

例如,假设E是一个int.型的数组,并且我们想计算E【i】,在此,E的地址存放在寄存器 %edx 中,而 i 存放在寄存器 %ecx中。然后,如下指令会执行地址计算 Xa+4i,读这个存储器位置的值,并将结果存放到寄存器%eax中

2. 指针运算

基本运算条例

  • C语言允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型的大小进行伸缩。如果p是一个指向类型为T的数据的指针,p的值为Xp,那么表达式p+i的值为 Xp+L·i,这里L是数据类型T的大小。

  • 单操作数的操作符&和*可以产生指针和间接引用指针

扩展例子

一个整形数组E的起始地址, 数组索引 i 的值,存在 %edx 和 %ecx,
表达式计算结果存在 % eax 中

3. 嵌套数组

声明和分配:

多维数组可以化成 单维数组,因此数组的分配和引用的一般原则都是成立的。

对于一个数组声明为:

T  D[R][C]   D[I][J] 的存储地址为  &D[i][j] = Xd + L(C * I + J )

在寄存器中的表示

例如:将 A【i】【j】复制到寄存器 %eax 中

小知识: 这段代码计算元素的地址为 Xa +4j + 12i = Xa +4(3i+j),它使用移位、加法和伸缩的组合来避免开销更大的乘法操作。

4. 定长数组

编译器对定长数组的处理

如二维数组: A【i】【j】

如果循环只会访问A的 I 行的元素,那么编译器会创角一个局部指针变量,命名为 Arow, 提供对矩阵第 i 行的直接访问。 这样 汇编指令会相比用A【i】【j】直接取元素少很多。

例子:

5. 变长数组

定义

可以说是在运行期才 规定大小并分配的数组。

汇编代码如下:

缺陷与优化

性能问题:由于是动态参数 n, 因此计算时需要使用乘法指令,使得对 i 伸展 n 倍。

编译器的简单优化: Bptr 增加时可以 增加 sizeof(int) * n

注意: 每次循环中,代码从存储器中取出n的值,检查循环是否终止(第7行)。

这是一个寄存器溢出(register spilling)的例子∶没有足够多的寄存器来保存需要的临时数据,因此编译器必须把一些局部变量放在存储器中。在这个情况下,编译器选择把n溢出,因为它是一个"只读"的值——在循环中不会改变它的值。因为IA32处理器的寄存器数量太少,必须常常将循环值溢出到存储器中。通常,读存储器完成起来比写存储器要容易得多,因此将只读变量溢出是比较合适的。

异质的数据结构

1. 结构体

是什么?

C语言的struct 声明创建一个数据类型,将可能不同类型的对象聚合到一个对象中。结构的各个组成部分用名字来引用。

声明和内存分布

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

例子:

struct rec*类型的变量r放在寄存器%edx中。

2. 联合

是什么与能干什么?

联合提供了一种方式,能够规避C语言的类型系统,允许以多种类型来引用一个对象,能用不同的字段来引用相同的存储器块。

声明


特点:

  • 一个联合的总的大小,等于它最大字段的大小
  • 且其联合的指针和其数据项的字段的指针引用的都是 联合的起始地址。

应用面:

  • 对于一个数据结构的不同字段彼此之间是互斥的,以减少分配空间的总量。

例子:比如一个二叉树的数据结构,每一个叶子节点都有一个double类型的数据值,而每个内部节点都有只想两个孩子节点的指针。

  • 访问不同数据类型的位模式, 比如用一种数据类型来存储联合中的参数,又可以用另一种数据类型来访问它。

注意:字节序会影响数据

在IA32这样的小端法机器上,参数word0是d的低位4个字节,而wordl是高位4个字节。在大端法机器上,这两个参数的角色刚好相反。

3. 数据对齐

是什么?: 计算机系统对基本数据类型的合法地址的限制,要求其类型对象的地址必须是某个值 K (2,4, 8) 的倍数。

为什么需要对齐

  1. 简化形成处理器和存储器系统之间接口的设计和效率, 如果处理器总是从存储器取3字节大小, 如果未对齐,处理器访问起始地址是偶数的 int 类型的变量就要 进行两次。
  2. 有些指令是必须进行 其特定 字节对其,其无法对 未对齐的字节块做处理。

特定的对齐规则

IA32 系统惯例: 确保每一个栈帧的长度 是16字节的整数倍,编译器就可以在栈帧中以每个块都是16字节对其的方式分配存储。

X86-64 的对齐要求: 对于任何需要K字节的标量数据类型来说,其起始地址必须是K的倍数。

综合:理解指针

应用: 使用GDB调试

GDB 总结

存储器的越界引用和缓冲区溢出

原因:

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

例子:

理解下 gets 示例:


理解下超量字符串长度会破坏的状态

缓存区溢出攻击

缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法

比如:输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码(exploit code),另外,还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,执行 ret 指令的效果就是跳转到攻击代码。

如何对抗?

  • 栈随机化:使得栈的位置在程序每次运行都有变化。
  • 栈破坏检测:建立一个哨兵值,放置在栈内的仅可读区(比如返回地址附近)。

  • 限制可执行代码区域i

消除攻击者向系统中插入可执行代码的能力。一种方法是限制那些能够存放可执行代码的存储器区域。在典型的程序中,只有保存编译器产生的代码的那部分存储器才需要是可执行的。其他部分可以被限制为只允许读和写。

x86-64: 将IA32扩展到64位

我们正在经历一个向Intel指令集64位版本的过渡。最初由Advanced Micro Devices(AMD)提出并命名为x86-64,现在它被大多数AMD的处理器(AMD64)和Intel(Intel64)所支持。

本节作为我们对IA32描述的一个补充,描述了为适应x86-64在硬件和软件支持中所做的扩展。总结x86-64代码区别于IA32代码的主要特性,最后分别介绍每个特性。

1. X84-64 的历史和动因

IA32的32位字长已经成为限制微处理器能力不断增长的主要因素。最重要的是,机器的字长定义了程序能够使用的虚拟地址范围,32位字长就是4GB虚拟地址空间。现在机器很容易就可以配置4G以上的RAM,但是系统却不能有效利用它。对于需要处理大数据集的应用比如科学计算、数据库和数据挖掘来说,32位字长使程序员工作起来非常困难。他们必须使用核心外的(out-of-core)算法一来编写代码,也就是数据存放在磁盘上,并且显式地读到存储器中以便处理。

2. 简介

Intel和AMD 提供的新硬件和以这些机器为目标的GCC新版本的组合,使得x86-64代码与为IA32机器生成的代码有极大的不同。

主要特性如下∶

数据类型

汇编代码示例


它产生与任意IA32机器兼容的代码如下(我们为代码添加了注释以突出哪些指令是从存储器读数据(R),而哪些指令是向存储器写数据(W))∶

可以看出的是:IA32的指令对存储器的访问是更多的,且耗费的指令数目更多,也更加复杂,代码没有X86-64简洁。

3. 访问信息

寄存器相关改动

  • 寄存器的数量翻倍至16个

  • 所有的寄存器都是64位长。IA32寄存器的64位扩展分别命名为 %rax、%rcx、%rdx、%rbx、%rsi、%rdi、%rsp和%rbp。新 增加的寄存器命名为%r8~%r15。

  • 可以直接访问每个寄存器的低32位。这就给了我们IA32中熟悉的那些寄存器∶%eax、%ecx、%edx、%ebx、%esi、%edi、%esp和%ebp,以及8个新32位寄存器∶%r8d~%r15d。·同IA32中的情况一样,可以直接访问每个寄存器的低16位。新寄存器的字大小版本命名为号r8w~%r15w。

  • 可以直接访问每个寄存器的低8位。在IA32中,只有对前4个寄存器(%al、%cl、%dl 和 %bl)才可以这样。其他IA32寄存器的字节大小版本命名为*sil、%dil、*spl 和 *bpl。新寄存器的字节大小版本命名为%r8b~%r15b。

  • 为了后向兼容性,具有单字节操作数的指令可以直接访问 %rax、%rcx、%rdx和%rbx 的第二个字节

  • 寄存器 % rsp 有特殊的状态,会保存指向栈顶元素的指针,且没有栈指针寄存器, %rbp 是作为了通用寄存器。

  • 程序计数器命名为 %rip

  • 提供 PC 相对操作数寻址方式, 下图例子,立即数 0x200ad1 加上下面一条指令的地址得到 0x601028(gval2)


新的数据传输指令

注意的是:从较小的数据大小传送到较大的数据大小可以用符号扩展(MOVS)或者零扩展(MOVZ),传送或者产生32位寄存器值的指令也会将寄存器的高32位设置为0。 即当目的是寄存器时,movzbq 和 movzbl 有完全意义的行为。

算术指令改动

  • 增加了在四字上进行运算的指令,后缀为’q’。
  • 这些四字指令的例子包括leaq(加载有效地址)、incq(加1)、addq(加法)和salq(左移)。
  • 这些四字指令有与它们对应的较短操作数的指令一样的参数类型。

例子:

了解: 寄存器合并存储八字

4. 控制

测试指令改动

例子

5. 过程

过程调用 X84-64 的不同点

参数传递

最多可以有6个整型(整数和指针)参数可以通过寄存器进行传递。寄存器按照指定的顺序来使用,使用的寄存器名对应于所传递的数据的大小。

例子:

栈帧

我们已经看到许多编译后的函数并不需要栈帧。

可能需要栈的原因:

- 如果所有的局部变量都能保存在寄存器中,而且这个函数也不会调用其他函数(参考过程调用的树结构,有时称之为叶子过程(leaf procedure)),那么需要栈的唯一原因就是用来保存返回地址。
- 局部变量太多,不能都放在寄存器中。
- 有些局部变量是数组或者结构。
- 函数用取地址操作符(&)来计算一个局部变量的地址。
- 函数必须将栈上的某些参数传递到另一个函数。
- 在修改一个被调用者保存寄存器之前,函数需要保存它的状态。

栈帧会有固定的大小,且在过程开始时通过减小栈指针(寄存器 % rsp )设置, 访问时通过相对栈指针的偏移来访问数据,
因此不需要帧指针。

例子:

proc 执行时的栈:

寄存器保存惯例

调用者保存的寄存器有∶%rbx、%rbp和%r12~%r15。

调用者保存的临时寄存器: %r10 和%r11。当然,当参数少于6个或者当函数用完了参数时,就可以使用参数寄存器了,而在产生出最终的结果之前,%rax可以重复利用。

例子:

sfact_helper 是如何利用栈来存储被调用者保存寄存器的值:


另外:其不同寻常的特性是能够访问栈指针之外的存储器,要求虚拟存储器管理系统为这段区域分配存储器。(大概128字节的范围)

5. 数据结构

和IA32保持一致。

区别是x86-64遵循一组更严格的对齐要求。对于任何需要K字节的标量数据类型来说,它的起始地址必须是K的倍数。

浮点程序的机器级表示(简述)

我们把存储模型,指令,传递规则的组合成为 机器的体系结构。

计算机内功修炼:程序的机器级表示(C与汇编)相关推荐

  1. 第三章 程序的机器级表示

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

  2. 第 3 章 程序的机器级表示

    文章目录 第 3 章 程序的机器级表示 3.1 历史观点 3.2 程序编码 3.2.1 机器级代码 3.2.2 代码示例 3.2.3 关于格式的注解 3.3 数据格式 3.4 访问信息 3.4.1 操 ...

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

    深入,并且广泛-沉默犀牛 文章目录 文章导读 历史观点 程序编码 机器级代码 代码示例 关于格式的注解 数据格式 访问信息 操作数指示符 数据传送指令 数据传送示例 压入和弹出栈数据 算术和逻辑操作 ...

  4. CSAPP:第三章程序的机器级表示1

    CSAPP:程序的机器级表示1 关键点:数据格式.操作数指示符. 数据格式访问信息操作数指示符举例说明 数据格式   术语字(word)表示16位数据类型,32位数为双字(double words), ...

  5. 计算机系统导论第九章,计算机系统导论 -- 读书笔记 -- 第三章 程序的机器级表示 (持续更新)...

    计算机系统导论 -- 读书笔记 -- 第三章 程序的机器级表示 (持续更新) 第三章 程序的机器级表示 3.1 历史观点 3.2 程序编码 1. 命令行 (1)编译 Linux> gcc -Og ...

  6. 【计算机系统基础4】程序的机器级表示

    [计算机系统基础4]程序的机器级表示 4.程序的机器级表示 4.1(:school: CMU补充 )x86-64 Linux 寄存器使用 4.2传送指令 4.2.1`mov` 指令 4.2.2 `mo ...

  7. 深入理解计算机系统 第3章 程序的机器级表示

    目录 第3章 程序的机器级表示 数据格式 操作数指示符 练习题 数据传送指令 习题3.4 访问信息 压入和弹出栈数据 算数和逻辑操作 移位操作 讨论 特殊的算数操作 问题 第3章 程序的机器级表示 数 ...

  8. 第三章程序的机器级表示

    原地址:https://www.cnblogs.com/disturbia/p/4869229.html 第三章 程序的机器级表示 3.1 历史观点 Intel处理器系列俗称x86,开始时是第一代单芯 ...

  9. 第三章程序的机器级表示 学习报告

    第三章 程序的机器级表示 3.1 历史观点 Intel处理器系列俗称x86,开始时是第一代单芯片.16位微处理器之一. 每个后继处理器的设计都是后向兼容的--较早版本上编译的代码可以在较新的处理器上运 ...

最新文章

  1. 实用目标检测器 | 性能超YoloV5,推理耗时不变(附github源码)
  2. python资料百度云-Java+Python+前端 学习资料大全 百度云盘
  3. 给定一个数值,输出符合中国人习惯的读法--记一道笔试题
  4. Understanding Ethereum Smart Contracts
  5. 1.5 编程基础之循环控制 21 角谷猜想--方法二
  6. CAN总线技术 | 物理层02 - CAN总线的异步串行通讯
  7. 8. Sphinx 站内搜索
  8. 编程:获取股票实时行情数据大全
  9. ZZULIOJ 1014: 求三角形的面积
  10. ar ebs 销售订单关闭_ZARA母公司拟关闭1200家门店,拿什么拯救快时尚品牌?
  11. 2021-10-21python中spilt函数的学习
  12. 山东省技能兴鲁职业技能竞赛-人工智能工程技术人员
  13. 向量点积衡量相似度_点积相似度、余弦相似度、欧几里得相似度
  14. iOS Charts库绘制曲线
  15. 学习笔记 查分约束系统
  16. 阿里大文娱——优酷场景化营销
  17. Excel打开CSV格式,大数字乱码情况
  18. 厚涂简不简单?怎么入门厚涂
  19. echarts:中国地图实现
  20. 毕业两年的混社会经验:给应届生朋友们的建议【含求职经验】

热门文章

  1. ps制作食品网页总结
  2. QT 实现WINDOWS下网络设置功能(IP、子网掩码、网关、DNS)
  3. 初学爬虫-笔趣阁爬虫
  4. ABAQUS 2020 license冲突解决
  5. win10 任务栏全透明,一键设置(translucentTB)
  6. 2015年个人收获-成果、经验分享(项目经理、集成资质、网站开发、服务器等)
  7. 纳米金;AuNPs;直 径 1~100nm
  8. 前端读取mysql数据库_Servlet读取MySQL数据库并在前端调用
  9. 脑波艺术DAO·dream lab
  10. Jquery each continu