x86/x86_64汇编的常用指令的比较

寄语

我们这里只是针对比较两个架构环境中常用的指令以及寄存器的差异,首先,读者要理解我写整个汇编基础的文集的意图是为了通过基本汇编指令更好地去理解C/C++堆和栈内部的。我们不可能去比较x86和x86_64汇编的方方面面。

理解内存操作→理解C/C++编译器的内存操作原理→改善自己写代码时的陋习。

因此我整个汇编基础文集是为《C/C++内存管理》文集服务的,在讨论常用的汇编指令的同时,我们不得不要去讨论堆和栈,而堆和栈的操作又离不开寄存器,因此很多知识点都是一环套一环的。

还有整个汇编文集是基于GAS汇编分支,因此所有语法的说明都遵循GAS汇编的规范。其他厂商的汇编分支不在我讨论范围之内。如有不适合你阅读需求请点击浏览器左上角退出,谢谢!

x86_64寄存器

x86_64架构中有16个寄存器,就像x86架构中的一样,有一些寄存器还是有特殊用途的%rsp%rbp,如下图寄存器列表中的64bit那一列的寄存器是x86_64架构中独有的。

image
  • r8r15这8个物理寄存器是x86_64架构新增的
  • raxrsp这8个物理寄存器其实分别对应x86中的eaxesp扩展版本,数据宽度扩展至64位,也就是64位的寄存器在每个CPU周期吞吐数据量相比32位的寄存器翻了一倍。
  • 32bit那一列的r8dr15d这8个寄存器只是逻辑上的寄存器, IA32硬件中是不可能存在的,因为他们是有x86_64中的 r8r15这8个物理寄存器的低阶32位模拟出来的。


从上面最后一点,我们顺便讨论一下x86和x86_64之间寄存器的关系,我们已经知道x86_64的寄存器的数据宽度都是64位,但我们可以将它们的低阶的32位作为一个“逻辑上”的x86寄存器来访问,例如我们熟悉的x86寄存器eax,就是使用x86_64寄存器rax的低阶32位来模拟的,这也从硬件特征上已经回答了一个常见的问题,这就是“为什么x86_64的硬件能够兼容32位程序?”的原因,当然还少不了操作系统的x86的api支持。

32位 vs 64位寄存器使用约定比较

此表总结了32位寄存器和64位寄存器使用约定的差异,但这些都是函数栈的内容,这是“约定”意味着不同厂商的汇编不一定完全遵守,这里没打算详细展开,如果你对下表的用途一列中的名词不熟悉的话,请参考我这篇文章《戏说程序栈-寄存器和函数状态》

ss6.png

32位 vs 64位常用指令

现在我们比较一下32位和64位模式下的常用指令的差别,

  • 在x86环境下movl中的L表示它的操作数的长度是4个字节的数据,而在x86_64环境中,movq中的Q表示它的操作数的长度是8个字节,其他常用指令如此类推。
  • 在86_64环境中依然可以使用32位的指令生成32位的结果,就像我们刚才所说的64位的寄存器只要高阶的32位全set为0,只用低阶32位来模拟32位的寄存器,配合32位的指令集即可实现。

示例导入

这是一个被用烂了的C代码示例,为了简化汇编代码,并且在编译时也去掉了ssp选项

void swap(int *x,int *y){
      int a=*x;
      int b=*b;
      *x=b;
      *y=a;
}

x86版本的汇编代码

swap:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $16, %esp

        movl    8(%ebp), %eax
        movl    (%eax), %eax
        movl    %eax, -4(%ebp)
        movl    12(%ebp), %eax 
        movl    (%eax), %eax
        movl    %eax, -8(%ebp)
        movl    8(%ebp), %eax
        movl    -8(%ebp), %edx
        movl    %edx, (%eax)
        movl    12(%ebp), %eax
        movl    -4(%ebp), %edx
        movl    %edx, (%eax)

        leave
        ret

x86_64版本的汇编代码
下图是执行gcc优化选项-O1前后的汇编代码对比:

  • 左侧的汇编代码是没有添加优化选项-O1的,它仍然大量保留x86风格的栈帧操作,因为代码里面使用了大量32位的寄存器和栈内存之间的复制操作。
  • 右侧的汇编代码使用了如下指令,只要函数的参数个数少于64位寄存器使用约定中的6个用于缓存参数的寄存器个数,在启用优化选项下,能到得出最精简的汇编代码,当然汇编指令越少即代表你的编译后的程序运行的性能就越好。

本示例右侧汇编代码使用的命令:
gcc -S swap_test.c -O1 -o swap.s -fno-asynchronous-unwind-tables


屏幕快照 2019-10-26 上午9.40.52.png

接着是进入swap函数,正如下图所示,我们注意到swap使用了寄存器rdi接收来自调用函数的第一个参数,使用寄存器rsi接受第二个参数,他们都是64位的指针,因为他们使用的movq的指令


优化后的x86_64版本的swap汇编指令只有4条指令,并且交换过程没有涉及到swap栈帧的读取参数并加载到寄存器这些操作,唯一修改栈中的状态数据只是,这些指令的含义不想再解析,请查看我之前写有关汇编基础的文章。

  • movl %edx,(%rdi)
  • movl %eax,(%rsi)

    这里需要注意的是,我们在交换两个指针指向的内存位置中的数据时,仍然是使用32位的数据,因为我们main栈中的两个变量a和b是4字节长,即便指针本身是64位长,但int指针指向的数据仍然是32位的,因此我们在汇编代码仍然是使用32位的movl指令来加载和存储数据。那么,同一个道理,如果main栈的变量是long int类型,那么最终的汇编代码使用的movq指令。

你可能感兴趣的:(x86/x86_64汇编的常用指令的比较)