代碼里-3>>1是-2但3>>1是1,而-3/2卻又是-1?

本文首發(fā)于微信公眾號(hào):Binfun解代碼 公眾號(hào)文章地址

之前群里有個(gè)同學(xué)向大家提出了類似這樣的問題。隨后這位同學(xué)公布了答案:右移運(yùn)算是向下取整,除法是向零取整。這句話對(duì)以上現(xiàn)象做了很好的總結(jié),可是本質(zhì)原因是什么呢?

我一直以為-3>>1的結(jié)果是-1。所以打算思考一下這個(gè)問題。

補(bǔ)碼

首先我們看看-3存儲(chǔ)的形態(tài)是怎么樣的:

int main()
{
    int n = -3;
    printf("0x%x",n);
}

打印結(jié)果為:

0xfffffffd

這是32位有符號(hào)數(shù)負(fù)數(shù)的補(bǔ)碼形式,即0x3按位取反之后0xfffffffc再加一,即為0xfffffffd

為什么會(huì)有這樣的“奇怪”的補(bǔ)碼形式呢?首先一個(gè)32位的寄存器的值的范圍是0~0xffffffff (8個(gè)f)。如果僅僅表示正數(shù)的話,即無符號(hào)整型數(shù),所有的值都是正數(shù)的情況下范圍是0~4294967295(0xffffffff)

那么如果我想表示負(fù)數(shù)呢???比如我想在計(jì)算機(jī)中表達(dá)-1這個(gè)數(shù)字,正1很簡單就0x1嘛。那么根據(jù)1和-1相加等于0以及整型相加溢出的那一bit會(huì)被丟棄的特性,-1就可以是0xffffffff

例如0xffffffff + 0x1 = 0x100000000(32bit計(jì)算機(jī)中此處最高位的1會(huì)被丟棄) = 0x00000000

0x1怎么轉(zhuǎn)化成0xffffffff,就是按位取反后(0xfffffffe)再加一嘛,這個(gè)就是補(bǔ)碼的說法了。

然后呢,正負(fù)兩種數(shù)的范圍就對(duì)半分吧。

正數(shù):0 ~ 0x7fffffff,負(fù)數(shù):0x80000000 ~ 0xffffffff

0x80000000 是很特殊的數(shù),和0一樣,0x80000000只有和自己相加才會(huì)等于“零”。如果把0x80000000 歸類成負(fù)數(shù)的話,那么就有一個(gè)明顯的規(guī)律了,那就是最高位的bit為1的數(shù)都是負(fù)數(shù),最高位bit為0的數(shù)都是正數(shù)。

這就是最高位是符號(hào)位的規(guī)定。

整型數(shù)字的移位(-3>>1為啥等于-2)

這里我們想確鑿地弄清楚這個(gè)過程,只能借助匯編代碼了。
方法即為:

  1. 準(zhǔn)備好一段C代碼
  2. 編譯這段代碼
  3. 反匯編可執(zhí)行文件,查看匯編代碼

因?yàn)槲腋瞄L一點(diǎn)arm的匯編代碼,所以需要在
https://www.linaro.org/downloads/上下載arm的交叉編譯工具鏈,這個(gè)比較方便,因?yàn)椴恍枰幾g,直接下載后就可以在Linux環(huán)境上執(zhí)行了。

準(zhǔn)備以下代碼:

#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

可以看到有符號(hào)的移位操作:

asr.w   r3, r2, r3

無符號(hào)數(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指令會(huì)在移位之后,將原來的最高位bit[31]重新賦值到結(jié)果里。

所以-3 >> 1的過程應(yīng)該是這樣的:

0xfffffffd右移一位是0x7ffffffe,然后再置位最高位符號(hào)位結(jié)果為:0xfffffffe,這就是-2的補(bǔ)碼表現(xiàn)形式。

整型數(shù)字的除法(-3/2為啥等于-1)

那么為啥-3/2等于-1,難道在做除法的時(shí)候不會(huì)用移位進(jìn)行優(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

顯然除法對(duì)于有符號(hào)數(shù)和無符號(hào)數(shù)做了區(qū)分,但是我們無法看到內(nèi)部的區(qū)別,所以要用armv7的編譯鏈反匯編,因?yàn)閍rmv7沒有直接的div指令,所以我們可以看到匯編中除法都做了什么。

此處我們主要看有符號(hào)數(shù)除法和無符號(hào)數(shù)除法的區(qū)別,而匯編篇幅太長,在此我只截取有符號(hào)數(shù)除法中有,而無符號(hào)數(shù)除法不存在也不需要的那部分代碼,這樣就能看到-3/2和3/2的區(qū)別。
有符號(hào)數(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ù)進(jìn)行異或并將結(jié)果存儲(chǔ)到ip寄存器中,但是不會(huì)更新cpsr寄存器
it      mi //判斷cpsr中的Negative Flag
negmi   r1, r1 //如果r1為負(fù)數(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為負(fù)數(shù)則改成正數(shù)
//接下來就進(jìn)行和無符號(hào)數(shù)一樣的常規(guī)除法算法

以及有符號(hào)數(shù)除法對(duì)結(jié)果的處理:

cmp.w   ip, #0 
it      mi //如果異或結(jié)果為負(fù),則表示被除數(shù)和除數(shù)的符號(hào)不相同,那么結(jié)果必然是負(fù)數(shù)
negmi   r0, r0 //如果異或結(jié)果為負(fù),把結(jié)果賦成負(fù)值
bx      lr //返回到函數(shù)調(diào)用處的后一個(gè)指令

以上可以看到對(duì)有符號(hào)數(shù)的除法處理會(huì)這樣:

  1. 記錄除數(shù)和被除數(shù)的符號(hào)是否相同
  2. 將被除數(shù)和除數(shù)都轉(zhuǎn)成正數(shù)
  3. 除法算法結(jié)束之后,根據(jù)第一步的結(jié)果,來決定是不是把結(jié)果賦值成負(fù)數(shù)。

所以-3/2的時(shí)候,會(huì)先計(jì)算3/2,得到1之后再賦值成-1

還記得那個(gè)神奇的數(shù)字0x80000000(-2147483648)嗎,0x80000000乘以-1依然是0x80000000如果是這個(gè)數(shù)字除以2會(huì)是什么結(jié)果呢。

0x80000000/2的步驟如下:

  1. 記錄兩個(gè)數(shù)字異或結(jié)果,如果兩個(gè)數(shù)字的符號(hào)位不同,說明結(jié)果為負(fù),反之為正
  2. 對(duì)0x80000000進(jìn)行乘以-1處理,結(jié)果依然還是0x80000000
  3. 將0x80000000當(dāng)作是無符號(hào)數(shù)進(jìn)行除以2操作得到:0x40000000
  4. 把0x40000000賦值為負(fù)數(shù)即為0xC0000000 (-1073741824)

以上就是arm中對(duì)于有符號(hào)數(shù)的移位和除法操作。如果你對(duì)匯編中的除法的具體步驟有興趣的話,點(diǎn)個(gè)贊,下一篇帶來arm除法匯編實(shí)現(xiàn)全面解析!謝謝!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容