试述shell的启动过程详情

嵌入式操作系统

57人已加入

描述

  Linux用户启动一个进程的通用方法是在shell中执行命令,命令中包括可执行程序的路径以及启动所需参数。新启动的进程是shell进程的子进程,shell使用wait系列函数等待用户进程的结束,在进程结束后wait函数会返回,从而shell收到通知并回收资源。本文主要说明shell如何启动用户进程,Linux系统中可执行文件格式ELF以及通过execve系统调用启动用户进程的过程。

  Shell

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

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分