使用C語言生成一個二進制文件
使用自己喜歡的文本編輯器寫一個test.c:
int main() { } |
再使用如下命令編譯:
gcc –c test.c ld –o test –Ttext 0x0 –e main test.o objcopy –R .note –R .comment –S –O binary test test.bin |
最后生成的二進制文件是test.bin,可以使用你喜歡的反匯編工具看看這個文件里到底是什么。我使用Linux下的objdump進行反匯編:
objdump –D –b binary –a i386 test.bin |
結果如下:
00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c9????????????????????????leave? ??11:??????????c3????????????????????????ret?? |
其中第一列是指令的內存地址;第二列是指令的機器碼;第三列是匯編指令。相信你的結果與此同。如果你的gcc與我的不一樣,例如2.7.x版本的gcc,你的結果很可能會有所不同,缺少如下的四條指令,這是正常的,這兩個版本的gcc所使用的堆棧框架不同(下面介紹的例子也會因為編譯器版本的不同造成其結果有別):
???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp #堆棧對齊,以16Bytes為單位分配局部變量空間 ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp |
上述代碼都是32-bit代碼,你需要在像Linux這樣的?32-bit環境下運行,并且是保護模式。也可以只用下面的指令直接生成test.bin:
gcc –c test.c ld –Ttext 0x0 –e main --oformat binary –o test.bin test.o |
上面的test.c中只有一個函數,而且還只是個框架。其反匯編代碼也沒什么難理解的。
3.?????????編寫帶局部變量的程序
再創建一個新的test.c,看看gcc是如何處理局部變量的。
int main() { int i; i=0x12345678; } |
使用上述兩種方法的人一種編譯,生成test.bin。然后使用objdump進行反匯編:
00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c7 45 fc 78 56 34 12????movl???$0x12345678,0xfffffffc(%ebp) ??17:??????????c9????????????????????????leave? ??18:??????????c3????????????????????????ret |
與第一個例子相比,開頭的六條指令和最后的兩條指令完全相同,僅有一條指令不同。這條語句是給局部變量賦值,其空間的分配在前面已經進行了。在gcc中,堆棧中的局部變量空間按16字節為單位進行分配,而不是通常的1字節為單位。如果將
int i; i=0x12345678; 改為 int i=0x12345678; |
其結果沒有區別。但是,如果是全局變量,就不一樣了。
4.?????????編寫帶全局變量的程序
將test.c改為:
int i; int main() { i=0x12345678; } |
使用同樣的方法編譯,然后再進行反匯編:
00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c7 05 1c 10 00 00 78???movl???$0x12345678,0x101c ??17:??????????56 34 12 ??1a:?c9????????????????????????leave? ??1b:??????????c3????????????????????????ret??? |
我們定義的全局變量被放到了0x101c處,這是gcc默認以page-align對齊數據段的結果,此處的page與頁式內存管理中的page沒有關系。在使用ld鏈接時,使用-N參數可以關閉對齊效果。
00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c7 05 1c 00 00 00 78???movl???$0x12345678,0x1c ??17:??????????56 34 12 ??1a:?c9????????????????????????leave? ??1b:??????????c3????????????????????????ret? |
正如我們看到的,數據段緊接著代碼段。我們也可以明確的指定數據段的位置,試試下面的命令再進行編譯:
gcc –c test.c ld –Ttext 0x0 –Tdata 0x1234 –e main –N --oformat binary –o test.bin test.o |
然后再使用objdump進行反匯編:
00000000 <.data>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c7 05 34 12 00 00 78???movl???$0x12345678,0x1234 ??17:??????????56 34 12 ??1a:?c9????????????????????????leave? ??1b:??????????c3????????????????????????ret??? |
現在,我們定義的全局變量被放到0x1234處了。通過給ld指定-Tdata參數,可以自由的定義數據段的地址,如果不指定,數據段在代碼段后。
再看看直接給全局變量進行初始化的情況。
const int I=0x12345678; int main() { } |
仍然使用上面的方法進行編譯、鏈接、反匯編,其結果如下:
00000000 <.data>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c9????????????????????????leave? ??11:??????????c3????????????????????????ret??? ??12:??????????00 00??????????????????????add????%al,(%eax) ??14:??????????78 56??????????????????????js?????0x6c ??16:??????????34 12??????????????????????xor????$0x12,%al |
代碼以4Bytes對齊,全局變量被直接存儲在代碼段之后的數據段,ld直接將常數放到了全局變量的位置,一步到位。
使用如下命令可以看到更多細節:
objdump –D test.o |
可以看到如下的結果:
test.o:?????file format elf32-i386 ? Disassembly of section .text: ? 00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c9????????????????????????leave? ??11:??????????c3????????????????????????ret??? Disassembly of section .data: Disassembly of section .rodata: ? 00000000 <i>: ???0:??????????78 56??????????????????????js?????58 <main+0x58> ???2:??????????34 12??????????????????????xor????$0x12,%al |
我們可以更清楚地看到,在.c文件中定義的全局常量被放在了只讀的數據段中了。再看下面的一段代碼:
int I=0x12345678; const int c=0x12345678; int main() { } |
還是使用上面的方法編譯、鏈接、反匯編,可以到到如下結果:
test.o:?????file format elf32-i386 ? Disassembly of section .text: ? 00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c9????????????????????????leave? ??11:??????????c3????????????????????????ret??? Disassembly of section .data: ? 00000000 <i>: ???0:??????????78 56??????????????????????js?????58 <main+0x58> ???2:??????????34 12??????????????????????xor????$0x12,%al Disassembly of section .rodata: ? 00000000 <c>: ???0:??????????78 56??????????????????????js?????58 <main+0x58> ???2:??????????34 12??????????????????????xor????$0x12,%al |
可以看出,整數I被放在了普通的數據段中,常數c被放在了只讀數據段中了。當使用全局變量(常量)時,ld會自動的使用合適的數據段存儲他們。
5.?????????處理指針
使用如下代碼來查看gcc處理指針變量的情況:
int main() { int I; int* p; p=&I; *p=0x12345678; } |
使用objdump查看生成的機器代碼:
00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:?????????8d 45 fc?????????????????????lea????0xfffffffc(%ebp),%eax ??13:?????????89 45 f8????????????????????mov????%eax,0xfffffff8(%ebp) ??16:?????????8b 45 f8????????????????????mov????0xfffffff8(%ebp),%eax ??19:?????????c7 00 78 56 34 12????movl???$0x12345678,(%eax) ??1f:?c9????????????????????????leave? ??20:??????????c3????????????????????????ret??? |
一開始,gcc已經為局部變量預分配了至少8Bytes的空間,并且使esp以16Bytes邊界對齊,如果還需要額外的空間,gcc將按照16Bytes為單位進行分配,而不是其他編譯器所使用的以1Byte為單位進行分配。變量I位于ebp-4,變量p位于ebp-8,lea指令將I的有效地址放入eax中,然后又被放入p中。最后,將0x12345678賦給p指向的變量I。
6.?????????關于函數調用
看如下代碼:
void func(); int main() { func(); } void func() { } |
再看生成的二進制代碼:
00000000 <.data>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????e8 03 00 00 00????????????????call???0x18 ??15:??????????c9????????????????????????leave? ??16:??????????c3????????????????????????ret??? ??17:??????????90????????????????????????nop??? ??18:??????????55????????????????????????push???%ebp ??19:??????????89 e5??????????????????????mov????%esp,%ebp ??1b:??????????c9????????????????????????leave? ??1c:?c3????????????????????????ret??? |
主函數main通過call指令調用了空函數func,該函數與main大同小異。為ld指定-Map開關輸出map文件,可以得到更詳細的信息。
.text???????????0x00000000???????0x1d ?*(.text .stub .text.* .gnu.linkonce.t.*) ?.text??????????0x00000000???????0x1d???????test.o ????????????????0x00000000????????????????main ????????????????0x00000018????????????????func |
第一列是段名,這里是.text;第二列是起始位置,第三列是段長度,最后一列是附加信息,如函數名、所出自的目標文件等。可以看到,.text段從0x0開始,長度為0x1d;函數func從0x18開始。
7.?????????函數的返回值
看下面的代碼,主函數main返回一個整型值:
int main() { return 0x12345678; } |
所生成的二進制代碼與其他編譯器大同小異:
00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????b8 78 56 34 12????????????????mov????$0x12345678,%eax ??15:??????????c9????????????????????????leave? ??16:??????????c3????????????????????????ret??? |
你已經看到了,gcc使用eax傳遞返回值。因為返回值就是eax寄存器的值,所以你可以隱含的返回,甚至什么都不返回。因為返回值保存在寄存器中,進行函數調用時,經常忽略返回值。例如,我們經常這樣調用函數:
printf(…); |
該函數是有返回值的。如果函數返回的數據大于4Bytes,就不能再使用這種方法返回數據了。再看下面的例子:
typedef strUCt mydef{ int a,b,c,d; int array[10]; }mydef; ? mydef func(); int main() { mydef d; d=func(); } mydef func() { mydef d; return d; } |
接著看反匯編的代碼:
00000000 <.data>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 48????????????????????sub????$0x48,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????8d 45 b8????????????????????lea????0xffffffb8(%ebp),%eax ??13:??????????83 ec 0c?????????????????????sub????$0xc,%esp ??16:??????????50????????????????????????push???%eax ??17:??????????e8 06 00 00 00????????????????call???0x22 ??1c:?83 c4 0c????????????????????add????$0xc,%esp ??1f:?c9????????????????????????leave? ??20:??????????c3????????????????????????ret??? ??21:??????????90????????????????????????nop??? ??22:??????????55????????????????????????push???%ebp ??23:??????????89 e5??????????????????????mov????%esp,%ebp ??25:??????????57????????????????????????push???%edi ??26:??????????56????????????????????????push???%esi ??27:??????????83 ec 40????????????????????sub????$0x40,%esp ??2a:?8b 7d 08????????????????????mov????0x8(%ebp),%edi ??2d:??????????8d 75 b8????????????????????lea????0xffffffb8(%ebp),%esi ??30:??????????fc?????????????????????????cld??? ??31:??????????b8 0e 00 00 00????????????????mov????$0xe,%eax ??36:??????????89 c1??????????????????????mov????%eax,%ecx ??38:??????????f3 a5???????????????????????repz movsl %ds:(%esi),%es:(%edi) ??3a:?8b 45 08????????????????????mov????0x8(%ebp),%eax ??3d:??????????83 c4 40????????????????????add????$0x40,%esp ??40:??????????5e????????????????????????pop????%esi ??41:??????????5f????????????????????????pop????%edi ??42:??????????c9????????????????????????leave? ??43:??????????c2 04 00????????????????????ret????$0x4 |
我們自定義的結構為0x38Bytes,gcc為了保持堆棧的16Bytes對齊,分配了0x40Bytes的空間。函數func并沒有參數,但是在調用時,卻將變量d的指針傳了進去。然后利用這個指針,使用指令movsl直接對d進行賦值。再看下面的例子:
typedef struct mydef{ int a,b,c,d; int array[10]; }mydef; ? mydef func(); int main() { func(); } mydef func() { mydef d; return d; } |
再看反匯編的結果:
00000000 <.data>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 48????????????????????sub????$0x48,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????8d 45 b8????????????????????lea????0xffffffb8(%ebp),%eax ??13:??????????83 ec 0c?????????????????????sub????$0xc,%esp ??16:??????????50????????????????????????push???%eax ??17:??????????e8 06 00 00 00????????????????call???0x22 ??1c:?83 c4 0c????????????????????add????$0xc,%esp ??1f:?c9????????????????????????leave? ??20:??????????c3????????????????????????ret??? ??21:??????????90????????????????????????nop??? ??22:??????????55????????????????????????push???%ebp ??23:??????????89 e5??????????????????????mov????%esp,%ebp ??25:??????????57????????????????????????push???%edi ??26:??????????56????????????????????????push???%esi ??27:??????????83 ec 40????????????????????sub????$0x40,%esp ??2a:?8b 7d 08????????????????????mov????0x8(%ebp),%edi ??2d:??????????8d 75 b8????????????????????lea????0xffffffb8(%ebp),%esi ??30:??????????fc?????????????????????????cld??? ??31:??????????b8 0e 00 00 00????????????????mov????$0xe,%eax ??36:??????????89 c1??????????????????????mov????%eax,%ecx ??38:??????????f3 a5???????????????????????repz movsl %ds:(%esi),%es:(%edi) ??3a:?8b 45 08????????????????????mov????0x8(%ebp),%eax ??3d:??????????83 c4 40????????????????????add????$0x40,%esp ??40:??????????5e????????????????????????pop????%esi ??41:??????????5f????????????????????????pop????%edi ??42:??????????c9????????????????????????leave? ??43:??????????c2 04 00????????????????????ret????$0x4 |
可以說,與上面的結果一字不差!我們沒有在main函數中聲明變量存儲func返回的結果,但是gcc替我們做了。它仍然為函數func傳遞了一個指針,并將結果傳了出來,盡管我們對返回值不感興趣,但編譯器對我們的興趣好像也沒有興趣,依然我行我素。(如果使用了優化選項,結果很可能有所相同)。
8.?????????給函數傳遞參數
gcc遵循一般的c語言標準,包括參數傳遞方式。看看下面的例子:
char res; char func(char a,char b); int main() { res=func(0x02,0x03); } char func(char a,char b) { return a+b; } |
再看看他的反匯編代碼:
00000000 <.data>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????83 ec 08????????????????????sub????$0x8,%esp ??13:??????????6a 03??????????????????????push???$0x3 ??15:??????????6a 02??????????????????????push???$0x2 ??17:??????????e8 0a 00 00 00????????????????call???0x26 ??1c:?83 c4 10????????????????????add????$0x10,%esp ??1f:?a2 44 00 00 00????????????????mov????%al,0x44 ??24:??????????c9????????????????????????leave? ??25:??????????c3????????????????????????ret??? ??26:??????????55????????????????????????push???%ebp ??27:??????????89 e5??????????????????????mov????%esp,%ebp ??29:??????????83 ec 04????????????????????sub????$0x4,%esp ??2c:?8b 45 08????????????????????mov????0x8(%ebp),%eax ??2f:?8b 55 0c????????????????????mov????0xc(%ebp),%edx ??32:??????????88 45 ff?????????????????????mov????%al,0xffffffff(%ebp) ??35:??????????88 55 fe?????????????????????mov????%dl,0xfffffffe(%ebp) ??38:??????????8a 45 fe?????????????????????mov????0xfffffffe(%ebp),%al ??3b:??????????02 45 ff?????????????????????add????0xffffffff(%ebp),%al ??3e:?0f be c0?????????????????????movsbl %al,%eax ??41:??????????c9????????????????????????leave? ??42:??????????c3????????????????????????ret??? |
如果你精通匯編語言,看完這段代碼,恐怕你已經口吐鮮血并暈倒在地了!gcc居然生成了這么啰嗦的代碼!但是,我們還是先說說C語言的函數調用規范吧。
我們已經看到了,參數從右到左依次入棧。下面的說明全部以32Bytes代碼為準,其規范具體可羅列以下幾條:
l??????????調用者負責將參數壓入堆棧,順序為從右到左依次入棧。也就是左邊的最后入棧。
l??????????調用者使用near call指令將控制權傳給被調用者。
l??????????被調用者得到控制權,一般需要創建堆棧框架(這不是必需的,通常都是這么做)。首先,將ebp壓入堆棧保存,再將esp放入ebp,使ebp成為訪問參數的基址指針。
l??????????被調用者通過ebp訪問參數。因為ebp已經先行壓入堆棧,所以[ebp+4]就是被call指令自動壓入堆棧的返回地址,顯然,從[ebp+8]開始,就是參數。由于函數最左邊的參數最后被壓入堆棧,所以[ebp+8]就是該參數,其他參數以此類推。像printf這樣的函數,具有個數不確定的參數,但是參數入棧順序的規則,說明被調用者通過[ebp+8]就能夠找到第一個參數,其他參數的類型和數目,則需要由第一個參數給出。
l??????????被調用者減小esp的值為堆棧中的臨時變量分配空間,然后使用ebp和一個負的偏移訪問。
l??????????被調用者使用al,ax,eax返回大小不同的值。浮點數可以通過ST0寄存器返回。
l??????????被調用者完成處理后,使用事先建立的堆棧框架,恢復esp,sbp的值,并使用ret指令返回調用者。
l??????????調用者重新得到控制權,通過給esp加上一個立即數清空堆棧(盡量不要使用多次pop指令清空堆棧)。如果因為使用了錯誤的函數原型通過堆棧多傳遞了或者少傳遞了參數,調用者仍然能夠將堆棧恢復到正確的狀態,因為調用者知道自己向堆棧壓了幾個字節的數據。
結合C語言的函數調用規則,上面的代碼不難理解。
從80386開始,push指令的操作數可以是8-bit,16-bit,32-bit,但是C語言統統按32-bit整型數處理,被調用者也按32-bit進行處理。這一點很重要,特別是匯編語言和C語言混合編程時。
9.?????????基本的數據類型間的轉換
gcc處理三類基本數據類型:
l??????????signed char , unsigned char??, 1 Byte
l??????????signed short , unsigned short , 2 Bytes
l??????????signed int , unsigned int , 4 Bytes
各種數據類型間的轉換,遵循一般C語言的規則,具體可以參考IA-32的標準。這里只舉一例說明:
int main() { char ch=’a’; int x=2; int y=-4; } |
使用同樣的方法進行編譯及反匯編:
00000000 <main>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 18????????????????????sub????$0x18,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c6 45 ff 61???????????????????moVB???$0x61,0xffffffff(%ebp) ??14:??????????c7 45 f8 02 00 00 00???movl???$0x2,0xfffffff8(%ebp) ??1b:??????????c7 45 f4 fc ff ff ff????????movl???$0xfffffffc,0xfffffff4(%ebp) ??22:??????????c9????????????????????????leave? ??23:??????????c3????????????????????????ret??? |
?
10.?????gcc編譯代碼的基本運行環境
這一部分,我查了很多的文檔,都沒有這方面的介紹。又請教了很多的高手,大致情況如下,我實在無法保證這里所說的都是正確的,并且將來也是正確的,僅供參考:
l??????????32-bit的保護模式下運行。
l??????????段寄存器CS,ds,es,fs,gs,ss必須指向同一段內存區域。
l??????????沒有初始化的全局變量被放在BSS的段內,該區域在代碼段之后。但是,如果你生成的文件是二進制文件,BSS段不是該文件的一部分,你需要自己小心使用。初始化的全局變量在DATA段內,它是二進制文件的一部分,并且位于代碼段之后。被聲明為const的全局變量被放在RODATA段內,它也是二進制文件的一部分,并放在代碼段之后。
l??????????確保堆棧沒有溢出,小心代碼段和全局數據不要被破壞。
我也查了Intel提供的幫助文檔“Intel Architecture Software Developer’s Manual”,一共有三卷之多!參考了其中關于內存組織(Volume 1:Memory Organization)中的說法(建議你去好好研究)。總之,使cs,ds,ss總是指向同一內存區域應該可以使代碼正確運行。如果運行環境不是這樣,我就不知道結果了。
11.?????訪問外部的全局變量
看看在非C語言程序中如何訪問C語言程序中的全局變量。如果你想使用其他程序加載C程序,例如匯編語言寫的程序,這部分很有用,特別是在核心開發時經常用到。
int myVal=0x5; int main() { } |
編譯這段代碼:
gcc –c test.c ld –Ttext 0x0 –e main –N –oformat binary –Map memmap.txt –o test.bin test.o objdump –D –b binrary –m i386 test.bin |
得到如下結果:
00000000 <.data>: ???0:??????????55????????????????????????push???%ebp ???1:??????????89 e5??????????????????????mov????%esp,%ebp ???3:??????????83 ec 08????????????????????sub????$0x8,%esp ???6:??????????83 e4 f0?????????????????????and????$0xfffffff0,%esp ???9:??????????b8 00 00 00 00????????????????mov????$0x0,%eax ???e:?29 c4??????????????????????sub????%eax,%esp ??10:??????????c9????????????????????????leave? ??11:??????????c3????????????????????????ret??? ??12:??????????00 00??????????????????????add????%al,(%eax) ??14:??????????05????????????????????????.byte 0x5 ??15:??????????00 00??????????????????????add????%al,(%eax) |
全局變量myVal存儲在0x14。剛才已經使用-Map開關使ld生成了內存映像文件memmap.txt,應該能夠找到:
.data???????????0x00000014????????0x4 ?*(.data .data.* .gnu.linkonce.d.*) ?.data??????????0x00000014????????0x4 test.o ????????????????0x00000014????????????????myVal |
說明myVal位于test.o模塊的0x00000014位置。使用地址作為偏移量,就可以直接在其他語言中訪問myVal變量了。另為也可以通過memmap.txt查到BSS段的大小:
cat memmap.txt grep ‘/.bss’ grep ‘0x’ sed ‘s/.*0x/0x/’ |
本例子,BSS的大小是0x0。
無法直接訪問C程序中的使用static修飾的全局變量。因為這樣的變量是靜態的,map文件中沒有列出他們的地址。也許你可以使用其他辦法做到,但是,最好不要這樣做。
12.?????生成其他格式的二進制文件的選項
生成不同格式的二進制文件是一件相當麻煩的事。它需要使用很多不常使用的選項,并且有些在man的幫助信息中沒有被列出。
首先是gcc的選項:-nostdinc。很顯然,使用該選項后,gcc就不搜索默認的include路徑了,通常是/usr/include。如果需要使用的自定義的頭文件,可以使用-I選項添加搜索路徑。
然后是ld的選項。第一個是-nostdlib,就是忽略標準庫。如果需要,可以使用-L選項指定庫的搜索路徑。第二個是-Ttext,就是指定代碼段的地址,如果沒有繼續指定其他段的地址,則他們將自動的一次被放在代碼段之后。第三個是-e,就是指定代碼的入口地址,默認的是_start,如果代碼不是以其開頭,就應該指定入口點。第四個是—oformat binary,就是輸出的文件是原始的二進制文件,而是如文件可以使系統支持的任何文件。但是,中間模塊文件不能是原始的二進制文件,因為還需要很多符號和重定位信息。可以使用—iformat選項指定輸入文件的格式,但通常很少使用。第五個是-static,如果使用了其他庫,用該使用靜態鏈接方式,除非你的程序支持動態鏈接。
另外還有代碼指示偽指令。匯編器可以編譯16-bit代碼,也可以編譯32-bit代碼。但是,gcc總是生成32-bit的匯編代碼。通過在C代碼中使用asm()偽指令可以讓gcc生成16-bit匯編代碼。
第一個是.code16,即生成在16-bit段中運行的16-bit代碼;
第二個是.code32,即生成在32-bit段中運行的32-bit代碼,默認情況下gcc總是這么做;
第三個是.code16gcc,gcc將根據需要決定生成在16-bit段下運行的16-bit或32-bit代碼。GAS將會加上必要的前綴,指示32-bit的指令或寄存器等。這個選項是很有用的,它允許我們使用C語言寫在16-bit環境下運行的代碼,不論是實模式還是保護模式。
現在可以在一個C模塊中既有16-bit代碼,又有32-bit代碼,但是此時需要注意不同部分代碼的地址空間問題。
例如,我們想使用gcc生成在DOS下運行的.com程序和啟動引導程序。
首先,DOS中的.com文件是在實模式下運行的原始的二進制文件,其起始地址為0x100。要使用gcc生成.com文件,在每一個.c文件的開頭加上如下偽指令:
__asm__(“code16gcc/n”); |
如果需要引用其他庫文件,則這些庫文件也需要按這種方式生成。在鏈接時,加上如下選項:
-Ttext 0x100 –static –oformat binary |
如果程序中包含嵌入的匯編代碼,需要將其轉換為AT&T格式。
如果要寫引導程序,只需要在鏈接時使用0x7C00代替0x100!另外,最終生成的二進制代碼必須小于446個字節!
13.?????參考資料
l??????????Intel Architecture Software Developer’s Manual
l??????????Manual Pages in Linux
l??????????Redhat GNUPro Toolkit