Assembly 結(jié)構(gòu)體

2018-12-04 17:43 更新
在C語(yǔ)言中的結(jié)構(gòu)體用來(lái)將相關(guān)的數(shù)據(jù)集合到一個(gè)組合變量中。這項(xiàng)技術(shù)有幾個(gè)優(yōu)點(diǎn):


1. 通過(guò)展示定義在結(jié)構(gòu)體內(nèi)的數(shù)據(jù)是緊密相聯(lián)的來(lái)使代碼變得清晰明了。


2. 它使傳遞數(shù)據(jù)給函數(shù)變得簡(jiǎn)單。代替單獨(dú)地傳遞多個(gè)變量,它通過(guò)傳遞一個(gè)單元來(lái)傳遞多個(gè)變量。


3. 它增加了代碼的局部性 。


從匯編語(yǔ)言的觀點(diǎn)看,結(jié)構(gòu)體可以認(rèn)為是擁有不同大小的元素的數(shù)組。而真正的數(shù)組的元素的大小和類型總是一樣的。如果你知道數(shù)組的起始地址,每個(gè)元素的大小和需要的元素的下標(biāo),有這個(gè)特性就能計(jì)算出這個(gè)元素的地址。


結(jié)構(gòu)體中的元素的大小并不一定要是一樣的(而且通常情況下是不一樣的)。因?yàn)檫@個(gè)原因,結(jié)構(gòu)體中的每個(gè)元素必須清楚地指定而且需要給每個(gè)元素一個(gè)標(biāo)號(hào)(或者名稱),而不是給一個(gè)數(shù)字下標(biāo)。


在匯編語(yǔ)言中,結(jié)構(gòu)體中的元素可以通過(guò)和訪問(wèn)數(shù)組中的元素一樣的方法來(lái)訪問(wèn)。為了訪問(wèn)一個(gè)元素,你必須知道結(jié)構(gòu)體的起始地址和這個(gè)元素相對(duì)于結(jié)構(gòu)體的相對(duì)偏移地址。但是,和數(shù)組不一樣的是:不可以通過(guò)元素的下標(biāo)來(lái)計(jì)算該偏移地址,結(jié)構(gòu)體的元素的地址需要通過(guò)編譯器來(lái)賦值。


例如,考慮下面的結(jié)構(gòu)體:


struct S {
    short int x;         /* 2個(gè)字節(jié)的整形 */
    int y;                /* 4個(gè)字節(jié)的整形 */
    double z;           /* 8個(gè)字節(jié)的浮點(diǎn)數(shù) */
};


圖7.1展示了一個(gè)S結(jié)構(gòu)體變量在電腦內(nèi)存中是如何儲(chǔ)存的。ANSIC標(biāo)準(zhǔn)規(guī)定結(jié)構(gòu)體中的元素在內(nèi)存中儲(chǔ)存的順序和在struct定義中的順序是一樣的。它同樣規(guī)定第一個(gè)元素需恰好在結(jié)構(gòu)體的起始地址中(也
就是說(shuō)偏移地址為0)。它同樣在stddef.h頭文件中定義了另一個(gè)有用的宏offsetof()。這個(gè)宏用來(lái)計(jì)算和返回結(jié)構(gòu)體中任意元素的偏移地址。這個(gè)宏攜帶兩個(gè)參數(shù),第一個(gè)是結(jié)構(gòu)體類型的變量名,第二個(gè)是需要得到偏移地址的元素名。因此,圖7.1中的,offsetof(S, y)的結(jié)果將是2。


結(jié)構(gòu)體s

內(nèi)存地址對(duì)齊

如果在gcc編譯器中你使用offsetof宏來(lái)得到y(tǒng)的偏移地址,那么它們將找到并返回4,而不是2!為回想一下一個(gè)地址如果除 什么呢? 因此gcc(和其它許多編譯器),在缺省情況下,變量是對(duì)齊在雙字界上的。在32位保護(hù)模式下,如果數(shù)據(jù)是從雙字界開始儲(chǔ)存的,那么CPU能快速地讀取內(nèi)存。圖7.2展示了如果使
用gcc,那么S結(jié)構(gòu)體在內(nèi)存中是如何儲(chǔ)存的。編譯器在結(jié)構(gòu)體中插入了兩個(gè)沒(méi)有使用的字節(jié),用來(lái)將y(和z)對(duì)齊在雙字界上。這就表明了在C中定義的結(jié)構(gòu)體,使用offsetof計(jì)算偏移來(lái)代替元素自己來(lái)計(jì)算自己的偏移為什么是一個(gè)好的想法。

當(dāng)然,如果只是在匯編程序中使用結(jié)構(gòu)體,程序員可以自己決定偏移地址。但是,如果你需要使用C和匯編的接口技術(shù),那么在匯編代碼和C代碼中約定好如何計(jì)算結(jié)構(gòu)體元素的偏移地址是非常重要的!一個(gè)麻煩的地方是不同的C編譯器給出的元素的偏移地址是不同的。例如:就像我們已經(jīng)知道的,gcc編譯器創(chuàng)建結(jié)構(gòu)體S如圖7.2;但是,Borland的編譯器將創(chuàng)建結(jié)構(gòu)體如圖7.1。C編譯器提供了指定數(shù)據(jù)對(duì)齊的方法。但是,ANSI C標(biāo)準(zhǔn)并沒(méi)有指定它們?cè)撊绾瓮瓿?,因此不同的編譯器使用不同的方法來(lái)完成內(nèi)存地址對(duì)齊。

結(jié)構(gòu)2

gcc編譯器有一個(gè)靈活但是復(fù)雜的方法來(lái)指定地址對(duì)齊。它允許你使用特殊的語(yǔ)法來(lái)指定任意類型的地址對(duì)齊。例如,下面一行:

typedef short int unaligned _int   _attribute_ (( aligned (1)));

定義了一個(gè)名為unaligned_int的新類型,它采用的是字節(jié)界對(duì)齊方式。(是的,所以在__attribute__ 后面的括號(hào)都是需要的!)aligned的參數(shù)1可以用其它的2的乘方值來(lái)替代,用來(lái)表示采用的是其它對(duì)齊方式。(2為字邊界,4表示雙字界,等等。)如果結(jié)構(gòu)體里的y元素改為unaligned_int類型,那么gcc給出的y的偏移地址為2.但是,z依然處在偏移地址8的位置,因?yàn)殡p精度類型的缺省對(duì)齊方式為雙字對(duì)齊。要想z的偏移地址為6,那么還要改變它的類型定義。

使用gcc的壓縮結(jié)構(gòu)體

gcc編譯器同樣允許你壓縮一個(gè)結(jié)構(gòu)體。它告訴編譯器使用盡可能小的空間來(lái)儲(chǔ)存這個(gè)結(jié)構(gòu)體。圖7.3展示了S如何以這種方法來(lái)定義。這種形式下的S將使用可能的最少的字節(jié)數(shù),14個(gè)字節(jié)。

Microsoft和Borland的編譯器都支持使用#pragma指示符的方法來(lái)指定對(duì)齊方式。

#pragma pack(1)

上面的指示符告訴編譯器采用字節(jié)界的對(duì)齊方式來(lái)壓縮結(jié)構(gòu)體中的元素。(也就是說(shuō),沒(méi)有額外的填充空間)。其中的1可以用2,4,8或16代替,分別用來(lái)指定對(duì)齊方式為字邊界,雙字界,四字界和節(jié)邊界。這個(gè)指示符在被另一個(gè)指示符置為無(wú)效之前保持有效。這就可能會(huì)導(dǎo)致一些問(wèn)題,因?yàn)檫@些指示符通常使用在頭文件中。如果這個(gè)頭文件在包含結(jié)構(gòu)體的其它頭文件之前被包含到程序中,那么這些結(jié)構(gòu)體的放置方式將和它們?nèi)笔〉姆胖梅绞讲煌?。這將導(dǎo)致非常嚴(yán)重的查找錯(cuò)誤。程序中的不同模塊將會(huì)將結(jié)構(gòu)體元素放置在不同的地方。

有一個(gè)方法來(lái)避免這個(gè)問(wèn)題。Microsoft和Borland都支持這個(gè)方法:保存當(dāng)前對(duì)齊方式狀態(tài)值和隨后恢復(fù)它。圖7.4展示了如何使用這種方法。

使用Microsoft或Borland的壓縮結(jié)構(gòu)體

位域s

位域允許你指定結(jié)構(gòu)體中的成員的大小為只使用指定的比特位數(shù)。比特位數(shù)的大小并不一定要是8的倍數(shù)。一個(gè)位域成員的定義和unsigned int或int的成員定義是一樣,只是在定義的后面增加了冒號(hào)和位數(shù)的大小。圖7.5展示了一個(gè)例子。它定義了一個(gè)32位的變量,它由下面的幾部分組成:



第一個(gè)位域被指定到此雙字的最低有效位處。

但是,如果你看了這些比特位實(shí)際上在內(nèi)存中是如何儲(chǔ)存的,你就會(huì)發(fā)現(xiàn)格式并不是如此簡(jiǎn)單。難點(diǎn)發(fā)生在當(dāng)位域跨越字節(jié)界時(shí)。因?yàn)樵趌ittle endian處理器上的字節(jié)將以相反的順序儲(chǔ)存到內(nèi)存中。例如,S結(jié)構(gòu)體在內(nèi)存中將如下所示:

S結(jié)構(gòu)體

f2l變量表示f2 位域的末尾五個(gè)比特位(也就是,五個(gè)最低有效位)。f2m變量表示f2 的五個(gè)最高有效位。雙垂直線的地方表示字節(jié)界。如果你將所有的字節(jié)反向,f2 和f3 位域?qū)⒅匦陆Y(jié)合到正確的位置。

物理內(nèi)存的放置方式通常并不是很重要,除非有數(shù)據(jù)需要傳送到程序中或從程序中傳出(實(shí)際上這和位域是非常相同的)。硬件設(shè)備的接口使用奇數(shù)的比特位是非常普遍的,此時(shí)使用位域來(lái)描述是非常有用的。

SCSI就是一個(gè)例子。SCSI設(shè)備的直接讀命令被指定為傳送一個(gè)六個(gè)字節(jié)的信息到設(shè)備,格式指定為圖7.6中的格式。使用位域來(lái)描述這個(gè)的難點(diǎn)是邏輯區(qū)塊地址(logical block address),它在此命令中跨越了三個(gè)不同的字節(jié)。從圖7.6中,你可以看到數(shù)據(jù)是以big endian的格式儲(chǔ)存的。

SCSI讀命令格式

圖7.7展示了一個(gè)試圖在所有編譯器中工作的定義。前兩行定義了一個(gè)宏,如何代碼是由Microsoft或Borland編譯器來(lái)編譯時(shí),則它就為真??赡鼙容^混亂的部分是11行到14行。首先,你可能會(huì)想為什么lba_ mid和lba_lsb 位域要分開被定義,而不是定義成一個(gè)16位的域?原因是數(shù)據(jù)是以big en-dian順序儲(chǔ)存的。而編譯器將把一個(gè)16位的域以little endian順序來(lái)儲(chǔ)存。

其次,lba_msb和logical_unit 位域看起來(lái)似乎方向反了;但是,情況并不是這樣。它們必須得以這樣的順序來(lái)擺放。圖7.8展示了作為一個(gè)48位的實(shí)體,它的位域圖是怎樣的。(字節(jié)界同樣是以雙垂直線來(lái)表示。)當(dāng)它在內(nèi)存中是以little endian的格式來(lái)儲(chǔ)存,那么比特位將以要求的格式來(lái)排列。(圖7.6)

SCSI讀命令格式結(jié)構(gòu)

SCSI_read_cmd的位域圖

考慮得復(fù)雜一點(diǎn),我們知道SCSI_read cmd的定義在Microsoft C編譯器中不能完全正確工作。如果sizeof (SCSI read cmd)表達(dá)式被賦值了,MicrosoftC將返回8,而不是6!這是因?yàn)镸icrosoft編譯器使用位域的類型來(lái)決定如何繪制比特圖。因?yàn)樗械奈挥蚨急欢x為unsigned類型,所以編譯器在結(jié)構(gòu)體的末尾加了兩個(gè)字節(jié)使得它成為一個(gè)雙字類型的整數(shù)。這個(gè)問(wèn)題可以通過(guò)用unsignedshort替代所有的位域定義類型來(lái)修正?,F(xiàn)在,Microsoft編譯器不需要增加任何的填充字節(jié),因?yàn)榱鶄€(gè)字節(jié)是兩個(gè)字節(jié)字類型的整數(shù)。4有了這個(gè)改變,其它的編譯器也能正確工作。圖7.9展示了另外一種定義,能在所有的三種編譯器上工作。它通過(guò)使用unsignedchar避免了除2位的域以外的所有位域的問(wèn)題。

如果發(fā)現(xiàn)前面的討論非?;靵y的讀者,請(qǐng)不要?dú)怵H。它本來(lái)就是混亂的!通過(guò)經(jīng)常完全地避免使用位域而采用位操作來(lái)手動(dòng)地檢查和修改比特位,作者發(fā)現(xiàn)能避免一些混亂。

另一種SCSI讀命令格式的結(jié)構(gòu)

在匯編語(yǔ)言中使用結(jié)構(gòu)體

在匯編語(yǔ)言中訪問(wèn)結(jié)構(gòu)體就類似于訪問(wèn)數(shù)組。作為一個(gè)簡(jiǎn)單的例子,考慮一下你如何寫這樣一個(gè)匯編程序:將0寫入到S結(jié)構(gòu)體的y中。假定這個(gè)程序的原型是這樣的:


void zero_y( S * s_p );


匯編程序如下:


匯編程序


C語(yǔ)言允許你把一個(gè)結(jié)構(gòu)體當(dāng)作數(shù)值傳遞給函數(shù);但是,通常這都是一個(gè)壞主意。當(dāng)以數(shù)值來(lái)傳遞時(shí),在結(jié)構(gòu)體中的所有數(shù)據(jù)都必須復(fù)制到堆棧中,然后在程序中再拿出來(lái)使用。用一個(gè)結(jié)構(gòu)體指針來(lái)替代能有更高的效率。

C語(yǔ)言同樣允許一個(gè)結(jié)構(gòu)體類型作為一個(gè)函數(shù)的返回值。很明顯,一個(gè)結(jié)構(gòu)體不能通過(guò)儲(chǔ)存到EAX寄存器中來(lái)返回。不同的編譯器處理這種情況的方法也不同。一個(gè)編譯器普遍使用的解決方法是在內(nèi)部重寫函數(shù),讓它攜帶一個(gè)結(jié)構(gòu)體指針參數(shù)。這個(gè)指針用來(lái)將返回值放入到結(jié)構(gòu)體中,這個(gè)結(jié)構(gòu)體是在調(diào)用的程序外面定義的。

大多數(shù)匯編器(包括NASM)都有在你的匯編代碼中定義結(jié)構(gòu)體的內(nèi)置支持。查閱你的資料來(lái)得到更詳細(xì)的信息。


帶一個(gè)結(jié)構(gòu)體指針參數(shù)。這個(gè)指針用來(lái)將返回值放入到結(jié)構(gòu)體中,這個(gè)結(jié)構(gòu)體是在調(diào)用的程序外面定義的。
大多數(shù)匯編器(包括NASM)都有在你的匯編代碼中定義結(jié)構(gòu)體的內(nèi)置支持。查閱你的資料來(lái)得到更詳細(xì)的信息。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)