1. 程式人生 > >關於有符號數和無符號數的探討

關於有符號數和無符號數的探討

 這個問題,要是簡單的理解,是很容易的,不過要是考慮的深了,還真有些東西呢。
下面我就把這個東西儘量的擴充套件一點,深入一點和大家說說。
 
一、只有一個標準!
 
在組合語言層面,宣告變數的時候,沒有 signed 和 unsignde 之分,彙編器統統,將你輸入的整數字面量當作有符號數處理成補碼存入到計算機中,只有這一個標準!彙編器不會區分有符號還是無符號然後用兩個標準來處理,它統統當作有符號的!並且統統彙編成補碼!也就是說,db -20 彙編後為:EC ,而 db 236 彙編後也為 EC 。這裡有一個小問題,思考深入的朋友會發現,db 是分配一個位元組,那麼一個位元組能表示的有符號整數範圍是:-128 ~ +127 ,那麼 db 236 超過了這一範圍,怎麼可以?是的,+236 的補碼的確超出了一個位元組的表示範圍,那麼拿兩個位元組(當然更多的位元組更好了)是可以裝下的,應為:00 EC,也就是說 +236的補碼應該是00 EC,一個位元組裝不下,但是,別忘了“截斷”這個概念,就是說最後彙編的結果被截斷了,00 EC 是兩個位元組,被截斷成 EC ,所以,這是個“美麗的錯誤”,為什麼這麼說?因為,當你把 236 當作無符號數時,它彙編後的結果正好也是 EC ,這下皆大歡喜了,雖然彙編器只用一個標準來處理,但是借用了“截斷”這個美麗的錯誤後,得到的結果是符合兩個標準的!也就是說,給你一個位元組,你想輸入有符號的數,比如 -20 那麼彙編後的結果是符合有符號數的;如果你輸入 236 那麼你肯定當作無符號數來處理了(因為236不在一個位元組能表示的有符號數的範圍內啊),得到的結果是符合無符號數的。於是給大家一個錯覺:彙編器有兩套標準,會區分有符號和無符號,然後分別彙編。其實,你們被騙了。:-)
 
二、存在兩套指令!
 
第一點說明彙編器只用一個方法把整數字面量彙編成真正的機器數。但並不是說計算機不區分有符號數和無符號數,相反,計算機對有符號和無符號數區分的十分清晰,因為計算機進行某些同樣功能的處理時有兩套指令作為後備,這就是分別為有符號和無符號數準備的。但是,這裡要強調一點,一個數到底是有符號數還是無符號數,計算機並不知道,這是由你來決定的,當你認為你要處理的數是有符號的,那麼你就用那一套處理有符號數的指令,當你認為你要處理的數是無符號的,那就用處理無符號數的那一套指令。加減法只有一套指令,因為這一套指令同時適用於有符號和無符號。下面這些指令:mul div movzx … 是處理無符號數的,而這些:imul idiv movsx … 是處理有符號的。
舉例來說:
記憶體裡有 一個位元組x 為:0x EC ,一個位元組 y 為:0x 02 。當把x,y當作有符號數來看時,x = -20 ,y = +2 。當作無符號數看時,x = 236 ,y = 2 。下面進行加運算,用 add 指令,得到的結果為:0x EE ,那麼這個 0x EE 當作有符號數就是:-18 ,無符號數就是 238 。所以,add 一個指令可以適用有符號和無符號兩種情況。(呵呵,其實為什麼要補碼啊,就是為了這個唄,:-))
乘法運算就不行了,必須用兩套指令,有符號的情況下用imul 得到的結果是:0x FF D8 就是 -40 。無符號的情況下用 mul ,得到:0x 01 D8 就是 472 。(參看文後附錄2例程)
 
三、可愛又可怕的c語言。
 
為什麼又扯到 c 了?因為大多數遇到有符號還是無符號問題的朋友,都是c裡面的 signed 和 unsigned 宣告引起的,那為什麼開頭是從彙編講起呢?因為我們現在用的c編譯器,無論gcc 也好,vc6 的cl 也好,都是將c語言程式碼編譯成組合語言程式碼,然後再用匯編器彙編成機器碼的。搞清楚了彙編,就相當於從根本上明白了c,而且,用機器的思維去考慮問題,必須用匯編。(我一般遇到什麼奇怪的c語言的問題都是把它編譯成彙編來看。)
 
C 是可愛的,因為c符合kiss 原則,對機器的抽象程度剛剛好,讓我們即提高了思維層面(比彙編的機器層面人性化多了),又不至於離機器太遠(像c# ,java之類就太遠了)。當初K&R 版的c就是高階一點的彙編……:-)
 
C又是可怕的,因為它把機器層面的所有的東西都反應了出來,像這個有沒有符號的問題就是一例(java就不存在這個問題,因為它被設計成所有的整數都是有符號的)。為了說明它的可怕特舉一例:
 
#include <stdio.h> 
#include <string.h> 
 
int main()
{
  int x = 2; 
  char * str = "abcd"; 
  int y = (x - strlen(str) ) / 2;
   
  printf("%d\n",y);
}
 
結果應該是 -1 但是卻得到:2147483647 。為什麼?因為strlen的返回值,型別是size_t,也就是unsigned int ,與 int 混合計算時有符號型別被自動轉換成了無符號型別,結果自然出乎意料。。。
觀察編譯後的程式碼,除法指令為 div ,意味無符號除法。
解決辦法就是強制轉換,變成 int y = (int)(x - strlen(str) ) / 2; 強制向有符號方向轉換(編譯器預設正好相反),這樣一來,除法指令編譯成 idiv 了。
我們知道,就是同樣狀態的兩個記憶體單位,用有符號處理指令 imul ,idiv 等得到的結果,與用 無符號處理指令mul,div等得到的結果,是截然不同的!所以牽扯到有符號無符號計算的問題,特別是存在討厭的自動轉換時,要倍加小心!(這裡自動轉換時,無論gcc還是cl都不提示!!!)
 
為了避免這些錯誤,建議,凡是在運算的時候,確保你的變數都是 signed 的。
 
四、c的做法。
 
對於有符號和無符號的處理上,c語言層面做的更“人性化”一些。比如在宣告變數的時候,c 有signed 和 unsigned 字首來區別,而彙編呢,沒有任何區別,把握全在你自己,比如:你想在一個位元組中輸入一個有符號數,那麼這個數就別超過 -128 ~ +127 ,想輸入無符號數,要保證數值在 0~255 之間。如果你輸入了 236 ,你還要說你輸入的是有符號數,那麼你肯定錯了,因為有符號數236至少要兩個位元組來存放(為00 EC),不要小看了那一個位元組的00,在有符號乘法下,兩個位元組的00 EC 與 一個位元組的EC,在與同樣一個數相乘時,得到的結果是截然不同的!!!
 
我們來看下具體的列子(用vc6的cl編譯器生成):
 
C語言 編譯後生產的組合語言 
  ……
  char x;
  unsigned char y;
  int z;
   
  x = 3;
  y = 236;
 
  z = x*y;
  …… ……
  _x$ = -4
  _y$ = -8
  _z$ = -12
  …… 
  mov BYTE PTR _x$[ebp], 3
  mov BYTE PTR _y$[ebp], 236  
 
  movsx eax, BYTE PTR _x$[ebp]
  mov ecx, DWORD PTR _y$[ebp]
  and ecx, 255 
   
  imul eax, ecx
  mov DWORD PTR _z$[ebp], eax
  …… 


 
我們看到,在賦值的時候(綠色部分),彙編後與本文第一條論述相同,是否有符號把握全在自己,c比彙編做的更好這一點沒有得到體現,這也可以理解,因為c最終要被編譯成彙編,彙編沒有在變數宣告時區分有無符號這一功能,自然,c也沒有辦法。但既然c提供了signed和unsigned宣告,彙編後,肯定有程式碼體現這一點,表格裡的紅色部分就是。對有符號數x他進行了符號擴充套件,對無符號y進行了零擴充套件。這裡為了舉例的方便,進行了有符號數和無符號數的混合運算,實際程式設計中要避免這種情況。
 
(完)
 
 
附錄:
 
1.計算機對有符號整數的表示只採取一套編碼方式,不存在正數用原碼,負數用補碼這用兩套編碼之說,大多數計算機內部的有符號整數都是用補碼,就是說無論正負,這個計算機內部只用補碼來編碼!!!只不過正數和0的補碼跟他原碼在形式上相同,負數的補碼在形式上與其絕對值的原碼取反加一相同。
 
2. 兩套乘法指令結果例程:
 
;; 程式儲存為 x.s
 
extern printf 
global main 
 
section .data
  str1: db "%x",0x0d,0x0a,0 
  n: db 0x02
section .text 
main: 
  xor eax,eax
  mov al, 0xec
  mul byte [n] ;有符號乘法指令為: imul
 
  push eax
  push str1
  call printf 
   
  add esp,byte 4 
  ret 
   
編譯步驟:
1. nasm -felf x.s 
2. gcc x.o
 
ubuntu7.04 下用nasm和gcc編譯通過。結果符合文章所述。