讀書筆記《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&TIntel語法中幾乎所有指令操作碼的名稱都相同,只有幾個例外。例如,使用符號擴展從%al移動到%edxAT&T語句是”movsbl %al, %edx",即從bytelongbl,其他類似。
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.子區

彙編取得的字節數據通常位於 textdata區中。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把隨後的語句彙編到編號爲subsectiondata子區中。

7. .desc symbol, abs-expr

用絕對表達式的值設置符號symbol的描述符字段n_desc16位值。

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 使用eaxedx聯合(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加載到eaxdest加載到edi
gcc會自動優化操作,
e.g.

asm("leal (%1, %1, 4), %0"
	: "=r"(y)
	: "0"(x));

該例子計算 x*5的值,其中%0(輸出寄存器)和%1(輸入寄存器)是gcc自動分配的寄存器。
注:如果輸入寄存器爲"0"或者爲空的話,說明使用與相應輸出一樣的寄存器。
該例子中,若reax,則

”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 CC語言的另一個擴充是允許我們把一些變量值放到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語句以及遞歸函數。
編譯時,可以使用選項-Winlinegcc對標誌成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,最好使用inlineqstatic組合。否則需要使用選項 --std=gnu89
如果在定義一個函數時還指定了inlineextern關鍵詞,那麼該函數定義僅用於內聯集成,並且在任何情況下都不會單獨產生該函數自身的彙編代碼,即使明確引用了該函數的地址也不會產生。
關鍵詞inlineextern組合在一起的作用機會類同一個宏定義。使用這種組合方式就是把帶有組合關鍵詞的一個函數定義放在.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指向當前棧頂處的元素。
指令CALLRET用於處理函數調用和返回操作。
Intel慣例,
寄存器eax、edxecx的內容必須由調用者自己負責。
寄存器ebx、esiedi以及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使用兩張編譯器來生成內核代碼文件,第一種是as86ld86,第二種是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. 執行頭部分

目標文件的文件頭中含有一個長度爲32Bexec數據結構,通常稱爲文件頭結構或執行頭結構。
在這裏插入圖片描述
Linux 0.12系統使用了其中兩種類型:
模塊目標文件使用了OMAGIC (Old Magic)類型的a.out格式,它指明文件是目標文件或者是不純的可執行文件。其魔數是0x107
執行文件使用了ZMAGIC類型的a.out格式,它指明文件爲需求分頁處理(demand-paging,即需求加載,load on demand )的可執行文件。其魔數是0x10b
這兩種格式主要區別在於它們對各個部分的存儲分配方式上。
執行頭結構中的a_texta_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 鏈接程序預定義變量

在鏈接過程中,鏈接器ldld86會使用變量記錄執行程序中每個段的邏輯地址。
鏈接預定義的外部變量通常至少有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個目標文件又是如何依賴於8C源文件和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)
lodsblodsw:把DS:SI指向的存儲單元中的數據裝入ALAX,然後根據DF標誌(df=0)(df=1)SI
(2)
stosbstosw:把ALAX中的數據裝入ES:DI指向的存儲單元,然後根據DF標誌(df=0)(df=1)DI
(3)
movsbmovsw:把DS:SI指向的存儲單元中的數據裝入ES:DI指向的存儲單元中,然後根據DF標誌分別(df=0)(df=1)SIDI
(4)
scasbscasw:把ALAX中的數據與ES:DI指向的存儲單元中的數據相減,影響標誌位,然後根據DF標誌分別(df=0)(df=1)SIDI
(5)
cmpsbcmpsw:把DS:SI指向的存儲單元中的數據與ES:DI指向的存儲單元中的數據相減,影響標誌位,然後根據DF標誌分別(df=0)(df=1)SIDI
(6)
rep:重複其後的串操作指令。重複前先判斷CX是否爲0,爲0就結束重複,否則CX1,重複其後的串操作指令。主要用在MOVSSTOS前。一般不用在LODS前。
上述指令涉及的寄存器:
段寄存器DSES、變址寄存器SIDI、累加器AX、計數器CX
涉及的標誌位:DF、AF、CF、OF、PF、SF、ZF

Content

文章目錄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章