天天看點

Linux 可執行檔案程式載入和執行過程

作者:Linux碼農

今天分析下 Linux 下一個可執行檔案是怎麼載入和執行的。

Linux 下标準的可執行檔案格式是 ELF。

ELF (Executable and Linking Format) 是一種對象檔案的格式。

在 linux 系統中,一個ELF檔案主要用來表示3種類型的檔案

  • 可執行檔案 : 被作業系統中的加載器從硬碟中讀取,加載到記憶體中去執行。
  • 目标檔案(.o) :被連結器讀取,用來産生一個可執行檔案或者共享檔案。
  • 共享檔案(.so) :在動态連結的時候,由 ld-linux.so 來讀取。

今天分析一下可執行檔案類型。

當我們在 Linux 下的 bash 下輸入一個指令執行可執行程式時,bash 程序會調用 fork() 建立一個新的程序,然後新的程序調用 execve() 系統調用來執行指定的可執行程式。

原先的 bash 程序繼續傳回等待剛才啟動的新程序結束,然後繼續等待使用者的輸入。

execve() 系統調用原型如下:

int execve(const char *filename, char *const argv[], char *const envp[]);
           

他們的三個參數分别是被執行的程式檔案名、執行參數和環境變量。

當調用 execve() 系統調用時,進入核心調用過程如下

sys_execve()
--> do_execve() // 主要根據可執行檔案進行構造 linux_binprm 核心結構,該結構記錄可執行檔案資訊,然後從formats連結清單中找到執行該執行檔案的方法
--> load_elf_binary
           

do_execve 主要完成 linux_binprm 核心結構的初始化,該結構定義如下:

struct linux_binprm{
char buf[128];
/*與可執行檔案路徑名的處理一樣,每個參數的最大長度定為一個實體頁,是以設定為一個頁面指針數組,最大個數為32*/
unsigned long page[MAX_ARG_PAGES];
unsigned long p;
int sh_bang; //可執行檔案的性質,當時shell腳本時為1
struct inode * inode; //可執行檔案的inode
int e_uid, e_gid; //可執行檔案的屬性
int argc, envc; //指令行參數和環境變量數目
char * filename; //可執行檔案的路徑名 
};           

該結構主要記錄了執行可執行檔案所有需要的資訊。

其中 page 表示的是存放參數的頁面數組,而 p 表示的是在這些數組的頂部,因為這些字元串是按照棧的方式存放的,也就是說,先配置設定位址更高的數組,向低位址方向增長,p 就指向棧頂部。

結構如下圖

Linux 可執行檔案程式載入和執行過程

最終執行可執行檔案的接口為 load_elf_binary。

具體實作如下:

static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
struct file * file;
...

status = 0;
load_addr = 0;
elf_ex = *((struct elfhdr *) bprm->buf); /* exec-header */


//比對四個字元,必須是0x7f、‘E’、‘L’、和‘F’
if (elf_ex.e_ident[0] != 0x7f || strncmp(&elf_ex.e_ident[1], "ELF",3) != 0)
return -ENOEXEC;


//映像類型必須是ET_EXEC
if(elf_ex.e_type != ET_EXEC || (elf_ex.e_machine != EM_386 && elf_ex.e_machine != EM_486) || (!bprm->inode->i_op || !bprm->inode->i_op->default_file_ops || !bprm->inode->i_op->default_file_ops->mmap)){
return -ENOEXEC;
};


elf_phdata = (struct elf_phdr *) kmalloc(elf_ex.e_phentsize * elf_ex.e_phnum, GFP_KERNEL);

old_fs = get_fs();
set_fs(get_ds());
//擷取所有程式頭表資訊
retval = read_exec(bprm->inode, elf_ex.e_phoff, (char *) elf_phdata, elf_ex.e_phentsize * elf_ex.e_phnum);
set_fs(old_fs);

elf_ppnt = elf_phdata;

elf_bss = 0;
elf_brk = 0;

elf_exec_fileno = open_inode(bprm->inode, O_RDONLY);



file = current->files->fd[elf_exec_fileno];

elf_stack = 0xffffffff;
elf_interpreter = NULL;
start_code = 0;
end_code = 0;
end_data = 0;

old_fs = get_fs();
set_fs(get_ds());

/* 處了解釋器段,通過周遊每個段,找到PT_INTERP類型段,也即是解釋器段,找到說明需要運作過程中的動态連結。
“解釋器”段實際上隻是一個字元串,即解釋器的檔案名,如”/lib/ld-linux.so.2”, 或者64位機器上對應的叫做”/lib64/ld-linux-x86-64.so.2”
通過指令 readelf -l 可執行檔案 擷取解釋器段資訊 type 類型為 INTERP
*/ 
for(i=0;i < elf_ex.e_phnum; i++){
//檢查是否有需要加載的解釋器
if(elf_ppnt->p_type == PT_INTERP) { // 該類型表示動态連接配接器


elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
//根據其位置的p_offset和大小p_filesz把整個"解釋器"段的内容讀入緩沖區;
//從使用者程式的program header 中讀取動态連結器的路徑,比如 /lib64/ld-linux-x86-64.so
retval = read_exec(bprm->inode,elf_ppnt->p_offset,elf_interpreter, elf_ppnt->p_filesz);

if(retval >= 0) //擷取連接配接器的inod
retval = namei(elf_interpreter, &interpreter_inode);
if(retval >= 0) //解釋器也是一個elf格式的程式,讀入解釋器的前128個位元組,即解釋器映像的頭部
retval = read_exec(interpreter_inode,0,bprm->buf,128);

if(retval >= 0){
interp_ex = *((struct exec *) bprm->buf); /* exec-header */
interp_elf_ex = *((struct elfhdr *) bprm->buf); /* exec-header */

};

};
elf_ppnt++;
};

set_fs(old_fs);


//檢查并讀取解釋器(也可以叫動态連結器)的程式頭表
if(elf_interpreter){
...
}


if (!bprm->sh_bang) {
...
}

//在此清除掉了父程序的所有相關代碼 
flush_old_exec(bprm);

current->mm->end_data = 0;
current->mm->end_code = 0;
current->mm->start_mmap = ELF_START_MMAP;
current->mm->mmap = NULL;
elf_entry = (unsigned int) elf_ex.e_entry;

current->mm->rss = 0;
//建立環境變量參數的頁表映射,從虛拟位址 0xC0000000UL 處開始
bprm->p += setup_arg_pages(0, bprm->page);
current->mm->start_stack = bprm->p;


old_fs = get_fs();
set_fs(get_ds());

elf_ppnt = elf_phdata;
for(i=0;i < elf_ex.e_phnum; i++){

if(elf_ppnt->p_type == PT_INTERP) {

set_fs(old_fs);
//不裝入解釋器,那麼這個入口位址就是目标映像本身的入口位址
if(interpreter_type & 1) 
elf_entry = load_aout_interp(&interp_ex, interpreter_inode); //加載text data bss段

//如果需要裝入解釋器,就通過load_elf_interp裝入其映像, 并把将來進入使用者空間的入口位址設定成load_elf_interp()的傳回值,即解釋器映像的入口位址
if(interpreter_type & 2) 
elf_entry = load_elf_interp(&interp_elf_ex, interpreter_inode);

old_fs = get_fs();
set_fs(get_ds());

iput(interpreter_inode);
kfree(elf_interpreter);

if(elf_entry == 0xffffffff) { 
...
};
};

// 類型為 PT_LOAD 需要映射到程序的位址虛拟空間的
if(elf_ppnt->p_type == PT_LOAD) {
error = do_mmap(file, elf_ppnt->p_vaddr & 0xfffff000, elf_ppnt->p_filesz + (elf_ppnt->p_vaddr & 0xfff),
PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE, elf_ppnt->p_offset & 0xfffff000);

#ifdef LOW_ELF_STACK
if(elf_ppnt->p_vaddr & 0xfffff000 < elf_stack) 
elf_stack = elf_ppnt->p_vaddr & 0xfffff000;
#endif

if(!load_addr) 
load_addr = elf_ppnt->p_vaddr - elf_ppnt->p_offset;
k = elf_ppnt->p_vaddr;
if(k > start_code) start_code = k;
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if(k > elf_bss) elf_bss = k;
if((elf_ppnt->p_flags | PROT_WRITE) && end_code < k)
end_code = k; 
if(end_data < k) end_data = k; 
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
if(k > elf_brk) elf_brk = k; 
};
elf_ppnt++;
};
set_fs(old_fs);

kfree(elf_phdata);

if(interpreter_type != INTERPRETER_AOUT) sys_close(elf_exec_fileno);

...

current->executable = bprm->inode;
bprm->inode->i_count++;
#ifdef LOW_ELF_STACK
current->start_stack = p = elf_stack - 4;
#endif
bprm->p -= MAX_ARG_PAGES*PAGE_SIZE;
/*
create_elf_tables填寫目标檔案的參數環境變量等必要資訊
在完成裝入,啟動使用者空間的映像運作之前,還需要為目标映像和解釋器準備好一些有關的資訊,這些資訊包括正常的argc、envc等等,還有一些"輔助向量(Auxiliary Vector)"。
這些資訊需要複制到使用者空間,使它們在CPU進入解釋器或目标映像的程式入口時出現在使用者空間堆棧上。這裡的create_elf_tables()就起着這個作用。
*/
bprm->p = (unsigned long)create_elf_tables((char *)bprm->p, bprm->argc, bprm->envc, 
(interpreter_type == INTERPRETER_ELF ? &elf_ex : NULL), load_addr, 
(interpreter_type == INTERPRETER_AOUT ? 0 : 1));

if(interpreter_type == INTERPRETER_AOUT)
current->mm->arg_start += strlen(passed_fileno) + 1;

//調整記憶體映射内容
current->mm->start_brk = current->mm->brk = elf_brk;
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
current->suid = current->euid = bprm->e_uid;
current->sgid = current->egid = bprm->e_gid;


current->mm->brk = (elf_bss + 0xfff) & 0xfffff000;
sys_brk((elf_brk + 0xfff) & 0xfffff000);

padzero(elf_bss);

////eip和esp改成新的位址,就使得CPU在傳回使用者空間時就進入新的程式入口
start_thread(regs, elf_entry, bprm->p);
if (current->flags & PF_PTRACED)
send_sig(SIGTRAP, current, 0);
MOD_DEC_USE_COUNT;
return 0;
}



static inline void start_thread(struct pt_regs * regs, unsigned long eip, unsigned long esp)
{
regs->cs = USER_CS;
regs->ds = regs->es = regs->ss = regs->fs = regs->gs = USER_DS;
regs->eip = eip;
regs->esp = esp;
}
           

該函數主要完成的功能如下:

  • 檢查 ELF 可執行檔案的有效性,比如魔數、程式頭表中段的數量。
  • 尋找動态連結的 “.interp” 段,設定動态連結器路徑。
  • 根據 ELF 可執行檔案的程式頭表的描述,對ELF檔案進行映射,比如代碼段,資料段等。
  • 初始化 ELF 程序環境。
  • 将系統調用的傳回位址修改為 ELF 可執行檔案的入口點,這個入口點取決于程式的連結方式,若是靜态連結,則入口位址為 ELF 檔案的檔案頭中 e_entry 所指的位址;對于動态連結的ELF可執行檔案,程式入口點是動态連結器。

當 load_elf_binary() 執行完畢,傳回至 do_execve() 再傳回至 sys_execve() 時,由于上述步驟已經把系統調用的傳回位址改成了被裝載的ELF可執行程式的入口位址,是以當 sys_execve() 系統調用從核心态傳回到使用者态時,EIP 寄存器直接跳轉到了 LF 程式的入口位址,于是新的程式開始執行,ELF 可執行檔案裝載完成。

繼續閱讀