3.2 程序编码
1.gcc将c语言代码转化成可执行代码的步骤(以p1.c和p2.c为例)
1).c预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展成所有用#define声明指定的宏。
2).编译器产生原文件的汇编代码p1.s和p2.s
3).汇编器把p1.s转化成二进制目标代码文件p1.o和p2.o,目标代码是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入全局值的地址
4).链接器将两个目标代码文件合并,并产生最终的可执行文件
2.反汇编器可以根据机器代码产生一种类似于汇编代码的格式
左边是二进制机器代码,右边是对应的汇编代码
3.3 数据格式
1.c语言的数据类型对应intel的数据类型
3.4 访问信息
1.intel的寄存器,一共有16个,各64位,用于存放整数数据类型和指针
2.操作数指示符
在内存和寄存器之间操作数据,需要指定源和目的地址,有时可以直接用immediate表示数,有时需要用寄存器的名字表示寄存器中存的数,有时需要用内存中某一地址的数,而内存中的地址是存在寄存器中的,所以需要用寄存器中存的地址来取内存中对应地址的数。下图是表示操作数的多种方法。
3.数据移动指令
1)数据移动指令源操作数指定的是一个immediate,存放在寄存器或内存中,目的操作数指定的是一个位置,要么是一个寄存器,要么是一个内存地址。mov指令共有四类,主要区别在于他们操作的数据大小不同。
2)通常,mov指令只会更新对应目的操作数指定寄存器的部分,比如看上面的图3.2的第一个寄存器,有四个名字,分别对应不同的大小,如果目的操作数是%ax,那就只更新%ax的部分,高位不更新,但是有个例外,如果是movl指令以寄存器作为目的时,他会把该寄存器的高位4字节设为0,这是x86-64管理导致的,任何为寄存器生成32比特值的指令,都会把高位设置成0。
3)图3.4最后一条指令是处理64位immediate的,常规的movq指令只能移动32位补码数字的immediate,然后把这个值符号扩展(这个看前一章)到64位,放到目的地址,但是我不明白为什么。movabsq指令可以移动64位immediate,但是只能移动到寄存器中。
4) x86-64新增了一条限制,不能从内存中的一个位置移动到内存中的另一个位置,把一个数从内存的一处复制到另一处需要两次操作,即先从内存中取到寄存器中,在从寄存器中移动到内存中。
5)可以用小号的移动指令将一个大数移动到移动指令对应大小的寄存器中,但是不能用小号的移动指令将一个大数移动到内存中,例如
movb $-1, %al ✔
movb $si, (%rbq) ❌
第一条指令中$-1就是立即数0xFFFFFF......FF,这一指令会将对应寄存器的低位设为FF,其他高位不变。
6)还有两类指令,是用来从源(寄存器或内存中)获取到数据并复制到目的寄存器中,注意目的只能为寄存器,
也就是说,如果要进行类型转换,有几点要注意的:
第一要考虑是要扩展还是截断,如果是截断就不需要这两类指令,例如int到char,只要用movl从内存中取出放到32位的寄存器中,在用movb从对应寄存器的低位寄存器取出,在放回内存中即可。
如果是扩展,则需要考虑第二步,那就是看是有符号数还是无符号数,有符号数要进行符号扩展,无符号数要进行零扩展。
第三要考虑特殊情况,有两个,一个是要把32位的无符号数零扩展成64位,是没有这个指令的,因为movl在移动32位数时将寄存器的高位置0;第二种情况是有一个特殊的指令cltq,这个指令等同于movslq %eax,%rax,只不过编码更加紧凑。
第四是个要注意的点,因为这些扩展传送指令目的只能是寄存器,所以要先用这些指令将数据从内存中复制到寄存器,并在这途中实现扩展,在用正常的指令将其从寄存器移到内存中。
4.压入和弹出栈数据
1)入栈和出栈都是由单操作数的指令来完成的,这个操作数分别是入栈的数据和出栈的目的地。
2)栈指针%rsp保存着栈顶元素的地址
3)压栈操作分两步,先根据要入栈的数据大小减栈指针,例如pushq栈指针就要减8,其次在用数据移动指令将数据移到新的栈顶。
4)出栈操作分两步,先用数据移动指令,将栈顶数据移动到目的地址,再增加栈指针。
5)那由于不能从内存移动数据到内存,那是不是说明入栈的数据源和出栈的目的地址不能是内存,因为入栈的目的地址一定是%rsp指向的内存位置,而出栈的源一定也是%rsp指向的内存位置。
6)可以用标准的内存寻址方法访问栈内的任意位置,也就是复制指令也可以复制栈内的非栈顶元素。
3.5 算术和逻辑操作
1.load effective address 指令
leaq实际上是movq指令的变形,它的形式是从内存读数据到寄存器,但他的第一个操作数不是内存,就是寄存器中的立即数,例如leaq 7(%rdx, %rdx, 4), %rax,这条指令第一个操作数的形式是内存,但实际意义是7+%rdx+4*%rdx这个数,假设%rdx里的数是x,那整体的意义就是把5x+7写入寄存器%rax中,这个操作目的操作数必须是一个寄存器。
2.一元和二元操作
1)第二组中的是一元操作,第三组中的是二元操作,对于一元操作,这很容易联想到c语言的++和--,对于二元操作,很容易联想到c语言的x+=y。
2)二元操作是拿第二个操作数+-*/第一个操作数,再放到第二个操作数的位置上,subq %rax, %rdx,可以理解为从%rdx减去%rax。
3)第一个操作数可以是立即数、寄存器或是内存,第二个操作数可以是寄存器或内存。
4)如果两个数都为内存地址时,必须先从内存中取出,执行操作,然后再写回内存中。
3.移位操作
1)第一项是移位量,第二项是要移位的数据
2)移位量可以是一个立即数,或者要放在单字节寄存器%cl上
3)如果移位量由寄存器%cl决定,那就是由%cl低m位决定的,2的m次方=w,w是要移位的数据的位数,也就是说,如果是salb会看低3位,salw看低4位,sall看低5位,salq看低6位
4)左移不分算术和逻辑,所以sal和shl是一样的,但右移分,sar是算术移位(填符号位),shr是逻辑移位(填0)
4.特殊的操作
两个64位数相乘需要128位来表示,这需要特殊的操作来实现
这些特殊的操作会用到指定寄存器
1)乘法
imulq有两种形式,一种是双操作数的形式,表示两个数相乘,从两个64位数相乘中得到一个64位数,另一种是一个操作数的形式,也就是图中所给的形式,用以全128位乘法。
其中一个参数必须在寄存器%rax中,另一个显示写出的参数是源操作数,结果的高64位存在%rdx中,低64位存在%rax中。
注意这里结果往内存中存时会遇到大小端法机器的问题,大端法按字节划分以数据从左到右、内存从小到大的顺序存,小端法则是数据从右到左,内存从小到大
2)除法和取模
这也是个单操作数指令,他将寄存器%rdx(高64位)和%rax(低64位)作为128位的被除数,除数作为指令的操作数给出,商存在寄存器%rax中,余数存在%rdx中。
对于大多数64位除法来说,被除数也是64位,那么%rdx中的高64位就应该设置为全0(无符号运算)或是%rax的符号位(有符号数运算),对于无符号运算需要用别的指令将%rdx设为全0,对于有符号数运算cqto可以完成这个任务。