本文首發(fā)于微信公眾號:Binfun解代碼 公眾號文章地址
之前群里有個同學(xué)向大家提出了類似這樣的問題配猫。隨后這位同學(xué)公布了答案:右移運算是向下取整,除法是向零取整杏死。這句話對以上現(xiàn)象做了很好的總結(jié)泵肄,可是本質(zhì)原因是什么呢佳遣?
我一直以為-3>>1的結(jié)果是-1。所以打算思考一下這個問題凡伊。
補碼
首先我們看看-3存儲的形態(tài)是怎么樣的:
int main()
{
int n = -3;
printf("0x%x",n);
}
打印結(jié)果為:
0xfffffffd
這是32位有符號數(shù)負數(shù)的補碼形式零渐,即0x3按位取反之后0xfffffffc再加一,即為0xfffffffd
為什么會有這樣的“奇怪”的補碼形式呢系忙?首先一個32位的寄存器的值的范圍是0~0xffffffff (8個f)诵盼。如果僅僅表示正數(shù)的話,即無符號整型數(shù)银还,所有的值都是正數(shù)的情況下范圍是0~4294967295(0xffffffff)
那么如果我想表示負數(shù)呢风宁??蛹疯?比如我想在計算機中表達-1這個數(shù)字戒财,正1很簡單就0x1嘛。那么根據(jù)1和-1相加等于0以及整型相加溢出的那一bit會被丟棄的特性捺弦,-1就可以是0xffffffff
例如0xffffffff + 0x1 = 0x100000000(32bit計算機中此處最高位的1會被丟棄) = 0x00000000
0x1怎么轉(zhuǎn)化成0xffffffff饮寞,就是按位取反后(0xfffffffe)再加一嘛,這個就是補碼的說法了列吼。
然后呢幽崩,正負兩種數(shù)的范圍就對半分吧。
正數(shù):0 ~ 0x7fffffff寞钥,負數(shù):0x80000000 ~ 0xffffffff
0x80000000 是很特殊的數(shù)慌申,和0一樣,0x80000000只有和自己相加才會等于“零”理郑。如果把0x80000000 歸類成負數(shù)的話蹄溉,那么就有一個明顯的規(guī)律了,那就是最高位的bit為1的數(shù)都是負數(shù)您炉,最高位bit為0的數(shù)都是正數(shù)柒爵。
這就是最高位是符號位的規(guī)定。
整型數(shù)字的移位(-3>>1為啥等于-2)
這里我們想確鑿地弄清楚這個過程邻吭,只能借助匯編代碼了餐弱。
方法即為:
- 準備好一段C代碼
- 編譯這段代碼
- 反匯編可執(zhí)行文件宴霸,查看匯編代碼
因為我更擅長一點arm的匯編代碼囱晴,所以需要在
https://www.linaro.org/downloads/上下載arm的交叉編譯工具鏈,這個比較方便瓢谢,因為不需要編譯畸写,直接下載后就可以在Linux環(huán)境上執(zhí)行了。
準備以下代碼:
#include<stdio.h>
int shift(int a, int b)
{
return (a >> b);
}
unsigned int shift_u(unsigned int a, unsigned int b)
{
return (a >> b);
}
main(){
int a = shift(-3, 1);
unsigned int b = shift_u(3, 1);
printf("[%d][%u]",a,b);
}
下載好linaro的gcc和glibc之后執(zhí)行:
~/linro/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc test.c --sysroot=~/linro/sysroot-glibc-linaro-2.25-2019.12-arm-linux-gnueabihf/
然后反匯編:
~/linro/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-objdump -d a.out
可以看到有符號的移位操作:
asr.w r3, r2, r3
無符號數(shù)的移位操作:
lsr.w r3, r2, r3
以上指令的意思是將r2的值右移r3次氓扛,并將結(jié)果賦值到r3中枯芬。
關(guān)于asr和lsr可以在官方文檔中找到解釋:
https://developer.arm.com/documentation/dui0497/a/the-cortex-m0-instruction-set/about-the-instruction-descriptions/shift-operations
Arithmetic shift right by n bits moves the left-hand 32-n bits of the register Rm, to the right by n places, into the right-hand 32-n bits of the result, and it copies the original bit[31] of the register into the left-hand n bits of the result
asr和lsr不同之處在于论笔,asr指令會在移位之后,將原來的最高位bit[31]重新賦值到結(jié)果里千所。
所以-3 >> 1的過程應(yīng)該是這樣的:
0xfffffffd右移一位是0x7ffffffe狂魔,然后再置位最高位符號位結(jié)果為:0xfffffffe,這就是-2的補碼表現(xiàn)形式淫痰。
整型數(shù)字的除法(-3/2為啥等于-1)
那么為啥-3/2等于-1最楷,難道在做除法的時候不會用移位進行優(yōu)化嗎?
多說無益待错,只能按照套路來反匯編籽孙,還是一樣的套路代碼。
#include<stdio.h>
int div(int a, int b)
{
return (a / b);
}
unsigned int div_u(unsigned int a, unsigned int b)
{
return (a / b);
}
main(){
int a = div(-3, 2);
unsigned int b = div_u(3, 2);
printf("[%d][%d]",a,b);
}
如果使用linaro上的armv8的交叉編譯工具鏈火俄,那么可以看到div函數(shù)調(diào)用的指令是:
sdiv r3, r2, r3犯建,
div_u函數(shù)調(diào)用的指令是:
udiv r3, r2, r3
顯然除法對于有符號數(shù)和無符號數(shù)做了區(qū)分,但是我們無法看到內(nèi)部的區(qū)別瓜客,所以要用armv7的編譯鏈反匯編适瓦,因為armv7沒有直接的div指令,所以我們可以看到匯編中除法都做了什么谱仪。
此處我們主要看有符號數(shù)除法和無符號數(shù)除法的區(qū)別犹菇,而匯編篇幅太長,在此我只截取有符號數(shù)除法中有芽卿,而無符號數(shù)除法不存在也不需要的那部分代碼揭芍,這樣就能看到-3/2和3/2的區(qū)別。
有符號數(shù)除法一開始的處理:
//此處被除數(shù)是r0卸例,除數(shù)是r1
<__divsi3>:
cmp r1, #0 //判斷r1和0的關(guān)系称杨,并更新cpsr寄存器
beq.w 1098a <.divsi3_skip_div0_test+0x27c> //如果除數(shù)等于0,那么跳轉(zhuǎn)
<.divsi3_skip_div0_test>:
eor.w ip, r0, r1 //將除數(shù)和被除數(shù)進行異或并將結(jié)果存儲到ip寄存器中筷转,但是不會更新cpsr寄存器
it mi //判斷cpsr中的Negative Flag
negmi r1, r1 //如果r1為負數(shù)則改成正數(shù)
subs r2, r1, #1
beq.w 1095a <.divsi3_skip_div0_test+0x24c> //如果r1為1則跳轉(zhuǎn)
movs r3, r0
it mi
negmi r3, r0 //如果r0為負數(shù)則改成正數(shù)
//接下來就進行和無符號數(shù)一樣的常規(guī)除法算法
以及有符號數(shù)除法對結(jié)果的處理:
cmp.w ip, #0
it mi //如果異或結(jié)果為負姑原,則表示被除數(shù)和除數(shù)的符號不相同,那么結(jié)果必然是負數(shù)
negmi r0, r0 //如果異或結(jié)果為負呜舒,把結(jié)果賦成負值
bx lr //返回到函數(shù)調(diào)用處的后一個指令
以上可以看到對有符號數(shù)的除法處理會這樣:
- 記錄除數(shù)和被除數(shù)的符號是否相同
- 將被除數(shù)和除數(shù)都轉(zhuǎn)成正數(shù)
- 除法算法結(jié)束之后锭汛,根據(jù)第一步的結(jié)果,來決定是不是把結(jié)果賦值成負數(shù)袭蝗。
所以-3/2的時候唤殴,會先計算3/2,得到1之后再賦值成-1
還記得那個神奇的數(shù)字0x80000000(-2147483648)嗎到腥,0x80000000乘以-1依然是0x80000000如果是這個數(shù)字除以2會是什么結(jié)果呢朵逝。
0x80000000/2的步驟如下:
- 記錄兩個數(shù)字異或結(jié)果,如果兩個數(shù)字的符號位不同乡范,說明結(jié)果為負配名,反之為正
- 對0x80000000進行乘以-1處理啤咽,結(jié)果依然還是0x80000000
- 將0x80000000當作是無符號數(shù)進行除以2操作得到:0x40000000
- 把0x40000000賦值為負數(shù)即為0xC0000000 (-1073741824)
以上就是arm中對于有符號數(shù)的移位和除法操作。如果你對匯編中的除法的具體步驟有興趣的話渠脉,點個贊宇整,下一篇帶來arm除法匯編實現(xiàn)全面解析!謝謝芋膘!