讀書筆記《Linux核心完全剖析:基於0.12核心》——第三章 核心程式語言和環境
3.1 as86
彙編器
linux 0.1x
系統中使用了兩種彙編器(Assembler)
。一種是能產生16
位程式碼的as86
彙編器,配套ld86
連結器;另一種是GNU
的彙編器gas(as)
,使用GNU ld
連結器。
編譯器和連結器的原始碼可以從FTP
伺服器ftp.funet.fi
上或從網站www.oldlinux.org
下載。
3.1.1 as86
組合語言語法
彙編器專門用來把低階語言程式編譯成含機器碼的二進位制程式或目標檔案。
as [options] -o objfile srcfile
3.1.2 as86
組合語言程式
! ! boot.s -- bootsect.S frame-work.Usign code 0x07 replace 1 charater of string msg1 , and display on line one of screen.. ! .globl begtext, begdata, begbss, endtext, enddata, endbss !globl identifiers for ld86 link. .text !body begtext: .data begdata: .bss !uninitialized data begbss: .text !body BOOTSEG = 0x07c0 !BIOS loading original address of bootsect. entry start !Notice program start from here. start: jmpi go, BOOTSEG !Segment jump. go: mov ax, cs mov ds, ax mov es, ax mov [msg1+17], ah mov cx, #20 mov dx, #0x1004 !line 17 & column 4 of screen mov bx, #0x000c !red mov bp, #msg1 !located the display string mov ax, #0x1301 !write string and move cusor to the end. int 0x10 !BIOS interrupt 0x10, function is 0x13, child fuction 01. loop0: jmp loop0 msg1: .ascii "Loading system ..." .byte 13,10 .org 510 !It is mean start store follow statement from 510(0x1fe) .word 0xaa55 .text endtext: .data enddata: .bss endbss:
該程式是一個簡單的引導扇區啟動程式。編譯連結產生的執行程式可以放入軟盤第一個扇區直接用來引導計算機啟動。啟動後會在螢幕第17
行第5
列處顯示紅色字串'Loading system ......
,並且游標下移一行。然後在第27
行死迴圈。
以感嘆號’!
或者分號‘:’
開始的語句均為註釋文字。
‘.globl'
是彙編指示符(或成為彙編偽指令、偽操作符)。彙編指示符均以一個字元’.'
開始,並且不會在編譯時產生任何程式碼。
第14
行上的識別符號entry
是保留關鍵字,用於迫使連結器ld86
在生成的可執行檔案中包括進其後指定的標號start
。
第16
行上是一個段間(Inter-segment)
遠跳轉語句,就跳轉到下一條指令。
3.1.3 as86
彙編語音程式的編譯和連結
[/root]# as86 -0 -a -o boot.o boot.s //編譯。生成與 as 部分相容的目標檔案。
[/root]# ld86 -0 -a -o boot.o boot.s //連結。去掉符號資訊。
[/root]# dd bs=32 if=boot of=/dev/fd0 skip=1 //寫入軟盤或Image 盤檔案中
3.1.4 as86
&ld86
的使用方法和選項
3.2 GNU as
彙編
上節介紹的as86
彙編器僅用於編譯核心中的boot/bootsect.S
引導扇區程式和真實模式下的設定程式boot/setup.s
。核心中其餘所有彙編程式(包括C
gas
來編譯。
3.2.1 編譯as
組合語言程式
as [ option ] [ -o objfile ] [ srcfile.s ... ]
3.2.2 as
彙編語法
as
彙編器使用AT&T
系統V
的彙編語法(以下簡稱AT&T
語法)。
1.彙編程式預處理
as
彙編器具有對組合語言程式的簡單預處理功能。
2.符號、語句和常數
符號(Symbol
)是由字元組成的識別符號,組成符號的有效字元取自大小寫字符集、數字和3
字元“-”。“.”、“$”
。
語句(Statement
)以換行符或者行分割字元“;
”作為結束。
若在一行的最後使用反斜槓字元"\"
(在換行符前),可以使語句使用多行。
語句由零個或多個標號(label
)開始,後面可以跟隨一個確定語句型別的關鍵符號。
表3-1
as
彙編器支援的轉義字元序列
轉義碼 | 說明 |
---|---|
\b |
退格符(Backspace ),值為0x08 |
\f |
換頁符(FormFeed) , 值為0x0C |
\n |
換行符(NewLine) ,值為0x0A |
\r |
回車符(Carriage-Return) ,值為0x0D |
\NNN |
3 個八進位制表示的字元程式碼 |
\xNN... |
16 進位制數表示的字元程式碼 |
\\ |
反斜槓字元 |
\" |
表示雙引號 |
3.2.3 指令語句、運算元和定址
指令(Instructions)
是CPU
執行的操作,通常也稱操作碼(Opcode)
。
運算元(Operand)
是指令操作的物件。
地址(Address)
是指定資料在記憶體中的位置。
指令語句執行時通常由4
部分:標號,操作碼,運算元,註釋。
運算元可以是立即數、暫存器值、記憶體值。一個間接運算元(Indirect Operand)
含有實際運算元值的地址值。
#1) 立即運算元前需要加一個“$"
字元字首。
#2) 暫存器名前需要加一個”%“
字元字首。
#3) 記憶體運算元由變數名或者含有變數地址的一個騎車去指定。變數名隱含指出了變數的地址,並指示CPU
引用該地址處記憶體的內容。
1.指令操作碼的命名
AT&T
語法中指令操作碼名稱(指令助記符)最後一個字元用來指明運算元的寬度。
AT&T
與Intel
語法中幾乎所有指令操作碼的名稱都相同,只有幾個例外。例如,使用符號擴充套件從%al
移動到%edx
的AT&T
語句是”movsbl %al, %edx"
,即從byte
到long
是bl
,其他類似。
表3-2``AT&T
語法與Intel語法中轉換指令的對應關係
AT&T |
Intel |
說明 |
---|---|---|
cbtw |
cbw |
把%al 中的位元組值符號擴充套件到%ax 中 |
cwtl |
cwde |
把%ax 中的位元組值符號擴充套件到%eax 中 |
cwtd |
cwd |
把%ax 中的位元組值符號擴充套件到%dx:%ax 中 |
cltd |
cdq |
把%eax 中的位元組值符號擴充套件到%edx:%eax 中 |
2.指令操作碼字首
操作碼字首用於修飾隨後的操作碼。
例如,串掃描指令scas
使用字首執行重複操作:
repne scas %es:(%edi), %al
表3-3
操作碼字首列表
操作碼字首 | 說明 |
---|---|
cs,ds,ss,es,fs,gs |
區覆蓋操作碼字首。通過指定使用 區:記憶體運算元 記憶體引用形式會自動新增這種字首 |
data16,addr16 |
運算元\地址寬度字首。這兩個字首會把32 位運算元\地址改變為16 位的運算元\地址。注意,as 不支援16 位定址方式。 |
lock |
匯流排鎖存字首。用於在指令執行期間禁止中斷(僅對某些指令有效,參見80x86 手冊)。 |
wait |
協處理器指令字首。等待協處理器完成當前指令的執行。對於80386/80387 組合用不著這個字首 |
rep,repe,repne |
串指令操作字首。使串指令重複執行%ecx 中指定的次數 |
3.記憶體引用
Intel
語法的間接記憶體引用形式:
section:[base + index*scale + disp]
AT&T
語法形式:
section:disp(base, index, scale)
At&T
引用例子:
movl var, %eax # 把記憶體地址`var`處的內容放入暫存器`%eax`中。
movl %cs:var, %eax # 把程式碼段中記憶體地址 var 處的內容放入 %eax 中。
movb $0x0a, %es:(%ebx) # 把位元組值 0x0a 儲存到 es 段的 %ebx 指定的偏移處。
movl $var, %eax # 把 var 的地址放入 %eax 中。
movl array(%esi), %eax # 把 array+%esi 確定的記憶體地址處的內容放入 %eax 中。
movl (%ebx, %esi, 4), %eax # 把 %ebx+%esi*4 確定的記憶體地址處的內容放入 %eax 中。
movl array(%ebx, %esi, 4), %eax # 把 array+%ebx+%esi*4 確定的記憶體地址處的內容放入 %eax 中。
movl -4(%ebp), %eax # 把 %ebp -4 記憶體地址處 的內容放入 %eax 中,預設段 %ss 。
movl foo(, %eax, 4), %eax # 把記憶體地址 foo + %eax * 4 處內容放入 %eax 中, 預設段 %ds 。
4.跳轉指令
跳轉指令用於把執行行點轉移到程式另一個位置處繼續執行下去。
jmp NewLoc # 直接跳轉。無條件直接跳轉到標號 NewLoc 處繼續執行。
jmp *%eax # 間接跳轉。暫存器 %eax 的值是跳轉的目標位置。
jmp *(%eax) # 間接跳轉。從 %eax 指明的地址處讀取跳轉的目標位置。
3.2.4 區與重定位
區(Section)
(也稱為段、節或部分)用於表示一個地址範圍,作業系統講會以相同的方式對待和出來在該地址範圍中的資料資訊。
連結器ld
會把輸入的目標檔案中的內容按照一定規律組合生成一個可執行程式。
為區 分配執行時刻的地址的運算元就被稱作重定位(Relocation)操作。
as
彙編器輸出產生的目標檔案中至少具有3
個區,正文(.text)
、資料(.data)
和區(.bss)
。
為了執行重定位操作,在每次涉及目標檔案中的一個地址時,ld
必須知道:
#1) 目標檔案中對一個地址的引用是從什麼地方算起的?
#2) 該引用的位元組長度是多少?
#3) 該地址引用的是哪一個區?(地址)- (區的開始地址)的值等於多少?
#4) 對地址的引用與指令計數器PC(Programing Counter)
相關麼?
1.連結器涉及的區
2.子區
彙編取得的位元組資料通常位於 text
和data
區中。as
彙編器允許利用子區(Subsection)
來將某個區中可能分佈著一些不相鄰的資料組在彙編後聚集在一起存放。
使用子區是可選的。如果不使用子區,那麼所有物件都會被放在子區0
中。每個區都有一個位置計數器(Location Counter)
,它會對每個彙編進該區的位元組進行計數。
3.bss
區
bss
區用於儲存區域性公共變數。
3.2.5 符號
標號(Label)
是後面緊隨一個冒號的符號。
符號名以一個字母或"."、"_"
字元之一開始。
1.特殊點符號
特殊符號"."
表示as
彙編的當前地址。因此表示式"mylab:.long ."
就會把mylab
定義為包含它自己所處的地址值。給"."
賦值就如同彙編命令".org"
的作用。因此表示式".=.+4"
與“。space 4"
完全相同。
2.符號屬性
除了名字以外,每個符號都有”值“和”型別“屬性。根據輸出格式不同,符號也可以具有輔助屬性。如果不定義就使用一個符號,as
就會假設其所有均為0
。這指示該符號是一個外部定義的符號。
符號通常是32位的。ld
會對未定義符號的值進行特殊處理。符號的型別屬性含有用於連結器和偵錯程式的重定位資訊、指示符號是外部的表示以及一些其他可選的資訊。
3.2.6 as
彙編命令
彙編命令是指示彙編器操作方式的偽指令。
1. .align abs-expr1, abs-expr2, abs-expr3
.align
是儲存對齊彙編命令,用於在當前子區中把位置計數器值設定(增加)到下一個指定儲存邊界處。
2. .ascii “string”…
從位置計數器所指當前位置為字串分配空間並存儲字串,可使用逗號分開寫出多個字串。
3. .asciz “string”…
該彙編命令與".ascii"
類似,但是每個字串後面會自動新增NULL
字元。
4. .byte expressions
該彙編命令定義0
個或多個用逗號分開的位元組值。每個表示式的值是1
個位元組。
5. .comm symbol, length
在.bss
區中宣告一個命名的公共區域。
6. .data subsection
該彙編命令通知as
把隨後的語句彙編到編號為subsection
的data
子區中。
7. .desc symbol, abs-expr
用絕對錶達式的值設定符號symbol
的描述符欄位n_desc
的16
位值。
8. .fill repeat,size,value
該彙編命令會產生數個(repeat
個)大小為size
位元組的重複拷貝。
9. .global symbol
該彙編命令會使得連結器ld
能看見符號symbol
。
10. .int expressions(.long exoressions)
該彙編命令在某個區中設定0
個或多個整數值(8038系統為4B
,同 .long
)。每個用逗號分開的表示式的值就是執行時刻的值。
11. .lcomm symbol, length
為符號symbol
指定的區域性公共區域保留長度為length
位元組的空間。
12. .octa bignums
指定0
個或多個用逗號分開的16B
大數(.byte, .word, .long, .quad, .octa 分別對應1\2\4\8\16位元組數)
。
13. .org new_lc, fill
把當前區的位置計數器設定為值 new_lc
。當位置計數器值增長時,所跳躍過的位元組將被填入值fill
。
14. .quad bignums
指定0
個或多個用逗號分開的8B
大數bignums
。
15. .short expressions (.word expressions)
指定0
個或多個用逗號分開的2
位元組數。
16. .space size, fill
產生size
個位元組,每個位元組填值fill
。
17. .string “string”
定義一個或多個用逗號分開的字串。
18. .text subsection
通知as
把隨後的語句彙編進編號為subsection
的子區中。
3.2.7 編寫16
位程式碼
as
不區分16
位和32
位彙編語句,取決於.code16
還是.code32
。
3.2.8 AS
彙編器命令列選項
-a
:開啟程式列表
-f
:快速操作
-o
:指定輸出的目標檔名。
-R
:組合資料區和程式碼區。
-W
:取消警告資訊。
3.3 C
語言程式
3.3.1 C
程式編譯和連結
3.3.2 嵌入式彙編
基本格式:
asm("彙編語句"
:輸出暫存器
:輸入暫存器
:會被修改的暫存器);
除第一行以外,後面帶冒號的行若不適用就都可以省略。
asm是內聯彙編語句關鍵詞;
”彙編語句“寫彙編指令的地方;
”輸出暫存器“表示當這段嵌入式彙編執行完之後,哪些暫存器用於存放輸出資料(對應C
語言表示式值或一個記憶體地址)。
”輸入暫存器“表示在開始執行彙編程式碼時,這裡指向的一些暫存器中應存放的輸入值。
”會被修改的暫存器“表示你已對其中列出的暫存器中的值進行了改動,gcc
編譯器不能再依賴與它原先對這些暫存器載入的值。
e.g.
kernel/traps.c
# define get_seg_byte(seg, addr) \ //巨集函式名稱
({ \
register char _res; \ // 定義了一個暫存器變數 _res 。
_asm_("push %%fs; \ // 首先儲存 fs 暫存器原值(段選擇符)。
mov %%ax, %%fs; \ //然後用 seg 設定 fs 。
movb %%fs:%2, %%al; \ //取 seg:addr 處 1 位元組內容到 al 暫存器中。
pop %%fs" \ //恢復 fs 暫存器原值。
:"=a" (_res) \ //輸出暫存器列表。
:"0" (seg),"m" (*(addr))); \ //輸入暫存器列表。
_res;})
此程式碼定義了一個嵌入式組合語言巨集函式。通常使用匯編語句最方便的方式是把它們放在一個巨集內。
為了讓 GCC 編譯產生的組合語言程式中暫存器前有一個百分號”%“,在嵌入式彙編語句暫存器名稱前就必須寫上兩個百分號”%%“。
”=a"
中的a
稱為載入程式碼,“=”
表示輸出暫存器,並且其中的值講被輸出替代。
載入程式碼是CPU
暫存器、記憶體地址以及一些數值的簡寫字母代號。
第9
行表示在這段程式碼開始時將seg
放到eax
暫存器中,“0”
表示使用了與上面相同位置上的輸出暫存器。
(*(addr))
表示一個記憶體偏移地址值。為了在上面彙編語句中使用該地址,嵌入式彙編程式規定把輸出和輸入暫存器統一按順序編號,順序是從輸出暫存器序列從左到右從上到下以"%0"
開始,分別記為%0、%1、...%9
,。因此,輸出暫存器的編號是%0
(這裡只有一個輸出暫存器),輸入暫存器前一部分(“0“(seg))
的編號是%1
,而後部分的編號是%2
。上面地6
行上的%2
即代表(*(addr))
這個記憶體偏移量。
表3-4
常用暫存器載入程式碼說明
程式碼 | 說明 | 程式碼 | 說明 |
---|---|---|---|
a |
使用暫存器eax |
m |
使用記憶體地址 |
b |
使用暫存器ebx |
o |
使用記憶體地址並可以載入偏移量 |
c |
使用暫存器ecx |
I |
使用常數0-31 |
d |
使用暫存器edx |
J |
使用常量0-63 |
S |
使用暫存器esi |
K |
使用常數0-255 |
D |
使用暫存器edi |
L |
使用常量0-65535 |
q |
使用動態分配位元組可定址暫存器(eax, ebx, ecx, edx ) |
M |
使用常量0-3 |
r |
使用任意動態分配的暫存器 | N |
使用1 位元組常量(0-255) |
g |
使用統一有效的地址即可(eax, ebx, ecx, edx 或記憶體變數) |
O |
使用常量0-31 |
A |
使用eax 與edx 聯合(64 位) |
= |
輸出運算元。輸出值講替換前值 |
+ |
表示運算元可讀寫 | & |
早起會變的(earlyclobber )運算元。表示使用玩運算元之前,內容會改變。 |
第4~7
行程式碼的作用:
首先,fs
段暫存器內容入棧;
其次,eax
段值賦給fs
段暫存器;
再者,fs:(*(addr))
所指定的位元組放入al
暫存器中。
e.g.
asm("cld\n\t"
"rep\n\t"
"stol"
: /* 沒有輸出暫存器 */
: "c"(count-1), "a"(fill_value), "D"(dest)
: "%ecx", "%edi");
前3
行是通常的彙編語句,用來清方向,重複儲存值\n\n\t
為換行符和製表符(對齊程式作用)。
第5
行的含義是:將count-1
的值載入到ecx
,將fill_value
載入到eax
,dest
載入到edi
。
gcc
會自動優化操作,
e.g.
asm("leal (%1, %1, 4), %0"
: "=r"(y)
: "0"(x));
該例子計算 x*5
的值,其中%0
(輸出暫存器)和%1
(輸入暫存器)是gcc
自動分配的暫存器。
注:如果輸入暫存器為"0"
或者為空的話,說明使用與相應輸出一樣的暫存器。
該例子中,若r
為eax
,則
”leal (eax, eax, 4), eax"
可以通過關鍵詞volatile
來取消gcc
自動優化,程式碼如下:
_asm_ _volatile_ (...);
關鍵詞volatile
也可以放在函式名前修飾函式,通知gcc
該函式不會返回。
e.g.
mm/memory.c
31 volatile void do_exit(long code);
32
33 static inline vocatile void oom(void)
34 {
35 printk("out of memory\n\r");
36 do_exit(SIGSEGV);
37 }
練習:(未看懂)
字串命令檢視附件1
。
3.3.3 圓括號中的組合語句
花括號"{}"
用於把變數宣告和語句組合成一個複合語句(組合語句)或一個語句塊(等同於一條語句)。
組合語句的右花括號後面不再使用分好。
圓括號中的組合語句“({...})”
,可以在GNU C
中當一個表示式使用。
e.g.
({ int y = foo(); int z;
if (y > 0 ) z = y;
else z = -y;
3 + z; })
解釋:
表示式(“3+z”)
的值等價於整個圓括號闊住的語句的值。若最後一句不是表示式,那麼整個語句表示式具有void
屬性,即沒有值。該表示式裡宣告的任何區域性變數都會在整塊語句結束後失效。
該語句和普通表示式一樣。例如:
int i = 該語句;
這種表示式通常用來定義巨集。
e.g.
init/main.c
69 # define CMOS_READ(addr) ({ \ // 反斜槓連線兩行語句
70 outb_p(0x80 | addr, 0x70); \ //首先向 I/O 埠 0x70輸出欲讀取的位置 addr。
71 inb_p(0x71); \ //然後從埠 0x71 讀入該位置處的值作為返回值。
72 })
include/asm/io.h
05 # define inb(port) ({ \
06 unsigned char _v; \
07 _asm_ volatile ("inb %%dx, %%al":"=a" (_v):"d" (port)); \
08 _v; \
09 })
3.3.4 暫存器變數
GNU C
對C
語言的另一個擴充是允許我們把一些變數值放到CPU
暫存器中,即暫存器變數。
暫存器變數分全域性變數和區域性變數。
定義區域性暫存器變數的形式:
register int res _asm_("ax");
ax
是變數res
希望使用的暫存器。
定義這樣一個暫存器變數並不會專門保留這個暫存器不派其他用途,也並不保證編譯出來的程式碼會把變數一直放在指定的暫存器中。
3.3.5 行內函數
在程式中,通過把一個函式宣告為內聯(inline)
函式,就可以讓gcc
把函式的程式碼整合呼叫到該函式的程式碼中區。這樣處理的函式可以區掉函式呼叫時進入和退出時間開銷,從而寬度能夠加快執行速度。
內聯韓式嵌入呼叫者程式碼中的操作是一種優化操作,因此只有進行優化編譯時才會執行程式碼嵌入處理。若編譯過程中沒有使用優化選項-O
,那麼行內函數的程式碼就不會被真正地嵌入到呼叫者程式碼中,而是隻作為普通函式呼叫來處理。
把一個函式宣告為行內函數的方法是在函式宣告中使用關鍵字"inline"
。
e.g
fs/inode.c
inline int inc( int *a)
{
(*a)++;
}
函式中的某些語句用法可能會使的行內函數的替換操作無法正常進行,或者不適合進行替換操作。
例如,使用了可變引數、記憶體分配函式malloca()
、可變長度資料型別變數、非區域性goto
語句以及遞迴函式。
編譯時,可以使用選項-Winline
讓gcc
對標誌成inline
但不能被替換的函式給出警告資訊以及不能替換的原因。
當一個函式定義中既使用inline
關鍵字,又使用static
關鍵詞,即像下面檔案fs/inode.c
中的行內函數定義一樣,那麼如果所有對該行內函數的呼叫都被替換二整合在呼叫者程式碼中,並且程式中沒有引用過該行內函數的地址,則該行內函數自身的彙編程式碼就不會被引用。
在這種情況下,除非我們在編譯過程中使用選項 -fkeep-inline-function
,否則gcc
就不會為該行內函數自身生成生成彙編程式碼。
20 static inline void wait_on_inode(struct m_inode * inode)
21 {
22 cli();
23 while (inode->i_lock)
24 sleep_on(&inode->i_wait);
25 sti();
26 }
C99
預設”省略“了static
,為了相容C99
,最好使用inlineq
和static
組合。否則需要使用選項 --std=gnu89
。
如果在定義一個函式時還指定了inline
和extern
關鍵詞,那麼該函式定義僅用於內聯整合,並且在任何情況下都不會單獨產生該函式自身的彙編程式碼,即使明確引用了該函式的地址也不會產生。
關鍵詞inline
和extern
組合在一起的作用機會類同一個巨集定義。使用這種組合方式就是把帶有組合關鍵詞的一個函式定義放在.h
標頭檔案中,並且把不含關鍵詞的另一個相同函式定義放在一個庫檔案中。此時標頭檔案中的定義大多數對該函式的呼叫被替換嵌入。如果還有未被替換的對該函式的呼叫,那麼就會使用(引用)程式檔案中或庫中的副本。
e.g.
include/string.h、lib/string.c
字串命令檢視附件1
。
3.4 C
與彙編程式的相互呼叫
為了提高程式碼執行效率,核心原始碼中有的地方直接使用了組合語言編制。
3.4.1 C
函式呼叫機制
函式呼叫操作包括從一塊程式碼到另一塊程式碼之間的雙向資料傳遞和執行控制轉移。資料傳遞通過函式引數和返回值來進行。
1. 棧幀結構和控制轉移方式
大多數CPU
上的程式失效使用棧來支援函式呼叫操作。棧被用來傳遞函式引數、儲存返回資訊、臨時儲存暫存器原有值以備恢復以及用來儲存區域性資料。
單個函式呼叫操作所使用的棧部分被稱為棧幀(stack frame
)結構。如圖3-4
。.
棧結構的兩端由兩個指標來指定。
暫存器ebp
通常用作幀指標(frame pointer)
,
esp
則用作棧指標(stack pointer)
。
esp
會隨資料出棧和入棧而移動。
棧是往低(小)地址放心擴充套件的,而esp
指向當前棧頂處的元素。
指令CALL
和RET
用於處理函式呼叫和返回操作。
Intel
慣例,
暫存器eax、edx
和ecx
的內容必須由呼叫者自己負責。
暫存器ebx、esi
和edi
以及ebp、esp
的內容必須由被呼叫者負責保護。
2. 函式呼叫示例
/* exch.c */
void swap( int *a, int *b)
{
int c;
c = *a, *a = *b, *b = c;
}
int main()
{
int a, b;
a = 16, b = 32;
swap( &a, &b);
return(a - b);
}
這兩個函式的棧指標結構如圖3-5
所示。
圖中的位置資訊相對於暫存器ebp
中的幀指標。
棧幀左邊的數字指出了相對於幀指標的地址偏移值。
(在像GDB
這樣的偵錯程式中,這些數值都用2
的補碼錶示,e.g. -4 = 0xFFFF FFFC ; -12 = 0xFFFF FFF4
)
使用命令
gcc -Wall -S -o exch.s exch.c
生成C
程式對應的彙編程式exch.s
程式碼
3. main()
也是一個函式
在編譯連結時它將會作為crt0.s
彙編程式的函式被呼叫。
crt0.s
是一個樁(stub)
程式,名稱中的"ctr"
是“C run-time”
的縮寫。
Linux 0.12
中的crt0.s
彙編程式如下:
3.4.2 在彙編程式中呼叫C
函式
彙編程式呼叫一個C
函式時,程式需要首先按照逆向順序把函式引數壓入棧,即函式最後(最右邊)一個引數先入棧,如圖3-6
。
在執行CALL
指令時,CPU
會把CALL
指令的下一條指令的地址壓入棧中(圖3-6
中的EIP
)。
即使沒有事先將引數壓入棧,被呼叫函式還是會以EIP
位置以上的棧中的其他內容作為自己的引數使用。
e.g. parts
3.4.3 在C
程式中調用匯編函式
包含兩個函式的彙編程式callee.s
如下。
該彙編檔案中的第1
個函式mywirte()
利用系統中斷0x80
呼叫系統呼叫sys_write(int fd, char *buf, int count)
實現在螢幕上顯示資訊。對應的系統功能號
Note:C
語言調用匯編失敗(待解決)。
3.5 Linux 0.12
目標檔案格式
Linux 0.12
使用兩張編譯器來生成核心程式碼檔案,第一種是as86
和ld86
,第二種是GNU
的彙編器as(gas)
和C
語言編譯器gcc
以及相應的連結程式`gld``。
3.5.1 目標檔案格式
a.out
檔案是一種被稱為彙編與連結輸出(Assembly & linker editor output)
的目標檔案格式。
a.out
格式7
個區的基本定義和用途是:
#1) 執行頭部分(exec header)
。該部分中包含由一些引數(exec 結構)
,是有關目標檔案的整體結構資訊。例如程式碼和資料區的長度、未初始化資料區的長度、對應源程式檔名以及目標檔案建立時間等。
核心使用這些引數把執行檔案載入到記憶體中並執行,而連結程式(ld)
使用這些引數將一些模組檔案組合成一個可執行檔案。這是目標檔案唯一必要的組成部分。
#2) 程式碼區(text segment0
.由編譯器或彙編器生成的二進位制指令程式碼和資料資訊,含有程式執行時被載入到記憶體中的指令程式碼和相關資料。能以制度形式載入。
#3) 資料區(data segment)
。由編譯器或彙編器生成的二進位制指令和資料資訊,這部分含有已經初始化過的資料,總是被載入到可讀寫的記憶體中。
#4) 程式碼重定位資訊(text relocation)
。這部分含有供連結程式使用的記錄資料。在組合目標模組檔案時用於定位程式碼段中的指標或地址。當連結程式需要改變目的碼的地址時就需要修正和維護這些地方。
#5) 資料重定位部分(data relocation)
。類似於程式碼重定位部分的作用,但是用於資料段中指標的重定位。
#6) 符號表(symbol table)
。這部分用於含有供連結程式使用的記錄資料。這些記錄資料儲存這模組檔案中定義的全域性符號以及需要從其他模組檔案中輸入的符號,或者是由連結器定義的符號,用於在模組檔案之間對命名的變數和函式(符號)進行交叉引用。
#7) 字串表部分(string table)
。該部分含有與符號名相對應的字串,供除錯程式除錯目的碼,與連結過程無關。這些資訊可包含源程式程式碼和行號、區域性符號以及資料結構描述資訊等。
1. 執行頭部分
目標檔案的檔案頭中含有一個長度為32B
的exec
資料結構,通常稱為檔案頭結構或執行頭結構。
Linux 0.12
系統使用了其中兩種型別:
模組目標檔案使用了OMAGIC (Old Magic)
型別的a.out
格式,它指明檔案是目標檔案或者是不純的可執行檔案。其魔數是0x107
。
執行檔案使用了ZMAGIC
型別的a.out
格式,它指明檔案為需求分頁處理(demand-paging,即需求載入,load on demand )
的可執行檔案。其魔數是0x10b
。
這兩種格式主要區別在於它們對各個部分的儲存分配方式上。
執行頭結構中的a_text
和a_data
欄位分別指明後面只讀的程式碼段和可讀寫資料段的位元組長度。
a_entry
欄位指定了程式程式碼開始執行的地址,而a_syms、a_trsize和a_drsize
欄位則分別說明了資料段後符號表、程式碼和資料段重定位資訊的大小。
2. 重定位資訊部分
重定位的功能有兩個。一是當代碼段被重定位到一個不同的基地址處時,重定位項則用於指出需要修改的地方。二是在模組檔案中存在對未定義符號引用時,當此未定義符號最終被定義時連結程式就可以使用相應重定位項對符號的值進行修正。
3. 符號表和字串部分
由於GNU gcc
編譯器允許任意長度的識別符號,因此標誌符字串都位於符號表後的字串表中。
符號的主要型別包括:
#1) text、data或bss
指明是本模組檔案中定義的符號。此時符號值是模組中該符號的可重定位地址。
#2) abs
指明符號是一個絕對的(固定的)不可重定位的符號。符號的值就是該固定值。
#3) undef
指明是一個本模組檔案中未定義的符號。此時符號值通常為0
。
3.5.2 Linux 0.12
的目標檔案格式
檢視執行檔案頭結構的具體值
[/usr/root]# gcc -c -o name.o name.c
[/usr/root]# gcc -o name name.o
[/usr/root]#
[/usr/root]# hexdump -x name.o
[/usr/root]# objdump -h name.o
[/usr/root]# hexdump -x name | more
[/usr/root]# objdump -h name
刪除執行檔案中的符號表資訊命令。
[/usr/root]# strip exch
磁碟上a.out
執行檔案的各區在程序邏輯地址空間中的對應關係如圖3-8
所示。
3.5.3 連結程式輸出
連結程式對輸入的一個或多個模組檔案以及相關的庫函式模組進行處理,最終生成相應的二進位制執行檔案或一個由所有模組組合而成的大模組檔案。此過程中,連結程式的首要任務是給執行檔案(或者輸出的模組檔案)進行空間分配操作。 每個模組檔案中包括幾種型別的段,連結程式的第二個任務就是把所有模組中相同型別的段組合連線在一起,在輸出檔案中為指定段型別形成單一一個段。
3.5.4 連結程式預定義變數
在連結過程中,連結器ld
和ld86
會使用變數記錄執行程式中每個段的邏輯地址。
連結預定義的外部變數通常至少有etext、_etext、edata、_edate、end
和_end
。
下面程式可以顯示出幾個變數的地址。
執行結果為:
可以看出帶與不帶下劃線"_"
符號的地址值是相同的。
3.5.5 System.map
檔案
當執行GNU
連結器gld(ld)
時若使用了"-M"
選項,或者使用了"-nm“
命令,則會在標準輸出裝置(通常是螢幕)上列印處連結映像(link map)
資訊,即指由連結程式產生的目標程式記憶體地址映像資訊。其中列出了程式段裝入到記憶體中的位置資訊。具體有:
#1) 目標檔案即符號資訊對映到記憶體中的位置。
#2) 公共符號如何放置。
#3) 連結中包含的所有檔案成員及其引用的符號。
在編譯核心時,Linux/MakeFile
檔案產生的System.map
檔案就用於存放核心符號表資訊。
符號表是所有核心符號機器對應地址的一個列表,當然也包括上面說明的_etext、_edata
和_end
等符號的地址資訊。
符號表樣例如下:
第1
欄指明符號值(地址);第2
欄是符號型別,指明符號位於目標檔案的哪個區(Sections)
或其屬性;第3
欄是對應的符號名稱。
dmi_broken
的變數位於核心地址0x03441a0
處。
3.6 Make
程式和Makefile
檔案
有關make
的詳細使用方法請參考《GNU make使用手冊》
。
3.6.1 Makefile
檔案內容
一個Makefie
檔案可以包括五種元素:顯示規則、隱含規則、變數定義、指示符和註釋資訊。
**顯示規則(explicit rules
)**用於指定何時以及怎麼樣重新編譯一個或多個被稱作規則的目標(rule's targets)
的檔案。規則中明確列出了目標所依賴的被稱作為目標的先決條件(或依賴)的其他檔案,同時也會給出用於建立或更新目標的命令。
**隱含規則(implicit rules
**則是根據目標和物件的名稱來確定何時和如何重新編譯一個或多個被稱作規則的目標的檔案。
**變數定義(variable definitions
)**用於在一行上為一個變數定義一個文字字串。
**指示符(directives)
**是make
的一個命令,用於指示其在讀取makefile
檔案時執行的特定操作。
**註釋(comments)
**是指Makefile
檔案以”#”
字元開始的文字部分。
3.6.2 Makefile
檔案中的規則
簡單的Makefile
檔案中含有一些如下形式的規則。這些規則主要用來描述**操作物件(原始檔和目標檔案)**之間的依賴關係。
target
(目標)物件通常是指程式生成的一個檔案的名稱,
例如它可以是一個可執行檔案或者一個以".o"
結尾的目標檔案(Object file)
。
目標也可以是所要採取活動的名稱,
例如“清理”("clean")
。
prerequisite
(先決條件或稱依賴物件)是用以建立target
所必要或者依賴的一系列檔案或其他目標。
command
(命令)是值make
所執行的操作,通常就是一些shell
命令,是生成target
需要執行的操作。
3.6.3 Makefile
檔案示例
當make
依據Makefile
檔案中的內容重新編譯C
檔案時, 僅會對每個修改過的C
檔案進行重新編譯。
Makefile
示例檔案中的內容描述了一個名為eidt
的執行檔案依賴於8
個目標檔案的方式,以及這8
個目標檔案又是如何依賴於8
個C
原始檔和3
個頭檔案的。
要使用該Makefile
建立執行檔案“edit
,只需在命令列上簡單地鍵入make
即可。
若要使用該Makefile
從當前目錄中刪除編譯得到的執行檔案和所有目標檔案,只需要鍵入make clean
。
在該Makefile
檔案中,規則的目標包括執行檔案edit
和.o
目標檔案(object file)”main.o"
、”kbd.o"
等。先決條件(或依賴條件)檔案是諸如"main.o"
和"defs.h"
等原始檔。
當目標是一個檔案時,那麼其先決條件中的任何依賴條件被修改過時就需要進行重新編譯或連結。
Makefile
中規則的目標和先決條件的下一行是shell
命令。
3.6.4 make
處理Makefile
檔案的方式
預設情況下,make
會從Makefile
檔案中第一個目標開始執行(不包括"."
開始的目標)。
該目標被稱為Makefile
的預設最終目標(default goal)
。最終目標就是make
努力嘗試更新的目標。
3.6.5 Makefile
中的變數
定義變數的格式:
objects = something ...
引用變數格式:
$objects
3.6.6 讓make
自動推斷命令----(point)
make
隱含規則:
根據目標檔案的命名形式使用"cc -c"
命令根據相應的.c
檔案更新對應的.o
檔案。
e.g.
它會使用"cc -c main.c -o main.o"
把"main.c"
編譯成"main.o"
。因此我們可以省略.o
目標檔案規則中的命令。
當一個.c
檔案被以這種方式自動地使用,那麼它會被自動地新增到先決條件(依賴條件)中。因此我們可以省略規則先決條件中的".c"
檔案----假定我們同時省略了命令。
上述示例可以更新為:
3.6.7 隱含規則中的自動變數
略
[附1] 字串處理指令
(1)
lodsb
、lodsw
:把DS:SI
指向的儲存單元中的資料裝入AL
或AX
,然後根據DF
標誌(df=0)
增(df=1)
減SI
(2)
stosb
、stosw
:把AL
或AX
中的資料裝入ES:DI
指向的儲存單元,然後根據DF
標誌(df=0)
增(df=1)
減DI
(3)
movsb
、movsw
:把DS:SI
指向的儲存單元中的資料裝入ES:DI
指向的儲存單元中,然後根據DF
標誌分別(df=0)
增(df=1)
減SI
和DI
(4)
scasb
、scasw
:把AL
或AX
中的資料與ES:DI
指向的儲存單元中的資料相減,影響標誌位,然後根據DF
標誌分別(df=0)
增(df=1)
減SI
和DI
(5)
cmpsb
、cmpsw
:把DS:SI
指向的儲存單元中的資料與ES:DI
指向的儲存單元中的資料相減,影響標誌位,然後根據DF
標誌分別(df=0)
增(df=1)
減SI
和DI
(6)
rep
:重複其後的串操作指令。重複前先判斷CX
是否為0
,為0
就結束重複,否則CX
減1
,重複其後的串操作指令。主要用在MOVS
和STOS
前。一般不用在LODS
前。
上述指令涉及的暫存器:
段暫存器DS
和ES
、變址暫存器SI
和DI
、累加器AX
、計數器CX
涉及的標誌位:DF、AF、CF、OF、PF、SF、ZF