1、windows中可執行文件是PE格式的,以exe作為后綴結尾(當然驅動sys和動態鏈接dll也是PE格式的,但普通用戶用不上);用戶使用也很方便,直接雙擊exe文件就能開始運行了;linux也類似,可執行文件是ELF格式的,用戶雙擊也能運行;這么方便的功能在底層是怎么實現的了?先闡述一下大概的流程:
- 可執行文件是放磁盤的,既然要執行,用戶在雙擊后肯定要先加載到內存的高速緩存區
- 0號和1號進程都是操作系統的內核,其他用戶進程都是這兩個進程fork出來的,新進程也不例外,這里先調用fork創建新進程
- 從高速緩存區讀取文件頭,里面存了代碼段、數據段的起始和size信息,由此設置進程的ldt;
- 設置新進程的tss,保存運行時的context;
- 重定位操作系統提供的api地址、設置sp;
- 分配內存空間存放程序的參數(用戶傳遞的)和環境變量(操作系統內核傳遞的);
目前windows下部分殺毒軟件能監控用戶有沒有點擊運行危險的程序,大概率是hook了鼠標雙擊的中斷handler(也就是上述的第一步);一旦發現用戶嘗試打開危險程序就彈窗告警!
2、(1)正式解讀代碼前,先做一些鋪墊:早期ds和fs兩個寄存器都在使用,區別如下:如果代碼在用戶態運行,ds和fs都指向用戶程序的數據段,沒任何區別;但是如果代碼在內核態運行,ds指向內核的數據段,而fs仍然指向用戶態的數據段!不同的態取數據時需要設置不同的fs值!這點很重要,下面的copy_string()函數會頻繁設置fs的值!
(2)用戶輸入參數后,總要找個地方存嘛,既然是進程的參數,就放進程的內存空間唄!linux使用了32個頁面,一共128KB的內存空間存放用戶參數和環境變量,拷貝代碼如下(作用和三環下的memcpy是一樣的,沒本質區別):
/* * 'copy_string()' copies argument/envelope strings from user * memory to free pages in kernel mem. These are in a format ready * to be put directly into the top of new user memory. * * Modified by TYT, 11/24/91 to add the from_kmem argument, which specifies * whether the string and the string array are from user or kernel segments: * * from_kmem argv *(參數指針,也就是參數地址) argv **(真正的用戶輸入參數) * 0 user space user space * 1 kernel space user space * 2 kernel space kernel space * * We do this by playing games with the fs segment register. Since it * it is expensive to load a segment register, we try to avoid calling * set_fs() unless we absolutely have to. */ //// 復制指定個數的參數字符串到參數和環境空間中。 // 參數:argc - 欲添加參數個數; argv - 參數指針數組;page - 參數和環境空間頁面 // 指針數組。p - 參數表空間中偏移指針,始終指向已復制串的頭部;from_kmem - 字符 // 串來源標志。在 do_execve()函數中,p初始化為指向參數表(128kb)空間的最后一個長 // 字處,參數字符串是以堆棧操作方式逆向往其中復制存放的。因此p指針會隨着復制信 // 息的增加而逐漸減小,並始終指向參數字符串的頭部。字符串來源標志from_kmem應該 // 是TYT為了給execve()增添執行腳本文件的功能而新加的參數。當沒有運行腳本文件的 // 功能時,所有參數字符串都在用戶數據空間中。 // 返回:參數和環境空間當前頭部指針。若出錯則返回0. /* char *str="hello"; copy_strings(1,&str,page,p,1); */ static unsigned long copy_strings(int argc,char ** argv,unsigned long *page, unsigned long p, int from_kmem) { char *tmp, *pag=NULL; int len, offset = 0; unsigned long old_fs, new_fs; // 首先取當前段寄存器ds(指向內核數據段)和fs值,分別保存到變量new_fs和 // old_fs中。如果字符串和字符串數組(指針)來自內核空間,則設置fs段寄存器指向 // 內核數據段。 if (!p) return 0; /* bullet-proofing */ new_fs = get_ds(); old_fs = get_fs(); if (from_kmem==2)/*字符串和字符串數組(指針)來自內核空間*/ set_fs(new_fs);/*參數指針和參數本身都在內核,讓fs和ds指向的位置相同,表示當前代碼運行的所有數據都用內核態的數據段取*/ while (argc-- > 0) {/*遍歷每個參數*/ // 首先取需要復制的當前字符串指針。如果字符串在用戶空間而字符串數組(字 // 符串指針)在內核空間,則設置fs段寄存器指向內核數據段(ds).並在內核數 // 據空間中取了字符串指針tmp之后就立刻回復fs段寄存器原值(fs再指回用戶空 // 間)。否則不用修改fs值而直接從用戶空間取字符串指針到tmp. if (from_kmem == 1)/*參數指針在內核態,但是參數本身在用戶態*/ set_fs(new_fs);/*因為參數指針在內核態,而下面要用fs訪問內核態,所以需要把fs設置程ds*/ if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))/*tmp還是參數指針,並不是參數本身;指向最后一個參數,在argsc--的帶動下在遍歷每個參數*/ panic("argc is wrong"); if (from_kmem == 1) set_fs(old_fs);/*因為最終的參數在用戶態,而下面要用fs去訪問,所以要用之前保存的fs還原*/ // 然后從用戶空間取該字符串,並計算該參數字符串長度len.此后tmp指向該字 // 符串末端。如果該字段字符串長度超過此時參數和環境空間中還剩余的空閑長 // 度,則空間不夠了。於是回復fs段寄存器值(如果被改變的話)並返回0.不過因 // 為參數和環境空間留有128KB,所以通常不可能發生這種情況。 len=0; /* remember zero-padding */ do { len++; /*求單個參數的字符長度*/ } while (get_fs_byte(tmp++)); if (p-len < 0) { /* this shouldn't happen - 128kB :P:剩余可存參數和環境變量的字符個數*/ set_fs(old_fs); return 0; } // 接着我們逆向逐個字符地把字符串復制到參數和環境空間末端處。在循環復制 // 字符串的字符過程中,我們首先要判斷參數和環境空間相應位置是否已經有內 // 存頁面。如果還沒有就先為其申請1頁內存頁面。偏移量offset被用作為在一 // 個頁面中的當前指針偏移量。因為剛開始執行本函數時,偏移量offset被初始 // 化為0,所以(offset-1<0)肯定成立而使得offset重新被設置為當前p指針在頁 // 面返回內的偏移值。 while (len) {/*遍歷參數的每個字符*/ --p; /*這里偏移P也在變化,P的初始值PAGE_SIZE*MAX_ARG_PAGES-4,在do_execve中定義的*/ --tmp;--len; if (--offset < 0) { offset = p % PAGE_SIZE; // 如果字符串和字符串數組都在內核空間中,那么為了從內核數據空間 // 復制字符串內容,下面會把fs設置為指向內核數據段。 if (from_kmem==2) set_fs(old_fs); // 如果當前偏移量p所在的串空間頁面指針數組項page[p/PAGE_SIZE]== // 0,表示此時p指針所處的空間內存頁面還不存在,則需申請一空閑內 // 存頁,並將該頁面指針填入指針數組,同時也使頁面指針pag 指向該 // 新頁面,若申請不到空閑頁面則返回0. if (!(pag = (char *) page[p/PAGE_SIZE]) && !(pag = (char *) page[p/PAGE_SIZE] = (unsigned long *) get_free_page())) return 0; // 如果字符串在內核空間,則設置fs段寄存器指向內核數據段(ds)。 if (from_kmem==2) set_fs(new_fs); } // 然后從fs段中復制字符串的1字節到參數和環境空間內存頁面pag的offset出。 *(pag + offset) = get_fs_byte(tmp); } } // 如果字符串和字符串數組在內核空間,則恢復fs段寄存器原值。最后,返回參數和 // 環境空間已復制參數的頭部偏移值。 if (from_kmem==2) set_fs(old_fs); return p; }
- 上面的代碼稍微有點繁瑣,這里畫個圖直觀展示
- 代碼中好些地方涉及到set_fs,為啥要頻繁設置fs了?原因如下:argv本身也是個變量,本身也要內存來存放;argv*和argv同理,這兩個並不存用戶輸入的參數,僅僅是指針;argv**指向的地址才是最終存用戶輸入參數的地方,那么現在問題來了:argv*和argv**可能分別存在內核和用戶態,但代碼要get_fs函數來讀取這兩個變量,怎么辦了?只能不停的改變fs來反復讀取argv*和argv**了!比如from_kmem參數是1,argv*和argv**分別被存放在內核和用戶態,此時如果要用get_fs_long讀取argv*,需要把fs設置成ds,所以要調用set_fs(new_fs);讀取完argv*后如果要繼續讀取用戶態的argv**,就要把已經改成ds的fs還原成以前的fs了,所以要調用set_fs(old_fs);
* from_kmem argv *(參數指針) argv **(真正的用戶輸入參數) * 0 user space user space * 1 kernel space user space * 2 kernel space kernel space
(3)參數或環境變量要求用空格隔開,個數是這樣計算的:因為argv是二級指針,所以tmp每隔4個byte查找1次。如果指針是空,說明沒指向任何參數;
/* * count() counts the number of arguments/envelopes */ //// 計算參數個數 // 參數:argv - 參數指針數組,最后一個指針項是NULL // 統計參數指針數組中指針的個數。關於函數參數傳遞指針的指針的作用,在sched.c中。 static int count(char ** argv) { int i=0; char ** tmp; if ((tmp = argv)) while (get_fs_long((unsigned long *) (tmp++))) i++; return i; }
(4)總所周知,文件都是有頭部信息的,不同平台對不同文件都規定了不同的頭部信息,操作系統就是根據文件的頭部信息區分文件類型;早期linux 0.11版本的文件頭信息如下: 只有8個字段,非常簡單;
struct exec { unsigned long a_magic; /* Use macros N_MAGIC, etc for access;可執行文件的類型 */ unsigned a_text; /* length of text, in bytes */ unsigned a_data; /* length of data, in bytes */ unsigned a_bss; /* length of uninitialized data area for file, in bytes */ unsigned a_syms; /* length of symbol table data in file, in bytes */ unsigned a_entry; /* start address */ unsigned a_trsize; /* length of relocation info for text, in bytes */ unsigned a_drsize; /* length of relocation info for data, in bytes */ };
a_magic字段的取值:
#ifndef N_MAGIC #define N_MAGIC(exec) ((exec).a_magic) #endif #ifndef OMAGIC /* Code indicating object file or impure executable. */ #define OMAGIC 0407 /* Code indicating pure executable. */ #define NMAGIC 0410 /* Code indicating demand-paged executable. */ #define ZMAGIC 0413 #endif /* not OMAGIC */
(5)shell腳本中,上個可執行文件運行完后,如果要接着運行下一個腳本,需要更改ldt,linux 的代碼如下;注意:ldt中代碼段基址和數據段基址是一樣的!
//// 修改任務局部描述符表的內容 // 修改局部描述符表LDT中描述符的段基址和段限長,並將參數和環境空間頁面放置在數 // 據段末端。 // 參數:text_size - 執行文件頭部中a_text字段給出的代碼長度值; // page - 參數和環境空間頁面指針數組。 // 返回:數據段限長值(64MB) static unsigned long change_ldt(unsigned long text_size,unsigned long * page) { unsigned long code_limit,data_limit,code_base,data_base; int i; // 首先根據執行文件頭部代碼長度字段a_text值,計算以頁面長度為邊界的代碼段限 // 長。並設置數據段查高難度為64 MB.然后取當前進程局部描述符表代碼段描述符中 // 代碼段基址,代碼段基址與數據段基址相同。並使用這些新值重新設置局部表中代 // 碼段和數據段描述符中的基址和段限長。這里請注意,由於被加載的新程序的代碼 // 和數據段基址與原程序相同,因此沒有必要再重復去設置他們。 code_limit = text_size+PAGE_SIZE -1;/*PAGE_SIZE是文件頭長度,這里加上代碼段長度*/ code_limit &= 0xFFFFF000;/*代碼段低12bit清零,和頁對齊*/ data_limit = 0x4000000;/*數據段長度*/ code_base = get_base(current->ldt[1]); data_base = code_base;/*代碼段和數據段的基址都一樣*/ set_base(current->ldt[1],code_base); set_limit(current->ldt[1],code_limit); set_base(current->ldt[2],data_base); set_limit(current->ldt[2],data_limit); /* make sure fs points to the NEW data segment */ // fs段寄存器中放入局部表數據段描述符的選擇符(0x17)。即默認情況下fs都指向任 // 務數據段。__asm__("pushl $0x17\n\tpop %%fs"::); // 然后將參數和環境空間已存放數據的頁面(最多有MAX_ARG_PAGES頁,128kb)放到 // 數據段末端。方法是從進程空間末端逆向一頁一頁地放。函數put_page()用於吧物 // 理頁面映射到進程邏輯空間中。 __asm__("pushl $0x17\n\tpop %%fs"::); data_base += data_limit; for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) { data_base -= PAGE_SIZE; if (page[i]) put_page(page[i],data_base); } return data_limit; }
(6)前面做了大量鋪墊,本文最重要的函數do_execve終於閃亮登場。整個流程原理上並不復雜:
- 先把文件從磁盤讀到緩存區,然后檢查文件的各種權限
- 然后檢查文件頭a_magic字段,看看是shell還是elf(當然這個時候的文件頭和現在的elf格式差異還挺大的,但原理都是一樣的);
- 如果是shell,拷貝環境變量和參數,並逐行讀取shell命令執行
- 如果是可執行文件,同樣拷貝環境變量和參數,設置ldt;
- 根據文件頭的a_entry數據設置eip的值
前面4步都是准備工作,第5步相當於扣動扳機了!
/* * 'do_execve()' executes a new program. */ //// execve()系統中斷調用函數。加載並執行子進程 // 該函數是系統中斷調用(int 0x80)功能號__NR_execve調用的函數。函數的參數是進 // 入系統調用處理過程后直接到調用本系統嗲用處理過程和調用本函數之前逐步壓入棧中 // 的值。 // eip - 調用系統中斷的程序代碼指針。 // tmp - 系統中斷中在調用_sys_execve時的返回地址,無用; // filename - 被執行程序文件名指針; // argv - 命令行參數指針數組的指針; // envp - 環境變量指針數組的指針。 // 返回:如果調用成功,則不返回;否則設置出錯號,並返回-1. /* ./add 1 2 3 ./run.sh helloworld */ int do_execve(unsigned long * eip,long tmp,char * filename, char ** argv, char ** envp) { struct m_inode * inode; struct buffer_head * bh; struct exec ex; unsigned long page[MAX_ARG_PAGES];/*每個進程都有參數指針數組;注意:每個元素都是參數指針,並不直接存放參數*/ int i,argc,envc; int e_uid, e_gid; int retval; int sh_bang = 0; // 控制是否需要執行的腳本程序 /*p指向參數和環境空間的最后部;注意:P只是個偏移,不是絕對地址; 每次調用copy_string后P都會減少,以此確保copy到連續的內存空間 */ unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4; //p=128KB-4 // 在正式設置執行文件的運行環境之前,讓我們先干這些雜事。內核准備了128kb(32 // 個頁面)空間來存放化執行文件的命令行參數和環境字符串。上杭把p初始設置成位 // 於128KB空間中的當前位置。 // 另外,參數eip[1]是調用本次系統調用的原用戶程序代碼段寄存器CS值,其中的段 // 選擇符當然必須是當前任務的代碼段選擇符0x000f.若不是該值,那么CS只能會是 // 內核代碼段的選擇符0x0008.但這是絕對不允許的,因為內核代碼是常駐內存而不 // 能被替換掉的。因此下面根據eip[1]的值確認是否符合正常情況。然后再初始化 // 128KB的參數和環境串空間,把所有字節清零,並取出執行文件的i節點。再根據函 // 數參數分別計算出命令行參數和環境字符串的個數argc和envc。另外,執行文件必 // 須是常規文件。 if ((0xffff & eip[1]) != 0x000f)/*先判斷一下權限夠不夠*/ panic("execve called from supervisor mode"); for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table */ page[i]=0;/*參數指針清零*/ if (!(inode=namei(filename))) /* get executables inode:查看文件的元信息 */ return -ENOENT; argc = count(argv); envc = count(envp); restart_interp: if (!S_ISREG(inode->i_mode)) { /* must be regular file */ retval = -EACCES; goto exec_error2; } // 下面檢查當前進程是否有權運行指定的執行文件。即根據執行文件i節點中的屬性, // 看看本進程是否有權執行它。在把執行文件i節點的屬性字段值取到i中后,我們首 // 先查看屬性中是否設置了"設置-用戶-ID"(set-user_id)標志和“設置-組-ID”(set_group-id) // 標志。這兩個標志主要是讓一般用戶能夠執行特權用戶(如超級用戶root)的程序, // 例如改變密碼的程序passwd等。如果set-user-id標志置位,則后面執行進程的有 // 效用戶ID(euid)就設置成執行文件的用戶ID,否則設置成當前進程的euid。如果執 // 行文件set-group-id被置位的話,則執行進程的有效組ID(egid)就被設置為執行 // 文件的組ID。否則設置成當前進程的egid。這里暫時把這兩個判斷出來的值保存在 // 變量e_uid和e_gid中。 i = inode->i_mode; // 取文件屬性字段 e_uid = (i & S_ISUID) ? inode->i_uid : current->euid; e_gid = (i & S_ISGID) ? inode->i_gid : current->egid; // 現在根據進程的euid和egid和執行文件的訪問屬性進行比較。如果執行文件屬於運 // 行進程的用戶,則把文件屬性值i右移6位,此時最低3位是文件宿主的訪問權限標 // 志。否則的話如果執行文件與當前進程的用戶屬性同租,則使屬性值最低3位是執 // 行文件組用戶的訪問權限標志。否則此時屬性值最低3位就是其他用戶訪問該執行 // 文件的權限。 // 然后我們根據屬性字i的最低3bit值來判斷當前進程是否有權限運行這個執行文件。 // 如果選出的相應用戶沒有運行該文件的權利(位0是執行權限),並且其他用戶也沒 // 有任何權限或者當前進程用戶不是超級用戶,則表明當前進程沒有權利運行這個執 // 行文件。於是置不可執行出錯碼,並跳轉到exec_error2處去做退出處理。 if (current->euid == inode->i_uid) i >>= 6; else if (current->egid == inode->i_gid) i >>= 3; if (!(i & 1) &&/*是否有執行權限*/ !((inode->i_mode & 0111) && suser())) { retval = -ENOEXEC; goto exec_error2; } // 程序執行到這里,說明當前進程有運行指定執行文件的權限。因此從這里開始我們 // 需要取出執行文件頭部數據並根據其中的信息來分析設置運行環境,或者運行另一 // 個shell程序來執行腳本程序。首先讀取執行文件第1塊數據到高速緩沖塊中。並復 // 制緩沖塊數據到ex中。如果執行文件開始的兩個字節是字符'#!',則說明執行文件 // 是一個腳本文件。如果想運行腳本文件,我們就需要執行腳本文件的解釋程序(例 // 如shell程序)。 他指明了運行腳本 // 文件需要的解釋程序。運行方法從腳本文件第一行中取出其中的解釋程序名及后面 // 的參數(若有的話),然后將這些參數和腳本文件名放進執行文件(此時是解釋程序) // 的命令行參數空間中。在這之前我們當然需要先把函數指定的原有命令行參數和環 // 境字符串放到128KB空間中,而這里建立起來的命令行參數則放到它們前面位置處( // 因為是逆向放置)。最后讓內核執行腳本文件的解釋程序。下面就是在設置好解釋 // 程序的腳本文件名等參數后,取出解釋程序的i節點並跳轉去執行解釋程序。由於 // 我們需要跳轉去執行,因此在下面確認處並處理了腳本文件之后需要設置一個禁止 // 再次執行下面的腳本處理代碼標志sh_bang。在后面的代碼中該標志也用來表示我 // 們已經設置好執行的命令行參數,不用重復設置。 if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {/*當前執行程序或shell腳本文件頭結構,就是ELF的雛形*/ retval = -EACCES; goto exec_error2; } ex = *((struct exec *) bh->b_data); /* read exec-header:讀取的文件頭數據用結構體“格式化” */ if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {/*根據a_magic判斷文件類型,這里是shell腳本*/ /* * This section does the #! interpretation. * Sorta complicated, but hopefully it will work. -TYT */ char buf[1023], *cp, *interp, *i_name, *i_arg; unsigned long old_fs; // 從這里開始,我們從腳本文件中提取解釋程序名以及其參數,並把解釋程序名、 // 解釋程序的參數和腳本文件名組合放入環境參數塊中。首先復制腳本文件頭1 // 行字符'#!'后面的字符串到buf中,其中含有腳本解釋程序名,也可能包含解 // 釋程序的幾個參數。然后對buf中的內容進行處理。刪除開始空格、制表符。 strncpy(buf, bh->b_data+2, 1022);/*跳過#!兩個char字符,把后續所有的數據拷到buf*/ brelse(bh); iput(inode); buf[1022] = '\0';/*一個block是1024byte,除去文件頭的#!,只剩1022byte了;然后以0結尾*/ if ((cp = strchr(buf, '\n'))) { *cp = '\0'; for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++); } if (!cp || *cp == '\0') { retval = -ENOEXEC; /* No interpreter name found */ goto exec_error1; } // 此時我們得到了開頭是腳本解釋程序名的一行內容(字符串)。下面分析改行。 // 首先取第一個字符串,它應該是解釋程序名,此時i_name指向該名稱。若解釋 // 程序名后還有字符,則它們應該是解釋程序的參數串,於是令i_arg指向該串。 interp = i_name = cp; i_arg = 0; for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) { if (*cp == '/') i_name = cp+1; } if (*cp) { *cp++ = '\0'; i_arg = cp; } /* * OK, we've parsed out the interpreter name and * (optional) argument. */ // 現在我們要把上面解析出來的解釋程序名i_name及其參數i_arg和腳本文件名作 // 即使程序的參數放進環境和參數塊中。不過首先我們需要把函數提供的原來一 // 些參數和環境字符串先放進去,然后再放這里解析出來的。例如對於命令行參 // 數來說,如果原來的參數是"-arg1-arg2"、解釋程序名是bash、其參數是"-iarg1 // -iarg2"、腳本文件名(即原來的執行文件名)是"example.sh",那么放入這里 // 的參數之后,新的命令行類似於這樣: // "bash -iarg1 -iarg2 example.sh -arg1 -arg2" // 這里我們把sh_bang標志置上,然后把函數參數提供的原有參數和環境字符串 // 放入到空間中。環境字符串和參數個數分別是envc和argc-1個。少復制的一 // 個原有參數是原來的執行文件名,即這里的腳本文件名。[[??? 這里可以看 // 出,實際上我們需要去另行處理腳本文件名,即這里完全可以復制argc個參 // 數,包括原來執行文件名(即現在的腳本文件名)。因為它位於同一個位置上]] // 注意!這里指針p隨着復制信息增加而逐漸向小地址方向移動,因此這兩個復 // 制串函數執行完后,環境參數串信息塊位於程序命令行參數串信息塊的上方, // 並且p指向程序的第一個參數串。copy_strings()最后一個參數(0)指明參數 // 字符串在用戶空間。 if (sh_bang++ == 0) { /*每拷貝1次,P就減少拷貝的字節數*/ p = copy_strings(envc, envp, page, p, 0); /*下面傳入的P是上面的返回值;由於P是偏移,所以下面接着上面往下繼續copy*/ p = copy_strings(--argc, argv+1, page, p, 0); } /* * Splice in (1) the interpreter's name for argv[0] * (2) (optional) argument to interpreter * (3) filename of shell script * * This is done in reverse order, because of how the * user environment and arguments are stored. */ // 接着我們逆向復制腳本文件名、解釋程序的參數和解釋程序文件名到參數和環 // 境空間中。若出錯,則置出錯碼,跳轉到exec_error1。另外,由於本函數參 // 數提供的腳本文件名filename在用戶空間,而這里賦予copy_string()的腳本 // 文件名指針在內核空間,因此這個復制字符串函數的最后一個參數(字符串來 // 源標志)需要被設置成1.若字符串在內核空間,則copy_strings()的最后一個 // 參數要設置成2。 p = copy_strings(1, &filename, page, p, 1); argc++; if (i_arg) { p = copy_strings(1, &i_arg, page, p, 2); argc++; } p = copy_strings(1, &i_name, page, p, 2); argc++; if (!p) { retval = -ENOMEM; goto exec_error1; } /* * OK, now restart the process with the interpreter's inode. */ // 最后我們取得解釋程序的i節點指針,然后跳轉到上面去執行解釋程序。為了 // 獲得解釋程序的i節點,我們需要使用namei()函數,但是該函數所使用的參數 // (文件名)是從用戶數據空間得到的,即從段寄存器fs指向空間中取得。因此調 // 用namei()函數之前我們需要先臨時讓fs指向內核數據空間,以讓函數能從內 // 核空間得到解釋程序名,並在namei()返回后恢復fs的默認設置。因此這里我 // 們先臨時保存原fs段寄存器(原指向用戶數據段)的值,將其設置成指向內核 // 數據段,然后取解釋程序的i節點。之后再恢復fs的原值。並跳轉到restart_interp // 出重新處理新的執行文件——腳本文件解釋程序。 old_fs = get_fs(); set_fs(get_ds()); if (!(inode=namei(interp))) { /* get executables inode */ set_fs(old_fs); retval = -ENOENT; goto exec_error1; } set_fs(old_fs); goto restart_interp; } // 此時緩沖塊中的執行文件頭結構數據已經復制到了ex中。於是先釋放該緩沖塊,並 // 開始對ex中的執行頭信息進行判斷處理。對於Linux0.11內核來說,它僅支持ZMAGIC // 執行文件格式,並且執行文件代碼都從邏輯地址0開始執行,因此不支持含有代碼 // 或數據重定位信息的執行文件。當然,如果執行文件實在太大或者執行文件殘缺不 // 全,那么我們也不能運行它。因此對於下列情況將不執行程序:如果執行文件不是 // 需求頁可執行文件(ZMAGIC)、或者代碼和數據重定位部分不等於0,或者(代碼段 // + 數據段+堆)長度超過50MB、或者執行文件長度小於(代碼段+數據段+符號表長度 // +執行頭部分)長度的總和。 brelse(bh); if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize || ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||/*程序加上棧堆才4M,這么大的量肯定是錯的*/ inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) { retval = -ENOEXEC; goto exec_error2; } // 另外,如果執行文件中代碼開始處沒有位於1個頁面(1024字節)邊界處,則也不能 // 執行。因為需求頁(Demand paging)技術要求加載執行文件內容時以頁面為單位, // 因此要求執行文件映象中代碼和數據都從頁面邊界處開始。 if (N_TXTOFF(ex) != BLOCK_SIZE) { printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename); retval = -ENOEXEC; goto exec_error2; } // 如果sh_bang標志沒有設置,則復制指定個數的命令行參數和環境字符串到參數和 // 環境空間中。若sh_bang標志已經設置,則表明是將運行腳本解釋程序,此時環境 // 變量頁面已經復制,無須再復制。同樣,若sh_bang沒有置位而需要復制的話,那 // 么此時指針p隨着復制信息增加而逐漸向小地址方向移動,因此這兩個復制串函數 // 執行完后,環境參數串信息塊位於程序參數串信息塊上方,並且p指向程序的第1個 // 參數串。事實上,p是128KB參數和環境空間中的偏移值。因此如果p=0,則表示環 // 境變量與參數空間頁面已經被占滿,容納不下了。 if (!sh_bang) { p = copy_strings(envc,envp,page,p,0); p = copy_strings(argc,argv,page,p,0); if (!p) { retval = -ENOMEM; goto exec_error2; } } /* OK, This is the point of no return */ // 前面我們針對函數參數提供的信息對需要運行執行文件的命令行參數和環境空間進 // 行了設置,但還沒有為執行文件做過什么實質性的工作,即還沒有做過為執行文件 // 初始化進程任務結構信息、建立頁表等工作。現在我們就來做這些工作。由於執行 // 文件直接使用當前進程的“軀殼”,即當錢進程將被改造成執行文件的進程,因此我 // 們需要首先釋放當前進程占用的某些系統資源,包括關閉指定的已打開文件、占用 // 的頁表和內存頁面等。然后根據執行文件頭結構信息修改當前進程使用的局部描述 // 符表LDT中描述符的內容,重新設置代碼段和數據段描述符的限長,再利用前面處 // 理得到的e_uid和e_gid等信息來設置進程任務結構中相關的字段。最后把執行本次 // 系統調用程序的返回地址eip[]指向執行文件中代碼的其實位置處。這樣當本系統 // 調用退出返回后就會去運行新執行文件的代碼了。注意,雖然此時新執行文件代碼 // 和數據還沒有從文件中加載到內存中,但其參數和環境塊已經在copy_strings()中 // 使用get_free_page()分配了物理內存頁來保存數據,並在change_ldt()函數中使 // 用put_page()放到了進程邏輯空間的末端處。另外,在create_tables()中也會由 // 於在用戶棧上存放參數和環境指針表而引起缺頁異常,從而內存管理程序也會就此 // 為用戶棧空間映射物理內存頁。 // // 這里我們首先放回進程原執行程序的i節點,並且讓進程executable字段指向新執行 // 文件的i節點。然后復位原進程的所有信號處理句柄。再根據設定的執行時關閉文件 // 句柄(close_on_exec)位圖標志,關閉指定的打開文件,並復位該標志。 /* 一個shell腳本可能有N個文件順序執行;執行完前面的文件后,需要把當前線程的executable換成下一個文件的 */ if (current->executable) iput(current->executable); current->executable = inode;/*指向當前需要執行文件的inode節點*/ for (i=0 ; i<32 ; i++) current->sigaction[i].sa_handler = NULL;/*清空當前進程的信號處理函數*/ for (i=0 ; i<NR_OPEN ; i++) if ((current->close_on_exec>>i)&1)/*原線程執行后該關閉的文件都先關閉了,並清空位圖*/ sys_close(i); current->close_on_exec = 0; // 然后根據當前進程指定的基地址和限長,釋放原來程序的代碼段和數據段所對應的 // 內存頁表指定的物理內存頁面及頁表本身。此時新執行文件並沒有占用主內存區任 // 何頁面,因此在處理器真正運行新執行文件代碼時就會引起缺頁異常中斷,此時內 // 存管理程序執行缺頁處理而為新執行文件申請內存頁面和設置相關表項,並且把相 // 關執行文件頁面讀入內存中。如果“上次任務使用了協處理器”指向的是當前進程, // 則將其置空,並復位使用了協處理器的標志。 free_page_tables(get_base(current->ldt[1]),get_limit(0x0f)); free_page_tables(get_base(current->ldt[2]),get_limit(0x17)); if (last_task_used_math == current) last_task_used_math = NULL; current->used_math = 0; // 然后我們根據新執行文件頭結構中的代碼長度字段a_text的值修改局部表中描述符 // 基地址和段限長,並將128KB的參數和環境空間頁面放置在數據段末端。執行下面 // 語句之后,p此時更改成以數據段起始處為原點的偏移值,但仍指向參數和環境空 // 間數據開始處,即已轉換成為棧指針值。然后調用內部函數create_tables()在棧中 // 穿件環境和參數變量指針表,供程序的main()作為參數使用,並返回該棧指針。 p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;/*上一個文件已經執行完,當然要換成下一個文件的ldt*/ p = (unsigned long) create_tables((char *)p,argc,envc); // 接着再修改各字段值為新執行文件的信息。即令進程任務結構代碼尾字段end_code // 等於執行文件的代碼長度a_text;數據尾字段end_data等於執行文件的代碼段長度 // 加數據段長度(a_data+a_text);並令進程堆結尾字段brk=a_text+a_data+a_bss. // brk用於指明進程當前數據段(包括未初始化數據部分)末端位置。然后設置進程 // 棧開始字段為棧指針所在頁面,並重新設置進程的有效用戶id和有效組id。 current->brk = ex.a_bss + (current->end_data = ex.a_data + (current->end_code = ex.a_text)); current->start_stack = p & 0xfffff000; current->euid = e_uid; current->egid = e_gid; // 如果執行文件代碼加數據長度的末端不再頁面邊界上,則把最后不到1頁長度的內 // 存過空間初始化為零。 i = ex.a_text+ex.a_data; while (i&0xfff) put_fs_byte(0,(char *) (i++)); // 最后將原調用系統中斷的程序在堆棧上的代碼指針替換為指向新執行程序的入口點, // 並將棧指針替換為執行文件的棧指針。此后返回指令將這些棧數據並使得CPU去執 // 行新執行文件,因此不會返回到原調用系統中斷的程序中去了。 eip[0] = ex.a_entry; /* eip, magic happens :-):前面做了大量的准備工作,終於在這里改eip了 */ eip[3] = p; /* stack pointer */ return 0; exec_error2: iput(inode); exec_error1: for (i=0 ; i<MAX_ARG_PAGES ; i++) free_page(page[i]); return(retval); }
上述代碼很多,為方便理解,這里繼續畫個內存分布圖:大部分代碼都在往進程的空間拷貝各種數據!
總的來說,進程內存從上到下的分布:參數列表、環境變量、棧、數據段、代碼段;
某大廠有句廠訓名言:指哪打哪!意思就是領導要求基層員工干啥,基層員工就干啥,100%服從命令,不能有任何質疑!個人感覺cpu是典型的指哪打哪:
- ds指向的內存地址就是數據段的開始,mov等指令就從ds指定的數據段取數據;至於業務邏輯上是不是對的,cpu硬件是沒法判斷的,需要軟件程序員來確保!
- ss指向的內存地址就是棧段的開始,push、pop等指令就在ss指定的段讀寫數據;
- cs段指向的內存地址就是代碼段的開始,eip就把當前指向內存地址的二進制碼讀出來當成代碼執行;至於取出來的二進制碼是不是代碼(比如錯誤把數據當成了代碼),執行的邏輯是不是對的,cpu硬件也是沒法判斷的,同樣需要軟件程序員來保障!
理清了上面的邏輯后再去看代碼,發現代碼雖然多,但是並不難:主要就干了下面幾件事:
- 權限檢查、其他struct的屬性字段(文件inode、進程task struct)
- 數據來回復制倒騰(我感覺80%都是這類代碼)
- 設置eip,跳轉到目標地址