#头条创作挑战赛#
ARM和intel,是两大主流的CPU架构。
ARM是RISC精简指令集的代表,intel是CISC复杂指令集的代表。
ARM的指令长度固定,每条指令固定为4字节。
intel的指令长度不固定,最少1个字节,最多可以不超过16字节。
长度不固定,使用起来就比较灵活,但相应的编码格式就比较复杂。
长度固定,使用起来就比较死板,但相应的编码格式就比较简单。
ARM和intel在汇编层面的区别,都是因为指令长度是否固定。
1,ARM的让人头大的立即数,
intel的mov指令是可以携带任意大小的立即数的,所以 int a = 0x12345678; 这样的代码只需要一条汇编就可以实现。
但是ARM的指令总长度才4个字节,显然它携带不了长达4个字节的数字。
所以,立即数的加载成了ARM中的一个问题。
稍微大一点的立即数,在ARM中都没法用一条汇编实现。
上图的C代码,在ARM中会生成2条汇编:
先加载最低16位的0x5678,然后再加载高16位的0x1234:
一条简单的C代码 int a = 0x12345678,居然要分两次才能实现。
因为CPU的指令条数一般100-200条之间,所以指令码就要占1个字节。
另外,CPU的寄存器个数也有16-32个,所以寄存器的编号也要占5个二进制位。
然后,4个字节的ARM指令也就还剩下2个字节可以携带数字(其他信息再占2-3位)。
所以,ARM加载一个4字节的数字需要2条汇编,加载8字节的数字需要4条汇编。
ARM的mov指令的格式,其中5-20位用于携带立即数字
intel的指令因为长度不固定,就可以携带任意大小的数字:把数字添加在指令码的末尾就行。
intel的汇编
同样的C代码,在intel上编译之后只需要1条汇编,从上图可以看到按小端序排列的0x12345678:
b8是mov的机器码,然后低位在低字节,高位在高字节,所以数字的字节顺序是反着的。
指令长度不固定,对于反汇编工具来说写起来就相对麻烦一点,因为只能一个字节一个字节的解析。
但对于编译器的作者来说,写起来就比较舒服:因为不用考虑什么样的数字可以携带,什么样的数字没法携带。
在ARM上,没法在指令里携带的数字(常量),还得给常量分配寄存器,真是太难用了[捂脸]
这个代码在ARM上需要给0x12345678分配寄存器
一般来说,编译器框架只会给变量分配寄存器,常量是默认不分配寄存器的。
但是,ARM的指令没法携带大于16位(有效数字)的常量,所以对数字的运算非常麻烦!
mov指令可以携带的数字算多的(16位),加法指令可以携带的数字更少(12位),
也就是说,大于4095的数字都没法直接使用。
ARM的加法指令,只能携带12位的有效数字
我这两天开始给scf编译器添加ARM64的机器码生成,遇到的最大问题就是立即数问题。
2,intel的乘法,
英特尔的乘法也是个扯淡的设计。
它同时使用EAX和EDX存放乘法的结果,让编译器的寄存器分配特别的复杂!
如果EAX里存在别的变量怎么办?
如果EDX里存在别的变量怎么办?
都得先给它腾出来[捂脸]
如果其中一个乘数是常量数字(立即数)怎么办?
也得先把它放到寄存器里去,一般使用EAX或EDX其中之一。
int a = 0x12345678;
int b = 0x87654321;
int c = a * b;
本来c就是a乘以b的结果的最低32位,直接这么设计指令就行:mul c, a, b.
高32位溢出了,没法保存到变量c里,直接丢弃就行。
想保存高32位,可以把a, b, c都扩充到64位的乘法:
int64_t c = (int64_t)a * (int64_t)b;
这两个类型转换,就会在这两个数字的前面添加32位的0或1(扩充到64位),然后它们的乘法结果就可以全部保存下来了。
但是英特尔为了让乘法的指令长度短一些,居然要求先把其中一个乘数放到EAX里,而结果又同时使用EAX:EDX!
真是个难用的设计。
3,比乘法更难用的是,intel的除法[捂脸]
乘法的两个数字是对称的,结果与先后次序无关,但是除法的两个数字是不对称的:
被除数需要先放到EAX:EDX,然后再除以除数。
如果除数是一个常量数字,英特尔的除法又没法直接携带数字,还得给这个除数找一个空闲的寄存器!
而EAX和EDX这时又同时被被除数占据了,没法用来临时存放除数。
4,信息编码的代价,
固定了指令长度之后,指令的解码就比较简单,但怎么携带大的数字就成了问题。
不固定指令长度,那就要让指令使用一些特别的默认寄存器:这样代码的长度是短了,但编译器写起来就更麻烦了。
总之,二进制的位数与它表示的信息量是相关的:只能各种折中,难以面面俱到。
ARM体系结构与编程 第2版 ¥59 购买