把变量赋值给寄存器_第三章 程序的机器级表示 (一) 寄存器初步认识

本文介绍了程序的机器级表示,重点讨论了IA32架构中的程序计数器、整数和浮点寄存器、条件码寄存器。详细解析了机器级代码的编码,包括汇编代码的结构、指令长度和反汇编器的工作原理。还探讨了数据格式,如操作数指示符、寻址模式和数据传送指令,强调了mov指令的使用和限制,并通过实例说明了数据如何在寄存器和内存之间传递。
摘要由CSDN通过智能技术生成

68bd0a8984e80c0a2a9a65425efda0ae.png

3.2 程序编码

第一章已经介绍过代码如何编译成可执行程序的几个步骤,最终的可执行程序是二进制的,那么系统是怎么读这些二进制的呢?

3.2.1 机器级代码

能够理解汇编代码以及它是如何与原始的C代码相对应的,是理解计算机如何执行程序的关键一步。

基本概念:

  • 程序计数器 成为%eip,表示将要执行的下一条指令在存储器中的地址。
  • 整数寄存器有8个,可以存储32位的值。
  • 条件码寄存器保存着最近执行的算术指令的状态信息,用来实现条件控制。
  • 浮点寄存器也有8个。

到了汇编这一层,已经不区分有符号和无符号,各种数据结构在汇编代码中只是连续的字节。

程序存储器包含程序的目标代码,使用虚拟地址寻址。又操作系统转换成最终实际的存储器物理地址。

各种缓存、主存、显存,共同构成了一个连续的虚拟地址空间。

3.2.2 代码示例

本书以GCC为例分析编译的结果。

GCC使用自己的格式来产生汇编代码,这种格式被称为GAS(Gnu ASsembler,GNU汇编器)。

使用 gcc -O2 -S test.c 可以生产test.s汇编文件
使用 gcc -O2 -c test.c 可以产生test.o文件
使用 objdump -d test.o 可以看到一些汇编内容。 
objdump也就是反汇编器,可以将二进制一定程度上还原为汇编代码。

反汇编之后的特性:

  • IA32的指令长度1到15字节不等,指令编码被设计为常用的指令就比较短,不常用的就比较长
这个设计方法好啊,开发中的一些功能函数常常也如此设计。频繁使用的函数尽量减少参数数量。
  • 反汇编器仅根据字节序列来确定汇编代码,无需访问原始代码或者汇编。
  • 反汇编器使用的指令名称跟GAS略有不同,例如,省去很多指令结尾的“l”后缀。

GCC产生的汇编代码中有一些.开头的行,这些事指导汇编器和链接器的命令,可以忽略不管。例如:

.file "sum.c"
  .globl  _accum
  .bss
  .align 4

汇编代码解释:

// 源代码
int simple(int *xp, int y)
{
  int t = *xp + y;
  *xp = t;
  return t;
}
// 使用gcc -S 生成的代码段如下
simple:
  pushl %ebp        保存帧指针
  movl %esp,%ebp    创建新的帧指针
  movl 8(%ebp),%eax   获取xp
  mov1 (%eax),%edx    获取*xp
  addl 12(%ebp),%edx  把y加到t上
  movl %edx,(%eax)    把t赋值给*xp
  mov1 %edx,%eax      设置t为返回值
  movl %ebp,%esp      重置栈指针
  pop1 %ebp           重置帧指针
  ret
之前看过一点点汇编语言,在那本书上 ,例如 mov ax,10 表示把ax寄存器设为10 ,就是说第一参数作为目标寄存器。而GAS这边是反的,第二个参数(如果有)才是目标寄存器。 比如 movl %esp,%ebp 表示的是把esp里面的内容赋值给 ebp。

3.3 数据格式

GAS很多指令后面会有一个后缀,例如前面出现的l,这个是小写的L,不是数字1 。

01f09e43dd139f11851a931ee36f5270.png

如图所示,GAS中每个操作符都有一个后缀,用来表明操作数的大小。例如mov就有 movb movw movl等。movel既可以表示4字节整数,也可以表示8字节浮点,这不会产生歧义,因为浮点数使用独立的一组寄存器。

3.4 访问信息

以IA32为例,CPU包含8个32位寄存器。大多数情况下,前六个寄存器都可以看成是通用的寄存器。少数情况下,特定指令会使用特定的寄存器。最后两个寄存器ebp和esp,保存着程序栈的指针,不可随意使用。寄存器的布局如下:(其中前四个寄存器还可以通过ah al等方位低位的两个字节,这是为了兼容早起8086等早起处理器)

44e9c7604fd9dec8e9381b8ac436c3fc.png
寄存器其实也没什么神秘的,就是一些放数据的地方,只不过位置特殊,在cpu上,所以就是访问特别快。

3.4.1 操作数指示符

一个典型的指令,有一个或多个操作数,就如同函数有一个或多个参数一样。这些操作数的作用是指出源数据的位置,和结果存储的位置。

好比一个C函数 void move(int src, int dest); 把src的值赋给dest,这也就是 movl xxx,xxx 的作用。

而这个源数据怎么得到呢?有下面三种方式:

  1. 立即数。以<img src="https://www.zhihu.com/equation?tex= 开头就是直接给出,无需去内存中查找。例如" alt=" 开头就是直接给出,无需去内存中查找。例如" class="ee_img tr_noresize" eeimg="1">0x123
  2. 寄存器。%加寄存器的名称形式。例如 %eax,%esp。以
    表示寄存器a,用
    来表示寄存器的值,相当于把整个寄存器看做是数组,寄存器作为索引。
  3. 存储器引用。源数据在内存中,通过地址来访问。可以将内存看成是大型数组M,则可以表示为M[Addr] 。而这个Addr的计算成为寻址模式。

寻址模式

一个寻址表达式的完整形态为

,表达的地址是:
。其中
  • Imm表示写死的地址, 这里不需要加 $前缀,与立即数区别
  • 表示对应寄存器中存储的值。
  • s表示缩放因子,取值范围为1,2,4,8
  • 这四个部分都可以省略

例如 0x123, 0x123(%eax), (%eax), (%eax,%ebx), (,%ebx,2), 0x123(%eax,%ebx,4)都是合法的表示方式。各自表达的值如下图:

9de16667ac3013ce49419190a358c421.png
寻址说起来吓人,其实就是根据前面那个公式来计算一个地址,然后通过M[Addr]来得到地址所在内存的值。就好比计算数组下标,然后取数组中的值。

3.4.2 数据传送指令

即mov指令。有两个参数,一个源,一个目标。例如movl %eax,%edx 表示把eax中的值复制到edx中。

IA32中的一个限制,两个参数不可以都为存储器位置,就是说必须要经过寄存器,或者使用立即数。所以代码a=b;的汇编代码不是movl Addra,Addrb,而可能是

movl Addra,%eax
movl %eax,Addrb
这是为什么呢?一条指令只能访问一次内存?暂时还不明确

movl指令的五种组合情况:

movl $0x123,%eax        // 立即数 - 寄存器
movl %eax,%ebx          // 寄存器 - 寄存器
movl (%eax,%ebx),%ecx   // 内存   - 寄存器
movl $0x123, (%eax)     // 立即数 - 内存
movl %eax,(%ebx)        // 寄存器 - 内存
// 少了内存到内存的方式,因为不支持
到了这里,终于明白前面simple函数的汇编代码第三行 movl 8(%ebp),%eax 获取xp的意思了。 8(%ebp)表示 8 + %ebp的值代表的地址存储的值。就是把*xp读出来放到eax中。

特殊指令

movsblmovzbl是两条特殊的mov指令,他们只拷贝一个字节,不同于movb只修改目标单个字节,他们会修改目标寄存器的其他三个字节。movsbl 执行算术扩展,即目标数值的高位扩展到其他三字节,而movzbl则以0扩展。

等同 int a = (char)123; 和 unsign a = (char)123; 的逻辑。

push和pop更为特殊,像是语法糖,各自包含了两个操作。但是有实际的好处,就是字节码大大减少。push指令是有一个字节,它代表的两个指令需要6个字节。

pushl %eax
等价于
subl $4,%esp        // esp是栈顶指针,栈的地址是向下扩张的,所以栈大小增加,则栈顶地址减小4
movl %eax,(%esp)    // 填空目标值

pop %eax
等价于
movl (%esp),%eax    // 先把栈中的值拿出来,然后再把寄存器中的指针缩回来
addl $4,%esp
上面代码注意%esp 有无括号的区别,无括号表示寄存器值,有括号是内存中的值。
转换成编程语言来理解就是,%esp是操作指针的值,(%esp) 则是对指针指向的值 例如 int p = 0x112321; sub $4, %esp -> p -= 1; (int指针减1就是4个字节了) mov %eax,(%esp) -> p = 100; (假设eax中存的就是100吧)
再有一点就是,pop缩回来之后,是没有处理源栈中的值的,值其实还保留在内存中。

3.4.3 数据传送实例

按照惯例,所有返回整数或指针的函数,都是通过将结果放在寄存器%eax中来达到目的。

一些临时变量,可能会存储在寄存器中,如果寄存器够用的话。寄存器的访问比存储器访问无疑是要快的多。

根据这个说法,那么现代CPU是不是应该使用更多的寄存器呢? 知乎上倒是有个问题说这个问题,不太懂,无法辨别。为什么X86的寄存器数量没有随着性能的提升而增加?
通过前面这段内容的学习,我们知道了一些基本的指令,了解了指令的操作数如何访问数据,即寻址模式的几种情况。寻址模式是非常重要的,贯彻整个汇编指令的始终。寻址模式的公式为
,汇编中表达式为
四个部分都可以选填。

函数的栈地址是向下扩展的,压栈会使栈顶地址变小,弹出栈则会使栈顶地址回缩,而回缩后,并不会修改栈中的内存,会遗留以前的数据,等待下一次修改使用。
返回值通过最后修改eax来实现。寄存器比存储器快,这点暂时也不知道如何利用。临时变量比直接使用参数、指针要快?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值