Linux用戶啟動一個進程的通用方法是在shell中執行命令,命令中包括可執行程序的路徑以及啟動所需參數。新啟動的進程是shell進程的子進程,shell使用wait系列函數等待用戶進程的結束,在進程結束后wait函數會返回,從而shell收到通知并回收資源。本文主要說明shell如何啟動用戶進程,Linux系統中可執行文件格式ELF以及通過execve系統調用啟動用戶進程的過程。
shell的啟動過程
1)內核(/unix,/vmunix,/boot/zImage等)將加載至內存,直到系統關機;
2)init將掃描/etc/inittab(inittab列出可用的終端及其屬性),一旦找到活動的終端,mingetty會給出login提示符和口令,mingetty提示輸入用戶及口令;
3) 將用戶名及口令傳遞給login, login驗證用戶及口令是否匹配,如果身份驗證通過,login將會自動轉到其$HOME;
4)將控制權移交到所啟動的任務(在移交之前分別完成setgid,setuid)。 如在/etc/passwd文件中用戶的shell為/bin/sh。
5)讀取文件/etc/profile和$HOME/.profile中系統定義變量和用戶定義變量,系統給出shell提示符$PROMPT,對普通用戶用“$”作提示符,對超級用戶(root)用“#”作提示符。
6)在shell提示符,就可以鍵入命令名稱(或shell程序)及所需要的參數。
7)當用戶準備結束登錄對話進程時,可以鍵入logout命令、exit命令或按ctrl+d,結束后控制權將交給init。
shell工作方式
通過shell運行命令執行一個用戶進程的方法是,通過fork()創建一個子進程,在子進程中調用execve(pathname, argv, envp)加載新程序,為新進程建立文本段,創建棧、數據段以及堆,在shell進程中執行wait調用等待子進程返回。C程序代碼框架大致如下:
點擊(此處)折疊或打開#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的縮寫,是可執行二進制文件格式和目標文件等格式的相關標準。ELF文件由ELF頭、程序頭、節頭、存儲器表格和符合表格構成,以下重點介紹ELF頭、程序頭和節頭。
ELF頭
ELF頭的內容標識這是一個ELF文件,使用readelf的-h選項可以查看。ELF頭的結構如下:
點擊(此處)折疊或打開unsigned char e_ident[EI_NIDENT];
ElfN_Half e_type; //類型
ElfN_Half e_machine; //機器
ElfN_Word e_version; //版本
ElfN_Addr e_entry; //入口地址
ElfN_Off e_phoff; //程序頭的地址
ElfN_Off e_shoff; //節頭的起點
ElfN_Half e_ehsize; //標志
ElfN_Half e_phentsize; //頭的大小
ElfN_Half e_phnum; //程序頭的大小
ElfN_Half e_shentsize; //程序頭數
ElfN_Half e_shnum; // 節頭數
ElfN_Half e_shstrndx; // 節名的符號串表格
e_ident保存著ELF的幻數和其他信息,最前面四個字節里有如下幻數:0x7F 0x45 0x4C 0x46,用字符串表示為“\177ELF”;其后的字節如果是32位則為ELFCLASS32,如果是64位則用ELFCLASS64;其后的字節表示endian,little endian用ELFDATA2LSB,big endian用ELFDATA2MSB;在此之后,ELF版本和OS、ABI等信息用一個比特位表示。
e_type表示文件類型,用ET_REL, ET_EXEC, ET_DYN, ET_CORE分別表示可重定位文件、可執行文件、共享目標文件和內核文件。
e_machine表示架構類型,e_version表示ELF版本,e_entry表示ELF中開始執行的虛擬地址,e_ehsize表示ELF頭的大小,e_phoff、e_phentsize和e_phnum表示程序頭表格的位置和數量。
程序頭
程序頭表格是由ELF頭的e_phoff指定的偏移量和e_phentsize、e_phnum共同確定大小的表格組成。e_phentsize表示表格中程序頭的大小,e_phnum表示表格中程序頭的大小,e_phnum表示表格中程序頭的數量。程序頭可用readelf的-l選項查看。程序頭結構如下:
點擊(此處)折疊或打開ElfN_Word p_type; // 段類型
ElfN_Off p_offset; // 偏移量
ElfN_Addr p_vaddr; // 虛擬地址
ElfN_Addr p_paddr; // 物理地址
ElfN_Addr p_filesz; // 文件大小
ElfN_Addr p_memsz; // 內存大小
ElfN_Addr p_flags; // 標志
ElfN_Addr p_align; // 對齊
類型p_type表示的意義如下:
p_typevalue說明
PT_LOAD
1轉載的程序段
PT_DYNAMIC
2動態鏈接信息
PT_INTERP
3程序解釋器
PT_NOTE
4輔助信息
PT_PHDR
5程序頭表格本身
PT_TLS
6線程本地存儲器
PT_GNU_EH_FRAME
0x64744e550GNU .eh_frame_hdr段
PT_GNU_STACK
0x64744e551
堆棧的可執行性
節頭
節頭表格是由ELF頭的e_shoff指定的偏移量以及e_shentsize、e_shnum共同規定了大小的表格組成。readelf的-S選項可顯示節頭。節頭的構成如下:
點擊(此處)折疊或打開ElfN_Word sh_name; //名稱
ElfN_Word sh_type; //類型
ElfN_Word sh_flags; //標志
ElfN_Addr sh_addr; //地址
ElfN_Off sh_offset; //偏移
ElfN_Word sh_size; //大小
ElfN_Word sh_link; //鏈接
ElfN_Word sh_info; //節信息
ElfN_Word sh_addralign; //對齊
ElfN_Word sh_entsize; //節為表格時各個條目的大小
sh_type的類型如下:
sh_type值說明
SHT_PROGBITS1程序數據
SHT_SYMTAB
2符號表格
SHT_STRTAB
3存儲器格式
SHT_RELA
4帶加數的再配置條目
SHT_HASH
5符號散列數據表格
SHT_DYNAMIC
6動態鏈接信息
SHT_NOTE
7Notes
SHT_NOBITS
8文件上無數據部分
SHT_REL
9再配置條目
SHT_DYNSYM
11動態鏈接所使用的符號表格
SHT_INIT_ARRAY
14constructor的排列(.init)
SHT_FINI_ARRAY
15destructor的排列(.fini)
SHT_GNU_verdef0x6ffffffd版本定義節
SHT_GNU_verneed
0x6ffffffe
版本要求節
SHT_GNU_versym
0x6fffffff
版本符號表格
execve系統調用
execve在內核中的系統調用服務例程為sys_execve(), 函數定義在fs/exec.c文件中,相關代碼如下:
點擊(此處)折疊或打開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;
}
函數調用鏈為sys_execve()-》do_execve()-》do_execve_common()。 其中sys_execve()和do_execve()參數列表中的__user標簽表示參數中的變量指向用戶空間。
第46行unshare_files()用于解除與父進程共享文件描述符(fork后父進程和子進程共享打開的文件描述符),使用dup_fd()創建新的struct files_struct;
第62行do_open_exec()用于打開可執行文件;
第72行bprm_mm_init()用于初始化bprm數據結構,用于保存可執行文件的上下文;
第76行和第80行分別用于統計參數和環境變量個數;
第84行prepare_binprm()用于文件的inode信息來填充一些必要的變量信息;
第88行、92行及96行分別將程序名、參數和環境變量復制到bprm結構;
第100行exec_binprm()調用search_binary_handler()是核心函數,用于加載可執行程序,依次讓formats隊列中的成員使用load_binary()函數裝入可執行程序,若成功則讓可執行程序開始運行,在search_binary_handler()使用struct linux_binfmp來保存處理相應格式的可執行文件的指針,定義如下:
點擊(此處)折疊或打開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 */
};
其中函數指針load_binary用于加載新進程,load_shlib用于加載共享庫,core_dump用于將當前進程的上下文保存在一個名為core的文件中。
Linux內核允許用戶通過調用在include/linux/binfmt.h文件中定義的register_binfmt()和unregister_binfmt()來添加和刪除linux_binfmt結構體鏈表中的元素,以支持用戶特定的可執行文件類型。在調用特定的load_binary函數加載一定格式的可執行文件后,程序將返回到sys_execve()中繼續執行。該函數在完成最后幾步的清理工作后,將會結束處理并返回到用戶態中,最后,系統將會將CPU分配給新加載的程序。
評論
查看更多