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;
}
執行結果:
宏函式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)
這是GUN C相對於ANSI C的一個擴充套件,用於將陣列某個區間,初始化為非0值,比如:
int array[100] = { [0 ... 9] = 1, [10 ... 98] = 2, 3 };
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
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成員相對於結構體開始的偏移,知道目的後,再去理解這個宏函式就不難了。
但是,還有值得注意的地方:"&((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核心中的彙編程式碼
① 有些操作硬體的專用指令,在C語言沒有對應的語言成分,比如給GDTR/LDTR暫存器賦值的特權指令:LGDT/LLDT
② 某些場合,對效率要求極高,或者對執行時間有精準的要求
③ 某些場合,對程式的體積也有極端的要求,如載入程式
① 純彙編程式碼。比如學習到第10章"系統的引導和初始化"時,就會遇到純彙編檔案bootsect.S、sctup.S,另外,字尾如果是大S,裡面可能會包含一些C語言中的預處理語句,字尾為小s,就沒有任何其它語言的影子;
② 嵌入式彙編。在C函式中,可以按照__asm__ __volatile__ (“指令部:輸出部:輸入部:損壞部”)格式,嵌入一塊組合語言(稍後舉例)。
從網上可以查到很詳細的資料,我覺得剛開始學,只需要對一些符號(比如:$、%)有印象即可,在看程式碼時遇到了,再去確認具體的含義。
書1.5.2節,最後舉了核心中2個使用了內聯彙編的函式:__memcpy()、strncmp()。
彙編程式碼塊前後,是可以有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(),用不同的最佳化級別編譯,然後反編譯和核心中的這個函式對比,感受一下。