程序地机器级表示

使用现代地优化编译器最大的优点是:
用高级语言编写的程序可以在很多不同的机器上编译和执行,而汇编代码则是与特定机器密切相关的

为什么要花时间学习机器代码?

通过阅读这些汇编代码。我们能够理解编译器的优化能力,并分析代码中隐含的低效率。
尝试源代码的各种形式,每次编译并检查产生的汇编代码,从而了解程序将要运行的效率如何?
有些时候,高级语言提供的抽象层隐藏了我们想了解的程序运行行为
列如:用线程包写并发程序时,了解不同的线程是如何共享程序数据或保持数据私有的,以及准确知道如何在哪里访问共享数据。
再列如:
程序遭受攻击(使得恶意软件侵扰系统)的许多方式中都涉及程序存储运行时控制信息的方式细节。许多攻击利用了系统程序中的漏洞重写信息,从而获得了系统的控制权。漏洞如何产生以及如何防御它们,需要具备程序机器级表示相关的知识。


对于机器级编程来说,其中两种抽象尤为重要


第一种
是由指令集体系结构或指令集架构(Instruction Set Architecture,ISA)来定义机器级程序的格式和行为.它定义了处理器状态、指令的格式、以及每条指令对状态的影响。大多数ISA,包括x86-64将程序的行为描述成好像每条指令都是按顺序执行的。一条指令结束后,下一条再开始。处理器的硬件远比描述的更复杂,它们并发地执行许多指令。但是可以采取措施保证整体行为与ISA指定地顺序执行地完全一致。

第二种
机器级程序使用地内存地址是虚拟地址,提供地内存模型看上去是一个非常大地字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件结合起来

程序计算器(PC,在x86-64中用%rip表示)
给出将要执行的下一条指令在内存中的地址

整数寄存器文件包含了16个命名的位置,分别存储64位的值。这些寄存器可以存储地址(对应c语言的指针)或整数数据。有的寄存器被用来记录某些重要的程序状态。而其他的寄存器用来保存临时数据。列如:函数的参数和局部变量,以及函数的返回值。

条件码寄存器
保存最近执行的算术或逻辑指令的状态信息,他们用来实现控制或数据流中的条件变化。列如:用来实现if和while语句

一组向量寄存器可以存放一个或多个整数或浮点数
虽然c语言提供了一种模型,可以在内存中声明和分配各种数据类型的对象,但是机器代码只是简单地将内存看成地按字节寻址地数组。

c语言地聚合数据类型
列如:数组和结构,在机器代码中使用一组连续地字节表示,即使是对标量数据类型。汇编代码也不区分有符号或无符号整数。不区分各种类型地指针。甚至不区分指针和整数。


程序内存包含


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

程序内存用虚拟地址来寻址
在任意给定的时刻,只有有限的一部分虚拟地址被认为是合法的列如:
x86-64的虚拟地址是由64位的字来表示,在目前的实现中,这些地址的高16位必须置0,所以一个地址实际上能够指定的是2^48或256TB范围内的一个字节

一条机器指令只执行一个非常基本的操作
列如:将存放在寄存器中的两个数字相加、在存储器和寄存器之间传送数据与或是条件分支转移到新的指令地址。

编译器必须产生这些指令序列,从而实现(像算术表达式求值、循环或过程调用和返回这样的)程序结构。

实列:
在这里插入图片描述
在这里插入图片描述

利用gcc -Og -c mstore.c
这样会产生目标代码mstore.o,他是二进制格式,所以无法直接查看。
可以借助反汇编器根据机器代码产生一种类似汇编代码的格式
objdump -d mstore.o
在这里插入图片描述
注意:
1、x86-64的指令长度从1到15个字节不等,常用的指令以及操作数较少的指令所需的字节数较小,而那些不常用的指令字节数较多。
2、设计指令格式的方式是,从某个给定位置开始,可以将字节唯一解码成机器指令。列如:只有指令pushq %rbx是以字节值53开头的。
3、反汇编器只是基于机器代码文件中的字节序列来确定汇编代码,它不需要访问该程序的源代码或汇编代码。
4、反汇编器会增加后缀。如:b、l、w、q
生成时即可执行代码需要对一组目标代码文件运行链接器,而这一组目标代码文件中必须含有一个main函数。
假定:
在这里插入图片描述
进行反汇编后得到以下代码:
在这里插入图片描述
这段代码与mstore.c反汇编产生的代码几乎完全一样。其中一个主要的区别是左边列出的地址不同-----链接器将这段代码的地址移到一段不同的地址范围中。第二个不同之处在于链接器填上了callq指令调用函数mult2需要使用的地址。链接器的任务之一就是为了函数调用找到的函数的可执行代码的位置。最后一个区别是多了两行代码。他们出现在返回指令后面,插入这些指令是为了使函数代码变为16字节,使得就存储器系统性能而言,能更好地放置下一个代码块。

对于一些程序,程序员必须用汇编代码来访问机器的地特性。
一种方法是用汇编代码编写整个函数,在链接阶段把它们和c函数组合起来,另一种方法是利用gcc的支持,直接在c程序中嵌入汇编代码。


数据格式


由于是从16位体系结构扩展成32位的,Intel用术语“字word)”表示16位数据类型,因此,称32位为“双字(double word)”、64位为“四字(quad words)”。

大多数GCC生成的汇编代码指令都有一个字符的后缀,表明操作数的大小。列如,数据传送指令有四个变种:movb(传送字节)、movw(传送字)、movl(传送双字)、movq(传送四字)。汇编代码也使用后缀‘l’来表示4字节整数和8字节双精度浮点数。这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。

一个x86-64的中央处理单元(CPU)包含一组16个寄存器64位值的通用目
的寄存器,这些寄存器用来存储整数数据和指针。

指令可以对这16个寄存器的低位字节中存放的不同大小的数据进行操作。
当这些指令以寄存器为目标时,对于生成小于8字节结果的指令,寄存器中剩下的字节会怎么样,对此有两条规则:生成1字节和2字节数字的指令会保持剩下的字节不变;生成4字节数字的指令会把高位4个字节设置为0。后面这条规则是作为IA32到x86-64扩展的一部分而采用的。

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


操作数指示符


大多数指令有一个或多个操作数(operand),指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。
操作数可以被分为三种形式:

1、立即数(immed,用来表示常数值。在ATT格式的汇编代码中,立即数的书写方式是‘$’后面跟一个用标准C表示法表示的整数。不同的指令允许的立即数数值的范围不同,用汇编器会自动选择最紧凑的方式进行数值编码。

2、寄存器(register),它表示某个寄存器内的内容。我们用符号r表示寄存器a,用引用R[r]来表示它的值。

3、内存引用,它会根据计算出来的地址(通常称为有效地址)访问某个内存位置。因为将内存看成一个很大的字节数组。用M[Addr]表示对存储在内存中从地址Addr开始的连续字节的引用。

有多种不同的寻址模式,允许不同形式的内存引用。
Imm(r1,r2,s)
立即数偏移Imm、一个基址寄存器r1、一个变址寄存器r2,一个比例因子s
有效地址:Imm+R[ r1 ]+R[ r2 ]*s

这里的比例因子s只能是1、2、4、8

在这里插入图片描述


数据传送指令


最频繁使用的指令是将数据从一个位置复制到另外一个位置的指令

最简单形式的数据传送指令-------MOV类。这些指令把数据从源位置复制到母的位置,不做任何改变。
在这里插入图片描述

源操作数指定的可以是立即数,也可以是寄存器,还可以是内存引用。目的操作数指定一个位置,要么是一个寄存器或者,要么是一个内存地址。x86-64加一条限定,传送指令的两个操作数不能都指向内存位置。将一个值从一个内存位置复制到另一个内存地址需要两条指令------第一条指令把源数据传送到寄存器内,第二条指令把寄存器的值写入到目的内存位置。
在这里插入图片描述
在这里插入图片描述
MOV指令只会更新目的操作数指定的那些寄存器字节或内存位置。唯一的例外是movl指令以寄存器为目的时,它会把该寄存器的高位4字节设置为0.
造成的原因是x86-64采用的惯例,即任何为寄存器生成32位值的指令都会把该寄存器的高位部分置为0.

常规的movq指令只能以表示为32位的补码数字的立即数作为源操作数,然后把这个值符号扩展得到64位的值。放到目的位置。movabsq指令能够以任意64位立即数作为源操作数,并且只能以寄存器作为目的。

在将较小的源值复制到较大的目的时使用,所有这些指令都把数据从源(在寄存器或内存中)复制到目的寄存器。MOVZ类中指令把目的中剩余的字节填充为0,而MOVS类中的指令通过符号扩展来填充,把源操作数的最高位进行复制。
在这里插入图片描述
cltq指令没有操作数:他总是以寄存器%eax作为源,%rax作为符号扩展结果的目的,它的效果与指令movslq%eax,%rax完全一致,只不过编码更紧凑。

在这里插入图片描述

在这里插入图片描述


数据传送示例


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

按照寄存器的保存习惯,函数第一个参数保存在%rdi中,第二个参数保存在%rsi中。
从这里可以看出C语言所谓的指针就是地址。间接引用指针就是将该指针放在一个寄存器中,然后在内存引用中使用这个寄存器。其次,像y这样的局部变量通常是保存在寄存器中,而不是内存中。访问寄存器比访问内存快得多。


压入和弹出栈数据


栈具有一个属性:弹出的值永远是最近被压入而且仍然在栈中的值。栈可以实现一个数组,总是从数组的一端插入和删除元素。这一端成为“栈顶”。在x86—64中,程序栈存放在内存中某个区域。栈向下增加,这样一来,栈顶元素的地址是所有栈中元素地址中最低的。(根据惯例,我们的栈是倒过来画的,栈“顶”在图的底部)栈指针%rsp保存着栈顶元素的地址。在这里插入图片描述

将一个字节的数据压入栈中,首先会将栈指针地址减去8,然后将值写入到新的栈顶地址。对应pushb指令
将一个字节的数据弹出栈,首先把数据复制到目的,再把栈顶指针加上8,这样就把栈顶的数据弹出了。对应popb指令。
这样的回收地址,其实值还是存在那块已经被回收的地址中,直到下一次使用这段地址进行覆盖。

算术和逻辑操作

只有leaq没有其他大小的变种。
在这里插入图片描述


加载有效地址


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

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

一元和二元操作

一元操作,只有一个操作数,既是源又是目的。这个操作数可以是一个寄存器,也可以是一个内存位置。

二元操作中,第二个既是源操作数也是目的操作数。
第一个既可以是立即数,也可以是寄存器、还可以是内存,而第二个不能是立即数。当第二个操作数为内存地址时,处理器必须从内存读出数据,执行操作,再把结果写回内存。


移位操作


先给出移位量,然后第二项给出的是要移位的数。分为算术移位和逻辑移位。移位量可以是一个立即数,或者放在单字节寄存器%cl中。(这些指令很特别,因为只允许以这个特定的寄存器作为操作数),原则上,一个字节的移位量使得移位量的编码范围可以达到2^8-1=256。

x86-64中,移位操作对w位长的数据进行操作,移位量是由%cl的十六进制的低m位决定。这里2^m=w。高位会被忽略。所以,列如当寄存器%cl的十六进制为0xFF时,指令salb会移7位,salw会移15位,sall会移31位,salq会移63位。

算术左移和逻辑左移都是往右边补0,效果一样。
算术右移动左边补上符号位,逻辑右移补0。


条件码


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

CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号操作的溢出。

ZF:零标志。最近的操作得出结果为0

SF:符号标志。最近的操作得到的结果为负数。

OF:溢出标志。最近的操作导致一个补码溢出—正溢出或负溢出。

leaq指令不改变任何条件码,因为它是用来进行地址运算的。上面图3-10中列出的所有指令都会设置条件码。

CMP和TEST指令只设置条件码而不改变任何其他寄存器。
cmp指令根据两个操作数之差设置条件码。除了只设置条件码而不更新目的寄存器之外,cmp指令与SUB指令的行为一样。在ATT格式中,列出操作数的顺序是相反的,如果两个操作数相等,这些指令会将零标志设置为1,而其他的标志可以用来确定两个操作数之间的关系。
test指令的行为与AND指令一样,除了它们只设置条件码而不改变其他寄存器的值。典型的用法是:两个操作数一样的(列如,testq %rax,%rax 用来检查%rax是负数、零还是正数),或其中的一个操作数是一个掩码,用来指示哪些位应该被测试。
在这里插入图片描述


访问条件码


条件码通常不会直接读取,常用的使用方法有三种:1)可以根据条件码的某种组合将一个字节设置为0或者1,2)可以跳转到程序的某个其他部分,3)可以有条件地传送数据,对于第一种情况,下图描述地指令根据条件码的某种组合,将一个字节设置为0或者1。
注意:这些指令的后缀表示不同的条件而不是操作数的大小。
例如:指令setl和setb表示“小于时设置(set less)”和“低于时设置(set below)”。
一条set指令的目的操作数是低位单字节寄存器元素之一,或是一个字节的内存位置,指令会将这个字节设置为0或者1

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值