簡介:
前一段時間,曾先後移植了uClinux-2.0.x和uClinux-2.4.x的核心,我的移植基本上是從零做起,linux並沒有支援該目標機的代碼,所以這
個移植工作基本上是新增加對一種目標機的支援。
工作過程中,我學到了不少知識,除了作業系統,還瞭解了一些編譯,調試,彙編,連結的的技術,在此我會一併介紹,可能介紹比較多
的是連接器,因為這個相對和作業系統聯絡更加緊密一些。
我希望能夠與大家分享自己經驗,同時,有錯誤和不當的地方歡迎網友指出,共同進步,這是我寫這些原創帖的動力。
“編程並非零和的遊戲。將己所知教給程式員同胞,他們並不會奪你所知。能將我所知與人分享,我感到高興,因為我身在其中、熱愛編程。”
——John Carmack
uClinux下使用者程式的執行
之所以從使用者程式談起,是因為我們平常接觸最多的還是應用程式。從應用程式引出到作業系統我覺得比較自然。下面就從一個簡單例子介紹一個程式如何在作業系統中運行。
假如有個c程式:
int main(int argc, char **argv[])
{
printf("hello world!/n");
return 0;
}
這是一個最簡單不過的程式了,一般一個C語言程式,都從main開始執行。那麼,main函數是不是與其他函數有所區別,地位有些特殊呢?
不是的。main函數和其他函數地位一樣。其實,我們完全可以做到讓一個c程式從任何地方開始執行。比如linux,它就沒有main函數,大家都知道,系統執行過啟動的一段彙編後,就會跳轉到位於init/main.c中的start_kernel中開始執行。
那麼為什麼使用者程式都要從main函數執行呢?這就是使用者C庫的原因。一般使用者用c語言開發時會調用一些庫函數,編譯成obj檔案後,在連結過程中把庫函 數的二進位代碼連結進入程式,最後形成二進位可執行檔。連結過程中,連結器會在使用者程式前插入一些初始化的代碼。uClinux下是在crt0.s中 (我移植的是uClibc庫)。不管什麼平台下什麼形式的crt0.s,這個檔案最後幾行代碼中肯定有一個jmp(或者call或br等轉移指令) main(或__uClibc_main)。這就是為什麼你的程式都從main開始執行。如果你把這個跳轉標號改成任意一個標號,比如foo。而你的程式 裡面既有main,又有foo,則這種情況下,程式就先從foo開始執行。所以,main函數和其他函數一樣,並沒有特殊地位。
下面談談在uClinux中,main函數的argc,argv是參數怎樣傳遞的。我們以flat格式可執行檔為例。uClinux下支援一種叫 flat的可執行檔格式。這種檔案格式比較簡單,基本上是平鋪的,所以叫flat很形象。現在好像uClinux-2.4.x核心的版本已經能夠支援 elf格式的檔案執行了。不過為了舉例簡單,我還是用flat格式舉例。這裡暫不分析flat檔案格式,我們把注意力放到參數傳遞上。uClinux開發 使用者程式,首先當然是編碼,然後編譯,編譯產生的檔案是elf格式的,所以要用工具elf2flt將elf檔案轉換成flat,假設這個工作已經完成。
我們在uclinux的shell下執行一個檔案foo x y,foo是程式名,x, y是參數。學過C語言的都知道,x,y作為參數會傳遞給main,其中argc=3,
argv[0]="foo", argv[1]="x", argv[2]="y"。這些參數是如何傳遞進來的呢。在你執行一個程式的時候,作業系統會調用程中把庫函數的二進位代碼連結進入程式,最後形成二進位可 執行檔案。連結過程中,連結器會在使用者程式前插入一些初始化的代碼。uClinux下是在crt0.s中(我移植的是uClibc庫)。不管什麼平台下什 麼形式的crt0.s,這個檔案最後幾行代碼中肯定有一個jmp(或者call或br等轉移指令) main(或__uClibc_main)。這就是為什麼你的程式都從main開始執行。如果你把這個跳轉標號改成任意一個標號,比如foo。而你的程式裡面既有main,又有foo,則這種情況下,程式就先從foo開始執行。所以,main函數和其他函數一樣,並沒有特殊地位。
下面談談在uClinux中,main函數的argc,argv是參數怎樣傳遞的。我們以flat格式可執行檔為例。uClinux下支援一種叫 flat的可執行檔格式。這種檔案格式比較簡單,基本上是平鋪的,所以叫flat很形象。現在好像uClinux-2.4.x核心的版本已經能夠支援 elf格式的檔案執行了。不過為了舉例簡單,我還是用flat格式舉例。這裡暫不分析flat檔案格式,我們把注意力放到參數傳遞上。uClinux開發 使用者程式,首先當然是編碼,然後編譯,編譯產生的檔案是elf格式的,所以要用工具elf2flt將elf檔案轉換成flat,假設這個工作已經完成。
我們在uclinux的shell下執行一個檔案foo x y,foo是程式名,x, y是參數。學過C語言的都知道,x,y作為參數會傳遞給main,其中argc=3,
argv[0]="foo", argv[1]="x", argv[2]="y"。這些參數是如何傳遞進來的呢。
在你執行一個程式的時候,作業系統會調用do_execve(char *filename, char**argv, char**envp, struct pt_regs *regs),這個操作會根據檔案路徑開啟檔案,裝入記憶體,argv就是放到命令列參數,envp是環境變數參數。
在裝入檔案時,系統會根據不同的檔案格式調用不同檔案裝入的handler,如果是flat格式,就會調用load_flat_binary(),在 fs/binfmt_flat.c中。有關參數,會根據一路傳遞下來的argv,envp首先處理一遍計算出參數的個數argc,envc。然後在函數 create_flat_tables裡面建立好參數表。整個函數代碼如下:
static unsigned long create_flat_tables(unsigned long pp, struct linux_binprm *bprm)
{
(1) unsigned long *argv,*envp;
(2) unsigned long * sp;
(3) char * p = (char*)pp;
(4) int argc = bprm->argc;
(5) int envc = bprm->envc;
(6) char dummy;
(7) sp = (unsigned long *) /
((-(unsigned long)sizeof(char *))&(unsigned long) p);
(8) sp -= envc+1;
(9) envp = sp;
(10) sp -= argc+1;
(11) argv = sp;
(12) flat_stack_align(sp);
(13) if (flat_argvp_envp_on_stack()) {
(14) --sp; put_user((unsigned long) envp, sp);
(15) --sp; put_user((unsigned long) argv, sp);
(16) }
(17) put_user(argc,--sp);
(18) current->mm->arg_start = (unsigned long) p;
(19) while (argc-->0) {
(20) put_user((unsigned long) p, argv++);
(21) do {
(22) get_user(dummy, p); p++;
(23) } while (dummy);
(24) }
(25) put_user((unsigned long) NULL, argv);
(26) current->mm->arg_end = current->mm->env_start = (unsigned long) p;
(27) while (envc-->0) {
(28) put_user((unsigned long)p, envp); envp++;
(29) do {
(30) get_user(dummy, p); p++;
(31) } while (dummy);
(32) }
(33) put_user((unsigned long) NULL, envp);
(34) current->mm->env_end = (unsigned long) p;
(35) return (unsigned long)sp;
}
(1)-(6) 行是變數聲明。其中argc和envc分別記錄前面已經計算出來的參數個數和環境變數參數個數。p=pp是參數和環境變數數組的指標,sp是你要執行程式 的使用者區堆棧,就是foo程式執行時,使用者空間堆棧的起始地址。(8)-(11)是一個堆棧調整。首先sp移動envc+1個單位,這envc+1個用來 存放一共envc個envp[0]->envc[envp-1]元素地址的,多餘一個放0,表示envp數組結束。然後sp在移動argc+1各單 位,留出argc+1單位空間,這argc+1個單位是用來存放argc個argv[0]->argv[argc-1]元素地址的,多餘一個也放 0,表示argv數組結束。經過堆棧調整,argv和envp各自指向自己在堆棧中的位置。如果開始堆棧初值記為init_sp,則現在envp= init_sp-(envc+1),argv=envp-(argc+1)。
(12)無關緊要,略去不提。(13)-(17)又是一次堆棧調整。(14)是sp再移動1個單位,然後將envp放入這個地址(此時envp= init_sp-(envc+1)),然後(15)又將sp移動一個單位,將argv寫入. (17)是移動堆棧後將argc也寫入裡面.
(18)-(35)行是將argv[0]->argv[argc-1](在p所指向地方)依次寫入argv所指堆棧地區中.然後再將envp[0]->edummy, p); p++;
(31) } while (dummy);
(32) }
(33) put_user((unsigned long) NULL, envp);
(34) current->mm->env_end = (unsigned long) p;
(35) return (unsigned long)sp;
}
(1)-(6)行是變數聲明。其中argc和envc分別記錄前面已經計算出來的參數個數和環境變數參數個數。p=pp是參數和環境變數數組的指標,sp 是你要執行程式的使用者區堆棧,就是foo程式執行時,使用者空間堆棧的起始地址。(8)-(11)是一個堆棧調整。首先sp移動envc+1個單位,這 envc+1個用來存放一共envc個envp[0]->envc[envp-1]元素地址的,多餘一個放0,表示envp數組結束。然後sp在移 動argc+1各單位,留出argc+1單位空間,這argc+1個單位是用來存放argc個argv[0]->argv[argc-1]元素地址 的,多餘一個也放0,表示argv數組結束。經過堆棧調整,argv和envp各自指向自己在堆棧中的位置。如果開始堆棧初值記為init_sp,則現在 envp=init_sp-(envc+1),
argv=envp-(argc+1)。
(12)無關緊要,略去不提。(13)-(17)又是一次堆棧調整。(14)是sp再移動1個單位,然後將envp放入這個地址(此時envp= init_sp-(envc+1)),然後(15)又將sp移動一個單位,將argv寫入. (17)是移動堆棧後將argc也寫入裡面.
(18)-(35)行是將argv[0]->argv[argc-1](在p所指向地方)依次寫入argv所指堆棧地區中.然後再將envp[0] ->envp[envc-1](也是由p所指)寫入envp所指的堆棧地區中.在寫入同時,還要設定進程式控制制塊相應的資料結構,如 arg_start,env_start,env_end等.
下面舉例和畫圖來說明過程.比如執行foo x y,此時argc=3,argv[0]="foo",argv[1]="x", argv[2]="y", envc=1, envp[0]="path=/bin". 假設使用者堆棧起始
空間堆棧地址是sp=0x1f0000,pp=0x1c0000.則處理過後在foo執行前,他的使用者空間堆棧frame如下:
--------------------------------
0x1f0000 | 0000 |
--------------------------------
0x1efffc | envp[0] = 0x1c0008 | ---->指向"path=/bin"
--------------------------------
0x1efff8 | 0000 |
--------------------------------
0x1efff4 | argv[2] = 0x1c0006 | ----->指向"y"
--------------------------------
0x1efff0 | argv[1] = 0x1c0004 | ----->指向"x"
--------------------------------
0x1effec | argv[0] = 0x1c0000 | ----->指向"foo"
--------------------------------
0x1effe8 | start addr of envp = 0x1efffc|
--------------------------------
到r2-r6裡來傳遞。當然,如果超過5個,就要藉助堆棧了。
既然main帶了參數,那麼在調用main之前,要把argc放到r2裡面,argv放到r3裡面,envp放到r4裡面。剛才說了,sp是使用者空間堆棧 起始地址。所以在開始執行foo代碼時候,r0=sp,在上文例子裡r0等於0x1effe0.則如下偽彙編代碼可以讓參數裝入正確寄存器。
load r2, (r0) /* r2 = argc */
load r3, (r0, 4) /* r3 = argv */
load r4, (r0, 8) /* r4 = envp */
call main /* 跳轉到main函數 */
call _exit
以上代碼就是最簡單的進入main函數前的預先處理。當然,不同系統不同格式檔案處理方式是不同的,剛才的一些例子是我碰到的一些情景和解決方案。
我這個程式例子還沒有完全講完,比如後面printf怎麼處理等,不過手都酸了,就先講到main函數的參數傳遞吧。剛學c語言那陣覺得main挺神秘,做過系統就知道,其實main跟別的函數沒有任何區別:)
寫了半天,頭暈腦脹,肯定還有一些錯誤或者不夠最佳化的地方,歡迎拍磚。沒講清楚