代码里-3gt;gt;1是-2但3gt;gt;1是1,-3/2却又是-1,为什么?
之前群里有個同學向大家提出了類似這樣的問題。隨后這位同學公布了答案:右移運算是向下取整,除法是向零取整。這句話對以上現象做了很好的總結,可是本質原因是什么呢?
我一直以為-3>>1的結果是-1。所以打算思考一下這個問題。
補碼
首先我們看看-3存儲的形態是怎么樣的:
int?main() {int?n?=?-3;printf("0x%x",n); }打印結果為:
0xfffffffd這是32位有符號數負數的補碼形式,即0x3按位取反之后0xfffffffc再加一,即為0xfffffffd
為什么會有這樣的“奇怪”的補碼形式呢?首先一個32位的寄存器的值的范圍是0~0xffffffff (8個f)。如果僅僅表示正數的話,即無符號整型數,所有的值都是正數的情況下范圍是0~4294967295(0xffffffff)
那么如果我想表示負數呢???比如我想在計算機中表達-1這個數字,正1很簡單就0x1嘛。那么根據1和-1相加等于0以及整型相加溢出的bit會被丟棄的特性,-1就可以是0xffffffff
例如:0xffffffff + 0x1 = 0x100000000(32bit計算機中此處最高位的1會被丟棄) = 0x00000000
0x1怎么轉化成0xffffffff,就是按位取反(0xfffffffe)后再加一嘛,這個就是補碼的說法了。
然后呢,正負兩種數的范圍就對半分吧。正數:0 ~ 0x7fffffff,負數:0x80000000 ~ 0xffffffff
0x80000000 是很特殊的數,和0一樣,0x80000000只有和自己相加才會等于“零”。如果把0x80000000 歸類成負數的話,那么就有一個明顯的規律了,那就是最高位的bit為1的數都是負數,最高位bit為0的數都是正數。
這就是最高位是符號位的規定。
整型數字的移位(-3>>1為啥等于-2)
這里我們想確鑿地弄清楚這個過程,只能借助匯編代碼了。方法即為:
準備好一段C代碼
編譯這段代碼
反匯編可執行文件,查看匯編代碼
因為我更擅長一點arm的匯編代碼,所以需要在 https://www.linaro.org/downloads/上下載arm的交叉編譯工具鏈,這個比較方便,因為不需要編譯,直接下載后就可以在Linux環境上執行了。
準備以下代碼:
#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之后執行:
~/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無符號數的移位操作:
lsr.w???r3,?r2,?r3以上指令的意思是將r2的值右移r3次,并將結果賦值到r3中。
關于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]重新賦值到結果里。
所以-3 >> 1的過程應該是這樣的:
0xfffffffd右移一位是0x7ffffffe,然后再置位最高位符號位,結果為:0xfffffffe,這就是-2的補碼表現形式。
整型數字的除法(-3/2為啥等于-1)
那么為啥-3/2等于-1,難道在做除法的時候不會用移位進行優化嗎?
多說無益,只能按照套路來反匯編,還是一樣的套路代碼。
#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函數調用的指令是:
sdiv????r3,?r2,?r3,div_u函數調用的指令是:
udiv????r3,?r2,?r3顯然除法對于有符號數和無符號數做了區分,但是我們無法看到內部的區別,所以要用armv7的編譯鏈反匯編,因為armv7沒有直接的div指令,所以我們可以看到匯編中除法都做了什么。
此處我們主要看有符號數除法和無符號數除法的區別,而匯編篇幅太長,在此我只截取有符號數除法中有,而無符號數除法不存在也不需要的那部分代碼,這樣就能看到-3/2和3/2的區別。有符號數除法一開始的處理:
//此處被除數是r0,除數是r1 <__divsi3>: cmp?????r1,?#0?//判斷r1和0的關系,并更新cpsr寄存器 beq.w???1098a?<.divsi3_skip_div0_test+0x27c>?//如果除數等于0,那么跳轉<.divsi3_skip_div0_test>: eor.w ? ip, r0, r1 //將除數和被除數進行異或并將結果存儲到ip寄存器中,但是不會更新cpsr寄存器 it??????mi?//判斷cpsr中的Negative?Flag negmi???r1,?r1?//如果r1為負數則改成正數 subs????r2,?r1,?#1 beq.w???1095a?<.divsi3_skip_div0_test+0x24c>?//如果r1為1則跳轉 movs????r3,?r0 it??????mi negmi???r3,?r0?//如果r0為負數則改成正數 //接下來就進行和無符號數一樣的常規除法算法以及有符號數除法對結果的處理:
cmp.w???ip,?#0? it ???? mi //如果異或結果為負,則表示被除數和除數的符號不相同,那么結果必然是負數 negmi???r0,?r0?//如果異或結果為負,把結果賦成負值 bx??????lr?//返回到函數調用處的后一個指令以上可以看到對有符號數的除法處理會這樣:
記錄除數和被除數的符號是否相同
將被除數和除數都轉成正數
除法算法結束之后,根據第一步的結果,來決定是不是把結果賦值成負數。
所以-3/2的時候,會先計算3/2,得到1之后再賦值成-1
還記得那個神奇的數字0x80000000(-2147483648)嗎,0x80000000乘以-1依然是0x80000000如果是這個數字除以2會是什么結果呢。
0x80000000/2的步驟如下:
記錄兩個數字異或結果,如果兩個數字的符號位不同,說明結果為負,反之為正
對0x80000000進行乘以-1處理,結果依然還是0x80000000
將0x80000000當作是無符號數進行除以2操作得到:0x40000000
把0x40000000賦值為負數即為0xC0000000 (-1073741824)
以上就是arm中對于有符號數的移位和除法操作。如果你對匯編中的除法算法的具體步驟有興趣的話,點個贊,下一篇arm除法匯編實現全面解析!Binfun已經把arm的匯編除法轉譯成C,并從中學習到了很多,敬請關注!
推薦閱讀:
專輯|Linux文章匯總
專輯|程序人生
專輯|C語言
我的知識小密圈
總結
以上是生活随笔為你收集整理的代码里-3gt;gt;1是-2但3gt;gt;1是1,-3/2却又是-1,为什么?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: R 语言 iris 数据集的可视化
- 下一篇: KNN算法实验-采用UCI的Iris数据