你的位置:首页 > 操作系统

[操作系统]Linux内核如何启动并装载一个可执行程序


2016-04-07

张超《Linux内核分析》MOOC课程//////////////////////////////////////////

 

一、理解编译链接的过程和ELF可执行文件格式

   

我给出了一个例子:

第一步:先编辑一个hello.c,如下

vi hello.c

1  #include <stdio.h>2  #include <stdlib.h>3  4  int main()5  {6    printf("Hello World!\n");7    return 0;8  }

hello.c

第二步:生成预处理文件hello.cpp(预处理负责把include的文件包含进来及宏替换等工作)

gcc -E -o hello.cpp hello.c -m32     

vi hello.cpp  (查看一下hello.cpp的内容,但是太多,这里就不显示了)

第三步:编译成汇编代码hello.s

gcc -x cpp-output -S -o hello.s hello.cpp -m32 

vi hello.s  (查看一下hello.s的内容,如下所示)

 1   .file  "hello.c" 2   .section  .rodata 3 .LC0: 4   .string "Hello World!" 5   .text 6   .globl main 7   .type  main, @function 8 main: 9 .LFB2: 10   .cfi_startproc 11   pushl  %ebp 12   .cfi_def_cfa_offset 8 13   .cfi_offset 5, -8 14   movl  %esp, %ebp 15   .cfi_def_cfa_register 5 16   andl  $-16, %esp 17   subl  $16, %esp 18   movl  $.LC0, (%esp) 19   call  puts 20   movl  $0, %eax 21   leave 22   .cfi_restore 5 23   .cfi_def_cfa 4, 4 24   ret 25   .cfi_endproc 26 .LFE2: 27   .size  main, .-main 28   .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4" 29   .section  .note.GNU-stack,"",@progbits                   

hello.s

第四步:编译成目标代码,得到二进制文件hello.o,(该文件中有机器指令,但不能在机器上直接运行)

gcc -x assembler -c hello.s -o hello.o -m32

vi hello.o (查看一下hello.o的内容,结果是乱码)

第五步:链接成可执行文件hello,(它是二进制文件)

gcc -o hello hello.o -m32

vi hello(查看一下hello,发现除了几个英文标示符可见,其他都是乱码文件,不过开头有ELF)

第六步:运行一下

./hello

其实:我们可以用一个命令直接只生成可执行文件hello,并运行

gcc -o hello hello.c

这里,hello和hello.o都是ELF格式的文件。

而这样编译出来的hello是使用的共享库。

我们也可以静态编译,(是完全把所有需要执行所依赖的东西放到程序内部)

gcc -o hello.static hello.o -m32 -static  

hello.static 也是ELF格式文件

运行一下hello.static

./hello.static

ls -l    (查看一下各个文件)

发现hello.static (733298)比 hello (7336)大的多。

 

静态链接于动态链接:

  1. 静态链接方式于动态链接方式
  • 静态链接方式:在程序运行之前完成所有的组装工作,生成一个可执行的目标文件
  • 动态链接方式:在程序已经为了执行被装载入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝

   2.静态链接库(.a(Linux下)  .lib(Windows下))与动态链接库(.so(Linux下)  .dll(Windows下))

    静态链接库与动态链接库都是共享代码的方式。如果采用静态链接库,则程序把所有执行需要所依赖的东西都加载到可执行文件中了。但若是使用动态库,则不必被包含在最终执行的文件中,可执行文件执行时可以“动态”的引用和卸载这个与可执行程序独立的动态库文件。

    简单的说,静态库和应用程序编译在一起了,在任何情况下都能运行,而动态库是动态链接,顾名思义就是在应用程序启动的时候再会链接的,所以,当用户的系统上没有该动态库时,应用程序就会运行失败。

  3.静态库于动态库的特点

    静态库:代码的装载速度快,执行速度也快,因为编译时它只会把你所需要的那部分链接进去,应用程序相对较大。但是如果没有多个应用程序的话,会被装载多次,浪费内存。

    动态库:

      共享:多个应用程序可以使用同一个动态库,启动多个应用程序的时候,只需要将动态库加载到内存一次就好。

      开发模块好:要求设计者对功能划分的比较好。

  4.认识动态链接库

    动态链接库是相对静态链接库而言的。所谓的静态链接库只把要调用的函数或过程链接到可执行文件中,成为可执行文件的一部分。换句话说,函数和过程的代码就在程序的可执行文件中,该文件包含了运行时所需的全部代码。当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝,这样就浪费了宝贵的内存资源,而动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在系统的管理下,才在应用程序与相应的动态库之间建立链接关系。当要执行所调用的动态链接库中的函数时,根据链接产生的重定位信息,系统下转去执行状态链接库中相应的函数代码。一般情况下,如果一个应用程序使用了动态链接库,系统保证内存只有动态库的一份复制品。

    动态链接库的两种链接方法:

    1)装载时动态链接(Load-time Dynamic Linking):这种方法的前提是在编译之前已经明确知道要调用的动态库的哪些函数,编译时在目标文件中只保留必要的链接信息,而不含动态库函数代码;当程序执行时,调用函数的时候利用链接信息加载动态库函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。(动态加载程序,处在加载阶段,主要为了共享代码,共享代码内存)

    2)运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些动态库函数,完全是在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存);并标识内存地址,其他程序也可以使用该程序,并获得动态库函数的入口地址。(动态库在内存中只存在一份,处在运行阶段)

 

要想理解一个进程是怎样执行的,一个可执行程序是如何加载成进程的?我们这时需要知道可执行文件的内部是什么?

目标文件及链接///////////////////////////////////////////////////////////////////////////////

ELF目标文件格式:ELF(Executable And Linkable Format)

  • ELF文件格式--(中文翻译版本)///////////////////////////////////////////////////
     1 #define EI_NIDENT    16 2  3  typedef struct { 4    unsigned char    e_ident[EI_NIDENT]; 5    Elf32_Half     e_type; 6    Elf32_Half     e_machine; 7    Elf32_Word     e_version; 8    Elf32_Addr     e_entry; 9    Elf32_Off      e_phoff;10    Elf32_Off      e_shoff;11    Elf32_Word     e_flags;12    Elf32_Half     e_ehsize;13    Elf32_Half     e_phentsize;14    Elf32_Half     e_phnum;15    Elf32_Half     e_shentsize;16    Elf32_Half     e_shnum;17    Elf32_Half     e_shstrndx;18  } Elf32_Ehdr;

    ELF Header
  • 查看ELF文件的头部(hello)

   readlf -h hello

   

  • 查看该ELF所依赖的共享库(过程及结果如下图所示)

   ldd hello

   readelf -d  (也可以看依赖的so文件)

   

常见的目标文件的格式有如下:

   

A.out 是最古老的目标文件格式。  现在用的最多的PE(Windows下用的最多的)、ELF(Linux下用的最多的)

目标文件我们一般也叫ABI(应用程序二进制接口)

实际上在目标文件中它已经是二进制兼容(指目标文件已经是适应到某一个CPU体系上的二进制指令)

比如x86上的可执行文件链接成ARM上的可执行文件肯定是不行的

ELF格式的文件中有三种主要的目标文件

  1. 可重定位(relocatable)文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或是一个共享文件(主要是 .o 文件)
  2. 一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建序进程映像。
  3. 一个共享object文件保存这代码和合适的数据,用来被下面的两个链接器链接。第一个连接编译器(请查看ld(SD_CMD)),也可以和其他可重定位和共享object文件来创建他的object。第二是动态链接器,联合一个可执行文件和其他的共享object文件来创建一个进程映像。(主要是 .so 文件)

静态链接的ELF可执行文件于进程的地址空间:

对于32位的x86来讲,进程是4G的进程地址空间,最上面1G是内核用的,下面3G是用户态。0xc000000之上是内核,下面是用户态可访问。

默认进程是从0x8048000开始加载,先是头部(大小不固定),还把代码和数据加载到进程的地址空间。

ELF Header 中的Entry point address 即是可执行文件加载到内存中开始执行的第一行代码。(就像上面的程序,他的值是0x8048320)

一般静态链接会将所有代码放在一个代码段,而动态链接的进程会有多个代码段。

 

二、编程使用exec*库函数加载一个可执行文件

加载可执行程序前的工作:我们需要了解可执行程序的执行环境,一般是通过shell程序来启动一个可执行程序。

当我们执行可执行文件时,即发起了一个系统调用exec时,我们就要知道shell环境位我们准备了哪些执行的上下文环境。

这样我们就可以大概了解一下用户态的执行环境,然后再看一个exec的系统调用,它怎么把一个可执行文件加载在内核里面,又返回到用户它态。

命令行参数和shell 环境,一般我们执行一个程序的shell环境,我们的实验直接使用execve系统调用。

$ls -l /usr/bin 列出/usr/bin下的目录信息

shell本身不限制命令行参数的个数,命令行参数的个数受限于命令本身。

  例如, int main(int argc, char * argv[])

  又如, int main(int argc, char * argv[], char * envp[])

shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

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

  库函数exec*都是execve的封装例程

 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4  5 int main(int argc,char* argv[]) 6 { 7   int pid; 8   /*fork another process*/ 9   pid = fork(); 10   if(pid < 0) 11   { 12     /*error occurred*/ 13     fprintf(stderr,"Fork Failed!\n"); 14     exit(-1); 15   } 16   else if(pid == 0) 17   { 18     /* child process */ 19     execlp("/bin/ls","ls",NULL); 20   } 21   else 22   { 23     /* parent process */ 24     /* parent will wait for the child to complete */ 25     wait(NULL); 26     printf("Child Complete\n"); 27     exit(0); 28   } 29 }

View Code

 

命令行参数和环境变量的保存和传递是当我们创建一个子进程时,(fork是复制父进程),然后调用exece系统调用,它把要加载的可执行程序把原来的进程环境给覆盖掉了,覆盖了以后它的用户态堆栈也被清空。

这时命令行参数和环境变量会被压栈。

shell程序 -> execve -> sys_execve   然后在初始化新进程堆栈时拷贝进去。

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

创建了一个新的用户态堆栈的时候,实际上是把命令行参数(argv[])的内容和环境变量(envp[])的内容通过指针的方式传递给系统调用内核处理函数的,然后内核处理函数再建一个可执行程序新的用户态堆栈的时候,会把这些拷贝到用户态堆栈,初始化新的可执行程序执行的上下文环境。

先函数调用参数传递,再系统调用参数传递。

装载时动态链接和运行时动态链接举例:

 1 #include <stdio.h> 2  3 #include "shlibexample.h"  4  5 #include <dlfcn.h> 6  7 /* 8  * Main program 9  * input  : none10  * output  : none11  * return  : SUCCESS(0)/FAILURE(-1)12  *13 */14 int main()15 {16   printf("This is a Main program!\n");17   /* Use Shared Lib */18   printf("Calling SharedLibApi() function of libshlibexample.so!\n");19   SharedLibApi();20   /* Use Dynamical Loading Lib */21   void * handle = dlopen("libdllibexample.so",RTLD_NOW);22   if(handle == NULL)23   {24     printf("Open Lib libdllibexample.so Error:%s\n",dlerror());25     return  FAILURE;26   }27   int (*func)(void);28   char * error;29   func = dlsym(handle,"DynamicalLoadingLibApi");30   if((error = dlerror()) != NULL)31   {32     printf("DynamicalLoadingLibApi not found:%s\n",error);33     return  FAILURE;34   }  35   printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n");36   func(); 37   dlclose(handle);    38   return SUCCESS;39 }

main.c
 1 #ifndef _DL_LIB_EXAMPLE_H_ 2 #define _DL_LIB_EXAMPLE_H_ 3  4  5  6 #ifdef __cplusplus 7 extern "C" { 8 #endif 9 /*10  * Dynamical Loading Lib API Example11  * input  : none12  * output  : none13  * return  : SUCCESS(0)/FAILURE(-1)14  *15 */16 int DynamicalLoadingLibApi();17 18 19 #ifdef __cplusplus20 }21 #endif22 #endif /* _DL_LIB_EXAMPLE_H_ */

dllibexample.h
 1 #include <stdio.h> 2 #include "dllibexample.h" 3  4 #define SUCCESS 0 5 #define FAILURE (-1) 6  7 /* 8  * Dynamical Loading Lib API Example 9  * input  : none10  * output  : none11  * return  : SUCCESS(0)/FAILURE(-1)12  *13 */14 int DynamicalLoadingLibApi()15 {16   printf("This is a Dynamical Loading libary!\n");17   return SUCCESS;18 }

dllibexample.c
 1 #ifndef _SH_LIB_EXAMPLE_H_ 2 #define _SH_LIB_EXAMPLE_H_ 3  4 #define SUCCESS 0 5 #define FAILURE (-1) 6  7 #ifdef __cplusplus 8 extern "C" { 9 #endif10 /*11  * Shared Lib API Example12  * input  : none13  * output  : none14  * return  : SUCCESS(0)/FAILURE(-1)15  *16 */17 int SharedLibApi();18 19 20 #ifdef __cplusplus21 }22 #endif23 #endif /* _SH_LIB_EXAMPLE_H_ */

shlibexample.h
 1 #include <stdio.h> 2 #include "shlibexample.h" 3  4 /* 5  * Shared Lib API Example 6  * input  : none 7  * output  : none 8  * return  : SUCCESS(0)/FAILURE(-1) 9  *10 */11 int SharedLibApi()12 {13   printf("This is a shared libary!\n");14   return SUCCESS;15 }

shlibexample.c

 编译成libshlibexample.so

gcc -shared shlibexample.c -o libshlibexample.so -m32

编译成libdllibexample.so

gcc -shared dllibexample.c -o libdllibexample.so -m32

分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件

编译main时,注意这里只提供shlibexample的-L(库对应的接口头文件所在的目录),和-l(库名,如libshlibexample.so去掉lib和.so的部分)

并没有提供dllibexample的相关信息,只是指名了-dll

gcc main.c -o main -L ~/my/c/SharedLibDynamicLink -lshlibexample -ldl -m32         (其中 ~/my/c/SharedLibDynamicLink 是我的这几个文件所在的路径)

export LD_LIBRARY_PATH=$PWD    (将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以以将库文件copy到默认路径下)

./main

运行结果如下:

 

三、可执行程序的装载

sys_execve内部会解析可执行文件格式。在 /linux-3.18.6/fs/exec.c

1604SYSCALL_DEFINE3(execve,1605    const char __user *, filename,1606    const char __user *const __user *, argv,1607    const char __user *const __user *, envp)1608{1609  return do_execve(getname(filename), argv, envp);1610}

View Code
1549int do_execve(struct filename *filename,1550  const char __user *const __user *__argv,1551  const char __user *const __user *__envp)1552{1553  struct user_arg_ptr argv = { .ptr.native = __argv };1554  struct user_arg_ptr envp = { .ptr.native = __envp };1555  return do_execve_common(filename, argv, envp);1556}

do_execve

do_execve -> do_execve_common -> exec_binprm

1549int do_execve(struct filename *filename,1550  const char __user *const __user *__argv,1551  const char __user *const __user *__envp)1552{1553  struct user_arg_ptr argv = { .ptr.native = __argv };1554  struct user_arg_ptr envp = { .ptr.native = __envp };1555  return do_execve_common(filename, argv, envp);1556}

do_execve
1427/*1428 * sys_execve() executes a new program.1429 */1430static int do_execve_common(struct filename *filename,1431        struct user_arg_ptr argv,1432        struct user_arg_ptr envp)1433{1434  struct linux_binprm *bprm;1435  struct file *file;1436  struct files_struct *displaced;1437  int retval;14381439  if (IS_ERR(filename))1440    return PTR_ERR(filename);14411442  /*1443   * We move the actual failure in case of RLIMIT_NPROC excess from1444   * set*uid() to execve() because too many poorly written programs1445   * don't check setuid() return code. Here we additionally recheck1446   * whether NPROC limit is still exceeded.1447   */1448  if ((current->flags & PF_NPROC_EXCEEDED) &&1449    atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {1450    retval = -EAGAIN;1451    goto out_ret;1452  }14531454  /* We're below the limit (still or again), so we don't want to make1455   * further execve() calls fail. */1456  current->flags &= ~PF_NPROC_EXCEEDED;14571458  retval = unshare_files(&displaced);1459  if (retval)1460    goto out_ret;14611462  retval = -ENOMEM;1463  bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);1464  if (!bprm)1465    goto out_files;14661467  retval = prepare_bprm_creds(bprm);1468  if (retval)1469    goto out_free;14701471  check_unsafe_exec(bprm);1472  current->in_execve = 1;14731474  file = do_open_exec(filename);1475  retval = PTR_ERR(file);1476  if (IS_ERR(file))1477    goto out_unmark;14781479  sched_exec();14801481  bprm->file = file;1482  bprm->filename = bprm->interp = filename->name;14831484  retval = bprm_mm_init(bprm);1485  if (retval)1486    goto out_unmark;14871488  bprm->argc = count(argv, MAX_ARG_STRINGS);1489  if ((retval = bprm->argc) < 0)1490    goto out;14911492  bprm->envc = count(envp, MAX_ARG_STRINGS);1493  if ((retval = bprm->envc) < 0)1494    goto out;14951496  retval = prepare_binprm(bprm);1497  if (retval < 0)1498    goto out;14991500  retval = copy_strings_kernel(1, &bprm->filename, bprm);1501  if (retval < 0)1502    goto out;15031504  bprm->exec = bprm->p;1505  retval = copy_strings(bprm->envc, envp, bprm);1506  if (retval < 0)1507    goto out;15081509  retval = copy_strings(bprm->argc, argv, bprm);1510  if (retval < 0)1511    goto out;15121513  retval = exec_binprm(bprm);1514  if (retval < 0)1515    goto out;15161517  /* execve succeeded */1518  current->fs->in_exec = 0;1519  current->in_execve = 0;1520  acct_update_integrals(current);1521  task_numa_free(current);1522  free_bprm(bprm);1523  putname(filename);1524  if (displaced)1525    put_files_struct(displaced);1526  return retval;15271528out:1529  if (bprm->mm) {1530    acct_arg_size(bprm, 0);1531    mmput(bprm->mm);1532  }15331534out_unmark:1535  current->fs->in_exec = 0;1536  current->in_execve = 0;15371538out_free:1539  free_bprm(bprm);15401541out_files:1542  if (displaced)1543    reset_files_struct(displaced);1544out_ret:1545  putname(filename);1546  return retval;1547}

do_execve_common
1405static int exec_binprm(struct linux_binprm *bprm)1406{1407  pid_t old_pid, old_vpid;1408  int ret;14091410  /* Need to fetch pid before load_binary changes it */1411  old_pid = current->pid;1412  rcu_read_lock();1413  old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));1414  rcu_read_unlock();14151416  ret = search_binary_handler(bprm);1417  if (ret >= 0) {1418    audit_bprm(bprm);1419    trace_sched_process_exec(current, old_pid, bprm);1420    ptrace_event(PTRACE_EVENT_EXEC, old_vpid);1421    proc_exec_connector(current);1422  }

exec_binprm

search_binary_handler符合寻找文件格式对应的解析模板,如下:(对于给定的文件名,根据文件头部信息寻找对应的文件格式处理模块)

1349/*1350 * cycle the list of binary formats handler, until one recognizes the image1351 */1352int search_binary_handler(struct linux_binprm *bprm)1353{1354  bool need_retry = IS_ENABLED(CONFIG_MODULES);1355  struct linux_binfmt *fmt;1356  int retval;13571358  /* This allows 4 levels of binfmt rewrites before failing hard. */1359  if (bprm->recursion_depth > 5)1360    return -ELOOP;13611362  retval = security_bprm_check(bprm);1363  if (retval)1364    return retval;13651366  retval = -ENOENT;1367 retry:1368  read_lock(&binfmt_lock);1369  list_for_each_entry(fmt, &formats, lh) {1370    if (!try_module_get(fmt->module))1371      continue;1372    read_unlock(&binfmt_lock);1373    bprm->recursion_depth++;1374    retval = fmt->load_binary(bprm);1375    read_lock(&binfmt_lock);1376    put_binfmt(fmt);1377    bprm->recursion_depth--;1378    if (retval < 0 && !bprm->mm) {1379      /* we got to flush_old_exec() and failed after it */1380      read_unlock(&binfmt_lock);1381      force_sigsegv(SIGSEGV, current);1382      return retval;1383    }1384    if (retval != -ENOEXEC || !bprm->file) {1385      read_unlock(&binfmt_lock);1386      return retval;1387    }1388  }1389  read_unlock(&binfmt_lock);13901391  if (need_retry) {1392    if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&1393      printable(bprm->buf[2]) && printable(bprm->buf[3]))1394      return retval;1395    if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)1396      return retval;1397    need_retry = false;1398    goto retry;1399  }14001401  return retval;1402}1403EXPORT_SYMBOL(search_binary_handler);1404

search_binary_handler

对于ELF格式的可执行文件fmt->load_binary(bprm); 执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读。

load_elf_binary在/linux-3.18.6/fs/binfmt_elf.c

Linux内核是如何支持多种不同的可执行文件格式的? 在 /linux-3.18.6/fs/binfmt_elf.c

 elf_format(观察者)和init_elf_binfmt(被观察者),他们是观察者模式。

 

四、实验,使用gdb跟踪sys_execve内核函数的处理过程

实验步驟

先切换到 LinuxKernel目录下

rm menu -rf    //删除menu

git clone https://github.com/mengning/menu.git         //下载克隆新的menu

cd menu //切换到menu目录下

mv test_exce.c test.c    //把test_exce.c 改成 test.c ,这里是应为Makefile里面用的是test.c

vi test.c   //查看一下test.c的内容

vi makefile   //查看一下Makefile的内容

make rootfs   //

这时候就运行了,结果如下:

我们在打开一个新的终端,在终端里右键,选择打开终端(或水平分割)用2表示新的终端

2  gdb

2  file ../linux-3.18.6/vmlinux

2  target remote:1234

然后我选取了几个断点

2 b sys_execve  (可以先停在sys_exceve然后再设置其他断点)

2 b search_bianry_handler

2 b load_elf_binary

2 b start_thread

运行的部分过程如下:

以上实验是静态链接可执行程序。而大部分执行程序是用动态链接,需要用到动态链接库。需要动态链接的可执行文件先加载连接器ld,将cpu控制权交给ld来加载依赖库并完成动态链接。对于静态链接的文件elf_entry是新进程的执行起点。

实际上动态链接库的依赖关系会形成一个图。其实不是由内核负责加载可执行程序依赖的动态链接库的,而是由动态链接器。动态链接库的装载过程是一个图的遍历。装载和链接之后ld将cpu的控制权交给可执行程序。

五、总结

我们通过跟踪以及查看分析代码,执行的流程是:(代码见上面)

sys_execve -> do_execve -> do_execve_common -> exec_binprm -> search_binary_handler -> load_binary ->(对于我们这里的ELF,会跳转到)load_elf_binary (也执行了elf_format)-> start_thread

我们调用execve的可执行程序时,当执行到exceve时,系统调用exceve陷入内核,这时会创建一个新的用户态堆栈,实际是把命令行参数的内容和环境变量的内容通过指针的方式传递给系统调用内核处理函数的,然后内核处理函数在创建可执行程序新的用户态堆栈的时候,会把这些拷贝到用户态堆栈初始化新的可执行程序的执行上下文环境(先函数调用参数传递,再系统调用参数传递)。这时就加载了新的可执行程序。系统调用exceve返回用户态的时候,就变成了被exceve加载的可执行程序。