A 汇编语言简介

要讲汇编语言,就不得不提计算机是如何工作的。众所周知,计算机(硬件)是看不懂你写的程序的。需要用编译器或者解释器将程序变成计算机能看懂的二进制码才可以。

汇编语言其实是一种“助记符”。汇编语句和二进制机器码是一一对应的。相比于二进制机器码,汇编语言多少是可以读懂的。

A.1 基本操作

CPU会做些什么事呢?基本就是通过内存地址从内存里取几个字节到自己的寄存器里,然后对各个寄存器进行一番操作,什么加减乘除啊移位啊与或非啊什么的,然后还能把结果再存到内存里。

那么,条件判断呀调用函数呀是怎么实现的呢?条件判断是分为两步的,第一步做比较,然后一些特别的寄存器会根据比较结果设置为相应的值;第二步,根据一个或几个特别的寄存器里面的值是0或者1选择是否跳转到另一个位置。而跳转实际就是把指向当前指令的指针加或减一个合适的值,而这个指针的值其实也是保存在一个寄存器中的。函数调用呢,就是先把一些需要保护的寄存器内容暂存到内存里,然后在合适的寄存器或内存地址里设置好要传递的参数,并且把当前的指令指针保存到内存中,然后跳转到相应函数位置。函数执行完毕后再把返回值放到指定的寄存器或者内存位置,然后再把保存起来的指令指针恢复到寄存器里。关于哪些寄存器需要调用方保护,哪些需要被调用方保护,用哪些寄存器传递参数和返回值则取决于编程语言的调用约定。

汇编语言中的数值拷贝(从内存到寄存器,寄存器到寄存器,寄存器到内存,立即数到寄存器等等)被称作“mov”,虽然事实上并不是移动,“mov”完后原来位置的信息并不会被消掉。

立即数指的是直接有一个数字。从立即数到寄存器的“mov”相当于把寄存器设置成一个指定的值。

A.2 神奇的“装载有效地址”

有一条指令lea,是“load effective address”的缩写。这条指令本来是用于计算内存地址的,通过一个基地址,一个偏移量和每个元素的大小来计算出需要的地址。但是卑鄙的人类竟然动起了歪心思,用这条指令来做代数运算。

举个例子,比如要计算一个整数乘以5再加上3,汇编代码有可能是这样的

  lea 0x3(%rdi,%rdi,4), %rax

然后rax寄存器中的值就等于rdi寄存器的值乘上5然后再加三。

真正用于装载地址的时候,括号里面第一个值相当于基地址,第二个是偏移量,第三个是每一个偏移对应的字节数(只能是1,2,4,8),括号前面的数字是以byte位单位的常数偏移量。

所有编译器就发现了这东西完全可以用于装载地址以外的用途。上面的例子中,计算了rdi+rdi*4+3,也就是我们想要的乘5加3.

A.3 c语言中的内联汇编

不同编译器里使用内联汇编的方法可能有微小的不同。这里是gcc的情况

gcc中,内联汇编写在__asm__里面。如果在__asm__后加了__volatile__则表示要求编译器不要对这里面的汇编代码进行优化。

这里用前面第一部分的AVX汇编举例

__asm__ __volatile__(
            "movq %0, %%rax \n\t"
            "movq %1, %%rbx \n\t"
            "movq %2, %%rcx \n\t"
            "movq %3, %%rdx \n\t"
            "movq %4, %%r8  \n\t"
            "shr  $2, %%r8  \n\t"
            "movq $0, %%r9  \n\t"
            "jmp  .check_%= \n\t"
            ".loop_%=:         \n\t"
            "shl $2, %%r9   \n\t"
            "leaq (%%rax, %%r9, 8), %%r10  \n\t"
            "vmovupd (%%r10), %%ymm0       \n\t"
            "leaq (%%rbx, %%r9, 8), %%r10  \n\t"
            "vmovupd (%%r10), %%ymm1       \n\t"
            "leaq (%%rcx, %%r9, 8), %%r10  \n\t"
            "vmovupd (%%r10), %%ymm2       \n\t"
            "vmulpd %%ymm0, %%ymm1, %%ymm3 \n\t"
            "vaddpd %%ymm2, %%ymm3, %%ymm3 \n\t"
            "leaq (%%rdx, %%r9, 8), %%r10  \n\t"
            "vmovupd %%ymm3, (%%r10)       \n\t"
            "shr $2, %%r9                  \n\t"
            "add $1, %%r9                  \n\t"
            ".check_%=:                    \n\t"
            "cmpq %%r8, %%r9               \n\t"
            "jl .loop_%=                   \n\t"
            :
            :"m"(a), "m"(b), "m"(c), "m"(d), "m"(N)
            :"%rax", "%rbx", "%rcx", "%rdx", "%r8", "%r9", "%r10",
             "%ymm0", "%ymm1", "%ymm2", "%ymm3", "memory"
            );

首先,每行写一句只是为了看起来方便。在c语言中,一个长的字符串是可以换行的,分到每一行后分别加引号,中间没有逗号就会当作一个长的字符串。

每一句后面加\n\t,否则多条汇编语句会在结果中连成一行,无法被汇编器读懂。

汇编代码一行一句,不用分号

__asm__的括号里首先要写的就是我们要执行的汇编代码之后用冒号隔开三个部分,分别是要写入的变量,要读取的变量和发生变动的寄存器。

变量前面引号里面“m”表示内存内容,用“r”则表示这个变量应该被放入寄存器(由编译器指定一个寄存器)。要写入的寄存器变量要写成“=r”。

后面寄存器列表里面,把汇编代码中用到的寄存器都新进来就对了。似乎改变了内存的话要写“memory”,但没有验证过,反正写了不会出问题。

在汇编代码里引用变量要用“%”,百分号后面的数字表示变量的编号(后面从写入变量到读取变量从0开始连续编号,数一数是第几个)。由于这里使用了百分号,使用寄存器就要多写一个百分号,比如“%eax”就要变成“%%eax”.

在寄存器外面加一个括号表示把这个寄存器中的值当作一个地址,要的是这个地址的内存里面的东西(除了装载有效地址的时候这个只是当作一个地址,不会实际访问内存)。

汇编代码中以冒号结尾的是一个标签,跳转的时候可以跳转到标签。最后编译好多二进制里是没有这个标签的,跳转会全部被翻译为跳转到相应的地址。在gcc的内联汇编里面,跳转标签要在后面加上_%

cmpq %%r8, %%r9这句,比较了r9寄存器和r8寄存器中的值,并相应地设置了特别的用于比较和跳转的寄存器。后面紧跟的一句jl .loop_%,是说如果判出来大于,就要跳转,否则不跳。