汇编变种后缀的应用场景
前置知识:
0.C语言数据格式
#include <iostream>
using namespace std;
int main() {
cout << "sizeof(char)=" << sizeof(char) << endl;
cout << "sizeof(short)=" << sizeof(short) << endl;
cout << "sizeof(int)=" << sizeof(int) << endl;
cout << "sizeof(long)=" << sizeof(long) << endl;
cout << "sizeof(long long)=" << sizeof(long long) << endl;
cout << "sizeof(void*)=" << sizeof(void *) << endl;
}
在64位ubuntu上的运行结果
root@deutschball-virtual-machine:/home/deutschball/mydir
root@deutschball-virtual-machine:/home/deutschball/mydir
sizeof(char)=1
sizeof(short)=2
sizeof(int)=4
sizeof(long)=8
sizeof(long long)=8
sizeof(void*)=8
在64位windows上的运行结果稍有不同
sizeof(char)=1
sizeof(short)=2
sizeof(int)=4
sizeof(long)=4
sizeof(long long)=8
sizeof(void*)=8
在32位windows上的运行结果
sizeof(char)=1
sizeof(short)=2
sizeof(int)=4
sizeof(long)=4
sizeof(long long)=8
sizeof(void*)=4
操作系统\大小(字节) | char | short | int | long | long long | void* |
---|
linux64位 | 1 | 2 | 4 | 8 | 8 | 8 | windows64位 | 1 | 2 | 4 | 4 | 8 | 8 | windows32位 | 1 | 2 | 4 | 4 | 8 | 4 |
1.寄存器规格
大多数的后缀都与寄存器规格有关, 下图将会被多次用到
2.寻址方式
mov类
操作数长度相关后缀b,w,l,q
mov类命令的数据流动方向有五种,
立即数->寄存器
寄存器->寄存器
主存 ->寄存器
立即数->主存
寄存器->主存
显然立即数->立即数 是不可能的,这里立即数相当于右值,就好比说把5存到6上
并且规定不能从主存直接到主存即主存->主存 ,必须经过寄存器
与数据长度有关的后缀有
这里两个字节等于一个字
便于记忆,可以了解后缀的含义:
b:byte,一个字节
w:word,一个字(两个字节)
使用哪种后缀需要与数据流动方向一起决定,具体规则是:
1.主存没有决定后缀的权利
2.如果源或者目的其一是寄存器则根据寄存器大小决定,如果使用的是`%al,%spl`这种单字节寄存器则mov命令用b后缀,
同理如果使 `%rax,%rsp`这种8个字节寄存器则mov命令用q后缀.
如果源和目的都是寄存器则跟随目的寄存器规格
3.寄存器比立即字的优先级高
记住这三条规则才能完成3.2
1.movl %eax,(%rsp)
源%eax 为32位(=4字节=2字)寄存器,
目的(%rsp) 是采用简介寻址,实际地址位于内存中,没有发言权,
因此mov 的后缀跟随%eav 即传送双字,使用l后缀
2.movw (%rax),%dx
源(%rax) 在内存中,没有发言权,
目的%dx 是一个16位(=2字节=1字)寄存器
因此mov 的后缀跟随%dx 即传送单字,使用w后缀
3.movb $0xFF %bl
源$0xFF 是一个立即字,优先级低
目的%bl 是一个字节寄存器,优先级高
mov 后缀跟随%bl 使用b
4.movb (%rsp,%rdx,4) %dl
源(%rsp,%rdx,4) 在内存上,没有发言权
目的%dl 是一个字节寄存器
mov 后缀跟随%dl 使用b
5.movq (%rdx),%rax
源(%rdx) 在内存上,没有发言权
目的%rax 是一个四字寄存器
mov 后缀跟随%rax 使用q
6.movw %dx,(%rax)
源%dx 是一个单字寄存器
目的(%rax) 在内存上,没有发言权
因此mov 后缀跟随%dx 用w
长度类后缀的其他细节差异
1.movb,movw 只会修改目标寄存器的对应低位
2.movl 不光会修改目标寄存器的对应低位,并且会将高位全部置零
3.对于64位的立即数,1.只能用movabsq 2.将其存到寄存器中,movq 只能处理32位的立即数
数据拓展相关后缀z,s
零拓展和符号拓展的区别:
R(%dl)=AA=10101010 符号位为1
第4行符号拓展直接将高位全都置1得到一串F
第5行零拓展直接将高位全都置0得到一串0
有符号数拓展时使用符号拓展
无符号数拓展时使用0拓展,可以理解为无符号拓展
1.首先,使用指针的目的是,使实际操作的地址在内存中,如此需要在寄存器中过度一次,将转型分成两个阶段,即源内存->寄存器 和寄存器->目的内存 两个阶段
2.然后一定注意"当执行强制类型转换即涉及大小变化又涉及C语言中的符号变化时,操作应该先改变大小"
这里"先改变大小"的意思是,无符号源用z,有符号源用s,然后决定大小的后缀看目的的大小
1.long到long
不涉及大小变化,不涉及符号变化,只需要使用传送四字指令movq ,两个阶段相同
2.char到int
只涉及大小变化,首先从字节内存到双字寄存器需要符号拓展指令movsbl 然后从双字寄存器到内存根据寄存器规格决定使用双字传送指令movl
3.char到unsigned
既涉及大小变化,又涉及符号变化
首先改变大小,从有符号字节内存到双字寄存器需要符号拓展指令movsbl
然后从双字寄存器到双字内存根据寄存器规格决定使用双字传送指令movl ,即目的符号不起作用
char到unsigned int和char到int形成的汇编语言是相同的
这一点可以实验验证
对于char的最小值-128=0x80,如果强制转型到unsigned,可能的结果:
1.首先变化符号,然后变化大小,即首先使用movzbl ,然后movl ,这样unsigned的值为0x00000080=128
2.首先变化大小,然后变化符号,即首先使用movsbl ,然后movl ,这样unsigned值为0xFFFFFF80=4294967168
基于上述两种猜想,可以写如下程序验证
证明猜想2是正确的
4.unsigned char到long
既涉及大小变化,又涉及符号变化
首先改变大小,从无符号字节内存到四字寄存器,要使用无符号拓展指令movzbq
然后从四字寄存器到内存,使用四字传送指令movq
然而实际上首先使用的是movzbl
查阅了知乎
然后官方给出的解释:
(Clarification, not an erratum) Figure 3.5.
Although there is an instruction movzbq, the GCC compiler typically generates the instruction movzbl for this purpose, relying on the property that an instruction generating a 4-byte with a register as destination will fill the upper 4 bytes of the register with zeros.
尽管应该使用movzbq指令,但是GCC编译器通常使用movzbl指令来达到相同的目的,
这是因为只要是以寄存器为目的并且生成低位4字节的指令都会将高位的4字节置零
博客上其他人的解释
这就很明白了
5.int到char
只涉及大小转换,大变小直接截取
直接从内存中取出双字数据放到寄存器里然后截取低8位传送给内存
即首先使用movl 然后movb
6.unsigned到unsigned char
只涉及大小转换,大变小直接截取
直接从内存中取出双字数据放到双字寄存器然后截取低8位传送给内存
即首先使用movl 然后movb
7.char到short
只涉及大小变换,小变大需要拓展
首先使用有符号拓展movsbw 将字节数据传送到单字寄存器
然后使用movw 从单字寄存器传送到内存
|