1. 程式人生 > >資訊的表示和處理(一)資訊儲存

資訊的表示和處理(一)資訊儲存

資訊儲存

大多數計算機使用8位的塊,或者位元組(byte),作為最小的可定址的記憶體單位,而不是訪問記憶體中單獨的位。機器級程式將記憶體視為一個非常大的位元組陣列,稱為虛擬記憶體。記憶體的每個位元組都由一個唯一的數字來標識,稱為它的地址,所有可能地址的集合就稱為虛擬地址空間。顧名思義,這個虛擬地址空間只是一個展現給機器級程式的概念性映像。實際的實現是將動態隨機訪問儲存器(DRAM)、快閃記憶體、磁碟儲存器、特殊硬體和作業系統軟體結合起來,為程式提供一個看上去統一的位元組陣列。

十六進位制表示法

一個位元組由八位組成。在二進位制表示法中,它的值域是0000 00002~1111 11112。如果看成十進位制整數,它的值域就是010

~25510。兩種符號表示法對描述位模式來說都不是非常方便。二進位制法太冗長,而十進位制表示法與位模式轉化很麻煩。代替的方法時,以16位基數,或者叫做十六進位制數,來表示位模式。十六進位制(簡寫為“hex”)使用數字0~9以及字元A~F來表示十六個可能的值。圖1-1展示了十六個十進位制數字對應的十進位制值和二進位制值。用十六進位制書寫,一個位元組的值域為0016~FF16

圖1-1   十六進位制法。每個十六進位制數字都對十六個值中的一個進行了編碼

在C語言中,以0x或0X開頭的數字常量被認為是十六進位制的值。字元A~F既可以大寫也可以小寫,甚至可以大小寫混合。比如FA1D37B16

可以寫作0xFA1D73B、0xfa1d37b又或者0xFa1D37b。

字資料大小

每臺計算機都有一個字長,指明指標資料的標稱大小。因為虛擬地址是以這樣的一個字來編碼的,所以字長決定的最重要的系統引數就是虛擬地址空間的最大大小。也就是說,對一個字長為w位的機器而言,虛擬地址的範圍為0~2w-1,程式最多訪問2w個位元組。

大多數64位機器也可以執行32位機器編譯的程式,這是一種向後相容。因此,舉例來說,當程式prog.c用如下偽指令編譯後:

# gcc -m32 prog.c

  

該程式就可以在32位或64位機器上正確執行。另一方面,若程式用下述偽指令編譯:

# gcc -m64 prog.c

  

那就只能在64位機器上執行。因此,我們將程式稱為“32位程式”或“64位程式”時,區別在於該程式是如何編譯的,而不是其執行的機器型別。

計算機和編譯器支援多種不同方式編碼的數字格式,如不同長度的整數和浮點數。比如,許多機器都有處理單個位元組的指令,也有處理表示為2位元組、4位元組或者8位元組整數的指令,還有些指令支援表示為4位元組和8位元組的浮點數。

圖1-2   基本C資料型別的典型大小(以位元組為單位)。分配的位元組數受程式是如何編譯的影響而變化。

C語言支援整數和浮點數的多種資料格式。圖1-2展示了為C語言各種資料型別分配的位元組數。有些資料型別的確切位元組數依賴於程式是如何被編譯的。我們給出的是32位和64位程式的典型值。整數或者為有符號的,即可以表示負數、零和正數;或者為無符號的,即只能表示非負數。C的資料型別char表示一個單獨的位元組。儘管“char”是由於它被用來儲存文字串中的單個字元這一事實而得名,但它也能被用來儲存整數值。資料型別short、int和long可以提供各種資料大小。即使是為64位系統編譯,資料型別int通常也只有4個位元組。資料型別long一般在32位程式中為4位元組,在64位程式中則為8位元組。

為了避免由於依賴“典型”大小和不同編譯器設定帶來的奇怪行為,ISO C99引入了一類資料型別,其資料大小是固定的,不隨編譯器和機器設定而變化。其中就有資料型別int32_t和int64_t,它們分別為4個位元組和8個位元組。使用確定大小的整數型別是程式設計師準確控制資料表示的最佳途徑。

大部分資料型別都編碼為有符號數值,除非有字首關鍵字unsigned或對確定大小的資料型別使用了特定的無符號宣告。資料型別char是一個例外。儘管大多數編譯器和機器將它們視為有符號數,但C標準不保證這一點。相反,正如方括號指示的那樣,程式設計師應該用有符號字元的宣告來保證其為一個位元組的有符號數值。不過,在很多情況下,程式行為對資料型別char是有符號的還是無符號的並不敏感。

對關鍵字的順序以及包括還是省略可選關鍵字來說,C語言允許存在多種形式。比如,下面所有的宣告都是一個意思:

unsigned long
unsigned long int
long unsigned 
long unsigned int

  

圖1-2還展示了指標(例如一個被宣告為型別為“char *”的變數)使用程式的全字長。大多數機器還支援兩種不同的浮點數格式:單精度(在C中宣告為float)和雙精度(在C中宣告為double)。這些格式分別使用4位元組和8位元組。

程式設計師應該力圖使他們的程式在不同的機器和編譯器上可移植。可移植性的一個方面就是使程式對不同資料型別的確切大小不敏感。C語言標準對不同資料型別的數字範圍設定了下界,但是卻沒有上界。在之前,32位機器和32位程式是主流的組合,許多程式的編寫都假設為圖1-2中32位程式的位元組分配。隨著64位機器的日益普及,在將這些程式移植到新機器上時,許多隱藏的對字長的依賴性就會顯現出來,成為錯誤。比如,許多程式設計師假設一個宣告為int型別的程式物件能被用來儲存一個指標。這在大多數32位的機器上能正常工作,但是在一臺64位的機器上卻會導致問題。

定址和位元組順序

對於跨越多位元組的程式物件,我們必須建立兩個規則:這個物件的地址是什麼,以及在記憶體中如何排列這些位元組。在幾乎所有的機器上,多位元組物件都被儲存為連續的位元組序列,物件的地址為所使用位元組中最小的地址。例如,假設一個型別為int的變數x的地址為0x100,也就是說,地址表示式&x的值為0x100。那麼,(假設資料型別int為32位表示)x的4個位元組將被儲存在記憶體的0x100、0x101、0x102和0x103位置。

排列表示一個物件的位元組有兩個通用的規則。考慮一個w位的整數,其位表示為[xw-1,xw-2,…,x1,x0],其中xw-1是最高有效位,而x0是最低有效位。假設w是8的倍數,這些位就能被分組成為位元組,其中最高有效位元組包含位[xw-1,xw-2,…,xw-8],而最低有效位元組包含位[x7,x6,…,x0],其他位元組包含中間的位。某些機器選擇在記憶體中按照從最低有效位元組到最高有效位元組的順序儲存物件,而另一些機器則按照從最高有效位元組到最低有效位元組的順序儲存。前一種規則——最低有效位元組在最前面的方式,稱為小端法(little endian)。後一種規則——最高有效位元組在最前面的方式,稱為大端法(big endian)。

假設變數x的型別為int,位於地址0x100處,它的十六進位制值為0x01234567。地址範圍0x100~0x103的位元組順序依賴於機器的型別:

對於大多數應用程式設計師來說,其機器所使用的位元組順序是完全不可見的。無論為哪種型別的機器所編譯的程式都會得到同樣的結果。不過有時候,位元組順序會成為問題。首先是在不同型別的機器之間通過網路傳送二進位制資料時,一個常見的問題是當小端法機器產生的資料被髮送到大端法機器或者反過來時,接收程式會發現,字裡的位元組成了反序的。為了避免這類問題,網路應用程式的程式碼編寫必須遵守已建立的關於位元組順序的規則,以確保傳送方機器將它的內部表示轉換成網路標準,而接收方機器則將網路標準轉換為它的內部表示。

當閱讀表示整數資料的位元組序列時位元組順序也很重要。這通常發生在檢查機器級程式時。作為一個示例,從某個檔案中摘出了下面這行程式碼,該檔案給出了一個針對Intel x86-64處理器的機器級程式碼的文字表示:

4004d3:   01   05   43   0b   20   00            add    %eax,0x200b43(%rip)

  

這一行是由反彙編器(disassembler)生成的,反彙編器是一種確定可執行程式檔案所表示的指令序列的工具。這條命令表述的意思是:十六進位制位元組串01 05 43 0b 20 00是一條指令的位元組級表示,這條指令是把一個字長的資料加到一個值上,該值的儲存地址由0x200b43加上當前程式計數器的值得到,當前程式計數器的值即為下一條將要執行指令的地址。如果取出這個序列的最後4個位元組:43 0b 20 00,並且按照相反的順序寫出,我們得到00 20 0b 43。去掉開頭的0,得到值0x200b43,這就是右邊的數值。當閱讀像此類小端法機器生成的機器級程式表示時,經常會將位元組按照相反的順序顯示。書寫位元組序列的自然方式是最低位位元組在左邊,而最高位位元組在右邊,這正好和通常書寫數字時最高有效位在左邊,最低有效位在右邊的方式相反。

位元組順序變得重要的第三種情況是當編寫規避正常的型別系統的程式時。在C語言中,可以通過使用強制型別轉換(cast)或聯合(union)來允許以一種資料型別引用一個物件,而這種資料型別與建立這個物件時定義的資料型別不同。大多數應用程式設計都強烈不推薦這種編碼技巧,但是它們對系統級程式設計來說是非常有用,甚至是必需的。

下面展示了一段C程式碼,它使用強制型別轉換來訪問和列印不同程式物件的位元組表示。我們用typedef將資料型別byte_pointer定義為一個指向型別為“unsigned char”的物件的指標。這樣一個位元組指標引用一個位元組序列,其中每個位元組都被認為是一個非負整數。第一個例程show_bytes的輸入是一個位元組序列的地址,它用一個位元組指標以及一個位元組數來指示。該位元組數指定為資料型別size_t,表示資料結構大小的首選資料型別。show_bytes打印出每個以十六進位制表示的位元組。C格式化指令“%.2x”表明整數必須用至少兩個數字的十六進位制格式輸出。

#include <stdio.h>

typedef unsigned char *byte_pointer;

void show_bytes(byte_pointer start, size_t len)
{
    size_t i;
    for (i = 0; i < len; i++)
        printf(" %.2x", start[i]);
    printf("\n");
}

void show_int(int x)
{
    show_bytes((byte_pointer)&x, sizeof(int));
}

void show_float(float x)
{
    show_bytes((byte_pointer)&x, sizeof(float)); 
}

void show_pointer(void *x)
{
    show_bytes((byte_pointer)&x, sizeof(void *));
}

  

過程show_int、show_float和show_pointer展示瞭如何使用程式show_bytes來分別輸出型別為int、float和void *的C程式物件的位元組表示。可以觀察到它們僅僅傳遞給show_bytes一個指向它們引數x的指標&x,且這個指標被強制型別轉換為“unsigned char *”。這種強制型別轉換告訴編譯器,程式應該把這個指標看成指向一個位元組序列,而不是指向一個原始資料型別的物件。然後,這個指標會被看成是物件使用的最低位元組地址。

這些過程使用C語言的運算子sizeof來確定物件使用的位元組數。一般來說,表示式sizeof(T)返回儲存一個型別為T的物件所需要的位元組數。使用sizeof而不是一個固定的值,是向編寫在不同機器型別上可移植的程式碼邁進了一步。

void test_show_bytes(int val)
{
    int ival = val;
    float fval = (float)ival;
    int *pval = &ival;
    show_int(ival);
    show_float(fval);
    show_pointer(pval);
}

  

圖1-3為在不同機器上執行上述程式碼的結果。

圖1-3   不同資料值的位元組表示。除了位元組順序以外,int和float的結果是一樣的。指標值與機器相關

引數12345的十六進位制表示為0x00003039。對於int型別的資料,除了位元組順序以外,我們在所有機器上都得到相同的結果。特別地,我們可以看到在Linux 32、Windows和Linux 64上,最低有效位元組值0x39最先輸出,這說明它們是小端法機器;而在Sun上最後輸出,這說明Sun是大端法機器。同樣地,float資料的位元組,除了位元組順序以外,也都是相同的。另一方面,指標值卻是完全不同的。不同的機器/作業系統配置使用不同的儲存分配規則。一個值得注意的特性是Linux 32、Windows和Sun的機器使用4位元組地址,而Linux 64使用8位元組地址。

可以觀察到,儘管浮點型和整型資料都是對數值12345編碼,但是它們有截然不同的位元組模式:整型為0x00003039,而浮點數為0x4640E400。一般而言,這兩種格式使用不同的編碼方法。如果我們將這些十六進位制模式擴充套件為二進位制形式,並且適當地將它們移位,就會發現一個有13個相匹配的位的序列,用一串星號標識出來:

這並不是巧合。當我們研究浮點數格式時,還將再回到這個例子。

布林代數

布林注意到通過將邏輯值TRUE(真)和FALSE(假)編碼為二進位制值1和0,能夠設計出一種代數,以研究邏輯推理的基本原則。

圖1-4   布林代數的運算

圖1-4中的運算子~、&、|、^分別表示邏輯運算NOT、AND、OR、EXCLUSIVE-OR。

C語言的一個很有用的特性就是它支援按位布林運算。事實上,我們在布林運算中使用的那些符號就是C語言所使用的:就是OR(或),&就是AND(與),~就是NOT(取反),而^就是EXCLUSIVE-OR(異或)。這些運算能運用到任何“整型”的資料型別上,包括圖1-2所示內容。以下是一些對char資料型別表示式求值的例子:

正如示例說明的那樣,確定一個位級表示式的結果最好的方法,就是將十六進位制的引數擴充套件成二進位制表示並執行二進位制運算,然後再轉換回十六進位制。

C語言中的位級運算

C語言的一個很有用的特性就是它支援按位布林運算。事實上,我們在布林運算中使用的那些符號就是C語言所使用的:就是OR(或),&就是AND(與),~就是NOT(取反),而^就是EXCLUSIVE-OR(異或)。這些運算能運用到任何“整型”的資料型別上,包括圖1-2所示內容。以下是一些對char資料型別表示式求值的例子:

正如示例說明的那樣,確定一個位級表示式的結果最好的方法,就是將十六進位制的引數擴充套件成二進位制表示並執行二進位制運算,然後再轉換回十六進位制。

C語言中的邏輯運算

C語言還提供了一組邏輯運算子‖、&&和!,分別對應於命題邏輯中的OR、AND和NOT運算。邏輯運算很容易和位級運算相混淆,但是它們的功能是完全不同的。邏輯運算認為所有非零的引數都表示TRUE,而引數0表示FALSE。它們返回1或者0,分別表示結果為TRUE或者為FALSE。以下是一些表示式求值的示例。

可以觀察到,按位運算只有在特殊情況下,也就是引數被限制為0或者1時,才和與其對應的邏輯運算有相同的行為。

邏輯運算子&&和‖與它們對應的位級運算&和之間第二個重要的區別是,如果對第一個引數求值就能確定表示式的結果,那麼邏輯運算子就不會對第二個引數求值。因此,例如,表示式a&&5/a將不會造成被零除,而表示式p&&*p++也不會導致間接引用空指標。

C語言中的移位運算

C語言還提供了一組移位運算,向左或者向右移動位模式。對於一個位表示為[xw-1,xw-2,…,x0]的運算元x,C表示式x<<k會生成一個值,其位表示為[xw-k-1,xw-k-2,…,x0,0,…,0]。也就是說,x向左移動k位,丟棄最高的k位,並在右端補k個0。移位量應該是一個0~w-1之間的值。移位運算是從左至右可結合的,所以x<<j<<k等價於(x<<j)<<k。

有一個相應的右移運算x>>k,但是它的行為有點微妙。一般而言,機器支援兩種形式的右移:邏輯右移和算術右移。邏輯右移在左端補k個0,得到的結果是[0,…,0,xw-1,xw-2,…,xk]。算術右移是在左端補k個最高有效位的值,得到的結果是[xw-1,…,xw-1,xw-1,xw-2,…,xk]。這種做法看上去可能有點奇特,但是我們會發現它對有符號整數資料的運算非常有用。

讓我們來看一個例子,下面的表給出了對一個8位引數x的兩個不同的值做不同的移位操作得到的結果:

斜體的數字表示的是最右端(左移)或最左端(右移)填充的值。可以看到除了一個條目之外,其他的都包含填充0。唯一的例外是算術右移[10010101]的情況。因為運算元的最高位是1,填充的值就是1。

C語言標準並沒有明確定義對於有符號數應該使用哪種型別的右移——算術右移或者邏輯右移都可以。不幸地,這就意味著任何假設一種或者另一種右移形式的程式碼都可能會遇到可移植性問題。然而,實際上,幾乎所有的編譯器/機器組合都對有符號數使用算術右移,且許多程式設計師也都假設機器會使用這種右移。另一方面,對於無符號數,右移必須是邏輯的。

與C相比,Java對於如何進行右移有明確的定義。表達是x>>k會將x算術右移k個位置,而x>>>k會對x做邏輯右移。