1. 程式人生 > >2018-2019-1 20189219《Linux核心原理與分析》第五週作業

2018-2019-1 20189219《Linux核心原理與分析》第五週作業

以前學習計算機作業系統的時候也學習過系統呼叫的三層機制,但是當時都是純理論學習,沒有親身實踐,很多都理解的比較模糊,這裡藉助老師的方法使用內嵌彙編加深理解。

系統呼叫

要想理解系統呼叫的具體含義,我們需要先了解使用者態核心態中斷三個概念。簡單的來說:

在使用者態下,我們可以執行使用者態程序,而在核心態下,我們不僅僅可以執行使用者態下的程序,還可以執行更高級別的核心態程序。如果在使用者態下我們需要使用核心態下的程序,那麼我們可以藉助中斷操作來從使用者態進入核心態。

使用者態程序

上述的使用者態可能概念比較模糊,這裡我們舉個栗子:

  • C
#include<stdio.h>
int main(){
    int input,output,temp;
    input=1;
    temp=0;
    output=input;
    printf("%d%d",temp,output);
    return 0;
}
  • 內嵌彙編
#include<stdio.h>
int main(){
    int input,output,temp;
    input=1;
    asm volatile(
        "movl $0, %%eax\n\t"
        "movl %%eax, %1\n\t"
        "movl %2, %%eax\n\t"
        "movl %%eax, %0\n\t"
        :"=m"(output),"=m"(temp)
        :"r"(input)
        :"eax"
    );
    printf("%d%d",temp,output);
    return 0;
}

這個例子是書上用來講解內嵌彙編程式碼的寫法,這裡我們就不再過多討論。首先我們觀察C語言下的程式碼,我們發現這就是個簡單的賦值操作,核心程式碼為:

temp=0;
output=input;

觀察其彙編程式碼,我們得知這裡使用了eax暫存器,將0和intput變數值傳遞給記憶體中的temp和output,那麼我們把這個過程叫做使用者態。可以看到,這裡的賦值操作無需藉助任何的高階許可權,申明變數之後直接賦值就好了。那麼什麼叫做核心態?

核心態程序

這裡我們依然以具體的C語言程式碼為例。

#include<stdio.h>
#include<time.h>
int main()
{
    time_t tt;
    struct tm *t;
    __asm__ __volatile__(
        "movl $0, %%ebx\n\t"
        "movl $0xd, %%eax\n\t"
        "int $0x80\n\t"
        "movl %%eax, %0\n\t"
        :"=m"(tt)
    );
    t = localtime(&tt);
    printf("time:%d/%d/%d\n",t->tm_year+1900,t->tm_mon+1,t->tm_mday);
    return 0;
}

當你的程式需要使用到系統函式time()的時候,我們把這個系統函式以及它的執行過程叫做核心態,因為它使用了高級別指令time。當然,這裡的

"movl $0, %%ebx\n\t"
"movl $0xd, %%eax\n\t"
"int $0x80\n\t"
"movl %%eax, %0\n\t"
:"=m"(tt)

語句就包含了從使用者態使用系統呼叫這一特殊中斷陷入核心態的整個過程。

中斷以及系統呼叫

想要從使用者態進入到核心態不是平白無故就能實現的,這裡我們需要藉助中斷的力量。這裡我們僅僅討論系統呼叫這一特殊中斷。

系統呼叫中斷處理過程

  • SAVE_ALL
    當中斷標誌出現時,先儲存使用者態的cs:eip&ss:esp&eflags(current)至核心棧中,然後將系統呼叫的中斷服務程式的入口載入到cs:eip中,把當前的核心態ss:esp也載入到cpu中。這樣,當前cpu的下一條指令即為中斷程式的入口。在linux中使用int 0x80語句來觸發系統呼叫的執行,即執行中斷向量0x80所對應的服務system_call
  • restore_all & INTERRUPT_RETURN
    中斷結束後,執行restore_all & INTERRUPT_RETURN,此時將pop之前儲存的使用者態的cs:eip&ss:esp&eflags(current),從而恢復到之前的使用者態中。
    至此,系統呼叫過程就結束了。

    API

    對應上述的彙編程式碼,我們給出C語言程式碼
#include<stdio.h>
#include<time.h>
int main()
{
        time_t tt;
        struct tm *t;
        tt = time(NULL);
        t = localtime(&tt);
        printf("time:%d/%d/%d\n",t->tm_year+1900,t->tm_mon,t->tm_mday);
        return 0;
}

這裡tt = time(NULL);就包含了整個內嵌彙編的所有含義,這就是API的作用。API的全稱為應用程式程式設計介面,是一個函式定義,如同這裡的time(),其實libc函式庫早已定義好它的系統呼叫封裝例程,所以我們才可以直接拿來用,這就是我們常說的庫函式。

link函式的系統呼叫及引數傳遞

瞭解了完整的系統呼叫的三層機制和API使用,我們挑選了編號為9的link庫函式進行實驗。

  • C
#include<stdio.h>
#include<unistd.h>
int main(){
    int ret;
    char * oldpath = "time-asm";
    char * newpath = "timetest";
    ret = link(oldpath,newpath);
    if(ret==0)printf("link successfully");
    else printf("Unable to link the file");
    return 0;
}
  • 內嵌彙編
#include<stdio.h>
#include<unistd.h>
int main(){
    int ret;
    char * oldpath = "time-asm";
    char * newpath = "timetest-asm";
    asm volatile(
        "movl %2, %%ecx\n\t"
        "movl %1, %%ebx\n\t"
        "movl $0x09, %%eax\n\t"
        "int $0x80\n\t"
        "movl %%eax, %0"
        :"=m"(ret)
        :"b" (oldpath),"c"(newpath)
    );
    if(ret==0)printf("link successfully\n");
    else printf("Unable to link the file\n");
    return 0;
}

執行過程如圖:


這裡的link函式和教材中所舉的rename函式傳參一樣,均為兩位。這裡我簡單說明下link函式以及ln命令。

首先link分為兩種型別:

  • 【硬連線】
    硬連線指通過索引節點來進行連線。在Linux的檔案系統中,儲存在磁碟分割槽中的檔案不管是什麼型別都給它分配一個編號,稱為索引節點號(Inode Index)。在Linux中,多個檔名指向同一索引節點是存在的。一般這種連線就是硬連線。硬連線的作用是允許一個檔案擁有多個有效路徑名,這樣使用者就可以建立硬連線到重要檔案,以防止“誤刪”的功能。其原因如上所述,因為對應該目錄的索引節點有一個以上的連線。只刪除一個連線並不影響索引節點本身和其它的連線,只有當最後一個連線被刪除後,檔案的資料塊及目錄的連線才會被釋放。也就是說,檔案真正刪除的條件是與之相關的所有硬連線檔案均被刪除。

圖中為顯示的硬連結數目。

  • 【軟連線】
    另外一種連線稱之為符號連線(Symbolic Link),也叫軟連線。軟連結檔案有類似於Windows的快捷方式。它實際上是一個特殊的檔案。在符號連線中,檔案實際上是一個文字檔案,其中包含的有另一檔案的位置資訊。
  • link函式
    標頭檔案
    #include <unistd.h>
    函式原型
    int link (const char * oldpath,const char * newpath);
    說明
    link()以引數newpath指定的名稱來建立一個新的連線(硬連線)到引數oldpath所指定的已存在檔案。如果引數newpath指定的名稱為一已存在的檔案則不會建立連線。函式在執行成功時則返回0,失敗時則返回-1,錯誤原因存於errno。 link()所建立的硬連線無法跨越不同檔案系統,如果需要請改用symlink()。
    此函式對應著linux中的ln 'target' 'file'命令。
  • symlink函式
    標頭檔案
    #include <unistd.h>
    函式原型
    int symlink(const char *oldpath, const char *newpath);
    說明
    與link函式的返回值一致,可以跨越不同檔案系統。

    勘誤

  • 1.書中一直沒有提到關於不同gcc對應不同彙編程式碼的問題。在書中的實驗裡,gcc的版本為4.*,而實際中最新版本的gcc已經升級為7.3,所以在對書中提供的內嵌彙編程式碼進行編譯的時候總是會出錯。在使用objdump命令對兩種不同版本gcc生成的二進位制*.o檔案進行反彙編的時候,我們發現兩種不同版本gcc所生成的彙編檔案是不同的。因此我們在實驗過程中需要使用低版本的gcc進行編譯。
sudo apt install gcc-4.8
ln -s /usr/lib/gcc-4.8 /usr/lib/gccl
  • 2.書中給出的程式碼存在一些手誤。雖然後面有解析。