试述shell的启动过程详情

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分配给新加载的程序。

韦豪创芯荣获“IC风云榜”多项大奖
防孤岛保护装置的作用是什么
大家是如何看待比特币减半大火
苹果13何时上市 什么价位
智能手机的刷新率和触控采样率哪个比较重要
试述shell的启动过程详情
中国电信的下一个云VR计划,将创造出十亿级的商业价值
都是骁龙835:一加5曝光,或月底狙击小米6,你选谁?
光纤式浊度传感器—90°散射光测定原理的说明
iPhone相机大升级,或配备后置三摄
基于Wi-Fi的固定无线接入方案是怎样的
纳微半导体 (Navitas) 在重要亚洲电子会议上 展示氮化镓(GaN) 功率IC
什么是EMC、EMI、EMS?
红芯:站在巨人的肩膀上创新
如何通过开源API在DSP上进行视频处理
中国移动和爱立信合作开发云部署
一文阅尽2017年上半年中国半导体重大事件
OLED与LCD并非取代与被取代关系
如何制作自定义宏机械键盘
基于DM642的嵌入式图像型火灾探测系统