Linux核心筆記003 - Linux核心程式碼裡面的C語言和組合語言

jmpcall發表於2020-05-14

1. Linux核心中的C語言

    Linux核心是用GNU C編寫,從而必須用gcc編譯,另外gcc編譯器在發展過程中,在不斷的擴充套件和捨棄一些東西,所以就會出現一種情況:高版本gcc編譯不了低版本核心、低版本gcc也編譯不了高版本核心。所以在編譯核心時,一定要使用跟核心版本差不多時期的gcc版本。

  • 帶返回值的巨集函式
#include <stdio.h>
#define FUN() ({ \
    int ret = 100; \
    printf("do something ..\n"); \
    ret; \
})

int main()
{
    int m = FUN();

    printf("%d\n", m);
    return 0;
}
       
 執行結果:
do something ..
100

  • do {} while (0)
        巨集函式DUMP_WRITE(),在如下情況被呼叫時:
if (addr)
    DUMP_WRITE(addr,nr);
else
    do_something_else();
      
 按照如下兩種方式定義,在被呼叫處展開後,都有問題:
#define DUMP_WRITE(addr,nr) memcpy(bufp,addr,nr); bufp += nr;
#define DUMP_WRITE(addr,nr) { memcpy(bufp,addr,nr); bufp += nr; }
       
 而利用"do {} while (0)"將多條語句包在一起,無論怎麼呼叫,都不會有問題:
#define DUMP_WRITE(addr, nr) do { \
    memcpy(bufp, addr, nr); \
    bufp += nr; \
} while (0)

  • [first ... last] = value
        這是GUN C相對於ANSI C的一個擴充套件,用於將陣列某個區間,初始化為非0值,比如:
int array[100] = { [0 ... 9] = 1, [10 ... 98] = 2, 3 };

  • include/linux/list.h
        GUN C相對於ANSI C的擴充套件還有很多,書上已經列舉了一小部分,更具體的,可以到後期學習程式碼時遇到了,再逐個瞭解。相對的,我認為include/linux/list.h這個標頭檔案裡面的程式碼,必須要在一開始就看懂,因為這裡面的介面,大量用於核心的程式碼中,同時我覺得這種設計的思維也很精妙。

        它用於構造雙向迴圈連結串列, 從如下兩份程式碼的對比中,應該就可以感受到它的好處:
      
  程式碼1:
#include <stdio.h>

struct data_node {
    int               n;
    char              c;
    struct data_node *prev;
    struct data_node *next;
};

int main()
{
    struct data_node datas[4] = {
        { 1, 'a', NULL, NULL },
        { 2, 'b', NULL, NULL },
        { 3, 'c', NULL, NULL }
    };
    struct data_node *head = &datas[0];

    // 新增節點、遍歷連結串列,需要業務層本身實現
    datas[0].prev = &datas[2];
    datas[0].next = &datas[1];

    datas[1].prev = &datas[0];
    datas[1].next = &datas[2];

    datas[2].prev = &datas[1];
    datas[2].next = &datas[0];

    while (1) {
        printf("datas[%d]: n=%d, c=%c, prev=%p, next=%p\n",
            head->n - 1, head->n, head->c, head->prev, head->next);
        if (head->n == 3)
            break;
        head = head->next;
    };

    return 0;
}
        
執行結果:
datas[0]: n=1, c=a, prev=0xbfc5c84c, next=0xbfc5c83c
datas[1]: n=2, c=b, prev=0xbfc5c82c, next=0xbfc5c84c
datas[2]: n=3, c=c, prev=0xbfc5c83c, next=0xbfc5c82c
        
程式碼2:
#include <stdio.h>
#include "list.h"

struct data_node {
    int              n;
    char             c;
    struct list_head list;
};

int main()
{
    struct data_node datas[4] = {
        { 1, 'a', { NULL, NULL } },
        { 2, 'b', { NULL, NULL } },
        { 3, 'c', { NULL, NULL } }
    };
    struct list_head head = LIST_HEAD_INIT(head);
    struct list_head *pos;

    // 新增節點、遍歷連結串列,業務層直接呼叫介面
    list_add_tail(&datas[0].list, &head);
    list_add_tail(&datas[1].list, &head);
    list_add_tail(&datas[2].list, &head);

    printf("head: %p, prev=%p, next=%p\n", &head, head.prev, head.next);

    list_for_each(pos, &head) {
        struct data_node *data = list_entry(pos, struct data_node, list);
        printf("datas[%d]: n=%d, c=%c, prev=%p, next=%p\n",
            data->n - 1, data->n, data->c, data->list.prev, data->list.next);
    }

    return 0;
}

執行結果:
head: 0xbf9ad48c, prev=0xbf9ad474, next=0xbf9ad454
datas[0]: n=1, c=a, prev=0xbf9ad48c, next=0xbf9ad464
datas[1]: n=2, c=b, prev=0xbf9ad454, next=0xbf9ad474
datas[2]: n=3, c=c, prev=0xbf9ad464, next=0xbf9ad48c

  • list_entry()
        include/linux/list.h這份程式碼整體不難,但是如果是剛開始學習,可能會對巨集函式list_entry()有些疑惑:
#define list_entry(ptr, type, member) \
	((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
     
① 首先要明白的是:為什麼需要這個巨集函式?
             
程式碼1中,datas[1].prev,直接就是&datas[0],而程式碼2中,datas[1].list->prev,只是&datas[0].list而已,想知道&datas[0]的話,還需要減去list成員相對於結構體開始的偏移,知道目的後,再去理解這個巨集函式就不難了。
Linux核心筆記003 - Linux核心程式碼裡面的C語言和組合語言
 
但是,還有值得注意的地方:"&((type *)0)->member",按照平時開發程式的經驗,可能覺得這裡會coredump!
            
直接舉例:

#include <stdio.h>

struct test {
    int  n;
    char c;
};

int main()
{
    struct test *p = NULL;

    //printf("%d, %c\n", p->n, p->c);    // coredump
    printf("%p, %p\n", &p->n, &p->c);    // 不coredump
    printf("%p, %p\n", &((struct test*)0)->n, &((struct test*)0)->c);    // 不coredump

    return 0;
}

這樣,就可以推測出來一個過程,我們平常在C程式中寫的"p->n",其實等價於"*(&p->n)","&p->n"前面有個顯式的&,則只是根據p的地址,計算成員n的地址,哪怕"p=NULL"也沒關係,coredump只是發生在訪問這個錯誤地址的時候而已。

2. Linux核心中的彙編程式碼

  • Linux核心使用匯編的場合
        ① 有些操作硬體的專用指令,在C語言沒有對應的語言成分,比如給GDTR/LDTR暫存器賦值的特權指令:LGDT/LLDT
        ② 某些場合,對效率要求極高,或者對執行時間有精準的要求
        ③ 某些場合,對程式的體積也有極端的要求,如載入程式

  • Linux核心使用匯編的形式
        ① 純彙編程式碼。比如學習到第10章"系統的引導和初始化"時,就會遇到純彙編檔案bootsect.S、sctup.S,另外,字尾如果是大S,裡面可能會包含一些C語言中的預處理語句,字尾為小s,就沒有任何其它語言的影子;
        ② 嵌入式彙編。在C函式中,可以按照__asm__ __volatile__ (“指令部:輸出部:輸入部:損壞部”)格式,嵌入一塊組合語言(稍後舉例)。

  • AT&T格式與intel格式比較
        從網上可以查到很詳細的資料,我覺得剛開始學,只需要對一些符號(比如:$、%)有印象即可,在看程式碼時遇到了,再去確認具體的含義。
  • 嵌入式彙編/內聯彙編
        書1.5.2節,最後舉了核心中2個使用了內聯彙編的函式:__memcpy()、strncmp()。

    Linux核心筆記003 - Linux核心程式碼裡面的C語言和組合語言
        (這個圖我在chinaunix發過,所以不是抄襲)

        彙編程式碼塊前後,是可以有C語句的。

    ① 所有用雙引號包圍的內容,為"指令部",從緊接著指令部的冒號開始,依次標記為0、1、2、..
        指令部後面第一個冒號開始,為“輸出部”,用於指定C的各個變數,由哪些寄存代替。例子中,用ecx作d0,用edi作d1,用esi作d2;
        指令部後面第二個冒號開始,為“輸入部”,用於初始化變數。例子中,"0"標號對應的"ecx/d0"=n/4,"q"讓編譯器從eax/ebx/ecx/edx選擇一個進行使用,並且初始化為n,"1"標號對應的"edi/d1"=to,"2"標號對應的"esi/d2"=from;
        指令部後面第三個冒號開始,為“損壞部”,指令部和輸出部中的暫存器,在執行過程中,都有可能被修改掉,如果希望某些暫存器的值保持不變,就可以在損壞部指定,編譯器就會在入口新增壓棧儲存、在出口新增出棧恢復的指令。

    ② "指令部"分析:
        "輸入部"已經完成了初始化:ecx = n/4,eax = n,eid = to,esi = from;
        第一條指令"rep; movsl\n\t":重複執行movsl n/4(ecx)次,每次從from(esi)拷由4byte到to(edi),執行完可能還剩1~3位元組沒有拷貝;
        第二條指令"testb $2,%b4\n\t":AT&T格式中,"$數字"表示立即數,"%數字"表示輸入/輸出部相應標號處的內容,所以這條指令就是判斷%4(拷貝長度n)的低第二位是否為1(程式碼中為"%b4",不確實是不是告訴編譯器儘量用ebx的意思),如果為1,則至少還剩2位元組,所以執行第四條指令"movsw",再拷貝2位元組,否則執行第三條指令"je 1f\n\t"("f"表示forward,往前方最近的1標號處跳轉),直接跳轉到"1:\ttestb $1,%b4\n\t";
        第五條指令,同樣的道理,判斷是否還剩1位元組沒的拷貝,如果是,則會執行"movsb"指令,再拷貝1位元組。

    ③ 執行效率
        由於這個函式的使用頻率極高,所以核心直接使用匯編實現,讓函式簡潔到了極限,沒有任何一條多餘的指令。感興趣的話,可以用C語言寫一個__memcpy(),用不同的優化級別編譯,然後反編譯和核心中的這個函式對比,感受一下。

相關文章