Linux用戶啟動(dòng)一個(gè)進(jìn)程的通用方法是在shell中執(zhí)行命令,命令中包括可執(zhí)行程序的路徑以及啟動(dòng)所需參數(shù)。新啟動(dòng)的進(jìn)程是shell進(jìn)程的子進(jìn)程,shell使用wait系列函數(shù)等待用戶進(jìn)程的結(jié)束,在進(jìn)程結(jié)束后wait函數(shù)會(huì)返回,從而shell收到通知并回收資源。本文主要說明shell如何啟動(dòng)用戶進(jìn)程,Linux系統(tǒng)中可執(zhí)行文件格式ELF以及通過execve系統(tǒng)調(diào)用啟動(dòng)用戶進(jìn)程的過程。
shell的啟動(dòng)過程
1)內(nèi)核(/unix,/vmunix,/boot/zImage等)將加載至內(nèi)存,直到系統(tǒng)關(guān)機(jī);
2)init將掃描/etc/inittab(inittab列出可用的終端及其屬性),一旦找到活動(dòng)的終端,mingetty會(huì)給出login提示符和口令,mingetty提示輸入用戶及口令;
3) 將用戶名及口令傳遞給login, login驗(yàn)證用戶及口令是否匹配,如果身份驗(yàn)證通過,login將會(huì)自動(dòng)轉(zhuǎn)到其$HOME;
4)將控制權(quán)移交到所啟動(dòng)的任務(wù)(在移交之前分別完成setgid,setuid)。 如在/etc/passwd文件中用戶的shell為/bin/sh。
5)讀取文件/etc/profile和$HOME/.profile中系統(tǒng)定義變量和用戶定義變量,系統(tǒng)給出shell提示符$PROMPT,對(duì)普通用戶用“$”作提示符,對(duì)超級(jí)用戶(root)用“#”作提示符。
6)在shell提示符,就可以鍵入命令名稱(或shell程序)及所需要的參數(shù)。
7)當(dāng)用戶準(zhǔn)備結(jié)束登錄對(duì)話進(jìn)程時(shí),可以鍵入logout命令、exit命令或按ctrl+d,結(jié)束后控制權(quán)將交給init。
shell工作方式
通過shell運(yùn)行命令執(zhí)行一個(gè)用戶進(jìn)程的方法是,通過fork()創(chuàng)建一個(gè)子進(jìn)程,在子進(jìn)程中調(diào)用execve(pathname, argv, envp)加載新程序,為新進(jìn)程建立文本段,創(chuàng)建棧、數(shù)據(jù)段以及堆,在shell進(jìn)程中執(zhí)行wait調(diào)用等待子進(jìn)程返回。C程序代碼框架大致如下:
點(diǎn)擊(此處)折疊或打開#include 《stdio.h》
#include 《stdlib.h》
#include 《unistd.h》
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid《0)
{
/* error occurred */
exit(-1);
}
else if (pid==0)
{
execve(pathname, argv, envp);
}
else
{
wait(NULL);
exit(0);
}
}
ELF文件格式
ELF是Executable and Linking Format的縮寫,是可執(zhí)行二進(jìn)制文件格式和目標(biāo)文件等格式的相關(guān)標(biāo)準(zhǔn)。ELF文件由ELF頭、程序頭、節(jié)頭、存儲(chǔ)器表格和符合表格構(gòu)成,以下重點(diǎn)介紹ELF頭、程序頭和節(jié)頭。
ELF頭
ELF頭的內(nèi)容標(biāo)識(shí)這是一個(gè)ELF文件,使用readelf的-h選項(xiàng)可以查看。ELF頭的結(jié)構(gòu)如下:
點(diǎn)擊(此處)折疊或打開unsigned char e_ident[EI_NIDENT];
ElfN_Half e_type; //類型
ElfN_Half e_machine; //機(jī)器
ElfN_Word e_version; //版本
ElfN_Addr e_entry; //入口地址
ElfN_Off e_phoff; //程序頭的地址
ElfN_Off e_shoff; //節(jié)頭的起點(diǎn)
ElfN_Half e_ehsize; //標(biāo)志
ElfN_Half e_phentsize; //頭的大小
ElfN_Half e_phnum; //程序頭的大小
ElfN_Half e_shentsize; //程序頭數(shù)
ElfN_Half e_shnum; // 節(jié)頭數(shù)
ElfN_Half e_shstrndx; // 節(jié)名的符號(hào)串表格
e_ident保存著ELF的幻數(shù)和其他信息,最前面四個(gè)字節(jié)里有如下幻數(shù):0x7F 0x45 0x4C 0x46,用字符串表示為“\177ELF”;其后的字節(jié)如果是32位則為ELFCLASS32,如果是64位則用ELFCLASS64;其后的字節(jié)表示endian,little endian用ELFDATA2LSB,big endian用ELFDATA2MSB;在此之后,ELF版本和OS、ABI等信息用一個(gè)比特位表示。
e_type表示文件類型,用ET_REL, ET_EXEC, ET_DYN, ET_CORE分別表示可重定位文件、可執(zhí)行文件、共享目標(biāo)文件和內(nèi)核文件。
e_machine表示架構(gòu)類型,e_version表示ELF版本,e_entry表示ELF中開始執(zhí)行的虛擬地址,e_ehsize表示ELF頭的大小,e_phoff、e_phentsize和e_phnum表示程序頭表格的位置和數(shù)量。
程序頭
程序頭表格是由ELF頭的e_phoff指定的偏移量和e_phentsize、e_phnum共同確定大小的表格組成。e_phentsize表示表格中程序頭的大小,e_phnum表示表格中程序頭的大小,e_phnum表示表格中程序頭的數(shù)量。程序頭可用readelf的-l選項(xiàng)查看。程序頭結(jié)構(gòu)如下:
點(diǎn)擊(此處)折疊或打開ElfN_Word p_type; // 段類型
ElfN_Off p_offset; // 偏移量
ElfN_Addr p_vaddr; // 虛擬地址
ElfN_Addr p_paddr; // 物理地址
ElfN_Addr p_filesz; // 文件大小
ElfN_Addr p_memsz; // 內(nèi)存大小
ElfN_Addr p_flags; // 標(biāo)志
ElfN_Addr p_align; // 對(duì)齊
類型p_type表示的意義如下:
p_typevalue說明
PT_LOAD
1轉(zhuǎn)載的程序段
PT_DYNAMIC
2動(dòng)態(tài)鏈接信息
PT_INTERP
3程序解釋器
PT_NOTE
4輔助信息
PT_PHDR
5程序頭表格本身
PT_TLS
6線程本地存儲(chǔ)器
PT_GNU_EH_FRAME
0x64744e550GNU .eh_frame_hdr段
PT_GNU_STACK
0x64744e551
堆棧的可執(zhí)行性
節(jié)頭
節(jié)頭表格是由ELF頭的e_shoff指定的偏移量以及e_shentsize、e_shnum共同規(guī)定了大小的表格組成。readelf的-S選項(xiàng)可顯示節(jié)頭。節(jié)頭的構(gòu)成如下:
點(diǎn)擊(此處)折疊或打開ElfN_Word sh_name; //名稱
ElfN_Word sh_type; //類型
ElfN_Word sh_flags; //標(biāo)志
ElfN_Addr sh_addr; //地址
ElfN_Off sh_offset; //偏移
ElfN_Word sh_size; //大小
ElfN_Word sh_link; //鏈接
ElfN_Word sh_info; //節(jié)信息
ElfN_Word sh_addralign; //對(duì)齊
ElfN_Word sh_entsize; //節(jié)為表格時(shí)各個(gè)條目的大小
sh_type的類型如下:
sh_type值說明
SHT_PROGBITS1程序數(shù)據(jù)
SHT_SYMTAB
2符號(hào)表格
SHT_STRTAB
3存儲(chǔ)器格式
SHT_RELA
4帶加數(shù)的再配置條目
SHT_HASH
5符號(hào)散列數(shù)據(jù)表格
SHT_DYNAMIC
6動(dòng)態(tài)鏈接信息
SHT_NOTE
7Notes
SHT_NOBITS
8文件上無數(shù)據(jù)部分
SHT_REL
9再配置條目
SHT_DYNSYM
11動(dòng)態(tài)鏈接所使用的符號(hào)表格
SHT_INIT_ARRAY
14constructor的排列(.init)
SHT_FINI_ARRAY
15destructor的排列(.fini)
SHT_GNU_verdef0x6ffffffd版本定義節(jié)
SHT_GNU_verneed
0x6ffffffe
版本要求節(jié)
SHT_GNU_versym
0x6fffffff
版本符號(hào)表格
execve系統(tǒng)調(diào)用
execve在內(nèi)核中的系統(tǒng)調(diào)用服務(wù)例程為sys_execve(), 函數(shù)定義在fs/exec.c文件中,相關(guān)代碼如下:
點(diǎn)擊(此處)折疊或打開SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execve_common(filename, argv, envp);
}
static int do_execve_common(struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp)
{
struct linux_binprm *bprm;
struct file *file;
struct files_struct *displaced;
int retval;
if (IS_ERR(filename))
return PTR_ERR(filename);
/*
* We move the actual failure in case of RLIMIT_NPROC excess from
* set*uid() to execve() because too many poorly written programs
* don‘t check setuid() return code. Here we additionally recheck
* whether NPROC limit is still exceeded.
*/
if ((current-》flags & PF_NPROC_EXCEEDED) &&
atomic_read(¤t_user()-》processes) 》 rlimit(RLIMIT_NPROC)) {
retval = -EAGAIN;
goto out_ret;
}
/* We’re below the limit (still or again), so we don‘t want to make
* further execve() calls fail. */
current-》flags &= ~PF_NPROC_EXCEEDED;
retval = unshare_files(&displaced);
if (retval)
goto out_ret;
retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files;
retval = prepare_bprm_creds(bprm);
if (retval)
goto out_free;
check_unsafe_exec(bprm);
current-》in_execve = 1;
file = do_open_exec(filename);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
sched_exec();
bprm-》file = file;
bprm-》filename = bprm-》interp = filename-》name;
retval = bprm_mm_init(bprm);
if (retval)
goto out_unmark;
bprm-》argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm-》argc) 《 0)
goto out;
bprm-》envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm-》envc) 《 0)
goto out;
retval = prepare_binprm(bprm);
if (retval 《 0)
goto out;
retval = copy_strings_kernel(1, &bprm-》filename, bprm);
if (retval 《 0)
goto out;
bprm-》exec = bprm-》p;
retval = copy_strings(bprm-》envc, envp, bprm);
if (retval 《 0)
goto out;
retval = copy_strings(bprm-》argc, argv, bprm);
if (retval 《 0)
goto out;
retval = exec_binprm(bprm);
if (retval 《 0)
goto out;
/* execve succeeded */
current-》fs-》in_exec = 0;
current-》in_execve = 0;
acct_update_integrals(current);
task_numa_free(current);
free_bprm(bprm);
putname(filename);
if (displaced)
put_files_struct(displaced);
return retval;
out:
if (bprm-》mm) {
acct_arg_size(bprm, 0);
mmput(bprm-》mm);
}
out_unmark:
current-》fs-》in_exec = 0;
current-》in_execve = 0;
out_free:
free_bprm(bprm);
out_files:
if (displaced)
reset_files_struct(displaced);
out_ret:
putname(filename);
return retval;
}
函數(shù)調(diào)用鏈為sys_execve()-》do_execve()-》do_execve_common()。 其中sys_execve()和do_execve()參數(shù)列表中的__user標(biāo)簽表示參數(shù)中的變量指向用戶空間。
第46行unshare_files()用于解除與父進(jìn)程共享文件描述符(fork后父進(jìn)程和子進(jìn)程共享打開的文件描述符),使用dup_fd()創(chuàng)建新的struct files_struct;
第62行do_open_exec()用于打開可執(zhí)行文件;
第72行bprm_mm_init()用于初始化bprm數(shù)據(jù)結(jié)構(gòu),用于保存可執(zhí)行文件的上下文;
第76行和第80行分別用于統(tǒng)計(jì)參數(shù)和環(huán)境變量個(gè)數(shù);
第84行prepare_binprm()用于文件的inode信息來填充一些必要的變量信息;
第88行、92行及96行分別將程序名、參數(shù)和環(huán)境變量復(fù)制到bprm結(jié)構(gòu);
第100行exec_binprm()調(diào)用search_binary_handler()是核心函數(shù),用于加載可執(zhí)行程序,依次讓formats隊(duì)列中的成員使用load_binary()函數(shù)裝入可執(zhí)行程序,若成功則讓可執(zhí)行程序開始運(yùn)行,在search_binary_handler()使用struct linux_binfmp來保存處理相應(yīng)格式的可執(zhí)行文件的指針,定義如下:
點(diǎn)擊(此處)折疊或打開struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
};
其中函數(shù)指針load_binary用于加載新進(jìn)程,load_shlib用于加載共享庫(kù),core_dump用于將當(dāng)前進(jìn)程的上下文保存在一個(gè)名為core的文件中。
Linux內(nèi)核允許用戶通過調(diào)用在include/linux/binfmt.h文件中定義的register_binfmt()和unregister_binfmt()來添加和刪除linux_binfmt結(jié)構(gòu)體鏈表中的元素,以支持用戶特定的可執(zhí)行文件類型。在調(diào)用特定的load_binary函數(shù)加載一定格式的可執(zhí)行文件后,程序?qū)⒎祷氐絪ys_execve()中繼續(xù)執(zhí)行。該函數(shù)在完成最后幾步的清理工作后,將會(huì)結(jié)束處理并返回到用戶態(tài)中,最后,系統(tǒng)將會(huì)將CPU分配給新加載的程序。
評(píng)論