CSAPP要点总结 第3章 程序的机器级表示 part2 汇编寻址与运算指令
ATT和Intel汇编代码格式
ATT是根据AT&T命名的代码格式,是GCC、OBJDUMP等工具的默认格式, 而Microsoft工具以及来自Intel的文档采用Intel格式。 王爽的《汇编语言》就是Intel格式
GCC也可以通过命令行选项,生成Intel格式汇编:
~ $ gcc -O1 -S -masm=intel code.c
虽然格式不同,但内容一样,CSAPP以ATT格式来讲解内容。
以下将总结ATT的汇编语法:
数据格式
以下表格给出C语言基本数据类型对应的IA32格式:
C声明 | Intel 数据类型 | 汇编代码后缀 | 大小 (bytes) |
---|---|---|---|
char | Byte | b | 1 |
short | Word | w | 2 |
int Double | word | l | 4 |
long int | Double word | l | 4 |
long long int | — | — | 4 |
char * | Double word | l | 4 |
float | Single precision | s | 4 |
double | Double precision | l | 8 |
long double | Extended precision | t | 10/12 |
之所以32位后缀用l
是因为对于早期16位CPU,例如8086,32位就是long了,
所以这一习惯沿用至今。
注意char*是双字哦
访问信息
如下图为IA32的整数寄存器,共8个:
对比给出x86-64的寄存器:
在平坦寻址中,对于%ax,&al这样的特殊寄存器的需求已经极大降低了。主要为了向后兼容。
他们都是通用寄存器,但功能上有些区别:
其中%eax、%ecx、%edx一组
%ebx、%edi、&esi一组,
%esp栈指针和%ebp帧指针保存程序栈的重要位置指针, 要用栈管理的标准惯例使用。
操作数
指令的操作数(operand)有三类,IA32格式如下:
立即数Immediate
例如 $-577、$0x1F
寄存器Register
例如 %eax, %ax
存储器Memory
例如 4(%eax)、9(%eax, edx)、0xF9(%eax, edx, 4) 含寄存器就带括号的。 还有绝对地址的写法,例如 0x104
其中0xF9(%eax, edx, 4)中的 4 是变址的比例因子,必须是1、2、4或者8。 通常引用中有两个寄存器适合于二维的数组或者表。
操作数格式汇总如下:
数据传送指令
需要注意的是零扩展和符号扩展,类似于右移中的逻辑扩展和算术扩展。
跟8086一样,IA32也不支持内存到内存,还是要内存到寄存器再到内存。
关于pushl,popl中数据与指针执行顺序可以参考汇编语言第三单元 寄存器中关于弹夹的比喻。
此外,对于IA32,入栈、出栈猜测似乎只能用于双字, 那么访问栈元素是4的倍数,例如4(%esp), 同时,指令似乎只有pushl和popl一种写法,而没有pushb、popw之类的。
对于一个实际的C函数,指针实际就是将地址保持到寄存器,调用指针就是操作寄存器值对应的内存地址。 函数中的某个局部变量通常保存在寄存器中,因为寄存器较快。
算术与逻辑操作
基本的算术与逻辑操作汇总如下图:
上图中 S代表Source,D代表Destination,k代表一个数或者%cl
可见基本的算术逻辑操作分为四类:加载有效地址,一元操作,二元操作和移位
加载有效地址
leal是加载有效地址(load effective address)实际上是movl的变形 类似于C语言中的取地址,为内存引用产生指针。
关于leal,还有特殊用法跟取地址无关。 例如:
leal 7(%edx, %edx, 4), %eax
这条指令中,7(%edx, %edx, 4) = (%edx + 4%edx + 7) = (5 * %edx + 7) 再取其地址,则实际上是将5 * %edx + 7赋值给%eax。 所以leal在此处实际上用作计算。 编译器中经常能看到leal的灵活用法。
一元操作和二元操作
需要注意的是二元操作的两个操作数与mov一样不能都是存储器位置, 对于两个存储器的数据交换必须通过寄存器。
这从CPU执行指令的过程很好理解, 总线可以是外部数据总线到寄存器 或者 寄存器到外部总线, 而外部内存空间数据之间没有直接数据线相连(不知道其他计算机构架有没有直连的,猜测计算显卡可能会有吧)。
移位操作
移位量用单个byte也只考虑byte内的低5位(IA-32)。
还记得第二章第一小节说到的,当移位的位数大于数的宽度的情况么?对移位数取余。 这里取低5位实际上就是对32位取余。 可以想到,对于64位数,取低6位; 16位数,取低4位
移位量k可以是立即数,或者cl寄存器(正好是1个byte)。这跟8086的机制很相似。 被执行移位的可以是寄存器,或者虚拟内存地址(指向的虚拟内存)。
注意SHR和SAR(shift algorithm right)的区别。 SHR逻辑右移,右补0,用在无符号 SAR算术右移,正数右补0,负数右补1,用在补码。
讨论
上述运算中,除了位移运算需要区分无符号数和补码数外, 其他运算对两者运算都没区别,这也正是补码的优点!
特殊的算术操作
在基本的算术与逻辑操作汇总图中的乘法imul有两个操作数, 而实际上,还有一个操作数的乘除法, 乘法扩展到64位,而除法,可得到商和余数, 这与古老的8086的乘除法方式一致。具体如下:
其中cltd即convert long to double,用来辅助补码除法计算的。 (Intel汇编格式对应命令为cdq convert double to quard)
对于除法,为何要区分补码和无符号呢? 一个原因是,对于64位被除数是先由32位扩展而来, 那么,就有逻辑扩展(对应无符号)和算术扩展(对应补码)
所以要区分div(无符号除法)和idiv(补码除法)
对于乘法为何区分补码和无符号还没想清楚,感觉一样啊。