第二章 環境搭建及基礎知識介紹

田宇發表於2015-03-04

這一章會給大家介紹編寫本作業系統所需的基礎知識、系統環境及環境搭建方法,大家不必在這方面耗費太多心血,本著夠用就好的原則就行。 我們可以在實踐中慢慢摸索,不斷完善和豐富這些知識。相信聰明的你應該早已具備了一定的開發能力, 如果感覺自己這方面能力不足,網上有很多這方面的學習資料供大家學習,所以這裡就不浪費篇幅詳細講解了。

對於開發使用的系統環境,目前作者使用的是windows系統,開發作業系統的編譯環境是Linux的開源發行版——CentOS6。因此,作者選用了VMware虛擬機器來搭載CentOS6作業系統。雖然我們打算將我們的作業系統執行在物理平臺上。但是,在剛開始寫作業系統的時候,直接使用物理平臺會讓我們除錯程式碼變得很艱難。選用Bochs虛擬機器來除錯系統環境是一個不錯的選擇。

對於基礎知識,不管你是精通C和組合語言,能夠寫出高效的讓人看不懂程式碼的大神,還是剛開始學習程式語言抱著《譚浩強C語言》亂啃的菜鳥。都請你們靜下心來,平靜的看完這一章。讓心沉澱,再踏上征途。

這章可以作為複習章,或者作為提高自己知識技能的學習章。總而言之,這一章的知識很重要,不然接下來的內容你會看的很吃力,俗話說:不是一番寒徹骨,怎得梅花撲鼻香。我們今天止步不前,是為了明天大踏步的前進。菜鳥們,你們不必太擔心,只要你有決心,就一定能成功。我們都是地才!

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.2 組合語言

這一節主要介紹的是AT&T組合語言和Intel組合語言的基本格式以及一些明顯的區別,困惑的你一定會想問,為什麼要介紹這些呢?

因為我們們的作業系統最開始引導部分的程式碼用的是Intel組合語言, NASM編譯器不知道讀者是否使用過,這個編譯器用的就是Intel組合語言,NASM的程式碼格式簡潔,給人感覺使用起來會很舒服。還有YASM編譯器,如果你習慣使用YASM編譯器也沒有問題,畢竟啟動階段到核心程式的跳轉是在記憶體裡完成的,這兩個階段是獨立編譯的不會在編譯的過程中產生依賴關係,您可以放心大膽的使用。這兩種編譯器都可以在CentOS上安裝使用,並且是開源免費的,省得讓微軟、IBM、Google等大叔們找我們的麻煩。

然後當載入程式完成進入核心後,核心會使用GNU C編譯器(GCC),並在核心啟動部分程式碼中嵌入了AT&T組合語言。。。。。沒辦法GNU的彙編編譯器(AS)用的就是AT&T組合語言,它在暫存器前面非要加“%”,書寫起來有些彆扭,就算是在GNU C裡面嵌入組合語言的時候也要保持這種風格,看來只能學著習慣了。

所以,就有了下文書介紹的這些內容。但由於每個人的基礎不一樣,沒有辦法面面俱到,只能介紹一些以後會用到的知識。現在是不是覺得書到用時方恨少了呢?沒關係,我會盡量把彙編程式碼解釋的詳細些。同時還希望讀者們課下多多努力呦~!!~!

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.2.1 AT&T 彙編 與 Intel 彙編

AT&T彙編與Intel彙編在功能上沒有什麼太大的區別,但是,在語法格式,賦值方向,字首等地方卻各自有各自的特點。下面我們就開始介紹:

語法格式

  • Intel 彙編

    mov ax, 0x10  
    

注意:在Intel格式中指令需要使用大寫字母,但是我們使用的nasm卻不需要使用大寫字母,有可能是編譯器的原因,但是我沒有具體驗證過,其他的編譯器可能會有這方面要求

  • AT&T 彙編

    mov $0x10, %ax  
    

賦值方向

  • Intel 彙編

在Intel語法中,第一個是目的運算元,第二個是源運算元,賦值方向從右向左。
例如:add指令

這是Intel技術文件內對add指令的一部分描述:

Adds the destination operand (first operand) and the source operand (second operand) and then stores the result in the destination operand.

翻譯:add指令的目的運算元是第一個運算元,源運算元是第二個運算元,執行的結果會儲存到目的運算元裡。

  • AT&T 彙編

在AT&T語法中,恰恰與Intel相反,第一個是源運算元,第二個是目的運算元,賦值方向從左向右。

呵呵,是不是有點意思。。~我們的作業系統都用到了這兩種組合語言,同學們要好好學,不要偏科呀。我有時候也容易把這兩個語言的賦值方向搞混,所以,你們要格外注意了!!!!

運算元字首

  • Intel 彙編

在Intel語法中,暫存器和立即數不需要新增字首。例如:

mov cx, 12
  • AT&T 彙編

在AT&T語法中,如果要使用暫存器的話,需要在前面加字首“%”;如果要使用立即數的話,需要在前面加“$”。 例如:

mov $12, %cx  

對於符號常數我們可以直接引用,不需要加字首。例如:

values: .long 0x5a5a5a5a  
movl values, %eax  

在這裡,符號values是一個符號常數,執行的結果是將常數0x5a5a5a5a裝入暫存器eax中。

如果在符號前面加字首$,表示引用的是該符號的地址。 例如:

values: .long 0x5a5a5a5a 
movl $values, %ebx  

這句彙編的意思是將values的地址裝入ebx暫存器中。

鎖匯流排字首“lock”,這個lock字首一般是用在多核CPU上的,它的目的是:鎖住系統前端匯流排,防止其他CPU通過前端匯流排訪問記憶體或其他系統硬體資源。像Linux的spinlock功能,就有lock的身影。

跳轉和呼叫指令

  • Intel 彙編

遠端跳轉指令使用的是“jmp”後面跟的是段地址和段內偏移。遠端呼叫指令使用的是“call”後面同樣跟的是段地址和段內偏移,遠端返回指令使用的是“ret”。

    call far section:offset
    jmp far section:offset  
    ret
  • AT&T 彙編

對於遠端跳轉指令和遠端呼叫過程的指令碼需要使用字首“l”,分別為“ljmp”和“lcall”,與“lcall”相對應的返回指令時“lret”。例如:

    lcall $section:$offset  
    ljmp $section:$offset  
    lret

記憶體間接定址格式

  • Intel 彙編

Intel使用“[”、“]”來表示間接定址,格式如下:

    section:[base+index*scale+displacement]

其中scale可以取值1,2,4,其預設值為1。section可以指定任意段暫存器作為段字首,不同情況下的預設段暫存器是不同的。

  • AT&T 彙編

AT&T使用“(”、“)”來表示間接定址,格式如下:

    section:displacement(base,index,scale)  

這裡的section,base,index,scale,displacement與Intel的使用規則相同。

指令的字尾

  • Intel 彙編

Intel中處理記憶體運算元時需要區分運算元大小,位元組:BYTE PTR、字:WORD PTR、雙字:DWORD PTR。例如:

mov al, bl
mov ax, bx
mov eax, dword ptr [ebx]
  • AT&T 彙編

AT&T 語法中大部分指令處理記憶體運算元時需要區分運算元大小,“b”表示byte(一個位元組);“w”表示word(2 個位元組);“l”表示long(4 個位元組)。例如:

movb %bl, %al  
movw %bx, %ax  
movl (%ecx), %eax  

另外,AT&T的跳轉指令標號後的字尾表示跳轉方向,“f”表示向前(forward),“b”表示向後(back)。例如:

jmp 1f  
1:  
jmp 1f  
1:

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.2.2 NASM編譯器

很多人在學習彙編的時候都可能是從I386學起的,使用的編譯器是MASM(MASM 是Microsoft Macro Assembler 的縮寫),其實NASM的格式與MASM總體上是差不多的。

值得說明的有如下幾點:

1. [ ]的使用

在NASM中,如果你引用標籤或者變數名, 都被認為引用的是該名字的地址,如果想訪問他們裡面的內容,必須使用 [ ]。這麼理解如果不太容易記憶的話,那麼你可以把他想象成C語言裡的陣列,陣列名字代表的是它的地址,加上[ ]就代表的是它裡面的內容。是不是一下子就明白了?其實,說不定C的編譯器就是這麼做的呢,畢竟C編譯器會把程式碼編譯成彙編程式碼,然後再編譯成二進位制檔案的,對吧~!

2. $

$表示當前地址——當前行被編譯後的地址。好像不太容易理解對吧,不要緊,請看下面的程式碼:

jmp $  

這句彙編的意思就是死迴圈,轉化成機器碼是E9 FD FF,其中E9的意思是jmp,FD FF是個地址,但是在x86裡面是小端排列的,所以要將數值轉換為地址:FFFD,其實就是-3,這個jmp是相對跳轉,跳轉的地址就是執行完這條命令後,指令暫存器-3的地址,正好這條指令的長度就是3個位元組,所以,又回到了這條指令重新執行。上述過程中,$指的就是地址E9啦。

3. $$

明白了$,那麼,$$是什麼意思呢?

它表示的是一個節(section)的開始處被編譯後的地址(就是這個節的起始地址)。我們一般寫彙編程式的時候,使用一個section就夠了,只有在寫複雜程式的時候,才會用到多個section。section既可以是資料段,也可以是程式碼段。所以,如果把section比喻成函式,還是不太恰當。

提示:

在寫程式的過程中,$-$$會經常被用到,它表示本行程式距離節(section)開始處的相對距離。如果只有一個節(section)的話,那麼他就表示本行程式距離程式頭的距離。在以後我們會把它與times聯合使用,如:

times 512 - ( $ - $$) db 0  

它的意思將在第三章給大家做詳細解釋,同時也希望有興趣的同學,自己學習一下,就當做是一個課後作業吧,嘿嘿。~

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.2.3 彙編呼叫C函式

講解這節的主要目的是在以後的作業系統開發過程中會用到這些內容。

比如:從系統引導過程中的彙編程式跳轉到系統主函式中,或者在中斷處理的彙編程式碼中跳轉到中斷處理函式(傳說中的中斷上部), 這些過程都是從彙編程式跳轉到C程式的,其中不可缺少的有:呼叫約定,引數傳遞方式,函式呼叫方式等。因為這些過程都是在系統核心中,所以,我們講解的是GNU C語言和AT&T組合語言。話不多說,下面讓我們逐一介紹。

函式的呼叫方式

函式的呼叫方式其實沒那麼複雜,基本上就是jmp、call、ret或者他們的變種而已。讓我們先看下面的程式。

int test()
{
    int i = 0;
    i =  1 + 2;
    return i;
}

int main()
{
    test();
    return 0;
}  

這段程式基本上沒有什麼難點,很簡單,對吧?唯一要注意的地方是main函式的返回值,這裡個人建議大家要使用int型別作為主函式的返回值,而不要使用void,或者其他型別。雖然,在主函式執行到return 0之後就跟我們沒有什麼關係了。但是,有的編譯器要求主函式要有個返回值,或者,在某些場合裡,系統環境會用到主函式的返回值。考慮到上述原因,要使用int型別作為主函式的返回值,如果處於某個特殊的或者可預測的環境下,那就無所謂了。

說了這麼多,反彙編一下這段程式碼,看看組合語言是怎麼呼叫test函式的。工具objdump,用於反彙編二進位制程式,它有很多引數,可以反彙編出各類想要的資訊。

objdump工具命令:

objdump -d test

下面是反彙編後的部分程式碼,把相關的系統執行庫等一些與上面C程式不相關的程式碼忽略掉。經過刪減後的反彙編程式碼如下:

0000000000400474 <test>:
  400474:    55                       push   %rbp
  400475:    48 89 e5                 mov    %rsp,%rbp
  400478:    c7 45 fc 00 00 00 00     movl   $0x0,-0x4(%rbp)
  40047f:    c7 45 fc 03 00 00 00     movl   $0x3,-0x4(%rbp)
  400486:    8b 45 fc                 mov    -0x4(%rbp),%eax
  400489:    c9                       leaveq 
  40048a:    c3                       retq   

000000000040048b <main>:
  40048b:    55                       push   %rbp
  40048c:    48 89 e5                 mov    %rsp,%rbp
  40048f:    b8 00 00 00 00           mov    $0x0,%eax
  400494:    e8 db ff ff ff           callq  400474 <test>
  400499:    b8 00 00 00 00           mov    $0x0,%eax
  40049e:    c9                       leaveq 
  40049f:    c3                       retq   

大家先看000000000040048b :這一行,這裡就是主函式,前面的000000000040048b其實是函式main的地址。一共16個數,16 * 4 = 64,對!這就是64位地址寬度啦。

乍一看,有好多個“%”符號,還記得2.2.1節裡講的AT&T彙編語法嗎?這就是那裡面說——引用暫存器的時候要在前面加“%”符號。

還有一些彙編指令的字尾,如:“l”、“q”。“l”的意思是雙字(long型),“q”的意思是四字(64位暫存器的字尾就是這個)。

如果您仔細觀察,是不是會發現有些暫存器rbp,rsp等,感覺會跟ebp和esp有關係呢?答對了,esp暫存器是32位暫存器,而rsp暫存器是64位暫存器。這是Intel對暫存器的一種向下繼承性,從最開始一位元組的al,ah,到兩位元組的ax(16位),四位元組的eax(32位),再到八位元組的rax(64位),暫存器的長度在不斷的擴充套件,對於相關指令的使用,也從“b”、“l”,“q”,也是不斷的向下繼承或擴充套件。

這裡有一條指令leaveq,它等效於 movq %rbp, %rsp; popq %rbp;

callq 400474 這句的意思就是跳轉到test函式裡執行。其實彙編呼叫C函式就這麼簡單,如果把這條callq指令改成jmpq指令也是可以的。這要從call和jmp的區別上說起,call會把在其之後的那條指令的地址壓入棧,在上面反彙編後的程式碼中,就是0000000000400499,然後再跳轉到test函式裡執行。而jmpq就不會把地址0000000000400499壓入棧中。當函式執行完畢,呼叫retq指令返回的時候,會把棧中的返回地址彈出到rip暫存器中,這樣就返回到main函式中繼續執行了。

實現jmpq代替callq的虛擬碼如下所示:

pushq    $0x0000000000400499  
jmpq     400474 <test>  

對於callq 400474 這條指令也可以使用retq來實現。它的實現原理是:指令retq會將棧中的返回地址彈出,並放入到rip暫存器中,然後處理器從rip暫存器所指的地址內取指令後繼續執行。根據這個原理,可以先將返回地址0000000000400499壓入棧中。然後再將test函式的入口地址0000000000400474壓入棧中,接著使用retq指令,以呼叫返回的形式,從main函式“返回”到test函式中。

實現retq代替callq的虛擬碼如下所示:

pushq $0x0000000000400499
pushq $0x0000000000400474
retq  

這些看起來是不是沒有想象的那麼難?其實把彙編的原理掌握清楚了,這些都是可以靈活運用的,希望這段內容能啟發讀者的靈感~!

呼叫約定

對於不同的公司,不同的語言以及不同的需求,都是用各自不同的呼叫約定,而且他們往往差異很大。在IBM相容機對市場進行洗牌後,微軟作業系統和程式設計工具佔據了統治地位,除了微軟之外,還有零星的一些公司,以及開源專案GCC,都各自維護著自己的標準。下面是比較流行的幾款呼叫標準,我們們寫的大多數程式都出自這個標準之一。

  • stdcall

    1.在進行函式呼叫的時候,函式的引數是從右向左依次放入棧中的。

    如:

    int function(int first,int second)  
    

    這個函式的引數入棧順序,首先是引數second,然後是引數first。

    2.函式的棧平衡操作是由被呼叫函式執行的,使用的指令是 retn X,X表示引數佔用的位元組數,CPU在ret之後自動彈出X個位元組的堆疊空間。例如上面的function函式,當我們把function的函式引數壓入棧中後,當function函式執行完畢後,由function函式負責將傳遞給它的引數first和second從棧中彈出來。

    3.在函式名的前面用下劃線修飾,在函式名的後面由@來修飾,並加上棧需要的位元組數。如上面的function函式,會被編譯器轉換為_function@8。

  • cdecl

    1.在進行函式呼叫的時候,和stdcall一樣,函式的引數是從右向左依次放入棧中的。

    2.函式的棧平衡操作是由呼叫函式執行的,這點是與stdcall不同之處。stdcall使用retn X平衡棧,cdecl則使用leave、pop、增加棧指標暫存器的資料等方法平衡棧。

    3.每一個呼叫它的函式都包含有清空棧的程式碼,所以編譯產生的可執行檔案會比呼叫stdcall約定產生的檔案大。

cdecl是GCC的預設呼叫約定。但是,GCC在x64位系統環境下,使用暫存器作為函式呼叫的引數。按照從左向右的順序,頭六個整型引數放在暫存器RDI, RSI, RDX, RCX, R8和R9上,同時XMM0到XMM7用來放置浮點變元,返回值儲存在RAX中,並且由呼叫者負責平衡棧。

  • fastcall

    1.函式呼叫約定規定,函式的引數在可能的情況下使用暫存器傳遞引數,通常是前兩個 DWORD型別的引數或較小的引數使用ECX和EDX暫存器傳遞,其餘引數按照從右向左的順序入棧。

    2.函式的棧平衡操作是由被呼叫函式在返回之前負責清除棧中的引數。

還有很多呼叫規則,如:thiscall、naked call、pascal等,有興趣的讀者可以自己去研究一下。

引數傳遞方式

函式引數的傳遞方式無外乎兩種,一種是通過暫存器傳遞,另一種是通過記憶體傳遞。這兩種傳遞方式在我們平時的開發中並不會被關注,因為不在特殊情況下,這兩種傳遞方式,都可以滿足要求。但是,我們要寫的是作業系統,在作業系統裡面有很多苛刻的環境要求,這使得我們不得不瞭解這些引數傳遞方式,來解決這些問題。

  • 暫存器傳遞

暫存器傳遞就是將函式的引數放到暫存器裡傳遞,而不是放到棧裡傳遞。這樣的好處主要是執行速度快,編譯後生成的程式碼量少。但只有少部分呼叫規定預設是通過暫存器傳遞引數,大部分編譯器是需要特殊指定使用暫存器傳遞引數的。

在X86體系結構下,系統呼叫一般會使用暫存器傳遞,由於作者看過的核心種類有限,也不能確定所有的核心都是這麼處理的,但是Linux核心肯定是這麼做的。因為應用程式的執行空間和系統核心的執行空間是不一樣的,如果想從應用層把引數傳遞到核心層的話,最方便快捷的方法是通過暫存器傳遞引數,否則需要使用很大的周折才能把資料傳遞過去,原因會在以後的章節中詳細講述。

  • 記憶體傳遞

記憶體傳遞引數很好理解,在大多數情況下引數傳遞都是通過記憶體入棧的形式實現的。

在X86體系結構下的Linux核心中,中斷或異常的處理會使用記憶體傳遞引數。因為,在中斷產生後,到中斷處理的上半部,中間的過渡程式碼是用匯編實現的。彙編跳轉到C語言的過程中,C語言是用堆疊儲存引數的,為了無縫銜接,彙編就需要把引數壓入棧中,然後再跳轉到C語言實現的中斷處理程式中。

以上這些都是在X86體系結構下的引數傳遞方式,在X64體系結構下,大部分編譯器都使用的是暫存器傳遞引數。因此,記憶體傳遞和暫存器傳遞的區別就不太重要了。

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.3 C語言

我想絕大部分讀者對C語言並不陌生,但是包括作者在內,沒有人敢說自己對C語言熟練掌握或者精通的。因為它的靈活性僅次於變換末次的組合語言。考慮到個人能力有限,在這裡就以本書作業系統的主要開發語言——GNU C為介紹內容。

這一節涉及到兩方面內容:

  • GNU C內嵌組合語言

  • GNU C語言對標準C語言的擴充套件

以上這些是以後寫作業系統時候會用到的內容,希望大家能夠熟練掌握。

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.3.1 GNU C內嵌組合語言

由於在很多情況下,C語言無法完全代替組合語言,比如:操作某些特殊的CPU暫存器,操作主機板上的某些IO埠或者效能達不到要求等情況下,我們必須在C語言裡面嵌入組合語言,以達到我們的需求。

當需要在C語言裡嵌入組合語言段的時候,對於GNU C我們可以使用它提供的關鍵詞“asm”來實現。先看下面一段程式碼:

#define nop()         __asm__ __volatile__ ("nop    \n\t")

這段內嵌彙編語句是什麼意思呢?根據函式名,讀者們大概也能猜到了。這個正是nop函式的實現,而且這個nop函式也是本作業系統核心裡面的庫函式。讓我們從這行程式碼入手,開始學習GNU C內嵌組合語言。

首先,介紹__ asm __ 和 __ volatile __ 這兩個關鍵字。

__ asm __ 修飾這個是一段組合語言,它是GNU C定義的關鍵字asm的巨集定義(#define __ asm __ asm),它用來宣告一個內嵌彙編表示式。所以,任何一個內嵌彙編表示式都以它開頭,它是必不可少的;如果要編寫符合ANSI C標準的程式碼(即:與ANSI C相容),那就要使用__ asm __;

__ volatile __ 修飾這段程式碼不被編譯器優化,保持程式碼原樣。這個volatile正是我們需要的,如果經過編譯器優化,很有可能將我們寫的程式修改,並達不到預期的執行效果了。如果要編寫符合ANSI C標準的程式碼(即:與ANSI C相容),那就要使用__ volatile __;

然後,該介紹內嵌彙編的語言了。 一般而言,C語言裡嵌入彙編程式碼片段都要比純組合語言寫的程式碼複雜得多。因為這裡有個怎樣分配暫存器、怎樣與C程式碼中的變數融合的問題。為了這個目的,必須要對所用的組合語言做更多的擴充,增加對組合語言的明確指示。

C語言裡的內嵌彙編程式碼可分為四部分,以“:”號進行分隔,其一般形式為:

指令部分輸出部分輸入部分損壞部分

指令部分

第一部分就是組合語言的語句本身,其格式與在組合語言程式中使用的格式基本相同,但也有不同之處。這一部分被稱為“指令部分”說明他是必須有的,而其它各部分則視具體情況而定,如果不需要的話是可以忽略的,所以在最簡單的情況下就與常規的彙編語句基本相同。

指令部分的編寫規則:

當指令列表裡面有多條指令時,可以在一對雙引號中全部寫出,也可將一條或多條指令放在一對雙引號中,所有指令放在多對雙引號中;

  • 如果是將所有指令寫在一對雙引號中,那麼,相鄰兩條指令之間必須用分號”;“或換行符(\n)隔開,如果使用換行符(\n),通常\n後面還要跟一個\t;或者是相鄰兩條指令分別單獨寫在兩行中;
  • 如果將指令放在多對雙引號中,除了最後一對雙引號之外,前面的所有雙引號裡的最後一條指令後面都要有一個分號(;)或(\n)或(\n\t);

在涉及到具體的暫存器時就要在暫存器名前面加上兩個"%"號,以免混淆。

輸出部分

第二部分,緊接在指令部分後面的是“輸出部分”,用來指定當前內嵌彙編語句的輸出表示式。
格式為:“操作約束”(輸出表示式)

用括號括起來的部分,它用於儲存當前內嵌彙編語句的一個輸出值。在輸出表示式內需要用(=)或(+)來進行修飾。 等號(=)與加號(+)是有區別的:等號(=)表示當前表示式是一個純粹的輸出操作,而加號(+)則表示當前表示式不僅僅是一個輸出操作,還是一個輸入操作;不管是等號(=)還是加號(+),所表示的都是可寫,只能用於輸出,只能出現在輸出部分,而不能出現在輸入部分;在輸出部分可以出現多個輸出操作表示式,多個輸出操作表示式之間必須用逗號(,)隔開;

用雙引號括起來的部分,被稱作是:”輸出操作約束“,也可以稱為”輸出約束“;關於約束部分將在後面一起進行講解。

輸入部分

第三部分用來指定當前內嵌彙編語句的輸入;稱為輸入表示式;
格式為:”操作約束“(輸入表示式)

輸入部分同樣也由兩部分組成:由雙引號括起來的部分和由圓括號括起來的部分;這兩個部分對於當前內嵌彙編語句的輸入來說,是必不可少的;用於約束當前內嵌彙編語句中的當前輸入;這個部分也成為”輸入操作約束“,也可以成為是”輸入約束“;與輸出表示式中的操作約束不同的是,輸入表示式中的操作約束不允許指定等號(=)和加號(+)約束,也就是說,它只能是隻讀的;

操作約束

每一個輸入和輸出表示式都必須指定自己的操作約束;約束的型別有:暫存器約束、記憶體約束、立即數約束;

  • 暫存器約束

當輸入或輸出需要藉助於一個暫存器時,需要為其指定一個暫存器約束;

可以直接指定一個暫存器名字;比如:

__asm__ __volatile__("movl %0,%%cr0"::"eax"(cr0));

也可以指定暫存器的縮寫名稱;比如:

__asm__ __volatile__("movl %0,%%cr0"::"a"(cr0));  

如果指定的是暫存器的縮寫名稱,比如:字母a;那麼,GNU C將會根據當前操作表示式的寬度來決定使用%rax、%eax、%ax還是%al;

常用的暫存器約束的縮寫:

r:I/O,表示任何暫存器;
q:I/O,表示使用一個通用暫存器,由GCC在%rax/%eax/%ax/%al、%rbx/%ebx/%bx/%bl、%rcx/%ecx/%cx/%cl或%rdx/%edx/%dx/%dl中選取一個GNU C認為是合適的;
g:I/O,表示使用暫存器或記憶體地址;
m、v、o:I/O,表示使用記憶體地址;
a、b、c、d:I/O,分別表示使用%rax/%eax/%ax/%al、%rbx/%ebx/%bx/%bl、%rcx/%ecx/%cx/%cl或%rdx/%edx/%dx/%dl;
D、S:I/O,表示使用%rdi/%edi/%di或%rsi/%esi/%si;
f:I/O,表示使用浮點暫存器;
i:I/O,表示使用一個整數型別的立即數;
F:I/O,表示使用一個浮點型別的立即數;

  • 記憶體約束

如果一個輸入/輸出操作表示式,表現為一個記憶體地址(指標變數),不想借助於任何暫存器,則可以使用記憶體約束;

例如:

__asm__ __volatile__ ("sgdt %0":"=m"(__gdt_addr)::);  
__asm__ __volatile__ ("lgdt %0"::"m"(__gdt_addr));

記憶體約束使用約束名“m”,表示的是使用系統支援的任何一種記憶體方式,不需要藉助於暫存器;

  • 立即數約束

如果一個輸入操作表示式是一個數字常數,不想借助於任何暫存器或記憶體,則可以使用立即數約束;立即數在表示式中只能作為右值使用,對於使用立即數約束的表示式而言,只能放在輸入部分;

比如:

__asm__ __volatile__("movl %0,%%ebx"::"i"(50));  

使用約束名“i”表示輸入表示式是一個整數型別的立即數,不需要藉助於任何暫存器,只能用於輸入部分;使用約束名“F”表示輸入表示式是一個浮點數型別的立即數,不需要藉助於任何暫存器,只能用於輸入部分;

  • 修飾符

等號(=)和加號(+)作為修飾符,已經在輸出部分講解過了,這裡主要講解“&”符。

符號“&”也只能寫在輸出表示式的約束部分,用於約束暫存器的分配,但是隻能寫在約束部分的第二個字元的位置上。因為,第一個字元的位置我們要寫(=)或(+)。

用符號“&”進行修飾,就表示不得為任何輸入操作表示式分配與此輸出操作表示式相同的暫存器;其原因是,GNU C會先使用輸出值對被修飾符“&”修飾的輸出操作表示式進行賦值,然後,才對輸入操作表示式進行賦值。這樣的話,如果不使用修飾符“&”對輸出操作表示式進行修飾,一旦後面的輸入操作表示式使用了與輸出操作表示式相同的暫存器,就會產生輸入和輸出資料混亂的情況;

值得注意的是:如果輸出操作表示式的暫存器約束被指定為某個暫存器,而在輸入操作表示式的暫存器約束中至少存在一個可選約束(意思是GNU C可以從多個暫存器中選取一個,或使用非暫存器方式)時,比如“q”、“r”或“g”時,此輸出操作表示式使用符號“&”修飾才有意義!如果為所有的輸入操作表示式指定了固定的暫存器,或使用記憶體/立即數約束時,則此輸出操作表示式使用符號“&”修飾沒有任何意義;

如果沒有使用修飾符“&”修飾輸出操作表示式會是什麼樣子呢?那就意味著GNU C會先把輸入操作表示式的值輸入到選定的暫存器中,然後經過處理,最後才用輸出值填充對應的輸出操作表示式;

序號佔位符

對於序號佔位符,在內嵌彙編語句中最多隻能有10個輸入/輸出操作表示式,這些操作表示式按照他們被列出來的順序,依次被賦予編號0至9;對於佔位符中的數字而言,與這些編號是一一對應的;比如:佔位符%0對應編號為0的操作表示式,佔位符%1對應編號為1的操作表示式,依次類推;

因為在佔位符前面必須要有一個百分號“%”,為了與暫存器區別開來,必須要在指令列表裡列出的暫存器名稱前面使用兩個百分號(%%)修飾。GNU C對佔位符進行編譯的時候,會將每一個佔位符替換為對應的輸入/輸出操作表示式所指定的暫存器/記憶體/立即數;

擴充套件:對於操作表示式,根據需要也可以指定為位元組操作或者字操作。對操作表示式進行的位元組操作,預設為對其最低位元組進行操作。字操作也是一樣。不過,在一些特殊的操作中,對操作表示式進行位元組操作時也允許明確指出是對哪一個位元組操作,此時在“%”與序號佔位符之間插入一個“b”表示最低位元組,插入一個“h”表示次低位元組。

損壞部分

有的時候,當您想通知GNU C當前內嵌彙編語句可能會對某些暫存器或記憶體進行修改,希望GNU C在編譯時能夠將這一點考慮進去;那麼您就可以在損壞部分宣告這些暫存器或記憶體;

  • 暫存器修改通知

這種情況一般發生在一個暫存器出現在指令列表中,但又不是輸入/輸出操作表示式所指定的,也不是在一些輸入/輸出操作表示式中使用“r”或“g”約束時由GNU C選擇的。同時,此暫存器被指令列表中的指令所修改,而這個暫存器只供當前內嵌彙編語句使用;比如:

__asm__("movl %0,%%ecx"::"a"(__tmp):"cx");  

這個內嵌彙編語句中,%ecx出現在指令列表中,並且被指令修改了,但是卻未被任何輸入/輸出操作表示式所指定。所以,必須要在損壞部分指定“cx”。

在損壞部分宣告這些暫存器的方法很簡單,只需要將暫存器的名字用雙引號括起來就可以了;如果要宣告多個暫存器,則相鄰兩個暫存器名字之間用逗號隔開,這點和輸入/輸出部分是一樣的。

注意:如果在輸入/輸出操作表示式中指定暫存器;或為一些輸入/輸出操作表示式使用“q”/“r”/“g”約束,讓GNU C為你選擇一個暫存器時;GNU C對這些暫存器的狀態是非常清楚的,它知道這些暫存器是被修改的,根本不需要在損壞部分宣告它們;但除此之外,GNU C對剩下的暫存器中哪些會被當前內嵌彙編語句所修改卻一無所知;所以,如果真的在當前內嵌彙編指令中修改了它們,最好在損壞部分宣告它們,讓GNU C針對這些暫存器做相應的處理;否則,有可能會造成暫存器不一致,從而造成程式執行錯誤;

暫存器名稱串:
“al”/“ax”/“eax”/“rax”:代表暫存器%rax
“bl”/“bx”/“ebx”/“rbx”:代表暫存器%rbx
“cl”/“cx”/“ecx”/“rcx”:代表暫存器%rcx
“dl”/“dx”/“edx”/“rdx”:代表暫存器%rdx
“si”/“esi”/“rsi”:代表暫存器%rsi
“di”/“edi”/“rdi”:代表暫存器%rdi

如果要使用暫存器名稱串,只需要使用“ax”,“bx”,“cx”,“dx”,“si”,“di”就可以了。

如果在一個內嵌彙編語句的損壞部分向GNU C宣告瞭某個暫存器會發生改變。那麼,在GNU C編譯時,如果發現這個被宣告的暫存器的內容在此內嵌彙編之後還要繼續使用,GNU C會首先將此暫存器的內容儲存起來,然後在此內嵌彙編語句的相關程式碼生成之後,再將其內容恢復。

另外需要注意的是:如果在損壞部分宣告瞭一個暫存器,那麼這個暫存器將不能再被用作當前內嵌彙編語句的輸入/輸出操作表示式的暫存器約束,如果輸入/輸出操作表示式的暫存器約束被指定為“q”/“r”/“g”,GNU C也不會選擇已經被宣告在損壞部分中的暫存器;

  • 記憶體修改通知

除了暫存器的內容會被修改之外,記憶體的內容同樣也會被修改。如果一個內嵌彙編語句的指令列表中的指令對記憶體進行了修改,或者在此內嵌彙編出現的地方,記憶體內容可能發生改變,而且被改變的記憶體地址沒有在其輸出操作表示式中使用“m”約束。這種情況下,您需要在損壞部分使用字串“memory”向GNU C宣告。

如果一個內嵌彙編語句的損壞部分存在“memory”,那麼GNU C會保證在此內嵌彙編之前,如果某個記憶體的內容被裝入了暫存器,那麼,在這個內嵌彙編之後,如果需要使用這個記憶體處的內容,就會直接到這個記憶體處重新讀取,而不是使用被存放在暫存器中的拷貝;因為這個時候暫存器中的拷貝很可能已經和記憶體處的內容不一致了。

  • 標誌暫存器修改通知

當一個內嵌彙編中包含影響標誌暫存器r|eflags的條件,那麼也需要在損壞部分中使用“cc”來向GNU C宣告這一點。

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.3.2 GNU C對標準C語言的擴充套件

為了方便使用,GNU C在標準C語言的基礎上進行了部分方便開發的擴充套件。這裡講解一些開發中可能會用到的,或者使用頻率比較高的內容。

  1. 零長度陣列和變數長度陣列

    GNU C 允許使用零長度陣列,比如:

    char data[0];  
    

    GNU C 允許使用一個變數定義陣列的長度如:

    int n=0;
    scanf("%d",&n);
    int array[n];  
    
  2. case 範圍

    GNU C支援 case x...y這樣的語法,[x,y]之間數均滿足條件。

    case 'a'...'z':  /*from 'a' to 'z'*/
    break;  
    
  3. 語句表示式
    GNU C 把包含在括號中的複合語句看作是一個表示式,稱為語句表示式。

     #define min_t(type,x,y)\
             ({type __x=(x); type __y=(y);__x<__y?__x:__y;})
    

    這種寫法可以避免

     #define min_t(x,y) ((x)<(y)?(x):(y))  
    

    在min_t(x++,++y)中出現的副作用

  4. typeof 關鍵字

    typeof(x)可以獲得x的型別藉助typeof關鍵字我們可以重新定義min_t:

    #define min_t(x,y)\
        ({typeof(x) __x=(x); typeof(y) __y=(y);__x<__y?__x:__y;})  
    
  5. 可變引數巨集

    GNU C中巨集也支援可變引數

    #define pr_debug(fmt,arg...) \
            printk(fmt,##arg)  
    

    這裡,如果可變引數被忽略或為空,“##”操作將使前處理器去掉它前面的那個逗號。如果你在巨集呼叫時,確實提供了一些可變引數,GNU C也會工作正常,它會把這些可變引數放到逗號的後面。

  6. 標號元素

    標準C要求陣列或結構體的初始化值必須以固定的順序出現,在GNU C中,通過指定索引或結構體成員名,允許初始化以任意順序出現。

    unsigned char data[MAX] =
    {
             [0]=10,
             [10]=100,
    };
    
    
    struct file_operations ext2_file_operations=
    {
            open:ext2_open,
            close:ext2_close,
    };
    

    在linux 2.6中推薦如下方式:

    struct file_operations ext2_file_operations=
    {
         .read=ext2_read,
         .write=ext2_write,
    };  
    
  7. 當前函式名

    GNU C中預定義兩個標誌符儲存當前函式的名字,__ FUNCTION __ 儲存函式在原始碼中的名字, __ PRETTY__ FUNCTION __儲存帶語言特色的名字。在C函式中這兩個名字是相同的.

    void func_example()
    {
         printf("the function name is %s",__FUNCTION__);
    }
    

    在C99中支援__ func __ 巨集,因此建議使用 __ func __ 替代 __ FUNCTION __ 。

  8. 特殊屬性宣告

    GNU C 允許宣告函式、變數和型別的特殊屬性,以便進行手工的程式碼優化和定製。如果要指定一個屬性宣告,只需要在宣告後新增__ attribute __((ATTRIBUTE))。其中ATTRIBUTE為屬性說明,如果存在多個屬性,則以逗號分隔。GNU C 支援noreturn,noinline, always_inline, pure, const, nothrow, format, format_arg, no_instrument_function, section, constructor, destructor, used, unused, deprecated, weak, malloc, alias warn_unused_result nonnull等十個屬性。

    noreturn屬性作用於函式,表示該函式從不返回。這會讓編譯器優化程式碼並消除不必要的警告資訊。例如:

    #define ATTRIB_NORET __attribute__((noreturn)) ....
    asmlinkage NORET_TYPE void do_exit(long error_code) ATTRIB_NORET;  
    

    packed屬性作用於變數和型別,用於變數或結構域時,表示使用最小可能的對齊,用於列舉、結構或聯合型別時表示該型別使用最小的記憶體。如對於結構體,就是它告訴編譯器取消結構在編譯過程中的優化對齊,按照實際佔用位元組數進行對齊。例如:

    struct example_struct
    {
             char a;
             int b;
             long c;
    } __attribute__((packed));    
    

    regparm屬性用於指定最多可以使用n個暫存器(eax, edx, ecx)傳遞引數,n的範圍是0~3,超過n時則將引數壓入棧中(n=0表示不用暫存器傳遞引數)。

    注意:以上這些屬性都是在X86處理器體系結構下的,在X64體系結構下,大部分內容都是同樣有效的。但是,這裡要注意regparm屬性,由於在X64體系結構下,GUN C的預設呼叫約定使用暫存器傳遞引數。所以,如果regparm屬性裡使用的暫存器個數超過3個,也仍然會使用其他暫存器來傳遞引數。這一點要遵循X64體系結構的呼叫約定。

    下面可以看一個例子。

    int q = 0x5a;
    int t1 = 1;
    int t2 = 2;
    int t3 = 3;
    int t4 = 4;
    #define REGPARM3 __attribute((regparm(3)))
    #define REGPARM0 __attribute((regparm(0)))
    void REGPARM0 p1(int a)
    {
         q = a + 1;
    }
    
    
    void REGPARM3 p2(int a, int b, int c, int d)
    {
         q = a + b + c + d + 1;
    }
    
    
    int main()
    {
        p1(t1);
        p2(t1,t2,t3,t4);
        return 0;
    }  
    

    使用objdump命令反彙編,相關命令如下:

    objdump -D 可執行程式  
    

    其中-D選項用於反彙編所有的程式段,包括:程式碼段、資料段、只讀資料段以及一些系統段等等。而-d命令只反彙編程式碼段的內容。

    反彙編後的關鍵程式碼如下:

    Disassembly of section .text:
    0000000000400474 <p1>:
      400474:    55                       push   %rbp
      400475:    48 89 e5                 mov    %rsp,%rbp
      400478:    89 7d fc                 mov    %edi,-0x4(%rbp)
      40047b:    8b 45 fc                 mov    -0x4(%rbp),%eax
      40047e:    83 c0 01                 add    $0x1,%eax
      400481:    89 05 3d 04 20 00        mov    %eax,0x20043d(%rip)        # 6008c4 <q>
      400487:    c9                       leaveq 
      400488:    c3                       retq   
    
    
    0000000000400489 <p2>:
      400489:    55                       push   %rbp
      40048a:    48 89 e5                 mov    %rsp,%rbp
      40048d:    89 7d fc                 mov    %edi,-0x4(%rbp)
      400490:    89 75 f8                 mov    %esi,-0x8(%rbp)
      400493:    89 55 f4                 mov    %edx,-0xc(%rbp)
      400496:    89 4d f0                 mov    %ecx,-0x10(%rbp)
      400499:    8b 45 f8                 mov    -0x8(%rbp),%eax
      40049c:    8b 55 fc                 mov    -0x4(%rbp),%edx
      40049f:    8d 04 02                 lea    (%rdx,%rax,1),%eax
      4004a2:    03 45 f4                 add    -0xc(%rbp),%eax
      4004a5:    03 45 f0                 add    -0x10(%rbp),%eax
      4004a8:    83 c0 01                 add    $0x1,%eax
      4004ab:    89 05 13 04 20 00        mov    %eax,0x200413(%rip)        # 6008c4 <q>
      4004b1:    c9                       leaveq 
      4004b2:    c3                       retq   
    
    
    00000000004004b3 <main>:
      4004b3:    55                       push   %rbp
      4004b4:    48 89 e5                 mov    %rsp,%rbp
      4004b7:    53                       push   %rbx
      4004b8:    8b 05 0a 04 20 00        mov    0x20040a(%rip),%eax        # 6008c8 <t1>
      4004be:    89 c7                    mov    %eax,%edi
      4004c0:    e8 af ff ff ff           callq  400474 <p1>
      4004c5:    8b 0d 09 04 20 00        mov    0x200409(%rip),%ecx        # 6008d4 <t4>
      4004cb:    8b 15 ff 03 20 00        mov    0x2003ff(%rip),%edx        # 6008d0 <t3>
      4004d1:    8b 1d f5 03 20 00        mov    0x2003f5(%rip),%ebx        # 6008cc <t2>
      4004d7:    8b 05 eb 03 20 00        mov    0x2003eb(%rip),%eax        # 6008c8 <t1>
      4004dd:    89 de                    mov    %ebx,%esi
      4004df:    89 c7                    mov    %eax,%edi
      4004e1:    e8 a3 ff ff ff           callq  400489 <p2>
      4004e6:    b8 00 00 00 00           mov    $0x0,%eax
      4004eb:    5b                       pop    %rbx
      4004ec:    c9                       leaveq 
      4004ed:    c3                       retq   
      4004ee:    90                       nop
      4004ef:    90                       nop
    
    
    Disassembly of section .data:
    00000000006008c0 <__data_start>:
      6008c0:    00 00                    add    %al,(%rax)
        ...
    
    
    00000000006008c4 <q>:
      6008c4:    5a                       pop    %rdx
      6008c5:    00 00                    add    %al,(%rax)
        ...
    
    
    00000000006008c8 <t1>:
      6008c8:    01 00                    add    %eax,(%rax)
        ...
    
    
    00000000006008cc <t2>:
      6008cc:    02 00                    add    (%rax),%al
        ...
    
    
    00000000006008d0 <t3>:
      6008d0:    03 00                    add    (%rax),%eax
        ...
    
    
    00000000006008d4 <t4>:
      6008d4:    04 00                    add    $0x0,%al
        ...
    

    如果讀者還記得2.2.3節中,關於GCC基於X64體系結構的呼叫約定的話,那就很容易可以看出,函式p1和p2都使用暫存器傳遞引數,順序就是RDI, RSI, RDX, RCX,這些細節已經跟regparm的規定完全不一致了。所以,在這裡作者覺得,regparm已經不起作用了。

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.1 虛擬機器及開發系統平臺介紹

任何軟體開發都需要有開發環境,作業系統同樣也需要有開發環境。

因為開發作業系統使用的語言是彙編和C語言再加上一些靈活多變的設計思想,導致開發作業系統不會像開發應用程式那樣有很多的除錯軟體和開發庫的支援。一切都需要我們從零做起,這也是他的魅力之一。

為了解決版權問題和一些收費軟體的麻煩,並隨著開源免費軟體大軍逐漸壯大。Linux家族的作業系統是首當其衝的選擇。如果您的系統平臺是Windows或Mac OS的話,VMware虛擬機器以它的穩定、方便、靈活、功能強大,深受開發者們喜愛。我們不會使用它的複雜功能,以方便自己的使用為原則,任何虛擬機器都是可以接受的。

對於Bochs的選擇也不是絕對的,如果您手頭有其他的可調式虛擬機器,只要能設定斷點、檢視記憶體、檢視暫存器狀態、反彙編記憶體程式碼等基本功能就可以。

希望讀者們能夠根據自己的喜好,搭建出一個順手的開發環境。

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.1.1 VMware的安裝

VMware這款虛擬機器大家並不陌生,基本上是屬於開發必備軟體了。

如果您正在使用的是Linux的某個發行版,請跳過這一節,下面內容是針對Windows使用者進行的介紹。

在這裡作者使用的作業系統是Win7 SP1,但是編譯環境選擇的是Linux的某個發行版。所以,需要一款虛擬機器來虛擬硬體平臺。VMware是當下的主流選擇,目前的最新版是VMware Workstation 10或VMware Player 7。這些版本對於開發作業系統已經足夠用了,如果使用的是老版本,影響也不會太大,只要能順利安裝上一款Linux發行版系統,並且可以掛載USB裝置就可以了。假如調皮的你想換一款虛擬機器也是沒有問題的,只要能滿足上述需求,那麼這款虛擬機器就足夠了,一切隨您心意。

注意事項:

1.在安裝完VMware後,使用優化軟體對電腦進行清理和優化時要特別注意。在優化過程中,優化軟體可能會關閉VMware的一些自動開啟的系統服務,所以有的時候無法連線網路和掛載USB裝置,這點讀者要注意一下。

解決辦法是:在執行欄內輸入services.msc把相關的VMware服務開啟,如果不知道該開啟那個的話,那就索性把VMware的相關服務全部開啟。

2.對於win7系統來說,執行VMware的時候儘量以管理員許可權執行,否則容易報錯。

以上兩點僅供參考——紙上得來終覺淺,欲知此事要躬行。

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.1.2 系統平臺——CentOS 6

系統安裝

對於作業系統,只要是Linux的發行版就可以,關鍵還是用著要順手,舒服。

如果想問為什麼要選擇CentOS作為作業系統而不選擇Ubuntu這種更新的作業系統?
其實,它們沒有什麼更高明之處,主要是工作習慣使用CentOS了。雖然大部分軟體不是最新的,但是對於企業來說,穩定更重要。而且,CentOS是redhat的免費版,提供的維護和更新時間更長。操作介面簡單、方便, 對於其他Linux發行版系統日漸複雜絢麗的操作介面和風格。CentOS對於初學者上手會比較快,消耗的系統資源低。

一些開發涉及的相關命令

對於開發作業系統主要使用的工具或命令屈指可數:

  • 編譯器或編譯工具:

gcc:GUN C語言編譯器,支援C99標準並且有擴充套件;
as:GAS組合語言編譯器,用於編譯AT&T格式的組合語言;
ld:連結器,用於將程式碼程式編譯後的中間檔案,連結成可執行檔案;
nasm:NASM編譯器,用於編譯Intel格式的組合語言;
make:編譯工具,根據編譯指令碼檔案的內容,編譯程式。

  • 系統工具或命令:

dd:用指定大小的塊拷貝一個檔案,並在拷貝的同時進行指定的轉換;
mount:掛載命令,用於將U盤,光碟機,軟盤等儲存裝置,掛載到指定路徑上。
umount:解除安裝命令,與mount命令相反。
cp:拷貝命令,用於將檔案或目錄拷貝到指定目錄下。
sync:同步資料命令,用於將檔案同步回寫到儲存裝置上。
rm:刪除命令,刪除指定檔案或目錄。
objdump:反彙編命令,用於將可執行檔案反編譯成組合語言。
objcopy:檔案提取命令,用於將原始檔中的內容,提取到目標檔案中。

這些工具基本上預設安裝的Linux發行版系統裡面都是預設就會安裝的。如果您的電腦裡沒有相關的命令,您也不需要擔心,根據相關的Linux發行版系統提供的更新軟體工具(yum、apt-get等)就能更新到最新版本。對於這些工具的版本要求幾乎沒有。因為我們使用的是最原始的功能——將程式碼編譯成二級制檔案,就連常用的軟體庫都不需要。我們要從零做起!

注意事項:

  • 對於VMware分配的記憶體和硬碟空間不用太多,硬碟可以配置成動態增長的,這樣會節省很大空間。

  • 對於Linux發行版系統的SWAP分割槽,這個對我們來說可有可無。swap分割槽是對少於4G實體記憶體的系統環境,有大記憶體開銷的時候才起作用(記憶體使用量低於記憶體管理單元的臨界值)。

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

2.1.3 Bochs——一個可除錯的虛擬機器

這是一個開源的可調式虛擬機器,在開發作業系統初期階段,可以將我們的程式執行在bochs虛擬機器上面。該虛擬機器可以設定記憶體斷點,檢視暫存器狀態或資料,將記憶體地址範圍內的內容反編譯成組合語言,檢視記憶體區的資料等功能。

Bochs環境安裝

對於bochs的版本還是相對新一些比較好,因為這款軟體還在完善中,新版本會解決不少bug,對於開發系統核心級軟體來說,可能會比較重要。因此,作者選擇的是最新的bochs-2.6.6。詳細的下載和編譯細節在這裡就不多講解了,這個問題就交給讀者自己解決了,O(∩_∩)O哈!

注意:

在這裡分享一下configure配置資訊,僅供參考。

./configure --with-x11 --with-wx --enable-debugger --enable-disasm --enable-all-optimizations --enable-readline --enable-long-phy-address --enable-debugger-gui  --enable-ltdl-install --enable-idle-hack --enable-plugins --enable-a20-pin --enable-x86-64 --enable-smp --enable-cpu-level=6 --enable-large-ramfile --enable-repeat-speedups --enable-fast-function-calls  --enable-handlers-chaining  --enable-trace-linking --enable-configurable-msrs --enable-show-ips --enable-cpp --enable-debugger-gui --enable-iodebug --enable-readline --enable-logging --enable-assert-checks --enable-fpu --enable-vmx=2 --enable-svm --enable-3dnow --enable-alignment-check  --enable-monitor-mwait --enable-avx  --enable-evex --enable-x86-debugger --enable-pci --enable-usb --enable-voodoo  

看著有些多,因為不清楚到底會用到多少功能,索性就全部新增上去了。記得當時編譯的時候還有幾個小錯誤,需要把字尾名為.cpp的檔案複製一個.cc的檔案副本就可以繼續編譯了。詳細命令如下:

cp misc/bximage.cpp  misc/bximage.cc  
cp iodev/hdimage/hdimage.cpp iodev/hdimage/hdimage.cc  
cp iodev/hdimage/vmware3.cpp iodev/hdimage/vmware3.cc  
cp iodev/hdimage/vmware4.cpp iodev/hdimage/vmware4.cc  
cp iodev/hdimage/vpc-img.cpp iodev/hdimage/vpc-img.cc  

Bochs執行環境配置

下面的工作和VMware是一樣的——建立一個虛擬機器環境。這個環境是以配置檔案的形式存在的,在bochs資料夾內會提供一個預設的系統環境配置檔案.bochsrc。裡面有配置選項的說明和例項供使用者參考使用。讀者們可以根據.bochsrc檔案稍作修改,就可以配置出一個自己的虛擬機器環境。

下面是配置檔案的詳細資訊:

# configuration file generated by Bochs
plugin_ctrl: unmapped=1, biosdev=1, speaker=1, extfpuirq=1, parallel=1, serial=1, iodebug=1
config_interface: textconfig
display_library: x
#memory: host=2048, guest=2048
romimage: file="/usr/local/share/bochs/BIOS-bochs-latest"
vgaromimage: file="/usr/local/share/bochs/VGABIOS-lgpl-latest"
boot: floppy
floppy_bootsig_check: disabled=0
floppya: type=1_44, 1_44="boot.img", status=inserted, write_protected=0
# no floppyb
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=none
ata0-slave: type=none
ata1: enabled=1, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata1-master: type=none
ata1-slave: type=none
ata2: enabled=0
ata3: enabled=0
pci: enabled=1, chipset=i440fx
vga: extension=vbe, update_freq=5

cpu: count=2:2:2, ips=4000000, quantum=16, model=corei7_haswell_4770,reset_on_triple_fault=1, cpuid_limit_winnt=0, ignore_bad_msrs=1, mwait_is_nop=0, msrs="msrs.def"

cpuid: x86_64=1,level=6, mmx=1, sep=1, simd=avx512, aes=1, movbe=1, xsave=1,apic=x2apic,sha=1,movbe=1,adx=1,xsaveopt=1,avx_f16c=1,avx_fma=1,bmi=bmi2,1g_pages=1,pcid=1,fsgsbase=1,smep=1,smap=1,mwait=1,vmx=1
cpuid: family=6, model=0x1a, stepping=5, vendor_string="GenuineIntel", brand_string="Intel(R) Core(TM) i7-4770 CPU (Haswell)"

print_timestamps: enabled=0
debugger_log: -
magic_break: enabled=0
port_e9_hack: enabled=0
private_colormap: enabled=0
clock: sync=none, time0=local, rtc_sync=0
# no cmosimage
# no loader
log: -
logprefix: %t%e%d
debug: action=ignore
info: action=report
error: action=report
panic: action=ask
keyboard: type=mf, serial_delay=250, paste_delay=100000, user_shortcut=none
mouse: type=ps2, enabled=0, toggle=ctrl+mbutton
speaker: enabled=1, mode=system
parport1: enabled=1, file=none
parport2: enabled=0
com1: enabled=1, mode=null
com2: enabled=0
com3: enabled=0
com4: enabled=0

megs: 2048

相關項說明:

  • boot: floppy 相當於BIOS的啟動項,這裡是軟盤啟動
  • floppya: type=1_44, 1_44="boot.img", status=inserted, write_protected=0 插入軟盤的型別:1.44MB,軟盤映象檔案的檔名:boot.img,狀態:已經插入,防寫:關閉
  • cpu、cpuid 這兩個欄位描述的是處理器的相關資訊,可以根據個人需求自行設定,bochsrc檔案也有詳細的說明
  • megs: 2048 虛擬機器使用的實體記憶體量,以MB為單位,目前的bochs上限是2048MB(2GB),這裡請注意,如果CentOS內沒有足夠的記憶體供Bochs使用的話,Bochs會執行失敗。
    失敗的提示資訊如下所示:

    terminate called after throwing an instance of 'std::bad_alloc'
    what():  std::bad_alloc
    Aborted (core dumped)
    

Bochs相關的除錯命令

指令 解釋說明 舉例
b address 在某實體地址上設定斷點 b 0x7c00
c 繼續執行,直到遇到斷點 c
s 單步執行 s
info cpu
r
sreg
reg
檢視暫存器資訊 info cpu
r
sreg
creg
xp /nuf addr 檢視記憶體實體地址內容 xp /10bx 0x100000
x /nuf addr 檢視線性地址內容 x /40wd 0x90000
u start end 反彙編一段記憶體 u 0x100000 0x100010

附加解釋: n 為顯示的單元個數; u 為顯示單元的大小(b:Byte、h:Word、w:DWord、g:QWrod(四位元組)); f 為顯示的格式(x:十六進位制、d:十進位制、t:二進位制、c:字元);

以上這些命令都會在以後的系統開發中使用到,命令雖然少也很簡單。如果沒有它,在一開始就讓我們的程式碼執行在物理平臺上的話,一旦出現問題,會讓分析錯誤變得舉步艱難。就連檢視暫存器狀態和記憶體區資料都會變得茫然失措、無從下手,甚至絕望。考慮到這些原因,先讓我們的程式在bochs裡執行一段時間,等達到一定規模以後再把它移植到物理平臺上執行。朋友們,讓我們等待它破繭而出的時刻吧~~~!!

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

相關文章