Linux內核筆記(三)內核編程語言和環境


學習概要:

Linux內核使用的編程語言、目標文件格式、編譯環境、內聯匯編、語句表達式、寄存器變量、內聯函數 c和匯編函數之間的相互調用機制Makefile文件的使用方法。

as86匯編語言語法

匯編器專門來把程序編譯成含機器碼的二進制程序目標文件。匯編器會把輸入的一個匯編語言程序(例如srcfile)編譯成目標文件。匯編的命令行基本格式是

as[選項] -o objfile srcfile

其中objfile是as編譯輸出的目標文件名稱,srcfile.s是as的輸入匯編語言程序名稱。如果沒有使用輸出文件名,那么as會編譯輸出名稱為a.out的默認目標文件選項用來控制編譯過程以產生指定格式和設置的目標文件。輸入的匯編語言程序srcfile是一個文本文件。該文件內容必須是有換行字符結尾的一系列文本行組成。雖然GNU as可使用分號在一行上包含多個語句,但通常在編制匯編語言程序時每行指包含一條語句。
語句可以是只包含空格、制表符和換行符的空行,也可以是賦值語句(或定義語句)、偽操作符語句和機器指令語句。例如BOOTSEG = 0X07C0

  • 偽操作符語句都是匯編器使用的指示符,它通常並不會產生任何代碼。它有偽操作碼和0個或多個操作數組成。每個操作碼都是由一個點字符'.'開始。
    點字符本身是一個特殊的符號,它表示編譯過程中的位置計數器。其值是點符號出現處機器指令第1個字節的地址。
  • 機器指令語句是可執行機器指令的助記符,它由操作碼和0個或多個操作數構成。另外任何語句之前都可以有標號。標號是由一個標識符后跟一個冒號:組成。在編譯過程,當匯編器遇到一個標號,那么當前位置計數器的值就會賦值給這個標號。
  • 一條匯編語句通常由標號(可選)、指令助記符(指令名)和操作數三個字段組成,標號位於一條指令的第一個字段。它代表其所在位置的地址,通常指明一個跳轉指令的的目標位置。最好還可以跟隨用注釋符開始的注釋部分。
  • 匯編器編譯產生的目標文件objfile通常起碼包含三個段或區
  • .text:正文段(代碼段,是一個已初始化過的段)通常其中包含程序的執行代碼或只讀代碼(.bss)
  • .data:數據段 :其中包含可讀/寫的數據。而未初始化數據段是一個未初始化的段。通常匯編器產生的輸出目標文件不會為該段保留空間,但在目標文件鏈接成執行程序被加載時操作系統會把該段的內容全部初始化為0.
    as86
    .bass:未初始化數據段

as86匯編語言程序

一個簡單的框架實例boot.s來說明as86匯編程序的結構以及程序中語句的語法,然后給出編譯鏈接和運行方法,最后再分別列出as86和ld86的使用方法和編制選項。

1 !
2 !boot.s -- bootsect.s的框架程序。用代碼0x07替換串msg1中1字符,然后在屏幕第一行上顯示。
3 !
4 .globl begtext,begdata,begbss,endtext,enddata,endbss !全局標識符,供ld86鏈接使用
5 .text  !正文段:
6 begtext:
7 .data  ! 數據段;
8 begdata
9 .bss   !未初始化數據段
10 begbss:
11 .text !正文段
12 BOOTSEG = 0X07c0 !BIOS加載bootsect代碼的原始段地址;
13
14 entry start  !告知鏈接程序,程序從start標號處開始執行。 
15 start:
16 jmpi go,BOOTSEG !段間跳轉。INITSET指出跳轉段地址,標號go是偏移地址。
17 go: mov  ax,cs !段寄存器cs值-->ax,用於初始化數據段ds和es。
18     mov  ds,ax
19     mov  ex,ax
20     mov  [msg1+17],ah !0x07-->替換字符串中1個點符號,喇叭就會響一聲
21    mov  cx,#20  !共顯示20個字符,包含回車換行符
22    mov dx,#0x1004 !共顯示在屏幕第17行、第5列處。
23    mov bx,#0x000c !字符顯示屬性(紅色)
24    mov bp,#msg1 !指向要顯示的字符串(中斷調用請求)
25    mov ax,#0x1301 !寫字符串並移動光標到串結尾處。
26 int  0x10         !BIOS中斷調用0x10,功能0x13,子功能01.
27 loop1: jmp loop1 !死循環
28msg1:.ascii  "Loading System..."!調用BIOS中斷顯示的信息。共20個ASCII碼字符
29 .byte  13,10
30 .org 510 !表示以后語句從地址510(0x1FE)開始存放。
31      .word 0XAA55  !有效引導扇區標志,供BIOS加載引導扇區使用
32  .text
33  endtext
34  .data
35  enddata
36 .bss
37 endbss:

該程序是一個簡單的引導扇區啟動程序。編譯鏈接產生的執行程序可以放入軟盤第1個扇區直接用來引導計算機啟動。啟動后會在屏幕第17行、第五列處顯示出紅色字符串"Loading system ..",並且光標下移一行。然后程序就在27行上死循環。
第4行上的'.globl'是匯編指示符(或稱匯編偽指令、偽操作符)。匯編指示符均以一個字符'.'開始,並且不會在編譯時產生任何代碼。匯編指示符由一個偽操作碼,后跟0個或多個操作數組成。第4行上的'glogl'是一個偽操作碼,而其后面的標號'begtext,begdata,begbss'等標號就是它的操作數。標號是后面帶冒號的標志符,例如第6行上的'begtext:'。但是在引用一標號時無須帶冒號。
第12行定義了一個賦值語句"BOOTSEG = 0x7c0"。等號'='(或符號'EQYU')用於定義標識符BOOTSEG所代表的值,因此這個標識符可稱為符號常量。這個值與C語言中的寫法一樣,可以使用十進制、八進制、十六進制。
第14行上的標識符'entry'是保留關鍵字,用於迫使鏈接器ld86
在生成的可執行文件中包括進其后指定的標號'start'。通常在鏈接多個目標文件生成一個可執行文件時應該在其中一個匯編程序中用關鍵詞指定一個入口標號,以便於調試。
第16行上是一個段間(Inter-segment)遠跳轉語句,就跳轉到下一條指令。由於當BIOS把程序加載到物理內存0x7c00處並跳轉到該處時,所有段寄存器(包括CS)默認值均為0,即此時CS:IP = 0X0000:0X7c00。因此這里使用段間跳轉語句就是為了給CS賦段值0x7c0。該語句執行后CS:IP=0X07C0:0X0005。隨后的兩條語句分別給DS和ES段寄存器賦值,讓它們都指向0x7c0。這樣便於對程序中的數據(字符串)進行尋址。
第20行上的MOV指令用於把ah寄存器中0x7c0段值的高字節(0x07)存放到內存中字符串msg1最后一個'.'位置處。這個字符將導致BIOS中斷在顯示字符串時鳴叫一聲。

! 直接寄存器尋址。跳轉到bx值指定的地址處,即把bx的值拷貝到IP中。
mov  bx,ax
jmp  bx
! 間接寄存器尋址。bx值指定內存位置處的內容作為跳轉的地址。
mov  [bx],ax
jmp  [bx]
!  把立即數1234放到ax中,把msg1地址值放到ax中。
mov ax,#1234
mov ax,#msg1
! 絕對尋址。把內存地址1234(msg1)處的內容放入ax中。
mov ax,1234
mov ax,msg1
mov ax,[msg1]
!  索引地址。把第二個操作數所指內存位置處的值放入ax中。
mov ax,msg1[bx]
mov ax, msg1[bx*4+si]
第21--25行的語句分別用於把立即數放到相對應的寄存器中。**把立即數前一定要加'#',否則將作為內存地址使用而使語句變成絕對尋址語句,**,例如,把一個標號(例如msg1)的地址值放入寄存器中時也一定要在前面加'#',否則會變成把msg1地址處的內容放到寄存器中!
第26行是BIOS屏幕顯示中斷調用int0x10。這里其功能19、子功能。1.該中斷的作用是把一字符串(msg1)寫到屏幕指定位置處。寄存器cx中是字符串長度值,dx中是顯示位置值,bx中是顯示使用的字符屬性,es:bp指向字符串。

第27行是一個跳轉語句,跳轉到當前指令處。因此這是一個死循環語句。這里采用死循環語句是為了讓顯示的內容能夠停留在屏幕上而不被刪除。死循環語句是調試匯編程序時常用的方法。
第28--29行定義了字符串msg1。定義字符串需要使用偽操作符'.ascii',並且需要使用雙引號括住字符串。偽操作符'.asciiz'還會自動在字符串后添加一個NULL(0)字符。另外,第29行上定義了回車和換行(13,10)兩個字符。定義字符需要使用偽操作符'.byte',並且需要使用單引號把字符括住。

3.1.3as匯編語言程序的編譯和鏈接

[/root]# as86 -0 -a -o boot.o boot.s  //編譯。生成與as部分兼容的目標文件。
[/root]# ld86 -0 -s -o boot boot.o    //鏈接。去掉符號信息。
[/root]# ls -l boot*
-rwx--x--x 1 root root 
-rw------- 1 root root
-rw------- 1 root root
[/root]# dd bs=32 if=boot of=/dev/fd0 skip =1//寫入磁盤或Image盤文件中。
16+0 records in
16+0 records out

其中第1條命令利用as86匯編器對boot.s程序進行編譯,生成boot.o目標文件。第二條命令使用鏈接器ld86對目標文件執行鏈接操作,最后生成MINIX結構的可執行文件boot。其中選項'-0'用於生成8086的16位目標程序;'-a'用於指定生成與GNU as 和ld 部分兼容的代碼。'-s'選項用於告訴鏈接器要去除最后生成的可執行文件中的符號信息。'-o'指定生成的可執行文件名稱。

3.1.4as86和ld使用方法和選項

as86和ld86的使用方法和選項如下:

as的使用方法和選項:

as [-03agjuw] [-b [bin]] [-lm [list] [-n name] [-o objfile] [-s sym]] srcfile
默認設置(除了以下默認值以外,其他選項默認為關閉或無;若沒有明確說明a標志,則不會有輸出):
-3     使用80386的32位輸出;

list   在標准輸出上顯示;

name   源文件的基本名稱(即不包括'.'后的擴展名);

各選項含義
-0  使用16比特代碼段;
-3  使用32比特代碼段;
-a  開啟與GNU as、ld的部分兼容性選項;
-b  產生二進制文件,后面可以分文件名;
-g  在目標文件中僅存入全局符號;
-j  使用所有跳轉語句均為長跳轉;
-l  產生列表文件,后面可以跟隨列表文件名;
-m  在列表中擴展宏定義;
-n  后面跟隨模塊名稱(取代源文件名稱放入目標文件中);
-o  產生目標文件,后跟目標文件名(objfile);
-s  產生符號文件,后跟符號文件名;
-u 將未定義符號作為輸入的未指定段的符號;
-w 不顯示警告信息;

ld連接器的使用語法和選項:

對於生成Minix a.out格式的版本:

ld  [-03Mims[-]] [-T textaddr] [-llib_extension] [-o outfile] infile...

對於生成GNU-Minix的a.out格式的版本:
ld [-03Mimrs[-]] [-T textaddr] [-llib_extension] [-o outfile] infile...
默認設置(除了以下默認值以外,其他選項默認為關閉或無):
-03   32位輸出;
outfile a.out格式輸出;

-0  產生具有16比特魔數的頭結構,並且對-lx選項使用i86子目錄;
-3  產生具有32比特魔數的頭結構,並且對-lx選項使用i386子目錄;
-M  在標准輸出設備上顯示已鏈接的符號;
-T  后面跟隨正文基地址(使用合適於strtoul的格式);
-i  分離的指令與數據段(I&D)輸出;
-lx 將庫/local/lib/subdir/libx.a加入鏈接的文件列表中;
-m  在標准輸出設備上顯示已鏈接的模塊;
-o  指定輸出文件名,后跟輸出文件名;
-r  產生適合於進一步重定位的輸出;
-s  在目標文件中刪除所有符號。

3.2 GNU as匯編

as匯編器僅用於編譯內核中的boot/bootsect.s引導扇區程序和實模式下的設置程序boot/setup.s。內核中其余所有匯編語言程序(包括C語言產生的匯編程序)均使用gas來編譯,並與C語言程序編譯產生的模塊鏈接。
在編譯C語言程序時,GNU gcc編譯器會首先輸出一個作為中間結果的as匯編語言文件,然后gcc會調用as匯編器把這個臨時匯編語言程序編譯成目標文件。即實際上as匯編器最初是專門用於匯編gcc產生的中間匯編語言程序的,而非作為一個獨立的匯編器使用。

3.2.1編譯as匯編語言程序

使用as匯編器編譯一個as匯編語言程序的基本命令行格式如下所示:

as [選項] [-o objfile] [srcfile.s...]

其中objfile是as編譯輸出的目標文件名,srcfile.s是as的輸入匯編語言程序名。如果沒有使用輸出文件名,那么as會編譯輸出名稱為a.out的默認目標文件。在as程序名之后,命令行上可包含編譯選項和文件名。所有選項可隨意放置,但是文件名的放置次序編譯結果密切相關。
一個程序的源程序可以被放置在一個或多個文件中,程序的源代碼是如何分割放置在幾個文件中並不會改變程序的語義。程序的源代碼是所有這些文件按次序的組合結果。每次運行as編譯器,它只編譯一個源程序。

3.2.2as匯編語法

as匯編器使用AT&T系統V的匯編語法與Intel的區別:

  • AT&T語法中立即操作數前面要加一個字符'$';寄存器操作數前要加字符百分號'%';絕對跳轉/調用(相對於程序計算器有關的跳轉/調用)操作數前面要加'*'號。而Intel匯編語法均沒有這些限制。

  • AT&T語法與Intel語法使用的源和目的操作數次序正好相反。AT&T的源和目的操作數是從左到右 '源,目的'。例如Intel的語句'add eax,4'對應AT&T的'addl $4,%eax'

  • AT&T語法中內存操作數的長度(寬度)由操作碼最后一個字符來確定。操作碼后綴'b'、'w'、和'l'分別指示內存引用寬度為8位字節(byte)、16位字(word)、32位長字(long)。Intel語法則通過在內存操作數前使用前綴'byte ptr'、'word ptr'和'dword ptr'來達到同樣目的。因此,Intel的語句'mov al,byte ptr foo'對應於AT&T的語句'movb $foo,%al'。

  • AT&T語法中立即形式的遠跳轉和遠調用為'ljmp/lcall $section,$offset',而Intel的是'jmp/call far section:offset'。同樣,AT&T語法中遠返指令’lret $stack-adjust'對應Intel的'ret far stack-adjust'

*AT&T匯編器不提供多代碼段程序的支持,UNIX類操作系統要求所有代碼在一個段中。

3.2.2.1匯編程序處理

as匯編器具有對匯編語言程序內置的簡單預處理功能。該預處理功能會調整並刪除多余的空格字符和制表符;刪除所有注釋語句並且使用單個空格或一些換行符替換它們;把字符常數轉換成對應的數值。但是預處理功能不會對宏定義進行處理,也沒有處理包含文件的功能。如果需要這方面的功能,那么可以讓匯編語言程序使用大寫的后綴'.S'讓as使用gcc的CPP預處理功能。
由於as匯編語言程序除了使用C語言注釋語句(即'/'和'/')以外,還使用'#'作為單行注釋開始字符,因此若在匯編之前不對程序執行預處理,那么程序中包含的所有以'#'開始的指示符或命令均被當作注釋部分。

3.2.2.2符號、語句和常數

符號(Symbol)是由字符組成的標識符,組成符號的有效字符取決於大小寫字符集、數字和三個字符'_.$'。符號不允許數字字符開始,並且大小寫含義不同。在as匯編程序中符號長度沒有限制,並且符號中所有字符都是有效的。符號使用其他字符(';')作為結束。文件最后語句必須以換行符作為結束處。
語句(Statement)以換行符或者行分割符(";")作為結束。文件最后語句必須以換行符作為結束。若在一行的最后使用反斜杠字符''(在換行符前),那么就可以讓一條語句使用多行。當as讀取到反斜杠加換行符時,就會忽略掉這兩個字符。
語句由零個或多個標號(Label)開始,后面可以跟隨一個確定語句類型的關鍵符號。標號由符號后面跟隨一個冒號(":")構成。關鍵符號確定了語句余下部分的語義。如果該關鍵符號以一個'.'開始,那么當前語句就是一個匯編命令(或稱為偽指令、指示符)。如果關鍵符號以一個字母開始,那么當前語句就是一條匯編語言指令語句。因此一條語句的通用格式為:

標號: 匯編指令  注釋部分 (可選)
或
標號: 指令助記符 操作數1,操作數2 注釋部分(可選)

常數是一個數字,可分為字符常數和數字常數兩類。字符常數還可分為字符串和單個字符;而數字常數可分為整數、大數和浮點數。
字符串必須用雙引號括住,並且其中可以使用反斜杠''來轉義包含特殊字符。例如'\'表示一個反斜杠字符。其中第一個反斜杠是轉義指示符,說明把第2個字符看作一個普通反斜杠字符。常用轉義字符序列,反斜杠若是其他字符,那么該反斜杠將不起作用並且as匯編器將會發出警告信息。
匯編程序中使用單個字符常數時可以寫成在該字符前面加一個單引號,例如"'A"表示值65、"'C"表示值67.表3-1中的轉義碼也同樣可以用於單個字符常數。例如"'\"表示是一個普通反斜杠字符常數。

整數數字常數有4中表示方法,即使用'0b'或者'0B'開始的二進制數('0-1'):以'0'開始的八進制數('0-7');以非‘0’數字開始的十進制數('0-9')和使用‘0x’或‘0X’開頭的十六進制數('0-9a-fa-F')。若要表示負數,只需要前面添加‘-’。
大數(Bignum)是位數超過32位二進制位的數,其表示方法與整數的相同。匯編程序中對浮點常數的表示方法與C語言中的基本一樣。由於內核代碼中幾乎不用浮點數。

3.2.3指令語句、操作數和尋址

指令(Instructions)是CPU執行的操作,通常指令也稱作操作碼(Opcode);操作數(Operand)是指令操作的對象;
而地址(Address)是指定數據在內存中的位置。指令語句是程序運行時刻執行的一條語句,它通常包含4個組成部分

  • 標號(可選)

  • 操作碼(指令助記符)

  • 注釋
    一條指令語句可以含有0個或最多3個用逗號分開的操作數。對於具有兩個操作數的指令語句,第1個是源操作數,第2個是目的操作數,即指令操作結果保存在第2個操作數中。
    操作數可以是立即數(即值是常數值的表達式)、寄存器(值在CPU的寄存器中)或(值在內存中)。一個間接操作數(Indirect operand)含有實際操作數值得地址值。AT&T語法通過在操作數前加一個'*'字符來指定一個間接操作數。只有跳轉/調用指令才能使用間接操作數。見下面對跳轉指令的說明。

  • 立即操作數前需要加一個'$'字符前綴

  • 寄存器名前需要加一個'%'字符前綴

  • 內存操作數有變量名或者含有變量地址的一個寄存器指定。變量名隱含指出了變量的地址,並指示CPU引用該地址處內存的內容。

3.2.3.1指令操作碼的命名

AT&T語法中指令操作碼名稱(即指令助記符)最后一個字符用來指明操作數的寬度。字符‘b’、‘w’和‘l’分別指定byte、word和long類型的操作數。如果指令名稱沒有帶這樣的字符后綴,並且指令語句中不含內存操作數,那么as就會根據目的寄存器操作數嘗試確定操作數寬度。例如指令語句'mov %ax,%bx'等同於'movw %ax,%bx'。同時,語句'mov $1,%bx'等同於'movw $1,%bx'。
AT&T與Intel語法中幾乎所有指令操作碼的名稱都相同,但仍有幾個例外。符號擴展和零擴展指令都需要2個寬度來指令,即需要為源和目的操作數指明寬度。AT&T語法中是通過使用兩個操作碼后綴來做到。AT&T語法中符號擴展和零擴展的基本操作碼名稱分別是'movs...'和movz...,Intel中分別是'movsx'和'movzx'。兩個后綴就附在操作碼基本名上。例如"使用符號擴展從%al到%edx"的AT&T語句是'movsbl %al,%edx',即從byte到long是bl、從byte到word是bw、從word到long是wl。AT&T語法與Intel語法中轉換指令的對應關系見表3-2所示

3.2.3.2指令操作碼前綴

操作碼前綴用於修飾隨后的操作碼。它們用於重復字符串指令、提供區覆蓋、執行總線鎖定操作、或指定操作數和地址寬度。通常操作碼前綴可作為一條沒有操作數的指令獨占一行並且必須直接位於所影響指令之前,但是最好與它修飾的指令放在同一行上。例如,串掃描指令'scas'使用前綴執行重復操作:

repne scas %es:(%edi),%al

操作碼前綴有表3-3中列出的一些。

3.2.3.3內存引用

Intel語法的間接內存引用形式:
section:[base+index*scale+disp]
對應於如下AT&T語法形式:
section:disp(base,index,scale)
其中base和index是可選的32位基寄存器和索引寄存器,disp是可選的偏移值。scale是比例因子,取值范圍是1、2、4、8。(分別是2的0次方,2的1次方,2的2次方、2的3次方)。scale其乘上索引index用來計算操作數地址。如果沒有指定scale,則scale取默認值1。section為內存操作數指定可選的段寄存器,並且會覆蓋操作數使用的當前默認段寄存器。請注意,如果指定的段覆蓋寄存器與默認操作的段寄存器相同,則as就不會為匯編的指令再輸出相同的段前綴。以下是幾個AT&T和Intel語法形式的內存引用例子:

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。

3.2.3.4跳轉指令

跳轉指令用於把執行點轉移到程序另一個位置繼續執行下去。這些跳轉的目的位置通常使用一個標號來表示。在生成目標代碼文件時,匯編器會確定所有帶標號的指令的地址,並且把跳轉到的指令的地址編碼到跳轉指令中。跳轉指令可以分為無條件跳轉和條件跳轉兩大類。跳轉指令將依賴於執行指令時標志寄存器中某個相關標志的狀態來確定是否進行跳轉,而無條件跳轉則不依賴於這些標志。
JMP是無跳轉指令,並可分為直接(direct)跳轉和間接(indirect)跳轉兩類,而條件跳轉指令只有直接跳轉的形式。對於直接跳轉指令,跳轉到的目標指令的地址作為跳轉指令的一部分直接編碼進跳轉指令中;對於間接跳轉指令,跳轉的目的位置取自於某個寄存器或某個內存位置中。直接跳轉語句的寫法是給出跳轉目標處的標號;間接跳轉語句的寫法是必須使用一個星字符'*'作為操作指示符的前綴字符,並且該操作指示符使用movl指令相同的語法。下面是直接和間接跳轉的幾個例子

jmp NewLoc           #直接跳轉。無條件直接跳轉到標號NewLoc處執行。
jmp *%eax            #間接跳轉。寄存器%eax的值是跳轉的目標位置。
jmp *(%eax)          #間接跳轉。從%eax指明的地址處讀取跳轉的目標位置。

同樣,與指令計數器PC無關的間接調用的操作數也必須有一個''作為前綴字符。若沒有使用''字符,那么as匯編器就會選擇與指令計數PC的相關的跳轉標號。還有,其他任何具有內存操作數的指令都必須使用操作碼后綴('b','w'或'l')指明操作數的大小(byte、word或long)。

3.2.4區與重定位

區(Section)(也稱為段、節或部分)用於表示一個地址范圍,操作系統將會以相同的方式對待和處理在該地址范圍中的數據信息。例如,可以有一個"只讀"的區,我們只能從該區中讀取數據而不能寫入。區的概念主要用來表示編譯生成的目標文件(或可執行程序)中的不同的信息區域,例如目標文件中的正文區或數據區。若要正確理解和編制一個as匯編語言程序,我們就需要了解as產生的輸出目標文件的格式安排。
鏈接器ld會把輸入的目標文件的內容按照一定規律組合成一個可執行程序。當as匯編器輸出一個目標文件時,該目標文件中的代碼被默認設置成從0開始。此后ld將會在鏈接過程中為不同目標文件中的各個部分分配不同的最終地址位置。ld會把程序中的字節塊移動到程序運行時的地址處。這些塊時作為固定單元進行移動的。它們的長度以及字節次序都不會被改變。這樣的固定單元就被稱作是區(或段、部分)。而為區分配運行時刻的地址的操作就被稱為重定位(Reclocation)操作,其中包括調整目標文件中記錄的地址,從而讓它們對應到恰當的運行時刻地址上。
as匯編器輸出產生的目標文件中至少具有3個區,分別被稱為正文(text)、數據(data)和bss區。每個區都可能是空的。如果沒有使用匯編指令把輸出放置在'.text'或'.data'區中,這些區會仍然存在,但內容是空的。在一個目標文件中,其text區從地址0開始,隨后就是data區,再后面是bss區。
當一個區被重定位時,為了讓鏈接器ld知道哪些數據會發生變化以及如何修改這些數據,as匯編器也會往目標文件中寫入所需要的重定位信息。為了執行重定位操作,在每次涉及目標文件中的一個地址時,ld必須知道:
* 目標文件中對一個地址的引用是從什么地方算起的?
* 該引用的字節長度是多少?
* 該地址引用的是哪個區?(地址)-(區的開始地址)的值等於多少?
* 對地址的引用與程序計數器PC(Program-Counter)相關嗎?
實際上,as使用的所有地址都可表示為:(區)+(區中偏移)。另外,as計算的大多數表達式都有這種與區相關的特性,在下面說明中,我們使用記號"{secname N}"來表示區secname中偏移N。
除了text、data和bss區,我們還需要了解絕對地址區(absolute區)。當鏈接器把各個目標文件組合在一起,absolute區中的地址將始終不變。例如,ld會把地址{absolute 0} "重定位"到運行時刻地0處。盡管鏈接器在鏈接后絕不會把兩個目標文件中的data區安排成重疊地址處,但是目標文件中的absolute區必會重疊而覆蓋。
另外還有一種名為"未定義的區(Undefined section)"在匯編時不能確定所在區的任何地址都被設置成{undefined U},其中U將會在以后填上。因為數值總是有定義的,所以出現未定義地址的唯一途徑僅涉及未定義的符號。對一個稱為公共塊(common block)的引用就是這樣一種符號:在匯編時它的值未知,因此它在undefined區中。
類似地,區名也用於描述已鏈接程序中區的組。鏈接器ld會把程序所有目標文件中的text區放在相鄰的地址處。我們習慣上所說的程序的text區實際上是指其所有目標文件text區組合構成的整個地址區域。對程序中data和bss區的理解也同樣如此。

3.2.4.1鏈接器涉及的區

鏈接器ld只涉及如下4類區

  • text區、data區 -- 這兩個區用於保存程序。as和ld會分別獨立而同等地對待它們。對其中text區的描述也同樣適合data區。然而當程序在運行時,則通常text區是不會改變的。text區通常會被進程共享,其中含有指令代碼和常數等內容。程序運行時data區的內容通常是會變化的。例如,C變量一般就存放在data區中。
  • bss區 --在程序開始運行時這個區中含有0值字節。該區用於存放未初始化的變量或作為公共變量存儲空間。雖然程序每個目標文件bss區的長度信息很重要,但是由於該區中存放的是0值字節,因此無須再目標文件中保存bss區。設置bss區的目的就是為了從目標文件中明確地排除0值字節。
  • absolute區 --該區的低地址0總是"重定位"到運行時刻地址0處。如果你不想讓ld在重定位操作時改變你所引用的地址,那么就使用這個區。從這種觀點來看,我們可以把絕對地址稱作是"不可重定位的:"在重定位操作期間它們不會改變。
  • undefined區 --對不在先前所述各個區對象的地址引用都屬於本區。

3.2.4.2子區

匯編取得的字節數據通常位於text或data區中。有時候在匯編源程序某個區中可能分布着一些不相鄰的數據組,但是你可以會想讓它們在匯編后聚集在一起存放。as匯編器允許你利用子區(subsection)來到達這個目的。在每個區中,可以有編號為0--8192的子區存在。編制在同一個子區中的對象會在目標文件中與該子區中其他對象放在一起。在這種情況下,編譯器就可以在每個會輸出的代碼區之前用'.text 0 '子區,並且在魅族會輸出的常數之前使用'.text 1 子區'。
使用子區是可選的。如果沒有使用子區,那么所有對象都會放在子區0中。子區會以其從小到大的編號順序出現在目標文件中,但是目標文件中並不包含表示子區的任何信息。處理目標文件的ld以及其他程序並不會看到子區的蹤跡,它們只會看到由所有text子區組成的text區;由所有data子區組成的data區。為了指定隨后的的語句被匯編到哪個子區中,可在'.text'表達式或'.data表達式'中使用數值參數。表達式結果應該是絕對值。如果只指定了'.text',那么就會默認使用'.text0'。同樣地,'.data'表示使用'.data 0'。每個區都有一個位置計數器(Location Counter),它會對每個匯編進該區的字節進行計數。由於子區僅供as匯編器使用方便而設置的,因此並不存在子區計數器。雖然沒有什么直接操作一個位置計數器的方法,但是匯編命令'.align'可以改變其值,並且任何標號定義都會取用位置計數器的當前值。正在執行語句匯編處理的區的位置計數器被稱為當前活動計數器。

3.2.4.3bss區

bss區用於存儲局部公共變量。你可以在bss區中分配空間,但是在程序運行之前不能再其中放置數據。因為當程序剛開始執行時,bss區中所有字節內容都將被清零。'.lcomm'匯編命令用於在bss區中定義一個符號;'.comm'可用於在bss區中聲明一個公共符號。

3.2.5符號

在程序編譯和鏈接過程中,符號(Sysmbol)是一個比較重要的概念。程序員使用符號來命名對象,鏈接器使用符號進行鏈接操作,而調試器利用符號進行調試。
標號(Label)后面緊跟隨一個冒號的符號。此時該符號代表活動位置計數器的當前值,並且,例如,可作為指令的操作數使用。我們可以使用等號'='給一個符號賦予任意數值。
符號名一個字母或'._'字符之一開始。局部符號用於協助編譯器和程序員臨時使用名稱。在一個程序中共有10個局部符號('0'....'9')可供重復使用。為了定義一個局部符號,只要寫出形如'N:'的標號(其中N代表任何數字)。若是引用前面最近定義的這個符號,需要寫成'Nb';若需引用下一個定義的局部標號,則需要寫成'Nf'。其中'b'意思是向后(backwards),'f'表示向前(forwards)。局部標號在使用方面沒有限制,到那時在任何時候我們只能向前/向后引用最遠10個局部標號。

3.2.5.1特殊點符號

特殊符號'.'表示as匯編的當前地址。因此表達式'mylab:.long.'就會把mylab定義包含它自己所處的地址值。給'.'賦值就如同匯編命令'.org'的作用。因此表達式'.=.+4'與'.space4'完全相同。

3.2.5.2符號屬性

除了名字以外,每個符號都有值"值"和"類型"屬性。根據輸出的格式不同,符號也可以具有輔助屬性。如果不定義就使用一個符號,as就會假設其所有屬性均為0.這指示該符號是一個外部定義的符號。
符號的值通常是32位的。對於標出text、data、bss或absolute區中一個位置的符號,其值是從區開始到標號處的地址值。對於text、data和bss區,一個符號的值通常會在鏈接過程中由於ld改變區的基地址而變化,absolute區中符號的值不會改變。這也是為何稱它們是絕對符號的原因。
ld會對未定義符號的值進行特殊處理。如果未定義符號的值是0,則表示該符號在本匯編源程序中沒有定義,ld會嘗試根據其他鏈接的文件來確定它的值。在程序使用了一個符號但沒有對符號進行定義,就會產生這樣的符號。若未定義符號的值不為0,那么該符號值就表示是.comm公共聲明的需要保留的公共存儲空間字節長度。符號指向該存儲空間的第一個地址處。
符號的類型屬性含有用於鏈接器或調試器的重定位信息、指示符號是外部的標志以及一些其他可選信息。對於a.out格式的目標文件,符號的類型存放在一個8位字段中(n_type字節)。

as匯編指令

匯編指令是指示匯編器操作方式的偽指令。匯編命令用於要求匯編器為變量分配空間、確定程序開始地址、指定當前匯編的區、修改位置計數器值等。所有匯編指令的名稱都以'.'開始,其余都是字符,並且大小寫無關。但是通常都使用小寫字符。

3.2.6.1 .align abs-expr1,abs-expr2,abs-expr3

.align是存儲對齊匯編命令,用於在當前子區中把位置計數器設置(增加)到下一個指定存儲邊界處。第1個絕對值表達式abs-expr1(absolute expression)指定要求的邊界對齊值。對於使用a.out格式目標文件的80x86系統,該表達式值式位置計數器值增加后其二進制值最右面0值位的個數,即是2的次方值。例如,'.align3'表示把位置計數器值增加到8的倍數上。如果位置計數器值本身就是8的倍數,那么就無需改變。但是對於使用ELF格式的80X86系統,該表達式值就是要求對齊的字節數。例如'.align 8'就是把位置計數器值增加到8的倍數上。
第2個表達式給出用於對齊而填充的字節值。該表達式與其前面的逗號可以省略。若省略,則填充字節值是0。第3個可選表達式abs-expr3用於指示對齊操作允許填充跳過的最大字節數。如果對齊操作要求跳過的字節數大於這個最大值,那么該對齊操作就被取消。若想省略第2個參數,可以在第1和第3個參數之間使用兩個逗號。

3.2.6.2 .ascii "string"

從位置計數器所值當前位置位字符串分配空間並存儲字符串。可使用逗號分開出多個字符串。例如,'.ascii "Hello world!","My assembler"'。該匯編命令會讓as把這些字符串匯編在連續的地址位置處,每個字符串后面不會自動添加0(NULL)字節。

3.2.6.3 .asciz "string"

該命令與‘.ascii’類似,但是每個字符串后面會自動添加NULL字符。

3.2.6.4 .byte expressions

該匯編命令定義0個或多個夠好分開的字節值。每個表達式的值是一個字節。

3.2.6.5 .common symbol,length

在bss區中聲明要給命名的公共區域。在ld鏈接過程中,某個目標文件中的一個公共符號會與其他目標文件中同名的公共符號合並。如果ld沒有找到一個符號的定義,而只是一個或多個公共符號,那么ld就會分配指定長度length字節的未初始化內存。length必須是一個絕對值表達式。如果ld找到多個長度不同但同名的公共符號,ld就會分配長度最大的空間。

3.2.6.6 .data subsection

該匯編指令通知as把隨后的語句匯編到編號為subsection的data子區中。如果省略編號,則默認使用編號0.編號必須是絕對值表達式。

3.2.6.7 .desc symbol,abs-expr

用絕對表達式的值設置符號symbol的描述字段n_desc的16位值。僅用於a.out格式的目標文件。

3.2.6.8 .filrepeat,size,value

該匯編命令會產生數個(repeat個)大小為size字節的重復拷貝。大小值可以為0或某個值,但是若size大於8,則限定為8.每個重復字節內容取自一個8字節數。高4字節為0,低4字節是數值value。這3個參數值都是絕對值,size和value是可選的。如果第2個逗號和value省略,value默認為0值;如果后面兩個參數都省略的話,則size默認為1。

3.2.6.9 .global symbol (或者.globl symbol)

該匯編命令會使得鏈接器ld能看見符號symbol如果在我們的目標中定義了符號symbol,那么它的值將能被鏈接過程中的其他目標文件使用。若目標文件中沒有定義該符號,那么它的屬性將從鏈接過程中其他目標文件的同名符號中獲得。這是通過設置符號symbol類型字段中的外部位N_EXT來做到的。

3.2.6.10 .int expressions

該匯編命令在某個區中設置0個或多個整數值(80386系統為4字節,同.long)。每個用逗號分開的表達式的值就是運行時刻的值。例如.int 1234 ,567,0x89AB

3.2.6.11 .lcomm symbol,length

為符號symbol指定的局部公共區域保留長度為length字節的空間。所在的區和符號symbol的值是新的局部公共塊的值。分配的地址在bss區中,因此在運行時刻這些字節值被清零。由於符號symbol沒有被聲明為全局的,因此鏈接器ld看不見

3.2.6.12 .long expressions

含義與.int相同

3.2.6.13 .octa bignums

這個匯編指令指定0個或多個用逗號分開的16字節大數(.byte,.work,.long,.quad,.octa)分別對應(1、2、4、8和16字節數)

3.2.6.14 .org new_lc,fill

這個匯編命令會把當前區的位置計數器設置為new_lc。new_lc是一個絕對值(表達式),或者是具有相同區作為子區的表達式,也即不能使用.org跨越各區。如果new_lc的區不對,那么.org就不會起作用。請注意,位置計數器是基於區的,即以每個區作為計數起點。
當位置計數器值增長時,所跳躍的字節將被填入值fill。該值必須是絕對值。如果省略了逗號和fill,則fill默認為0值。

3.2.6.15 .quad bignums

這個匯編命令指定00個或多個逗號分開的8字節大數bignum。如果大數放不進8字節中,則取低8個字節。

3.2.6.16 .short expressions(同.word expressions)

這個匯編命令指定0個或多個用逗號分開的8字節大數bignum。如果大數放不進8個字節中,則取低8個字節。

3.2.6.17 .space size,fill

該匯編命令產生size個字節,每個字節填fill。這個參數均為絕對值。如果省略了逗號和fill,那么fill的默認值就是0。

3.2.6.18 .string "string"

定義一個或多個用逗號分開的字符串。在字符串中可以使用轉義字符。每個字符串都自動附加一個NULL字符結尾。例如,.string "\n\nStarting","other strings"。

3.2.6.19 .text subsection

通知as把隨后的語句匯編進編號為subsection的子區中。如果省略了編號subsection,則使用默認編號值0。

3.2.6.20 .word expressions

對應32位機器,該匯編命令含義與.short相同。

3.2.7編寫16位代碼

雖然as通常用來編寫純32位的80X86代碼,但是1995年后它對編寫運行於實模式或16位保護模式的代碼也提供有限的支持。為了讓as匯編時產生16位代碼,需要在運行16位模式的指令語句之前添加匯編命'.code16',並且使用匯編命令'.code32'讓as匯編器切換回32位代碼匯編方式。
as不區分16位和32位匯編語句,在16位和32位模式下每條指令的功能完全一樣而與模式無關。as總是為匯編語句產生32位的指令代碼而不管指令將運行在16位還是32位模式下。如果使用匯編命令'.code16'讓as處於16位模式下,那么as會自動為所有指令加上一個必要的操作數寬度前綴而讓指令運行在16位模式。請注意,因為as為所有指令添加了額外的地址和操作數寬度前綴,所以匯編產生的代碼長度和性能上將會受到影響。
由於在1991年開發Linux內核0.11時as匯編器還不支持16位代碼,因此在編寫和匯編0.11內核實模式下的引導啟動代碼和初始化匯編程序時使用前面介紹的as匯編器。

3.2.8AS匯編器命令行選項

  • -a開啟程序列表
  • -f快速操作
  • -o指定輸出的目標文件名
  • -R組合數據區和代碼區
  • -W取消警告信息

3.3C語言程序

3.3.1C程序編譯和鏈接

使用gcc匯編器編譯C語言程序時通常會經過四個處理階段,即預處理階段、編譯階段、匯編階段和鏈接階段

在前處理階段中,gcc會把C程序傳遞給C前處理器CPP,對C語言程序中指示符和宏進行替換處理,輸出純C語言代碼;在編譯階段,gcc把C語言程序編譯生成對應的與機器相關的as匯編語言代碼;在匯編階段,as匯編器會把匯編代碼轉換成機器指令,並以特定二進制格式輸出保存在目標文件中;最后GNUld鏈接器把程序的相關目標文件組合鏈接在一起,生成程序的可執行映像文件。調用gcc的命令格式與編譯匯編語言的格式類似:

gcc [選項] [-o outfile] infile

其中infile是輸入的C語言文件;outfile是編譯產生的輸出文件。對於某次編譯過程,並非一定要全部執行這四個階段,使用命令行選項可以令gcc編譯過程在某個處理階段后就停止執行。例如,使用'-S'選項可以讓gcc在輸出了C程序對應的匯編語言之后就停止運行;使用'-c'選項可以讓gcc只生成目標文件而不執行鏈接處理,見如下所下。

gcc -o hello hello.c //編譯hello.c程序,生成執行文件hello。
gcc -S hello.s hello.c //編譯hello.c程序,生成對應匯編程序hello.s
gcc -c -o hello.o hello.c //編譯hello.c程序,生成對應目標文件hello.o而不鏈接。

在編譯像Linux內核這樣的包含很多源程序文件的大型程序時,通常使用make工具軟件對整個程序的編譯過程進行自動管理。

3.3.2

本節介紹內核C語言程序中接觸到的嵌入匯編(內聯匯編)語句。由於我們通常編制C程序過程中一般很少用到嵌入式匯編代碼,因此這里有必要對其基本格式和使用方法進行說明。具有輸入和輸出參數的嵌入匯編語句的基本格式為:

asm("匯編語句"
  :輸出寄存器
  :輸入寄存器
  :會被修改的寄存器
);

除第一行以外,后面帶冒號的行若不使用就都可以省略。其中,"asm"是內聯匯編語句關鍵詞;"匯編語句"是你寫匯編指令的地方;"輸出寄存器"表示當這段嵌入匯編執行完之后,哪些寄存器用於存放輸出數據。此地,這些寄存器分別對應一C語言表達式值或一個內存地址;"輸入寄存器"表示在開始執行匯編代碼時,這里指定的一些寄存器中應存放的輸入值,它們也分別對應着一C變量或常數值。"會被修改的寄存器"表示你已對其中列出的寄存器中的值進行了改動,gcc編譯器不能再依賴於它原先對這些寄存器加載的值。如果必要的話,gcc需要重新加載這些寄存器。因為我們需要把那些沒有在輸出/輸入寄存器部分列出,但是在匯編語句中明確使用到或隱含使用到的寄存器名列在這個部分中。
/kernel/traps.c文件中第22行開始的一段代碼作為例子來詳細解說。為了能看得更清楚一些,我們對這段代碼進行重新排列和編號。

#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字節內容到a1寄存器中。
        pop %%fs"\        //恢復fs寄存器原內容
        :"=a"(__res)    //輸出寄存器列表
        :"0"(seg),"m"(*(addr)) //輸入寄存器列表
")
__res;s})

10行代碼定義了一個嵌入匯編語言宏函數。通常使用匯編語句最方便的方式把它們放在一個宏內。用圓括號括住的組合語句(花括號中的語句):"({})"可以作為表達式使用,其中最后一行上的變量__res(第10行)是該表達式的輸出值。
因為宏語句需要定義在一行上,因此這里使用反斜杠''將這些語句連城一行。這條宏定義將被替換到程序中引用該宏名稱的地方。第一行定義了宏的名稱,也即是宏函數名稱get_seg_byte(seg,addr)。第3行定義了一個寄存器變量__res。該變量將被保存在一個寄存器中,以便於快速訪問和操作。如果想指定寄存器(例如eax),那么我們可以把該局寫成"register char __res asm("ax");",其中"asm"也可以寫成"asm"。第4行上的"asm"表示嵌入匯編語句的開始。從第4行到第7行的4條語句是AT&T格式的匯編語句。另外,為了讓gcc編譯產生的匯編語言程序中寄存器名稱前有一個百分號"%",在嵌入匯編語句寄存器名稱之前就必須寫上兩個百分號"%%"。
第8行即是輸出寄存器,這句的含義是在這段代碼運行結束后將eax所代表的寄存器的值放入__res變量中,作為本函數的輸出值,"=a"中的"a"稱為加載代碼,"="表示這是輸出寄存器,並且其中的值將被輸出值替代。第9行表示在這段代碼開始運行時將seg放到eax寄存器中,"0"表示使用與上面同個位置的輸出相同的寄存器。而((addr))表示一個內存偏移地址值。為了在上面匯編語句中使用該地址值,嵌入匯編程序規定把輸出和輸入寄存器統一按順序編號,順序是從輸出寄存器序列從左到右從上到下以"%0"開始,分別記為%0、%1、...%9。因此,輸出寄存器的編號是%0(這里只有一個輸出寄存器),輸入寄存器前一部分("0"(seg))的編號%1,而后部分的編號是%2。上面第6行上的%2即代表((addr))這個內存偏移量。
現在我們來研究4-7行上的代碼的作用。第一句將fs段寄存器的內容入棧;第二句將eax中的段值賦給fs段寄存器;第三句是把fs:(*(addr))所指定的字節放入al寄存器中。當執行完匯編語句后是,輸出寄存器eax的值將被放入__res,作為該宏函數(塊結構表達式)的返回值。
通過上面分析,我們指定,宏名稱中的seg代表一指定的內存值,而addr表示一內存偏移地址量。

asm("cld\n\t"
    "rep\n\t"
    "stol"
    :/*沒有輸出寄存器*/
    :/"c"(count-1),"a"(fill_value),"D"(dest)
    :"%ecx","%edi"
);

1-3行這三句是通常的匯編語句,用以清方向位,重復保存值。 其中兩行中的字符"\n\t"是用於gcc預處理輸出程序列表時能排的整齊而設置的,字符的含義與C語言中的相同。即gcc的運作方式是先產生與C程序對應的匯編程序,然后調用匯編器對其進行編譯產生目標代碼,如果在寫程序和調試程序時想看看C對應的匯編程序,那么就需要得到預處理程序輸出的匯編程序結果(這在編寫和調試高效的代碼時常用的做法)。為了預處理輸出的匯編程序格式整齊,就可以使用"\n\t"這兩個格式符號。
第4行說明這段嵌入匯編程序沒有用到輸出寄存器。第5行的含義是:將count-1的值加載到ecx寄存器中(加載代碼是C),fill_value加載到eax中,dest放到edi中。為什么要讓gcc編譯程序取做這樣的寄存器值的加載,而不讓我們自己做呢?因為gcc在它進行寄存器分配時可以進行某些優化工作。例如fill_value值可能已經在eax中。如果是在一個循環語句中的話,gcc就可能在整個循環操作中保留eax,這樣就可以在每次循環中少用一個movl語句。
最后一行的作用是告訴gcc這些寄存器中的值已經改變了。在gcc知道你拿這些什么后,能夠對gcc的優化操作有所幫助。表3-4中是一些可能用到的寄存器加載代碼及其具體的含義。

下面的例子不是讓你指定哪個變量使用哪個寄存器,而是讓gcc為你選擇

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

指令"leal"用於計算有效地址,但這里用它進行一些簡單計算。第1條匯編語句"leal(r1,r2,4),r3"語句表示r1+r2*4->r3。這個例子可以非常快地將x乘5。其中"%0"、"%1"是指gcc自動分配的寄存器。這里"%1"代表輸入值x要放入的寄存器,"%0"表示輸出值寄存器。輸出寄存器代碼前一定要加等於號。如果輸入寄存器的代碼是0或為空時,則說明使用與相應輸出一樣的寄存器。所以,如果gcc將r指定位eax的話,那么上面匯編語句的含義即為:

"leal (eax,eax,4),eax"

注意:在執行代碼時,如果不希望匯編語句被GCC優化而作修改,就需要在asm符號后面添加關鍵詞volatile,見下面所示。這兩種聲明的區別在於程序兼容性方面。建議使用后一種聲明方式。

asm volatile(........);
或這更詳細的說明為
__asm__ __volatile__(.....);

** 關鍵詞volatile也可以放在函數名前來修飾函數,用來通知gcc編譯器該函數不會返回。**這樣就可以讓gcc產生更好一些的代碼。另外,對於不會返回的函數,這個關鍵詞也可以避免gcc產生假警告信息。例如mm/memory.c中的如下語句說明函數do_exit()和oom()不會再返回到調用者代碼中:

volatile void do_exit(long code);
static inline volatile void oom(void)
{
 printk("out of memory\n\r");
 do_exit(SIGSEGV);
}

這段代碼是從include/string.h文件中文件中摘取的,是strcmp()字符串比較函數的一種實現。同樣,其中每行中的"\n\t"是用於gcc預處理程序輸出列表好看而設置的。

////字符串1與字符串2的前count字符進行比較
//參數:cs - 字符串1,ct -字符串2,count -比較的字符數。
// %0 - eax(__res)返回值, %1 - edi(cs)串1指針,%2 - esi(ct)串2指針,%3 - ecx(count)。
// 返回:如果串1>串2,則返回1;串1 = 串2,則返回0;串1<串2,則返回-1
extern inline int strncmp(const char* cs,const char char* ct,init count)
{
register int __res; //__res是寄存器變量
__asm__("cld\n      //清方向位
        "1:\tdecl %3\n\t" //count--。
        "js zf\n\t"  // 如果count<0,則向前跳轉到標號2。
        "lodsb\n\t"  //取串2的字符ds:[esi]->a1,並且esi++
        "scasb\n\t"  //比較a1與串1的字符es:[edi],並且edi++
        "jne 3f\n\t" //如果不相等,則向前跳轉到標號3。
        "testb %%al,%%al\n\t" //該字符是NULL字符嗎?
        "jne 1b\n" //不是,則向后跳轉到標號1,繼續比較。
        "2:\txorl %%eax,%%eax\n\t"//NULL字符,則eax清零(返回值)。
        "jmp 4f\n"//向前跳轉到標號4,結束。
         "3:\tmovl $1,%%eax\n\t" //eax中置1
        "jl 4f\n\t"//如果前面比較中串2字符<串1字符,則返回1,結束。
        "neg1 %%eax\n" //否則eax = -eax,返回負值,結束。
        "4:"
        :"=a"(__res):"D".(cs),"S" (ct),"c"(count):"si","di","cx")
  return __res; //返回比較結果。
}

3.3.3圓括號中的組合語句

花括號對"{...}"用於把變量聲明和語句組合成一個復合語句(組合語句)或一個語句塊,這樣在語義上這些語句就等同於一條語句。組合語句的右花括號后面不需要使用分號。圓括號中的組合語句,即形如"({....})"的語句,可以在GNU C中用作一個表達式使用。這樣就可以在表達式中使用loop、switch語句和局部變量,因此這種形式的語句通常稱為語句表達式。語句表達式具有如下示例的形式:

({
int y = foo();int z;
if(y>0) z = y;
else z = -y
3+z;
})

其中組合語句中最后一條語句必須市后面跟隨一個分號的表達式。這個表達式("3+z")的值即用作整個圓括號括住語句的值。如果最后一條語句不是表達式,那么整個語句表達式就具有void類型,因此沒有值。另外,這種表達式中語句聲明的任何局部變量都會在整個語句結束后失效。這個示例語句可以像如下形式的賦值語句來使用:

res = x + ({略...})+b

當然,人們通常不會像上面這樣寫語句,這種語句表達式通常都用來定義宏。例如內核源代碼init/main.c程序中讀取CMOS時鍾信息的宏定義:

#define CMOS_READ(addr)({{
\最后反斜杠起連接兩行語句的作用
outb_p(0x80|addr,0x70)\ //首先向I/O端口0x70輸出欲3讀取的位置addr。
intb_p(0x71);\ //然后從端口0x71讀入該位置處的值作為返回值。
})

再看一個include/asm/io.h頭文件中的讀I/O端口port的宏定義,其中最后變量_v的值就是inb()的返回值。

#define inb(port)({\
unsigned char_v;\
__asm__volatile("inb %%dx,%%al":"=a"(_v):"d"(port);\
_v;\
})

3.3.4寄存器變量
GNU對C語言的另一個擴充是允許我們把一些變量值放到CPU寄存器中,即所謂寄存器變量。這樣CPU就不用經常花費較長時間訪問內存去取值。寄存器變量可以分為2種:全局變量寄存器變量和局部寄存器變量。全局寄存器變量會在程序的整個運行過程種保留寄存器專門用於幾個全局變量。相反,局部寄存器不會保留指定的寄存器,而僅在內嵌asm匯編語句種作為輸入或輸出操作數時使用專門的寄存器。gcc編譯器的數據流分析功能本身有能力確定指定的寄存器何時含有正在使用的值,何時可派其他用場。當gcc數據流分析功能認為存儲在某個局部寄存器變量值無用時就可能會刪除之,並且對局部寄存器變量的引用也可能被刪除、移動或簡化。因此,若不想讓gcc作這些優化改動,最好在asm語句種加上volatitle關鍵詞。
如果想在嵌入匯編語句中把匯編指令的輸出直接寫到指定的寄存器中,那么此時使用局部寄存器變量就很方便。由於Linux內核中通常只使用局部寄存器變量,因此這里我們只對局部寄存器變量的使用方法進行討論。在GNU C程序中我們可以在函數中用如下形式定義一個局部寄存器變量:

register int res __asm__("ax")

這里ax是變量res所希望使用的寄存器。定義這樣一個寄存器變量並不會專門保留這個寄存器不派其他用途。在程序編譯過程中,當gcc數據流控制去欸的那個變量的值已經不用時就可能將該寄存器派作其他用途,而且對它的引用可能會被刪除、移動或被簡化。另外,gcc並不保證編譯出的代碼會把變量一致放在指定的寄存器中。因此在嵌入匯編指令的部分最好不要明確地引用該寄存器並且假設該寄存器肯定引用的是該變量值。然而把該變量用作為asm的操作數還是能夠保證指定的寄存器被用作該操作數。

3.3.5內聯函數

在程序中,通過把一個函數聲明為內聯(inline)函數,就可以讓gcc把函數的代碼集成到調用該函數的代碼中去。這樣處理可以去掉函數調用時進入/退出時間開銷,從而肯定能夠加快執行速度。因此把一個函數聲明為內聯函數的主要目的就是能夠盡量快速的執行函數體。另外,如果內聯函數中有常數值,那么在編譯期間gcc就可能用它進行一些簡化操作,因此並非所有內聯函數的代碼都會被嵌入進去。內聯函數方法對程序代碼的長度影響並不明顯。使用內聯函數的程序編譯產生的目標代碼可能會長一些也可能會短一些,這需要根據具體情況來定。
內聯函數嵌入調用者代碼中的操作是一種優化操作,因此只有進行優化編譯才會執行代碼嵌入處理。若編譯過程中沒有使用優化選項"-O",那么內聯函數的程序編譯產生的目標代碼可能會長一些也可能會短一些,這需要根據具體情況來定。
內聯函數嵌入調用者代碼中的操作是一個優化操作,因此只有進行優化編譯時才會執行代碼嵌入處理。。若編譯過程中沒有使用優化選項"-O",那么內聯函數的代碼就不會被真正地嵌入到調用這代碼中,而是只作為普通函數調用來處理。把一個函數聲明為內聯函數的方法是在函數聲明中使用關鍵詞"inline",例如內核文件/fs/inode.c的如下函數:

inline int inc(int *a)
{
  (*a)++;
}

函數中的某些語句用法可能會使得內聯函數的替換操作無法正常進行,或者不適合進行替換操作。例如使用了可變參數、內存分配函數mallocca()、可變長度數據類型變量、非局部goto語句、以及遞歸函數。編譯時可以使用選項 -Winline 讓gcc對標志成inline但不能被替換的函數給出警告信息以及不能替換的原因。
當在一個函數定義中既使用inline關鍵詞、又使用static關鍵詞,即像下面文件fs/inode.c中的內聯函數定義一樣,那么如果所有對該內聯函數的調用都被替換而集成在調用者代碼中,並且程序中沒有引用過該內聯函數的地址,則該內聯函數自身的匯編代碼就不會被引用過。在這種情況下,除非我們在編譯過程中使用選項 -fkeep-inline-functions,否則gcc就不會再為該內聯函數定義之前的調用語句。是不會被替換集成的,並且也都不能是遞歸定義的函數。如果存在一個不能被替換集成的調用,那么內聯函數就會像平常一樣被編譯成匯編代碼。因為對內聯函數地址的引用時不能被替換的。

static inline void wait_on_inode(struct m_inode* inode)
{
cli();
while(inode->i_lock)
sleep_on(&inode->i_wait)
sti();
}

請注意,內聯函數功能已經被包括在ISO標准C99中,但是該標准定義的內聯函數與gcc定義的有較大區別。ISO標准C99的內聯函數語義定義等同於這里使用組合關鍵詞inline和static的定義,即"省略"了關鍵詞static。若在程序需要使用C99標准的語義,那么就需要使用編譯選項 -std=gnu99。不過為了兼容起見,在這中情況下還是最好使用inline和static組合。以后gcc將最終默認使用C99的定義,在希望仍然使用這里定義的語義,就需要使用選項 -std=gnu89來指定。
若一個內聯函數的定義沒有使用關鍵詞static,那么gcc就會假設其他程序文件中也對這個函數有調用。因為一個全局符號只能被定義一次,所以該函數就不能再在其他源文件中進行定義。因此這里對內聯函數的調用就不能被替換集成。因此,一個非靜態的內聯函數總是會被編譯出自己的匯編代碼來。在這方面,ISO標准C99對不使用static關鍵詞的內聯函數定義等同於這里使用static關鍵詞的定義。
如果在定義一個函數時同時指定了inline和extren關鍵詞,那么該函數定義僅用於內聯集成,並且在任何情況下都不會單獨產生該函數自身的匯編代碼,即使明確了引用了該函數的地址也不會產生。這樣的一個地址會變成一個外部引用,就好像你僅僅聲明了函數而沒有定義函數一樣。
關鍵詞inLine和extern組合在一起的作用幾乎類同一個宏定義。使用這種組合方式就是把帶有組合關鍵詞的一個函數定義放在.頭文件中,並且不含關鍵詞的另一個相同函數定義放在一個庫文件中。此時頭文件中的定義會讓絕大多數對函數的調用被替換嵌入。如果還沒有被替換的對該函數的調用,那么就會使用(引用)程序文件中或庫中的拷貝。
Linux 0.1x內核源代碼中文件 include/string.h、lib/strings.c就是這種使用方式的一個例子。例如string.h定義了如下函數:

//將字符串(src)拷貝到另一個字符串(dest),直到遇到NULL字符后停止。
//參數dest - 目的字符串指針,src - 源字符串指針 %0 -esi(src),%1 -edi(dest)
extern inline char * strcpy(char* dest,const char *src)
{
__asm__(
"cld\n" //清方向位。
"1:\tlodsb\n\t" //加載DS:[esi]處1字節->al,並更新esi。
"stosb\n\t" //存儲字節al->ES:[edi],並更新edi
 "testb %%al,%%al\n\t" //剛存儲的字節是0?
  "jne 1b" //不是則向后跳轉到標號1處,否則結束。
::"S"(src),"D"(dest):"si","di","ax");
return dest   //返回目的字符串指針。
}

而在內核函數庫目錄中,lib/string.c文件把關鍵詞inline和extern都定義為空,見如下所示。因此實際上就在內核函數庫中又包含了string.h文件所有這類函數的一個拷貝,即又對這些函數重新定義了一次,並且"消除"了兩個關鍵詞的作用。

#define extern //定義為空
#define inline  //定義為空
#define LIBRARY
#include <string.h>

此時庫函數重新定義的上述strcpy()函數變成如下形式:

char * strcpy(char* dest,const char *src)
{
__asm__("cld\n"    //清方向位置
      "1:\tlodsb\n\t"//加載DS:[esi]處1字節->a1,並更新esi
      "stosb\n\t"//存儲字節al->ES:[edi],並更新edi
      "testb %%al,%%al\n\t"//剛存儲的字節是0?
      "jne 1b" //不是則向后跳轉到標號1處,否則結束
      ::"S"(src),"D"(dest):"si","di","ax");
return dest//返回目的字符串指針
}

3.4C與匯編程序的相互調用

為了提高代碼執行效率,內核源代碼中有地方直接使用了匯編語言編制。這就會涉及到在兩種語言編制的程序之間相互調用問題。本節首先說明C語言函數的調用機制,然后使用兩者函數之間的調用方法。

3.4.1C函數調用機制

在Linux內核程序boot/head.s執行完基本初始化操作之后,就會跳轉去執行init/main.c程序。那么head.s程序是如何把執行控制轉交給init/main.c程序的呢?即匯編程序是如何調用執行C語言程序的?這里我們首先描述一下C函數的調用機制、控制權傳遞方式,然后說明head.d程序跳轉到C程序的方法。
函數調用操作包括從一塊代碼到另一塊代碼之間的雙向數據傳遞和執行控制轉移。數據傳遞通過函數參數和返回值來進行。另外,我們還需要在進入函數時為函數的局部變量分配存儲空間,並且在退出函數時收回這部分空間。Intel80x86CPU為控制傳遞提供了簡單的指令,而數據的傳遞和局部變量存儲空間的分配與回收則通過棧操作來實現。

3.4.1.1棧幀結構和控制轉移權方式

大多數CPU上的程序實現使用棧來支持函數調用操作。棧被用來傳遞函數參數、存儲返回信息、臨時保存寄存器原有值以備恢復以及存儲局部數據。單個函數調用操作所使用的棧部分被稱為棧幀(Stack frame)結構,棧幀結構的兩端由兩個指針來指定。寄存器ebp通常用作幀指針(frame pointer),而esp則用作棧指針(stack pointer)。在函數執行過程中,棧指針esp會隨着數據的入棧和出棧而移動,因此函數中對大部分數據的訪問都基於幀指針ebp進行。

對於函數A調用函數B的情況,傳遞給B的參數包含在A的棧幀中。當A調用B時,函數A的返回地址(調用返回后繼續執行的指令地址)被壓入棧中,棧中該位置也明確指明了A棧的結束處。而B的棧幀則從隨后的棧部分開始,即圖中保存幀指針(ebp)的地方開始。再隨后則用於存放任何保存的寄存器值以及函數的臨時值。
B函數同樣也使用棧來保存不能放寄存器中的局部變量值。例如由於通常CPU的寄存器數量有限而不能夠存放函數的所有局部數據,或者有些局部變量是數組或結構,因此必須使用數組或結構引用來訪問。還有就是C語言的地址操作符'&'被應用到一個局部變量上時,我們就需要為該變量生成一個地址,即為變量的地址指針分配一空間。最后,B函數會使用棧來保存調用任何其它函數的參數。
棧是往低(小)地址方向擴展的,而esp指向當前棧頂處的元素。通過使用push和pop指令我們可以把數據壓入棧中或從棧中彈出。對於沒有指定初始值的數據所需要的存儲空間,我們可以通過幀指針遞減適當的值來做到。類似的,通過增加棧指針值我們可以回收棧中已分配的空間。
指令CALL和Ret用於處理函數調用和返回操作。調用指令CALL的作用是把返回地址壓入棧中並且跳轉到被調用函數開始處執行。返回地址是程序中緊隨調用CALL后面一條指令的地址。因此當被調用函數返回時就會從該位置繼續執行。返回RET指令用於彈出棧頂處的地址並跳轉到該地址處。在使用該指令之前,應該先正確處理棧中內容,使得當前棧指針所指位置內容正是先前CALL指令保存的返回地址。
另外,若返回值是一個整數或一個指針,那么寄存器eax將被默認用來傳遞返回值。
盡管某一時刻只有一個函數在執行,但我們還是需要確定在一個函數(調用者)調用其他函數(被函數者)時,被調用者不會修改或覆蓋調用者今后用到的寄存器內容。因此IntelCPU采用了所有函數必須遵守的寄存器用法統一慣例。該慣例指明,寄存器eax、edx、和ecx的內容必須由調用者自己負責保存。當函數B被A調用時,函數B可以在不用保存這些寄存器內容的情況下任意使用它們而不會毀壞函數A所需要的任何數據。另外,寄存器ebx、esi和edi的內容則必須由被調用者B來保護。當被調用者需要使用這些寄存器中的任何一個時,必須首先在棧中保存其內容,並在退出時恢復這些這些寄存器的內容。因為調用者A(或者一些更高的函數)並不負責保存這些寄存器內容,但可能在以后的操作中還需要用到原先的值。還有寄存器ebp和esp也必須遵守第二個慣例用法。

3.4.1.2函數調用舉例

作為一個例子,我們來觀察下面C程序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);
}

其中函數swap()用於交換兩個變量的值。C程序的主程序main()也是一個函數(將在下面說明),它在調用swap()之后返回交換的結果。這兩個函數的棧幀結構見圖。可以看出,函數swap()從調用者(main())的棧幀中獲取其參數。圖中的位置信息相對於寄存器ebp中的幀指針。棧幀左邊的數字指出了相對於幀指針的地址偏移值。在像gdb這樣的調試器中,這些數值都用2的補碼表示。例如'-4'被表示成'0xFFFFFFFC','-12'會被表示成'0xFFFFFFF4'
調用者main()的棧幀結構中包括局部變量a和b的存儲空間,相對於幀指針位於-4和-8偏移處。由於我們需要為這兩個局部變量生成地址,因此它們必須保存在棧中而非簡單地存放在寄存器中。

.text
_swap
    pushl %ebp   #保存原ebp值,設置當前函數的幀指針。
    movl  %esp,%ebp
    subl $4,%esp  #為局部變量c在棧內分配空間
    movl 8(%ebp),%eax #取函數第1個參數,該參數是一個整數類型值的指針。
    movl (%eax),%ecx  #取該指針所指位置的內容,並保存到局部變量c中。
    movl (%ecx),-4(%ebp)
    movl 8(%ebp),%eax  #再次取第1個參數,然后取第2個參數
    movl 12(%ebp),%edx
    movl(%edx),%ecx 
    movl %ecx,(%eax)
    movl 12(%ebp),%eax #再取第2個參數。
    movl -4(%ebp),%ecx #然后把局部變量c中的內容放到這個指針所指位置處。
    movl %ecx,(%eax)
    leave  $恢復原ebp、esp值(即movl %ebp,%esp;popl %ebp)
    ret
_main
    pushl %ebp     #保存原ebp值,設置當前函數的幀指針 
    movl  %esp,%ebp
    subl  $8,%esp  #為整型局部變量a和b在棧中分配空間
    movl  $16,-4(%ebp)
    movl  $32,-8(%ebp)
    leal  -8(%ebp),%eax #外調用swap()函數作准備,取局部變量b的地址。
    pushl %eax   #作為調用的參數並壓入棧中。即先壓入第2個參數。
    call _swap  #調用函數swap()
    movl -4(%ebp),%eax #取第1個局部變量a的值,減去第2個變量b的值
    movl -8(%ebp),%eax
    leave  #恢復原ebp、esp值(即movl %ebp,%esp;popl %ebp;)
    ret

這兩個函數均可以划分成三個部分:"設置",初始化棧幀結構;"主題",執行函數的實際計算操作;"結束",恢復棧狀態並從函數中返回。對於swap()函數,其設置部分代碼是3--5行。前兩行用來設置保存調用者的幀指針和設置本函數的的棧幀指針,第5行通過把棧指針esp下移4字節為局部變量c分配空間。第6--15行是swap函數的主題部分。第6--8用於取調用者的第一個參數&a,並以該參數作為地址取所有內容到ecx寄存器中,然后保存到為局部變量分配的空間中(-4(%ebp)。)第9--12行用於取第2個參數&b,並以該參數值作為地址取其內容放到第1個參數指定的地址處。第13--15行把保存在臨時局部變量c中的值存放到第2個參數指定的地址處。最后16--17行是函數結束部分。leave指令用於處理棧內容以准備返回,它的作用等價於下面兩個指令

movl %ebp,%esp #恢復原esp的值(指向棧幀開始處)
popl %ebp  #恢復原ebp的值(通常是調用者的幀指針)。

這部分代碼恢復了在進入swap()函數時寄存器esp和ebp的原有值,並執行返回指令ret。
第19--21行時main()函數的設置部分,在保存和重新設置幀指針后,main()為局部變量a和b在棧中分配了空間。第22-23行為這兩個局部變量賦值。從24-28行可以看出main()中是如何調用swap()函數的。其中首先使用leal指令(取有效地址)獲得變量b和a的地址並分別壓入棧中,然后調用swap()函數。變量地址壓入棧中的順序正好與函數申明的參數順序相反。即函數最后一個參數首先壓入棧中,而函數的第1個參數則是最后一個在調用函數指令callz之前壓入棧中。第29--30兩行將兩個已經交換過的數字相減,並放在eax寄存器中作為返回值。
從以上分析可知,C語言在調用函數時是在堆棧上臨時存放被調函數參數的值,即C語言是傳值類語言,沒有直接的方法可用來在被調用函數中修改調用者變量的值。因此為了達到修改的目的就需要向函數傳遞變量的指針(即變量的地址)。

3.4.1.3main()也是一個函數

上面這段匯編程序是gcc 1.40編譯產生的,可以看出其中有幾行多余的代碼。可見當時的gcc編譯器還不能產生最高效率的代碼,這也是為什么某些關鍵代碼需要直接使用匯編語言編制的原因。另外,上面提到c程序的主程序main()也是一個函數。這是因為在編譯鏈接時它將會作為ctr0.s匯編程序的函數被調用。crt0.s是一個樁(stub)程序,名稱中的"crt"是"C run-time"的縮寫。該程序的目標文件將被鏈接在每個用戶執行程序的開始部分,主要用於設置一些初始化全局變量等。Linux0.11中crt0.s匯編程序中見如下所示。其中建立並初始化全局變量_environ供程序中其它模塊使用。

.text
.global _environ  #聲明全局變量  _environ(對應C程序中的environ變量)
__entry  #代碼入口標號
    movl 8(%esp),%eax#取程序的環境變量指針envp並保存在__environ中。
    movl %eax,_environ #envp是execve函數在加載執行文件時設置的。
    call _main #調用我們的主程序。其返回狀態值在eax寄存器中。
    pushl  %eax  #壓入返回值為exit()函數的參數並調用該函數。
    call _exit  
    jmp lb
.data
_environ #定義變量_environ,為其分配一個長字空間
    .long 0

通常使用gcc編譯鏈接生成執行文件時,gcc會自動把該文件的代碼作為第一個模塊鏈接在可執行程序中。在編譯時使用顯示詳細信息選項'-v'就可以明顯地看出這個鏈接操作過程:

[/usr/root]# gcc -v -o exch exch.s
gcc version 1.40
/usr/local/lib/gcc-as -o exch.o exch.s
/usr/local/lib/gcc-ld -o exch /usr/local/lib/crt0.o exch.o /usr/local/lib/gnulib -lc
/usr/local/lib/gnulib
[/usr/root]#

因此在通常的編譯過程中我們無需特別指定stub模塊crt0.o,但是若想從上面給出的匯編程序手工使用ld(gld)從exch.o模塊倆鏈接產生可執行文件exch,那么我們就需要在命令行上特別指明crt0.o這個模塊,並且鏈接的順序應該是crt0.o所有程序模塊、庫文件。"
為了使用ELF格式的目標文件以建立共享庫模塊文件,現在的gcc編譯器(2.x)已經把這個crt0擴展成幾個模塊:crt1.o、crti.o、crtbegin.o、cretend.o和crtn.o這些模塊的鏈接順序為"crtl.o、crti.o、crtbegin.o(crtbeginS.o)"、所有程序模塊、crtend.o(crtendS.o)、crtn.o、庫模塊文件"。gcc的配置文件specfile指定了這種鏈接順序。其中ctrl.o、crti.o和crtn.o由C庫提供,是C程序的"啟動"模塊;crtbegin.o和crtend.o是C++語言的啟動模塊,由編譯器gcc提供;而ctrl.o則與crt0.o的作用類似,主要用於調用main()之前做一些初始化工作,全局符號__start就定義在這個模塊中。
crtbegin.o和crtend.o主要用於C++語言在.ctors和.dtors區中執行全局構造器(constructor)和析構器(destructor)函數。crtbeginS.o和crtendS.o的作用與前兩者類似,但用於創建共享模塊中。crti.o用於在.init區中執行初始化函數init()。.init區中包含進程的初始化代碼,即當前程序開始執行時,系統在調用main()之前先執行.init中的代碼。crtn.o則用於在.fini區中執行進程終止退出處理函數fini()函數,即當程序正常退出時(main()返回之后),系統會安排執行.fini中的代碼。
boot/head.s程序中第136--140行就是用於為跳轉到init/main.c中的main()函數作准備工作。第139行上的指令在棧中壓入了返回地址,而第140行則壓入了main()函數代碼的地址。當head.s最后在第218行上執行ret指令時就會彈出main()的地址,並把控制權轉移到init/main.c程序中。

3.4.2在匯編程序中調用C函數

在匯編程序中調用一個C函數時,程序需要首先按照逆向順序把函數參數壓入棧中,即函數最后(最右邊的)一個參數先入棧,而最左邊的第1個參數在最后調用指令之前入棧,見圖3-6所示。然后執行CALL指令去執行被調用的函數。在調用函數返回后,程序需要再把先前壓入棧中的參數清楚掉。

在執行CALL指令時,CPU會把CALL指令下一條指令的地址壓入棧中(見圖中EIP)。如果調用還涉及到代碼特權級變化,那么CPU還會進行堆棧切換,並且把當前堆棧指針、段描述符和調用參數壓入新堆棧中。由於Linux內核中只使用中斷門和陷阱門方式處理特權級別變化時的調用情況,並沒有使用CALL指令來處理特權變化的情況,因此這里對特權級別變化時的CALL指令使用方式不再進行說明
匯編中調用C函數比較"自由"。只要是在棧中適當位置的內容就可以作為參數供C函數使用。這里仍然以圖3-6中具有3個參數的函數調用為例,如果我們沒有專門為調用函數func壓入參數就直接調用它的話,那么func()函數仍然會把EIP位置以上的棧中其他內容作為自己的參數使用。如果我們為調用func()僅僅明確地壓入了第1、第2個參數,那么func()函數的第3個參數p3就會直接使用p2前的棧中內容。在Linux0.1x內核代碼中國就有幾處使用了這種方式。例如在kernel/system_call.s匯編程序中第217行上調用copy_process()函數(kernel/fork.c中第68行)的情況。在匯編程序函數__sys_fork中雖然把5個參數壓入棧中,但是copy_process()卻共帶有多達17個參數,見下面所示:

//kernel/system_call.s匯編程序_sys_fork部分
212        push%gs
213        pushl  %esi
214        pushl  %edi
215        pushl  %ebp
216        pushl  %eax
217        call _copy_process  #調用C函數copy_process()(kernel/fork.c,68)
218        addl   $20,%esp     #丟棄這里所有壓棧內容。
219        ret
//kernel/fork.c程序
68 int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,long ebx,long ecx,long edx,long fs, long es,long ds,long eip,long cs,long eflags,long esp,long ss)

我們知道參數越是最后入棧,它越是靠近C函數參數左側。因此實際上調用copy_process()函數之前入棧5個寄存器值就是copy_process()函數的最左面的5個參數。按順序它們分別對應為入棧的eax(nr)、ebp、edi、esi和寄存器gs的值。而隨后的其余參數實際上直接對應堆棧上已有的內容。這些內容是進入系統調用中斷處理過程開始,直到調用本系統調用處理過程時逐步入棧的各寄存器的值。
參數none是system_call.s程序第94行上利用地址跳轉表sys_call_table調用_sys_fork時的下一條指令的返回地址。隨后的參數是剛進入system_call時在83-88行壓入棧的寄存器ebx、ecx、edx和段寄存器fs、es、ds。最后5個參數是CPU執行中斷指令壓入返回地址eip和cs、標志寄存器eflags、用戶棧地址esp和ss。因為系統調用涉及到程序特權級別變化,所以CPU會把標志寄存器值和用戶棧地址也壓入了堆棧。在調用C函數copy_process()返回后,_sys_fork也只把自己壓入5個參數丟掉,棧中其他還均保存着。其它采用上述用法的函數還有kernel/signal.c中的do_signal()、fs/exec.c中的do_execve(),請自己分析。
另外,我們說匯編程序調用C函數比較自由的另一個原因是我們可以根本不用CALL指令而采用JMP指令同樣到達調用函數的目的。方法是在參數入棧后人工把下一條要執行的指令地址壓入棧中,然后直接使用JMP指令跳轉到被調用函數開始地址處去執行函數。此后當函數執行完成時就會執行RET指令把我們人工壓入棧中的下一條指令地址彈出,作為函數返回的地址。Linux內核中也有多處用到了這種函數調用方法,例如kernel/asm.s程序第62行調用執行了trap.c中的do_int3()函數的情況。

3.4.3在程序中調用匯編函數

從C程序調用匯編程序函數的方法與匯編程序中調用C函數的原理相同,但Linux內核程序中不常使用。調用方法的着重點仍然是對函數參數在棧中位置的確定上。當然,如果調用的匯編語言程序比較短,那么就可以直接在C程序中使用上面的介紹測內聯匯編語句來實現。以下我們以一個示例來說明編制這類程序的方法。包含兩個函數的匯編程序callees.s見如下所示。

/*
本匯編程序利用系統調用sys_write()實現顯示函數 int mywrite(int fd,char* buf,int count)函數 int myadd(int a,int b,int* res)用於執行a+b=res運算。若函數返回0,則說明溢出。
注意:如果在現在的Linux系統(例如RedHat 9)下編譯,則請去掉函數名的下划線'_'
*/
SYSWRITE = 4  #sys_write()系統調用號
.global _mywrite, _myadd
.text
_mywrite
    pushl  %ebp
    movl   %esp,%ebp
    pushl  %ebp
    movl   8(%ebp),%ebx  #取調用者第1個參數:文件描述符fd
    movl   12(%ebp),%ecx  #取第2個參數:緩沖區指針
    movl   16(%ebp),%edx  #取第三個參數:顯示字符數。
    movl   $SYSWRITE,%eax  #%eax中放入系統調用號4
    int    $0x80           #執行系統調用
    popl   %ebx
    movl   %ebp,%esp
    popl   %ebp
    ret
_myadd
    pushl  %ebp
    movl   %esp,%ebp
    movl   8(%ebp),%eax  #取第1個參數a。
    movl   12(%ebp),%edx #取第2個參數b。
    xorl   %ecx,%ecx     #%ecx為0表示計算溢出。
    addl   %eax,%edx     #執行加法運算
    jo     lf            #若溢出則跳轉
    movl   16(%ebp),%eax #取第3個參數的指針
    movl   %edx,(%eax)   #把計算結果放入指針所指位置處
    incl   %ecx  #沒有發生溢出,於是設置無溢出返回值
    movl   %ecx,%eax #%eax中是函數返回值 
    movl   %ebp,%esp
    popl   %ebp
    ret

該匯編文件中的第1個參數mywrite()利用系統中斷0x80系統調用sys_write(int fd,char* buf,int count)實現在屏幕上顯示信息。對應的系統調用功能號是4(參見include/unistd.h),三個參數分別為文件描述符、顯示緩沖區指針和顯示字符數。在執行int 0x80之前,寄存器%eax中需要放入調用功能號(4),寄存器%ebx、%ecx和%edx要按調用規定分別存放fd、buf和count。函數mywrite()的調用參數個數與sys_write()完全一樣。
第2個函數myadd(int a,int b,int *res)執行加法運算。其中參數res是運算的結果。函數返回值用於判斷是否發生溢出。如果返回值為0表示計算已發生溢出,結果不可用。否則計算結果將通過參數res返回給調用者。
注意:如果在現在Linux系統(例如RedHat 9)下編譯callee.s程序,則請去掉函數名前的下划線'_'。調用這兩個函數的C程序cller.c見如下所示

/*
調用匯編函數mywrite(fd,buf,count)顯示信息;調用myadd(a,b,result)執行加法運算。如果myadd()返回0,則表示加函數發生溢出。首先顯示開始計算信息,然后顯示運算結果。
*/
int  main()
{
  char buf[1024];
  int a, b,res;
  char* mystr="Calculating....\n"
  char* emsg = "Error in adding\n"
  a = 5;
  b = 10;
  mywrite(1,mystr,strlen(mystr));
  if(myadd(a,b,&res))
  {
  sprintf(buf,"The result is %d \n",res)
  }
  else
  {
   mywrite(1,emsg,strlen(emsg));
  }
  return 0 ;
}

該函數首先利用匯編函數利用mywrite()在屏幕上顯示開始計算的信息"Calculating....",然后調用加法計算匯編函數myadd()對a和b兩個數進行運算,並在第3個參數res中返回計算結果。最后再利用mywrite()函數把格式化過的結果信息字符串顯示在屏幕上。如果函數myadd()返回0,則表示加函數發生溢出,計算結果無效。這兩個文件的編譯和運行結果

[/usr/root]# as -0 callee.o callee.s
[/usr/root]# gcc -o caller caller.c callee.o
[/usr/root]# ./caller
Calculating...
The result is 15
[/usr/root]#

3.5Linux0.11目標文件格式

為了生成內核代碼文件,Linux0.11使用了兩種編譯器。第一種是匯編編譯器as86和相對應的鏈接程序(或稱為鏈接器)ld86。它們專門用於編譯和鏈接運行在實地址模式下的16位內核引導扇區程序bootsect.s和設置程序setup.s第二種是GNU的匯編器as(gas)和C語言編譯器gcc以及相對應的鏈接程序gld。編譯器用於為源程序文件產生對應的二進制代碼和數據目標文件。鏈接程序用於對相關的所有目標文件進行組合處理,形成一個可被內核加載執行的目標文件,即可執行文件。
本節首先簡單說明編譯器產生的目標文件結構,然后描述鏈接器如何把需要鏈接在一起的目標文件模塊組合在一起,以生成二進制可執行映像文件或一個大的模塊文件。最后說明Linux0.11內核二進制代碼文件Image的生成原理和過程。這里給出了Linux0.11內核所支持的a.out目標文件格式的信息。as86和ld86生成的MINIX專門的目標文件格式,我們將涉及這種格式的內核創建工具一章中給出。
為了便於描述,這里把編譯器生成的目標文件稱為目標模塊文件(簡稱模塊文件),而把鏈接程序輸出產生的可執行目標文件稱為可執行文件。並且它們都統稱為目標文件。

3.5.1目標文件格式

在Linux0.11系統中,GNU gcc或 gas編譯輸出的目標模塊文件和鏈接程序所生成的可執行文件使用了UNIX傳統的a.out格式。這是一種被稱為匯編或鏈接輸出(Assembly& linker editor output)的目標文件格式。對於具有內存分頁機制的系統來說,這是一種簡單有效的目標文件個是。a.out文件有一個文件頭和隨后的代碼區(Text section ,也稱為正文段)、已初始化數據區(Data section,也稱為數據段)、重定位信息區、符號表以及符號名字符串構成,見下圖其中代碼區和數據區通常也分別稱為正文段(代碼段)和數據段。

a.out格式7個區的基本定義和用途是:

  • 執行頭文件(exec header)。執行文件頭部分。該部分含有一些參數(exec結構),是有關目標文件的整體結構信息。例如代碼和數據區的長度、未初始化數據區的長度、對應源程序文件名以及目標文件創建時間等。內核使用這些參數把執行文件加載到內存中並執行,而鏈接程序(ld)使用這些參數將一些模塊文件組合成一個可執行文件。這是目標文件唯一必要的組成部分。
  • 代碼區(text segment)。由編譯器或匯編器生成的二進制指令代碼和數據信息,含有程序執行時被加載到內存中的指令代碼和相關數據。可以以只讀形式被加載。
  • 數據區(data segment)。由編譯器或匯編器生成的二進制指令代碼和數據信息,這些部分含有已經初始化過的數據,總是被加載到可讀寫的內存中。
  • 代碼重定位(text relocations)。這部分含有供鏈接程序使用的記錄數據。在組合目標模塊文件時用於定位代碼段中的指針或地址。當鏈接程序需要改變目標代碼的地址時就需要修正和維護這些地方。
  • 數據重定位(data relocations)。類似於代碼重定位部分的作用,但是用於數據段中指針的重定位。
  • 符號表部分(sysmbol table)。這部分同樣含有供鏈接程序使用的記錄數據。這些數據保存着模塊文件中定義的全局符號以及需要從其它模塊文件中輸入的符號,或者由鏈接器定義的符號,用於在模塊文件之間對命名的變量和函數(符號)進行交叉引用。
  • 字符串表部分(string table)。該部分有與符號相對應的字符串。用於調試程序調試目標代碼,與鏈接過程無關。這些信息可包含源程序代碼和行號、局部符號以及數據結構描述信息等。
    對於一個指定的目標文件並非一定包含所有以上信息。由於Linux0.11系統使用了IntelCPU的內存管理功能,因此它會為每個執行程序單獨分配一個64MB的地址空間(邏輯地址空間)使用。在這種情況下因為鏈接器已經把執行文件處理成一個固定地址開始運行,所以相關的可執行文件中就不再需要重定位信息。

3.5.5System.map文件

當運行GNU鏈接器gld(ld)時若使用了‘-M’選項,或者使用nm命令,則會在標准輸出設備(通常是屏幕)上打印出鏈接映像(link map)信息,即是指由鏈接程序產生的目標程序內存地址映像信息。其中列出了程序段裝入到內存中的位置信息。具體來講有如下信息:

  • 目標文件及符號信息映射到內存中的位置:
  • 公共符號如何放置;
  • 鏈接中包含的所有文件成員及其引用的符號。
    通常我們會把發送到標准輸出設備的鏈接映像信息重定向到一個文件中(例如System.map)。在編譯內核時,linux/Makefile文件產生的System.map文件就用於存放內核符號表信息。符號表是所有內核符號及其對應地址的一個列表,當然包括上面說明的_etext、_edata和_end等符號的地址信息。隨着每次內核的編譯,就會產生一個新的對應System.map文件。當內核運行出錯時,通過System.map文件中的符號表解析,就可以查到一個地址值對應的變量名,或反之。
    利用System.map符號表文件,在內核或相關程序出錯時,就可以獲得我們比較容易識別的信息。符號表的樣例子如下所示:
c03441a0 B dmi_broken
c03441a4 B is_sony_vaio_laptop
c03441c0 b dmi_ident
c0344200 b pci_bios_present
c0344204 b pirq_table

3.6Make程序和Makefile文件

Makefile(makefile)文件是make工具程序的配置文件。Make工具程序的主要用途是能自動地決定一個含有很多源程序文件的大型程序中哪個文件需要被重新編譯。Makefile的使用比較復雜,這里只是根據上面的Makefile文件作簡單的介紹。
為了使用make程序,你就需要Makefile文件來告訴make要做什么工作。通常,Makefile文件會告訴make如何編譯和鏈接一個文件。當明確指出時,Makefile還可以告訴make運行各種命令(例如,作為清理操作而刪除某些文件)。
make的執行過程分為兩個不同的階段。在第一個階段,它讀取所有的Makefile文件以及包含的makefile文件等,記錄所有的變量及其值、隱式的或顯示的規則,並構造出所有目標對象及其先決條件的一幅全景圖。在第二階段期間,make就使用這些內容結構來確定哪個目標對象需要被重建,並且使用相應的規則來操作。
當make重新編譯程序時,每個修改過的C代碼文件必須被重新編譯。如果一個頭文件被修改過了,那么為了確保正確,每一個包含該頭文件的C代碼程序都將被重新編譯。每次編譯操作都產生一個與源程序對應的目標文件。最終,如果任何源代碼文件被編譯過了,那么所欲的目標文件不管是剛編譯完的還是以前就編譯好的必須連接在一起以生成的可執行文件文件。
簡單的Makefile文件含有一些規則,這些規則具有如下的形式:

目標(target)...:先決條件(prerequisites)...
          命令(command)
          ....
          ....

其中'目標'對象通常是程序生成的一個文件的名稱;例如是一個可執行文件或目標文件。目標也可以是所要采取活動的名字,比如‘清楚’‘(clean)’‘先決條件’是一個或多個文件名,是用作產生目標的輸入條件。通常一個目標依賴幾個文件。而'命令'是make需要執行的操作。一個規則可以有多個命令,每一個命令自成一行。請注意,你需要在每個命令行之前鍵入一個制表符!這是粗心這常常忽略的地方。
如果一個先決條件通過目錄搜尋而在另外一個目錄中被找到,這並不會改變規則的命令;它們將被如期執行。因此,你必須小心地設置命令,使得命令能夠在make發現先決條件的目錄中找到需要的先決條件。這就需要通過使用自動變量來做到。自動變量是一種在命令行上根據情況能被自動替換的變量。自動變量的值是基於目標對象及其先決條件而在命令行執行前的設置。例如,’$^‘的值表示規則的所有先決條件,包含它們所處目錄的名稱;’$<‘的值表示規則中的第一個先決條件;’$@’表示目標對象;另外還有一些自動變量這里就不提了。
有時,先決條件還常包含頭文件,而這些頭文件並不願在命令中說明。此時自動變量‘$<’正是一個先決條件。例如

foo.o:foo.c  def.h hack.h
    cc -c $(CFLAGS) $< -o $@

其中的'$<'就會被自動地替換成foo.c,$@則會被替換為foo.o
為了讓make能使用習慣用法來更新一個目標對象,你可以不指定命令,寫一個不帶命令的規則或者不寫規則。此時make程序將會根據源程序文件的類型(程序的后綴)來判斷要使用哪個隱式規則。
后綴規則是為make程序定義隱式規則的老式方法(現在這種規則已經不用了,取而代之的是使用更通用更清晰的模式匹配規則)。下面規則就是一種雙后綴規則。雙后綴規則是用一對后綴定義的:源后綴和目標后綴。相應的隱式先決條件是通過使用文件名中的源后綴替換目標后綴后得到。因此,此時下面的‘$<’值是‘.c’文件名。而正條make規則的含義是將'.c'程序編譯成'*.s'代碼。

.c.s:
    $(CC)$(CFLAGS)\
    -nostdinc  -Iinclude  -S -o $*.s $

通常命令是屬於一個具有先決條件的規則,並在任何先決條件改變時用於生成一個目標(target)文件。然而,為目標而指定命令的也不一定要有先決條件。例如,與目標'clean'相關的含有刪除(delete)命令的規則並不需要有先決條件。此時,一個規則說明了如何以及何時來重新制作某些文件,而這些文件是特定規則的目標。make根據先決條件來執行命令以及創建或更新目標。一個規則也可以說明如何及何時執行一個操作。
一個Makefile文件也可以含有除規則以外的其他文字,但是一個簡單的Makefile文件只需要含有適當的規則。規則可能看上去要比上面示出的模板復雜得多,但基本上都是符合的。
Makefile文件最后生成的依賴關系是用於讓make來確定是否需要重建一個目標對象。比如當某個頭文件被改動后,make就通過這些依賴關系,重新編譯與該頭文件相關的所有‘*.c’文件。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM