《深入理解计算机系统》学习(3):高级语言和机器指令

Intel处理器系列

Intel处理器系列俗称x86,x86一开始是第一代单芯片、16位微处理器之一。一些Intel微处理器的模型如下:

  • 8086(1978年,29k个晶体管):第一代单芯片、16位微处理器之一。
  • i386(1985年,275k个晶体管):将体系结构扩展到32位,这是Intel系列中第一台全面支持Unix操作系统的机器。
  • Core 2(2006年,291M个晶体管):Intel第一个多核微处理器,多核在一个芯片上。
  • Core i7(2008年,781M个晶体管):最初的版本支持每个核上执行两个程序,每个芯片上最多四个核。

Intel处理器系列有好几个名字,包括IA32,也就是“Intel 32位体系结构”,以及最新的Intel64,即IA32的64位拓展,也称为x86-64。最常用的名字是“x86”,代指整个系列,也反映了直到i486处理器命名的惯例。

机器级编程的两种抽象

计算机系统使用多种形式的抽象,利用更简单的抽象模型来隐藏实现的细节。对于机器级编程来说,两种抽象尤为重要。

(1)第一种是指令集架构(比如x86-64)来定义机器级程序的格式和行为,定义了处理器的状态、指令的格式、以及每条指令对状态的影响(指令集决定了处理器的架构,在处理器基础上决定了操作系统和编译器的实现)。
(2)第二种抽象是机器级程序使用的内存地址是虚拟地址(堆、栈等),提供的内存模型看上去是一个非常大的字节数组。存储器系统实际上是将多个硬件存储器和操作系统软件组合起来。

1 机器级代码

1.1 编译过程

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

(1)C语言代码.c文件
在这里插入图片描述
(2)汇编代码.s文件
缩进的每一行对应一条机器指令。
在这里插入图片描述

(3)目标代码.o文件
目标文件为二进制格式,无法直接查看。机器执行的程序只是字节序列,是对一系列指令的编码。文件中对应汇编代码的字节序列的十六进制表示为:
在这里插入图片描述

1.2 寄存器

一个x86-64的中央处理单元(CPU)包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针。最初的8086中有8个16位的寄存器;IA32后,由16位拓展到32位;x86-64后,8个寄存器扩展到64位,此外还增加了8个新的寄存器。
在这里插入图片描述

1.3 操作数指示符

大多数指令有一个或多个操作数,指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。x86-64支持多种操作数格式,源数据值可以以常数形式给出,或是从寄存器或内存中读出,结果可以存放在寄存器或内存中。因此,可能的操作数被分为三种类型。
(1)立即数,用来表示常数值。" $ "后跟一个标准C表示法表示的整数:"$-577""$0x1F"
(2)寄存器,表示某个寄存器的内容。R[ra]表示寄存器中的内容,可能是数值、地址。
(3)内存引用,根据计算出来的地址访问某个内存位置。M[Addr]中的Addr是地址。
在这里插入图片描述

1.4 数据传送指令

1.4.1 MOV类

最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。最简单形式的数据传送指令是MOV类,这些指令把数据从源位置复制到目的位置,不做任何变化。MOV类由四条指令组成:movb、movw、movl、movq。这些指令执行相同的操作,区别在于操作的数据大小不同,分别是1、2、4和8字节。
在这里插入图片描述
当函数开始执行时,参数xp和y分别存储在寄存器%rdi%rsi中。然后,指令2从内存中xp位置读出x,存放在寄存器%rax中,实现了赋值操作long x = *xp;。指令3将y写入寄存器%rdi中的xp指向的内存位置,实现了赋值操作*xp = y;ret通过将值存储在寄存器%rax或该寄存器的某个低位部分中返回。

可以看到,C语言中所谓的”指针“其实就是地址,间接引用指针就是将该指针放在一个寄存器中,然后在内存引用中使用这个寄存器。像x这样的局部变量通常是保存在寄存器中,而不是内存中。

(2)压入和弹出栈数据:
栈是一种数据结构,可以添加或者删除数据。栈可以实现为一个数组,总从数组的一端插入和删除元素,该端被称为栈顶。栈顶元素的地址是所有栈中元素地址中最低的,被栈指针%rsp保存。

pushq和popq:两个数据传送操作可以将数据压入程序栈中,以及从程序栈中弹出数据。将一个四字值压入栈中,首先要将栈指针减8,然后将值写到新的栈顶地址。弹出一个四字的操作包括从栈顶位置读出数据,然后将栈指针加8。
在这里插入图片描述

1.5 算术和逻辑操作

1.5.1 加载有效地址指令leaq

指令leaq实际上是movq指令的变形,指令形式是从内存读数据到寄存器,但是并没有引用内存,源操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。

该指令可以为后面的内存引用产生指针,也可以描述普通的算术操作。例如,如果寄存器%rdx的值为x,那么指令leaq 7(%rdx, %rdx, 4), %rdx将设置寄存器%rdx的值为5x+7(x+4*x+7)。

1.5.2 一元和二元操作

(1)一元操作INC(加1)、DEG(减1)、NEG(取负)、NOT(取补):只有一个操作数,可以是一个寄存器或者一个内存地址,该操作数既是源,又是目的。

例如,指令incq(%rsp)会使栈顶的8字节元素加1。类似++x;

(2)二元操作ADD(加)、SUB(减)、IMUL(乘)、XOR(亦或)、OR(或)、AND(与):其中第一个操作数可以是立即数、寄存器或者是内存位置,第二个操作数可以是寄存器或是内存位置。

例如,指令subq %rax,%rdx使寄存器%rdx的值减去%rax中的值。类似x -= y;

1.5.2 移位操作

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

左移指令有两个名字:SALSHL。两者的效果是一样的,都是将右边填上0。右移指令不同,SAR执行算术移位(填上符号位),而SHR执行逻辑移位(填上0)。

1.6 控制

1.6.1 条件码

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

条件码含义置位条件
CF进位标志最近的操作使最高位产生了进位
ZF零标志最近的操作得出的结果为0
SF符号标志最近的操作得到的结果为负数
OF溢出标志最近的操作导致一个补码溢出

在1.5中的所有指令中,除了leaq指令,其它指令都会设置条件码。此外,还有两类指令只设置条件码而不改变任何其它寄存器。CMP指令根据两个操作数之差来设置条件码。TEST指令根据两个操作数的位与结果来设置条件码。
在这里插入图片描述

1.6.2 访问条件码

条件码通常不会直接读取,常用的方法有三种:
(1)可以根据条件码的某种组合,将一个字节设置为0或者1
(2)可以条件跳转到程序的某个其它的部分
(3)可以有条件地传送数据
根据条件码地组合将一个字节设置为0或者1的指令称为SET指令。指令之间的区别在于考虑条件码的组合是什么,不同的后缀指明了所考虑的条件码组合。
在这里插入图片描述

1.6.3 跳转指令

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

    jmp .L1
    movq (%rax),%rdx
.L1:
    popq %rdx

在产生代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。jmp指令是无条件跳转,
(1)可以是直接跳转,跳转目标作为指令的一部分编码,在汇编语言中,给出一个标号作为跳转目标。
(2)也可以是间接跳转,跳转目标从寄存器或内存位置中读出的,汇编语言中,“*”后接操作数指示符。
在这里插入图片描述

1.6.4 条件分支

(1)用条件控制来实现
将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。
请添加图片描述
汇编代码的实现首先比较了两个操作数,设置条件码,如果比较的结果表明x大于或者等于y,那么会跳转到第8行,增加全局变量ge_cnt,计算x-y作为返回值并返回。

(2)用条件传送来实现
通过使用控制来实现条件分支的机制简单通用,但是在现代处理器上,可能会非常低效。一种替代的策略是使用数据的条件转移。

条件传送策略: 计算一个条件操作的两种结果,然后根据条件是否满足从中选取一个。只有在一些受限制的情况中,这种策略才可行,如果可行,就可以用一条简单的条件传送指令来实现。条件传送指令更符合现代处理器的性能特性。
在这里插入图片描述
从汇编代码中可以看到,代码既计算了y-x,也计算了x-y,分别复制给寄存器raxrdx,然后cmpq比较x和y的大小,设置条件码,根据条件码将%rdx复制给寄存器rax

条件传送指令: 都有两个操作数,源寄存器或者内存地址S,和目的寄存器R。与不同的SET和跳转指令一样,这些指令的结果取决于条件码的值。源值可以从内存或者源寄存器中读取,但是只有在指定的条件满足时,才会被复制到目的寄存器中。
请添加图片描述
(3)两种方法区别: 基于条件数据传送的代码会比基于条件控制转移的代码性能要好。原因在于,处理器通过使用流水线来获得高性能,在流水线中,一条指令的处理要经过一系列的阶段,每个阶段执行所需操作的一小部分(从内存中取指令、确定指令类型、从内存中读数据、执行算术运算、向内存中写数据、以及更新程序计数器)。这种方法通过重叠连续指令的步骤来获得高性能。例如,在取一条指令的同时执行上一条指令的算术运算。而要做到这一点,要求能够事先确定要执行的指令序列,这样才能保持流水线中充满待执行的指令。但是,当机器遇到条件跳转时,只有当分支条件求值完成后,才能决定分支往哪边走。处理器采用非常精密的分支预测逻辑来猜测每条跳转指令是会执行,并基于预测结果执行跳转指令后的指令。但是,放预测结果错误,处理器必须丢掉为该跳转指令后的指令已做的工作,然后从正确的位置处起始的指令去填充流水线。

同条件跳转不同,处理器无需预测测试的结果就可以执行条件传送。处理器只是读源值,检查条件码,然后要么更新目的寄存器,要么保持不变。

1.6.5 循环

C语言提供了多种循环结构,即do-while、while和for。汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。GCC和其它汇编器产生的循环代码主要基于两种基本的循环模式。
(1)do-while循环
在这里插入图片描述
汇编代码将循环变成低级的测试和条件跳转的组合。局部变量result初始化之后,程序开始循环。首先执行循环体,更新变量result和n的值。然后测试n和1的大小,如果是真,跳转到循环开始,否则执行接下来的指令,返回。

(2)while循环
有很多种方法将while循环翻译成机器代码,GCC在代码生成中使用其中的两种方法,这两种方法使用同样的循环结构,与do while一样,不过它们实现初始测试的方法不同。

  • jump to middle,执行一个无条件跳转到循环结尾处的测试,以此来执行初始的测试。

在这里插入图片描述

  • guarded_do,首先使用条件分支,如果初始条件不成立就跳过循环。当使用较高优化等级编译时,GCC会采用这种策略。
    在这里插入图片描述
    在这里插入图片描述
    (3)for循环
    GCC为for循环产生的代码是while循环的两种翻译之一,这取决于优化的等级。一般是第一种形式。
    请添加图片描述

1.6.6 switch语句

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

和使用一组很长的if-else语句相比,使用跳转表的优点是执行开关语句的时间与开关情况的数量无关。GCC根据开关情况的数量和开关情况的稀疏程度来翻译开关语句,当开关情况数量比较多(例如四个以上),并且值的范围跨度比较小时,就会使用跳转表。
请添加图片描述
请添加图片描述
原始的C代码有针对值100、102-104、106的情况,但是开关变量 n 可以时任意整数。编译器首先将 n 减去100,把取值范围移到0到6之间,创建一个新的程序变量,即扩展版本中的index。补码表示的负数会映射为无符号表示的大正数,利用这一事实,将index看作无符号值,从未进一步简化了分支的可能性。因此可以通过测试index是否大于6来判定index是否在0~6的范围之外。

在C和汇编代码中,根据index的值,有五个不同的跳转位置:loc_Aloc_Bloc_Cloc_Dloc_def,最后一个是默认的目的地址。每个标号都标识一个实现某个情况分支的代码块。在C代码和汇编代码中,程序都是将index和6作比较,如果大于6就跳转到默认的代码处。

在汇编代码中,jmp指令的操作数有前缀‘*’,表明这是一个间接跳转,操作数指定一个内存位置,索引由寄存器%rsi给出,这个寄存器保存着index的值。在汇编代码中,跳转表用以下声明表示,这些声明表明,在叫做“.rodata”的目标代码文件的段中,应该有一组7个“四”字(8个字节),每个字的值都是与指定的汇编代码标号相关联的指令地址。

请添加图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值