这一节我们来介绍常见的X86汇编指令,汇编语言现在用的很少,基本上都是用C/C++,java,Python等这些高级语言进行编程,我们主要是通过C语言编译后的汇编,来理解C语言的本质,然后后面写OS跟编译器的时候会用到一些汇编,我们对汇编的要求是基本上能看懂就可以了。
X86是英特尔一系列微处理器的简称,第一代是8086它是16位的处理器,后面又发展到32位的处理器,里面的代表有i386 i486和奔腾4处理器,这些32位处理器它的体系结构是类似的,所以统称为IA32或者是X86-32,我们也可以通过i386来代表32位的处理器,后来又发展到64位处理器,64位处理器统称为X86-64。
对32位CPU它有8个32位的通用整数寄存器,就是说这些寄存器里面只能用来存放整数,而对于浮点数会有专门的寄存器跟指令来做处理,我们后面会介绍到,
这8个寄存器,它都可以按照4个字节或者是2个字节来访问,比如你访问si它就是访问前面的两个字节,访问esi访问的就是4个字节,前面4个寄存器它还可以单独访问前面两个字节,比如al访问的就是第一个字节,ah访问的是第二个字节,那么你用ax访问的就是前面两个字节,eax访问的就是前面4个字节,
除了这些通用的寄存器,还有一些特殊的寄存器,这些特殊的寄存器只在操作系统里面会用到,一般的应用程序用不到,比如IP就是指令指针寄存器,用来存放下一条指令的地址,eflags是标志寄存器,通过CPU运算的结果来设置一些标志,
比如我们假设CPU运算的是t=a+b,那么CF就是进位标志,把a b当成无符号数,看它相加的结果有没有进位,比如说两个4位的数相加变成5位数,那么它就产生了进位,就代表的是无符号溢出,ZF是0标志,看结果是不是等于0,SF是看结果是不是小于0,
OF是溢出标志,把a b去当成有符号数,看它相加的结果有没有溢出,比如两个正数相加是等于负数,或者是两个负数相加等于正数,那么就说明产出了溢出,其它的标志位我们还会用到if中断打开跟关闭的标志,我们后面写OS的时候会用到,段寄存器就是操作系统在打开分段跟分页的时候会用到,用来获取代码段跟数据段的地址,那么32位CPU它的地址空间就是2的32次方是4G,它最大支持4G的内存.
x86-64它把寄存器的个数扩展到16个,然后每个寄存器的大小是8个字节,每个寄存器可以单独访问1个字节2个字节4个字节或者是8个字节,然后前面的4个寄存器还可以单独去访问前面2个字节,x86-64它的地址范围是64位,但是英特尔它只用了48位,它支持的内存地址空间大小就是2的48次方,是256T,所以它内存地址空间是很大的,
我们看一下指令的结构,指令它就是有指令的名字后面加一个或者是多个操作数,然后操作数之间通过逗号来分割,操作数可以是立即数,标号,寄存器或者是内存地址,括号就代表的是内存地址,根据参数的不同可以有不同的寻址方式,
对于一个格式它所代表的地址,我们可以通过一个公式来计算如下图,比如这个格式里面的Imm就代表立即数偏移,Eb是基址寄存器,Ei是变址寄存器,s是比例因子,那么这个格式它所代表的地址就是,立即数加上基址寄存器的值,加上变址寄存器乘以这里的s比例因子,这里的立即数它只能是一个常数,然后Eb跟Ei它只能是寄存器,s比例因子它必须是1,2,4,8这几个值,然后这四个项它某些项可以省略,那么省略的项就是0,
我们通过mov指令的这些例子,来理解一下不同的寻址方式,立即数寻址就是把一个整数或者是标号作为操作数,这里就是把12赋给eax,这里是把Label所代表的地址赋给eax。寄存器寻址就是把寄存器的值来作为操作数,这里是把ebx的值赋给eax。
直接寻址就是把这个标号所对应的地址里面存储的内容赋给eax,而不是把这个地址赋给eax。间接寻址就是使用地址,这里就只使用了基址寄存器,那么基址寻址就是结合着立即数偏移跟基址寄存器,那么这个地址它就是4加上eax。
变址寻址是把基址寄存器给省略了,那么这个格式它所对应的地址就是4+edi4。基址加变址寻址就是4项都不省略,那么这个地址对应的就是4+eax+edi4。
首先看一下无条件跳转指令,跳转可以使用直接跳转跟间接跳转,直接跳转就是跳转到一个数字或者是标号所对应的地址,而间接跳转就是在前面加一个星号,就是类似C语言里面取值的意思,jmp *%eax就是把eax里面的值取出来作为跳转的地址,jmp *(%eax)是把eax这个地址里面存的值取出来做一个地址跳转,jmp *0x809089是把809089这个地址里面存的值取出来作为跳转的地址,而不是把这个809089本身作为跳转的地址,
call是函数调用,它也是一个跳转的指令,它在跳转之前会把下一条指令地址入栈,然后再跳转,函数执行完成以后,它会先把下一条指令地址出栈,然后跳转到下一条指令的地址去执行,也是可以直接跳转跟间接跳转.
传送指令就是把原操作数的值赋给目的操作数,mov指令跟push指令还有大多数的指令,它后面都会有一个后缀可以是b,w,l,q,就代表操作数的寄存器的大小,b是一个字节,w是两个字节,字,l是双字4个字节,q是4字8个字节,在指令中使用寄存器的时候,我们必须跟这个后缀对应,比如这里的movb (%eax),al,b是一个字节,那么就需要使用一个字节的寄存器al,地址就没有关系,
然后mov指令,它的原操作数可以是立即数,寄存器跟内存地址,而目的操作数可以是寄存器,内存地址,但是不能同时为内存地址,我们结合例子来看一下,movb (%eax),al就是把eax这个地址里面存储的内容赋给al,movl $12, %eax是把12赋给eax,movswl -26(%ebp),%eax是做符号扩展传送,
s代表sign符号的意思,w代表原操作数它的大小,是两个字节,l代表目的操作数的大小,是4个字节,就是把原操作数的两个字节符号扩展成4个字节,然后高位就补符号,就看原操作数它的符号,如果是1的话就补1,如果是0的话就补0,
然后movzwl -26(%ebp),%eax是零扩展传送,z是zero的意思,就是对原操作数进行零扩展,把两个字节扩展成4个字节,然后高位的两个字节补0,movzbl %al, %eax是把一个字节扩展成4个字节,然后高位的几个字节就补0.
lea是用来加载有效的地址,它的格式是跟mov指令是一样的,比如leal 4(%eax), %eax只是把eax+4这个地址赋给eax,而不是把这个地址里面存储的内容赋给eax,
push跟pop就是入栈跟出栈,栈也是在内存里面,栈顶的地址是通过esp这个寄存器来存储的,所以push %ebp就相当于把ebp里面的值赋给esp-4所指向的内存地址,然后会把esp的值减4,栈是从高地址往低地址去增长的,pop指令就是把栈顶里面的值赋给ebp,然后再把esp加4,
条件指令就是根据test指令跟comp指令的结果作为条件,然后进行set或者是跳转,test是做与操作,把两个操作数进行与操作,结果可能是0或者是1,通常用来判断一个变量它是等于0还是大于0的,产生的结果会影响标志位,
comp是用来比较两个数,把目的操作数去减去原操作数,产生的结果会影响标志位,然后set跟jump就根据test跟comp产生的标志位,来做相应的设置跟跳转,set是根据标志位来设置al的值,可以设置成1或是0,这里的l就代表是小于,比如eax如果小于12的话,那么就把al设置成1,否则就把它设置成0,
set的操作数可以是寄存器也可以是地址,因为它只能设置1或者是0,所以如果是寄存器的话,那么它只能是一个字节的寄存器,如果是设置地址的话,那么它只会设置这个地址的前一个字节,
然后是条件跳转指令,根据标志位的结果来进行跳转,je是等于,js是负数,jg是大于,jl是小于,ja是无符号大于,jb是无符号小于,比如这里的jg .L0就是大于的意思,如果cmpl $12, %eax,eax大于12的话,那么它就跳转到L0这个地址,
下面是条件传送,就根据条件来做相应的传送,比如这里的cmovge %edx , %eax就是大于等于的意思,如果eax是大于等于12的话,那么就把edx里面的值赋给eax,条件传送常用来对分支做优化,
比如上面的c语句,它编译成汇编语言,通常就是这种,有分支语句,把a跟12比较,如果是小于等于的话,那么就跳出这个if走下面的,如果是大于12的话,那么就走下面的,把3赋给b,那么这些语句就涉及到CPU的分支预测,如果预测错的话就会影响CPU的性能,
然后也可以编译成条件传送指令,比如它这里就是判断a大于12,如果大于12的话,那么就把3赋给b,这样看起来跟c语言对比就更直观一些,它也没有分支预测,不过具体是使用什么汇编语句还是要看编译器。
然后就是加减乘除这些运算指令,加减乘除它都有两个操作数,addl,subl,mull,divl是加减乘除,adcl是做进位加法,就是在32位CPU上,去把两个64位的整数进行相加,可以把64位分成两部分,把低位的32位相加产生一个进位,然后高位两个32位数相加再加上这个进位,也可以在64位的CPU上面做128位整数的加法,这里是incl加一,decl减一,negl取反,非,与,或,异或,
shl/shr是逻辑左移跟逻辑右移,移动的时候会在左边跟右边补0,sal/sar是算术左移跟算术右移,算术左移就是在低位补0,而算术右移是在高位去补符号位,就看它之前的符号位是什么,如果是1的话就在高位补1,这两种imull、idivl乘法跟除法它只有一个操作数,通常是两个32位整数相乘是64位的情况,我们后面在学习c语言的时候还会看到,
然后是X86-64位的汇编,它跟32位的汇编基本上是一样的,只不过它是在后面把l去变成了q,就代表4字8个字节,其它的基本上就是一样的,
然后我们结合着mov指令举例来看一下,movabsq就是把一个64位立即数赋给一个64位寄存器,movb是移动一个字节,那么它只会改变这个64位整数的前面的一个字节,w是两个字节它会改变两个字节,
movl是四个字节,但它这里比较特殊,它相当于是movzlq,它不仅会改变前面的四个字节,它还会把后面字节扩展成0,而movq就是8个字节,然后再看下面的,把一个64位整数赋给寄存器,把AA赋给dl,这里是把dl赋给al就改变一个字节,然后movs是符号扩展,这里就改变一个字节,并把后面几位做符号扩展,A它符号位是1,所以后面都扩展成1,z是零扩展,首先设置成AA,然后把后面几位扩展成0。
汇编语言它有两种语法AT&T跟Intel语法,在Linux Unix还有GCC它采用的是AT&T语法,然后Intel的官方文档,还有Windows上面它采用的是Intel语法,因为我们主要是在Linux上面,所以我们主要也是看AT&T语法,
我们来比较一下两种语法的区别,AT&T语法它通常采用的是小写也可以用大写,Intel语法通常采用的是大写也可以用小写,Intel语法它不会在指令的名字后面去加b,l这些后缀,它直接根据寄存器的大小来推断,然后寄存器它不会加%,立即数它不会去加$,然后Intel语法它的目的操作数是在前面原操作数是在后面,对于地址intel语法它使用的是中括号,然后寻址方式它也更直观一些,它直接把公式给展开了,[_array + ebx + eax*8]这种形式,
然后我们在使用gcc编译的时候,也可以把它编译成intel语法,就后面加一个参数gcc hello.c -S -masm=intel