2016-12-28 20:39:13 peiyao456 阅读数 3356
  • C语言嵌入式Linux编程第3期:程序的编译、链接和运行

    本课程为《C语言嵌入式Linux编程》第3期,主要对程序的编译、链接及运行机制进行分析。同时对静态库链接、动态链接的过程、插件原理、内核模块运行机进行探讨,后对嵌入式系统比较难理解的u-boot重定位、u-boot加载内核、内核解压缩、重定位过程进行分析,加深对程序的编译链接原理的理解。

    1212 人正在学习 去看看 王利涛

本文的主要内容:
1>程序运行的4个阶段。
2>浅析静态库和动态库。
3>浅析Linux下的主要目录的作用。


1、程序运行的4个阶段:
在前边的文章http://blog.csdn.net/peiyao456/article/details/51524533
已经对程序运行的4个阶段进行了分析,这里将在Linux下再次剖析。
第一阶段:预处理(宏替换 去注释 头文件展开 条件编译)
gcc -E test.c -o test.i(将-o左边的文件通过预处理写进右边的文件)
-E表示只做预处理工作
执行之后生成的 test.i 中的内容还是c语言,下边展示。
这里写图片描述
下边我们来看一下运行之后的文件和之前的c文件的区别:
这里写图片描述
第二阶段:编译 (c语言变成汇编)
gcc -S test.i -o test.s
这里写图片描述
第三阶段:汇编 ( 汇编程序变成二进制文件)
gcc -c test.s -o test.o
这里写图片描述
第四阶段:链接 (静态链接 动态链接)
gcc test.o -o test
这里写图片描述
从以上的图片我们可以看出:Linux下的gcc编译过程中,默认的是动态连接的方式。
总结:
程序的运行就是从C文件->汇编文件->二进制文件->可执行文件的一个过程。
说到这里,不得不说一下动态连接和静态连接的区别。


2、静态库和动态库
静态库:就是在链接的过程中将汇编生成的目标文件与引用到的库一起链接打包到可执行文件 中。
特点:静态库对函数库的连接是在编译时期完成的;
程序在运行时,就与库再没有任何关系(只要平台允许,程序在哪里都可以执 行);
但是会使得程序的体积变大。
图示解释静态库:
这里写图片描述
动态库:
程序在运行时,才把库函数的链接载入内存;
可以实现进程之间的资源共享;
程序的体积小(避免了空间的浪费)。
图解动态库:
这里写图片描述


3、Linux下的主要目录的作用
/proc下存储系统中运行的进程的虚拟镜像。
/lib 下存储的是标准程序设计库。像上边执行的libc.so.6就在此目录下。
/bin下存储的是用户常用的基本程序,如login文件等。
这里写图片描述
/root存储系统管理员的主目录。
这里写图片描述
/etc存储整个系统的配置文件。如passwd文件。
/sbin 存储基本的系统和系统的维护程序,并且只能root来执行(和/bin的区别)。
/boot 存放和系统启动有关的文件。
/dev 存放外部设备的镜像文件。
/usr存放与系统的用户直接相关的程序和文件。
这里写图片描述

2013-08-08 09:16:47 NEOSL 阅读数 781
  • C语言嵌入式Linux编程第3期:程序的编译、链接和运行

    本课程为《C语言嵌入式Linux编程》第3期,主要对程序的编译、链接及运行机制进行分析。同时对静态库链接、动态链接的过程、插件原理、内核模块运行机进行探讨,后对嵌入式系统比较难理解的u-boot重定位、u-boot加载内核、内核解压缩、重定位过程进行分析,加深对程序的编译链接原理的理解。

    1212 人正在学习 去看看 王利涛

最近把java web项目部署到weblogic上,一直通过windows端的F-Secure操作服务器,每次启动weblogic使用./startWebLogic.sh,但是发现退出终端后,weblogic也是自动退出,查了资料,发现Unix/Linux下一般想让某个程序在后台运行,很多都是使用 & 在程序结尾来让程序自动运行。比如我们要运行mysql在后台:

 

         /usr/local/mysql/bin/mysqld_safe --user=mysql &

 

但是我们很多程序并不象mysql一样可以做成守护进程,可能我们的程序只是普通程序而已,一般这种程序即使使用 & 结尾,如果终端关闭,那么程序也会被关闭。为了能够后台运行,我们需要使用nohup这个命令,比如我们有个start.sh需要在后台运行,并且希望在后台能够一直运行,那么就使用nohup:

nohup ./start.sh &

在shell中回车后提示:

 

        [~]$ appending output to nohup.out

        原程序的的标准输出被自动改向到当前目录下的nohup.out文件,起到了log的作用。

 

但是有时候在这一步会有问题,当把终端关闭后,进程会自动被关闭,察看nohup.out可以看到在关闭终端瞬间服务自动关闭

 

在当shell中提示了nohup成功后还需要按终端上键盘任意键退回到shell输入命令窗口,然后通过在shell中输入exit来退出终端(如果每次在nohup执行成功后直接点关闭程序按钮关闭终端.。所以这时候会断掉该命令所对应的session,导致nohup对应的进程被通知需要一起shutdown)

 

       正确启动weblogic服务

       nohup ./startWebLogic.sh &

       

在Linux中,如果要让进程在后台运行,一般情况下,我们在命令后面加上&即可,实际上,这样是将命令放入到一个作业队列中

 

       查看日志

       tail -100f nohup.out 

 

查看后台运行的进程

jobs -l

 

       退出F-secure

       exit

       

        附:nohup命令参考

 

nohup 命令

 

  用途:不挂断地运行命令。

 

  语法:nohup Command [ Arg ... ] [ & ]

 

描述:nohup 命令运行由 Command 参数和任何相关的 Arg 参数指定的命令,忽略所有挂断(SIGHUP)信号。在注销后使用 nohup 命令运行后台中的程序。要运行后台中的 nohup 命令,添加 & ( 表示"and"的符号)到命令的尾部。

 

无论是否将 nohup 命令的输出重定向到终端,输出都将附加到当前目录的 nohup.out 文件中。如果当前目录的 nohup.out 文件不可写,输出重定向到 $HOME/nohup.out 文件中。如果没有文件能创建或打开以用于追加,那么 Command 参数指定的命令不可调用。如果标准错误是一个终端,那么把指定的命令写给标准错误的所有输出作为标准输出重定向到相同的文件描述符。

 

退出状态:该命令返回下列出口值:

 

  126 可以查找但不能调用 Command 参数指定的命令。

 

  127 nohup 命令发生错误或不能查找由 Command 参数指定的命令。

 

  否则,nohup 命令的退出状态是 Command 参数指定命令的退出状态。

 

  nohup命令及其输出文件

 

nohup命令:如果你正在运行一个进程,而且你觉得在退出帐户时该进程还不会结束,那么可以使用nohup命令。该命令可以在你退出帐户/关闭终端之后继续运行相应的进程。nohup就是不挂起的意思( nohang up)。

 

  该命令的一般形式为:nohup command &

 

  使用nohup命令提交作业

 

如果使用nohup命令提交作业,那么在缺省情况下该作业的所有输出都被重定向到一个名为nohup.out的文件中,除非另外指定了输出文件:

 

  nohup command > myout.file 2>&1 &

 

在上面的例子中,输出被重定向到myout.file文件中。

 

使用 jobs 查看任务。

 

使用 fg %n 关闭。

 

另外有两个常用的ftp工具ncftpget和ncftpput,可以实现后台的ftp上传和下载,这样就可以利用这些命令在后台上传和下载文件了。

 

附:查看linux系统的几种方法:

  1. uname -a
  2. cat /proc/version
  3. cat /etc/issue
  4. lsb_release -a

 

       参考文章:http://www.einit.com/user1/11/archives/2006/3603.html  

                         http://www.williamlong.info/archives/482.html  

                         http://yakar.iteye.com/blog/687656

2013-03-19 12:57:16 wyt4455 阅读数 623
  • C语言嵌入式Linux编程第3期:程序的编译、链接和运行

    本课程为《C语言嵌入式Linux编程》第3期,主要对程序的编译、链接及运行机制进行分析。同时对静态库链接、动态链接的过程、插件原理、内核模块运行机进行探讨,后对嵌入式系统比较难理解的u-boot重定位、u-boot加载内核、内核解压缩、重定位过程进行分析,加深对程序的编译链接原理的理解。

    1212 人正在学习 去看看 王利涛
1.父进程的行为: 复制,等待

执行应用程序的方式有很多,从shell中执行是一种常见的情况。交互式shell是一个进程(所有的进程都由pid号为1的init进程fork得到,关于这个话题涉及到Linux启动和初始化,以及idle进程等,有空再说),当在用户在shell中敲入./test执行程序时,shell先fork()出一个子进程(这也是很多文章中说的子shell),并且wait()这个子进程结束,所以当test执行结束后,又回到了shell等待用户输入(如果创建的是所谓的后台进程,shell则不会等待子进程结束,而直接继续往下执行)。所以shell进程的主要工作是复制一个新的进程,并等待它的结束。

2.子进程的行为: "执行"应用程序

2.1 execve()

另一方面,在子进程中会调用execve()加载test并开始执行。这是test被执行的关键,下面我们详细分析一下。

execve()是操作系统提供的非常重要的一个系统调用,在很多文章中被称为exec()系统调用(注意和shell内部exec命令不一样),其实在Linux中并没有exec()这个系统调用,exec只是用来描述一组函数,它们都以exec开头,分别是:

#include
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

这几个都是都是libc中经过包装的的库函数,最后通过系统调用execve()实现(#define __NR_evecve 11,编号11的系统调用)。
 
exec 函数的作用是在当前进程里执行可执行文件,也就是根据指定的文件名找到可执行文件,用它来取代当前进程的内容,并且这个取代是不可逆的,即被替换掉的内容不再保存,当可执行文件结束,整个进程也随之僵死。因为当前进程的代码段,数据段和堆栈等都已经被新的内容取代,所以exec函数族的函数执行成功后不会返回,失败是返回-1。可执行文件既可以是二进制文件,也可以是可执行的脚本文件,两者在加载时略有差别,这里主要分析二进制文件的运行。

2.2 do_execve()

在用户态下调用execve(),引发系统中断后,在内核态执行的相应函数是do_sys_execve(),而do_sys_execve()会调用 do_execve()函数。do_execve()首先会读入可执行文件,如果可执行文件不存在,会报错。然后对可执行文件的权限进行检查。如果文件不是当前用户是可执行的,则execve()会返回-1,报permission denied的错误。否则继续读入运行可执行文件时所需的信息(见struct linux_binprm)。

2.3 search_binary_handler()

接着系统调用search_binary_handler(),根据可执行文件的类型(如shell,a.out,ELF等),查找到相应的处理函数(系统为每种文件类型创建了一个struct linux_binfmt,并把其串在一个链表上,执行时遍历这个链表,找到相应类型的结构。如果要自己定义一种可执行文件格式,也需要实现这么一个 handler)。然后执行相应的load_binary()函数开始加载可执行文件。

2.4 load_elf_binary()


加载elf类型文件的handler是load_elf_binary(),它先读入ELF文件的头部,根据ELF文件的头部信息读入各种数据 (header information)。再次扫描程序段描述表,找到类型为PT_LOAD的段,将其映射(elf_map())到内存的固定地址上。如果没有动态链接器的描述段,把返回的入口地址设置成应用程序入口。完成这个功能的是start_thread(),start_thread()并不启动一个线程,而只是用来修改了pt_regs中保存的PC等寄存器的值,使其指向加载的应用程序的入口。这样当内核操作结束,返回用户态的时候,接下来执行的就是应用程序了。
ps:elf文件是一种灵活的二进制文件,可以是包含了数据和文件的可执行的程序,可以是可重定位文件,这些数据是和其他重定位文件和共享的object文件一起链接起来使用的。# file libfoo.o libfoo.o: ELF 32-bit LSB relocatable, Intel 80386, version 1, not stripped 。也或者是一种共享库文件,这些数据是在连接时候被连接器ld和运行时动态连接器使用的,例如 ld-linux.so.1

2.5 load_elf_interp()

如果应用程序中使用了动态链接库,就没有那么简单了,内核除了加载指定的可执行文件,还要把控制权交给动态连接器(program interpreter,ld.so in linux)以处理动态链接的程序。内核搜寻段表,找到标记为PT_INTERP的段中所对应的动态连接器的名称,并使用 load_elf_interp()加载其映像,并把返回的入口地址设置成load_elf_interp()的返回值,即动态链接器入口。当 execve退出的时候动态链接器接着运行。动态连接器检查应用程序对共享连接库的依赖性,并在需要时对其进行加载,对程序的外部引用进行重定位。然后动态连接器把控制权交给应用程序,从ELF文件头部中定义的程序进入点开始执行。(比如test.c中使用了userlib.so中函数foo(),在编译的时候这个信息被放进了test这个ELF文件中,相应的语句也变成了call fakefoo()。当加载test的时候,知道foo()是一个外部调用,于是求助于动态链接器,加载userlib.so,解析foo()函数地址,然后让fakefoo()重定向到foo(),这样call foo()就成功了。)

简短的说,整个在shell中键入./test执行应用程序的过程为:当前shell进程fork出一个子进程(子shell),子进程使用execve来脱离和父进程的关系,加载test文件(ELF格式)到内存中。如果test使用了动态链接库,就需要加载动态链接器(或者叫程序解释器),进一步加载 test使用到的动态链接库到内存,并重定位以供test调用。最后从test的入口地址开始执行test。

PS:  现代的动态链接器因为性能等原因都采用了延迟加载和延迟解析技术,延迟加载是动态连接库在需要的时候才被加载到内存空间中(通过页面异常机制),延迟解析是指到动态链接库(以加载)中的函数被调用的时候,才会去把这个函数的起始地址解析出来,供调用者使用。动态链接器的实现相当的复杂,为了性能等原因,对堆栈的直接操作被大量使用,感兴趣的可以找相关的代码看看。
2020-01-09 23:04:40 w346665682 阅读数 114
  • C语言嵌入式Linux编程第3期:程序的编译、链接和运行

    本课程为《C语言嵌入式Linux编程》第3期,主要对程序的编译、链接及运行机制进行分析。同时对静态库链接、动态链接的过程、插件原理、内核模块运行机进行探讨,后对嵌入式系统比较难理解的u-boot重定位、u-boot加载内核、内核解压缩、重定位过程进行分析,加深对程序的编译链接原理的理解。

    1212 人正在学习 去看看 王利涛

       本文旨在以hello world程序在ubuntu 16.04 x86_64机器上运行为例,详细讲述这个程序从编译、链接(包括动态链接和静态链接)到加载到运行这个过程中,工具链gcc,运行库glibc,内核,他们是怎么分工协作让这个程序顺利完成加载和运行的;

程序编译运行的过程

总体流程如下图:

以hello world程序为例,使用gcc  --verbose参数查看编译的详细过程如下:

其中cc1为gcc工具链的C编译器,预处理和编译都由该工具完成,as为汇编器,完成汇编工作,collect2为链接器ld的封装形式,最终负责链接收尾工作;

汇编详述

汇编阶段就是使用as汇编器简单的参照指令编码表将汇编代码翻译为机器指令,并将解析汇编文件中重要的部分形成辅助的信息结构并(符号,函数名等)以分节(section)的方式组织起来, 汇编之后的hello文件,使用命令readelf -S main.o 可以查看该文件的分节结构:

可以看到该汇编文件由12个段组成,其中.symtab为符号表,记录了该文件中所有全局符号以及函数名引用但是未定义的符号名等,.rela.text为代码重定位,这些段都将在链接阶段被使用用来构成最终的可执行文件;

链接详述

       链接是编译的最后一个阶段,链接主要是将多个文件(目标文件或者库文件)合并为可执行文件,并分配运行时地址,然后进行重定位工作,在进行链接的时候链接器使用静态库来解析引用的,在符号解析阶段,链接器从左到右按照他们在编译器驱动命令行上出现的相同顺序来扫描可重定位目标文件和库文件,链接器维持一个可重定位目标文件的集合E,一个未解析的符号集合U吗,一个全局已定义符号表D,在最初RUD都为空;
          对于命令行上的每个输入文件F,链接器都会判断F是一个目标文件还是一个库文件,如果F是一个目标文件,那么链接器把F添加到E中,再将F中的所有符号更新到UD,并继续解析下一个文件;
          如果输入文件为库文件,那么链接器就尝试匹配U中的未解析的符号,库文件由多个目标文件所组成,如果某个库文件的目标文件成员M,定义了一个符号来解析U中的一个引用,那么就将M加到E中,并且链接器将M成员的符号更新到U,D中,接着处理库文件中的下一个目标文件,如果该目标文件中的所有符号均未被引用,则丢弃该目标文件(不会被加到E中),接着链接器将处理下一个库文件(或者目标文件);
           直至链接器处理完所有的输入文件,U是非空的,链接器将报链接未定义的错误,否则,他会合并和重定位E中的目标文件,构建出可执行文件;
注意:由此可以看出 链接的时候库文件和目标文件的顺序非常重要,依赖项一般要放在命令行的最后面,当库文件互相依赖的时候则需要重复书写库文件,例如:gcc main.o add.a sub.a add.a

链接又分为动态链接和静态链接,基于动态链接的优势,一般linux 机器中默认都使用动态链接的方式进行链接工作,我们将分别进行叙述;

静态链接:使用命令gcc -static main.o -o main -v 查看helloworld程序的详细链接过程如下:

其中collect2 是链接器ld的封装形式,可以看到链接器最终会将这些目标文件crt1.o,crti.o,crtbegin.o,main.o,crtn.o,crtend.o 和库-lgcc,-lc,-lgcc_s(除非我们制定链接静态库,默认情况下-lxx执行的链接库都是动态库)中所必需的的成员目标文件链接成一个可执行文件main;

       正如上面所说的,链接器解析库文件之后将会获得该程序所必需的库文件的成员目标文件(.o文件)以及其他目标文件,我们知道目标文件是以各个节(section)组织起来的一个结构,将目标文件合并的过程就是将节进行合并,静态链接的过程就是把所有目标文件的相似节进行合并;

(该图线条只是画出了简要的步骤)到此我们可以总结静态链接的过程为:

       首先链接器将以本节最开始的算法扫描gcc命令行中所有.o文件以及库文件中必要的.o文件,将这些.o文件的相同节进行合并,然后进行重定位工作最终形成可执行文件,注意在最终形成可执行文件可以使用命令readelf -S mains查看共有32个段,相比原来目标文件main.o的12个要大得多,不仅仅是因为多个.o文件的合并,这个过程中可执行文件中还要生成一些段表结构,例如该可执行程序被装载时如何映射到内存的策略这些信息都以额外辅助节的形式存储在可执行文件中,这将指导并辅助该可执行程序正确的加载到内存并完成运行;

动态链接:使用命令gcc -static main.o -o main -v 查看helloworld程序的详细链接过程如下:

可以对比静态链接,动态链接和静态链接所链接的文件都基本一致,唯一不同的是动态链接必须要指定动态链接器的路径,如图中ld-linux-x86-64.so.2为该程序的动态连接器;动态链接的编译时链接(链接还分为编译时链接和运行时链接,静态链接只有编译时链接,而动态链接有编译时链接和运行时链接,与GCC相关的为编译时链接)过程比较简单;

动态链接的流程与静态链接较为相似,不同的地方在于:

  • 静态链接时在查找完所有目标文件与库文件的依赖之后会将所有相关目标文件进行合并,而动态链接只是合并所有.o文件,关于库文件的部分,会解析并生成相关的间接访问信息节结构存储在可执行文件中,一切都为最终运行时链接;
  • 动态链接中会生成许多动态链接相关的节,可使用readelf -S maind查看可执行文件的所有节,一般动态链接段的数量一定比静态链接多,因为动态链接多一个动态连接器相关的节;

动态链接的过程总结如下:

首先链接器将以本节最开始的算法扫描gcc命令行中所有.o文件以及库文件中必要的.o文件,将.o文件合并,如下图动态链接将只合图中看的见得.o文件,并不会合并例如-lc -lg++ 这些库文件中的.o文件,这里只将crti.o crt1.o crtn.o rtbegin.o crtend.o文件与main.o文件合并,对于依赖的其他库文件中的成员目标文件,将间接或者优化访问的方式以段的形式存储于可执行文件中,真正的链接被放在了装载运行时进行;

ps:在使用gcc指定参数-static的时候,其所链接的-lc -lgcc等库均为静态库,不适用-static参数的时候其使用为动态库,一般系统中会存在同一个库的静态和动态两种形式;

详述ELF文件映射到内存(装载运行阶段)

      文件被编译成可执行文件之后被以节的形式组织起来存储在硬盘中,此时我们执行./main之后装载流程会将该文件读取并依据该文件在编译时形成的各种节(里面存储了其映射的策略),然后依照计划映射到内存中;其中内存映射是以页为单位进行的(32位机为4k),也就是说不管合并之后段为多大,在映射到内存中的时候都会至少占据一个页面大小,这样势必会造成内存的浪费,为了使内存使用更有效率以及安全性考虑,在将目标文件段映射到内存的时候,将相同属性的节合并为一个段(segment),比如可执行可读的段.init .fina .text ...几个合并为一个段,合并的结果可以使用命令readelf -l main 查看,该合并的信息在编译时链接过程已经形成,被以节的形式存储在可执行文件中;如下图为静态链接映射

可以看到该文件最终被合并为6个段,其中前两个为load可加载类型,第一个段合并了.note.ABI-tag .init .plt .text ...等节,其都有可执行可读共同属性,第二个段合并了.data .got .got.plt .bss...等节,这两个段最终会被映射到内存中;这个过程如下图所示:

紧接着使用命令查看该进程的内存映射区域为:

可以看到mains对应的两个VMA都被映射到内存中,第一个为可读可执行属性,第二个为可读可写属性;在映射完成之后程序将初始化ELF进程环境,最后将系统调用的返回值修改为可执行程序的入口地址,可执行文件的装载过程到这里就结束了,可执行程序开始运行;

动态链接文件映射内存相比静态链接要复杂一些,动态库文件,也叫共享文件,因此动态链接时并没有把程序依赖的库文件合并进可执行文件,而是使用一些间接访问的策略,而在装载阶段依赖的库文件并没有装载进内存,这个过程最终由动态连接器来完成;动态链接段的映射如下图:

可以看到动态链接文件最终形成了8个段,其中包括动态链接器interp和dynamic段,而interp段就是动态连接器的路径,所以动态链接文件的映射不仅会映射可执行文件的段,还会将动态链接器也加载进内存,如下图中的/lib/x86_64-linux-gnu/ld-2.23.so;同样在映射完成之后,将系统调用的返回值修改动态链接器的入口地址,动态链接器开始执行;

注意:图中的libc-2.23.so库文件在elf文件加载过程中还没有被加载到内存,这个加载过程最终会由动态链接器来进行;

动态链接步骤 

动态链接的步骤分为3步骤,首先启动动态链接器本身,然后装载所有需要的共享对象,最后是重定位和初始化;

       当可执行文件映射完毕,程序把最终控制权交给动态链接器的时候,动态链接器开始自举,动态链接器本身也是一个共享对象,对于普通的文件,他的重定位工作由动态链接器来完成,普通文件依赖的其他共享对象,也有动态链接器来链接和装载,而动态链接器本身也完成自己的重定位工作,首先动态链接器是静态链接的,所以他不依赖任何共享库,在没有完成重定位自己之前动态链接器中不能使用任何全局变量甚至函数,动态链接器自举完成之后就可以自由的访问函数和全局变量了,接着动态链接器寻找可执行文件的.dynamic段并找到该文件所依赖的库文件,然后读取该库文件的.dynamic继续寻找依赖库文件然后将相应的节都映射到(栈)内存空间,这个过程就是一个图的遍历,一般遵循广度优先搜索算法来遍历这个图;

这个过程就是加载libc-2.23.so运行库的过程,当依赖的共享对象都被加载进内存之后,动态链接器将开始最终的重定位和初始化工作,依然是依据可执行文件或者共享对象的重定位节来进行,将所有的文件重定位结束之后,动态链接器将程序的控制权交给可执行程序,可执行程序将开始运行;

        运行库部分相对比较简单,在这里就不详细描述了,这部分的知识是学习《程序员的自我修养》这本书梳理出来的,也算是总结吧,脉络要理清楚,明天就是2020年的第一天了,新年新气象,加油!!!

2019-03-26 21:47:10 xwy990 阅读数 332
  • C语言嵌入式Linux编程第3期:程序的编译、链接和运行

    本课程为《C语言嵌入式Linux编程》第3期,主要对程序的编译、链接及运行机制进行分析。同时对静态库链接、动态链接的过程、插件原理、内核模块运行机进行探讨,后对嵌入式系统比较难理解的u-boot重定位、u-boot加载内核、内核解压缩、重定位过程进行分析,加深对程序的编译链接原理的理解。

    1212 人正在学习 去看看 王利涛

Linux 进程执行过程分析

编号411,本文参考孟宁老师 Github 项目 https://github.com/mengning/linuxkernel

进程的基本要素

由于系统进程有一定的特殊性,这里主要分析普通用户进程。一般来讲,Linux 系统下的进程有几个基础要素:

  • 可执行代码

    可执行代码是进程的基本要素,这部分包含表示程序功能的进程私有代码和共享的链接库代码。

  • 系统专用系统堆栈空间

    进程专用的系统堆栈空间

  • 进程控制块

    即task_stuct数据结构,一方面,进程控制块包含的内容为内核调度提供了数据,另一方面,这个结构体记录了该进程的私有资源。

  • 独立存储空间

    独立的存储空间,即表示该进程拥有专有的用户空间(用户空间堆栈)。

除了以上的基本要素外,为了理解进程的执行过程,我们还需要理解以下基本内容:

  • 进程的生命周期

在这里插入图片描述
一个进程被fork出来后,进入就绪态;当被调度到获得CPU执行时,进入执行态;如果时间片用完或被强占时,进入就绪态;资源得不到满足时,进入睡眠态(深度睡眠或浅度睡眠),比如一个网络程序,在等对方发包,此时不能占着CPU,进入睡眠态,当包发过来时,进程被唤醒,进入就绪态;如果被暂停,进入停止态;执行完成后,资源释放,此时父进程wait4还未收到它的信号,进入僵死态。即整个周期可能会涉及的状态有:就绪态,执行态,僵死态,停止态,睡眠态。

  • 进程执行相关的系统调用

    • fork() 调用

    1)在父进程中,fork返回新创建子进程的进程ID;
    2)在子进程中,fork返回0;
    3)如果出现错误,fork返回一个负值;

    在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

    • execve() 调用

      • 预处理
        首先在内核空间分配一个物理页面,然后调用do_getname()从用户空间拷贝文件名字符串。
    • 调用主体函数do_execve()

      • 我们既然要执行参数中给的二进制文件,首先需要打开文件,获取文件句柄file

      • 然后我们需要一个linux_binprm结构体去保存函数具体的参数信息,包括文件名,argv,envp,还会将文件前128字节读到linux_binprm.buf中。

      • 因为可执行文件的种类很多,比如elf,a.out等格式。我们需要从内核全局linux_binfmt队列中找到一个能够处理参数中所给的可执行文件的linux_binfmt结构,具体就是依次试用linux_binfmt结构中各自的load_binary()函数。

    • 可执行文件的装载和投运(a.out为例)

      • 与过去决裂,释放用户空间。
        既然是要执行参数中给定的二进制文件,就需要放弃可能从父进程继承下来的用户空间,而使用本进程自己的用户空间。因此,需要检查是否与父进程通过指针共享用户空间,还是之前复制父进程用户空间。如果通过指针共享,说明本进程本身没有自己的用户空间,之前称为“进程”不合适,应该称作线程,就直接申请进程用户空间。如果复制父进程的用户空间,这是就需要全部释放。

      • 装载可执行文件数据段代码段
        这时可以将可执行文件装入进程的用户空间了,这时分两种情况:

        • 可执行文件不是"纯代码",需要通过do_brk()扩展数据段+代码段大小的空间,然后通过read()读取文件内容到用户空间

        • 否则,如果文件系统提供mmap(),并且数据段和代码段长度与页面大小对齐,直接通过文件映射读取到用户空间,否则,通过1方法读取。

      • 装载可执行文件堆栈段和bss段

      用户空间堆栈区顶部当然是用户虚存空间顶部,即TASK_SIZE,为3GB,虚存地址为0xC000 0000的位置。

      这里主要是设置用户堆栈区,包括envp[],argv[]以及argc

      • start_thread()

代码实测

  • 编写 fork() 和 execve() 的测试代码并进行测试

    编写以下两个文件

    • helloworld 文件
    // file helloword.c
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    int main()
    {
        printf("Hello world!\n");
        pid_t pid = getpid();
        printf("pid of helloworld from helloword is %d\n", pid);
    
        return 0;
    }
    // end helloword.c
    
    • start_process 文件
    // file start_process.c
    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    
    int main()
    {
            pid_t pid;
            char *argv_execve[]={"helloworld", NULL};
            char *envp[]={"PATH=/home/coolxxy/Code/ex03/", "USER=coolxxy", "STATUS=testing", NULL};
    
            pid = getpid();
            printf("pid of start_process is :%d\n",pid);
    
            printf("Starting systemcall execve......\n");
            pid_t temp = fork();
            if(temp == 0)
            {
                    printf("i am here because i find fork() return 0, pid getted after fork() %d\n", getpid());
                    if(execve("./helloworld", argv_execve, envp) < 0)
                    {
                            perror("Error on execve");
                    }
            }
            else
            {
                    printf("i am here because i find fork() return a posivate value %d, pid getted after fork() is %d\n", temp, getpid());
            }
    
            return 0;
    }
    // end file start_process.c
    

    分别编译以上两个文件并执行 start_process 文件,可以获得以下输出:

    在这里插入图片描述

    通过这个测试,可以发现 fork() 执行了两次返回,父进程中返回了子进程的 pid, 子进程中返回了0.

  • 通过 gdb 调试分析 _do_fork() 过程调用

    我分别给内核种的 _do_fork() 和 copy_process 函数添加断点并分析其调用过程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过追踪这两个函数的执行过程,可以发现,_do_fork() 的执行过程大致如下代码注释所示

long do_fork(unsigned long clone_flags,
        unsigned long stack_start,
        struct pt_regs *regs,
        unsigned long stack_size,
        int __user *parent_tidptr,
        int __user *child_tidptr)
{
  struct task_struct *p;//在内存中分配一个 task_struct 数据结构,以代表即将产生的新进程
  int trace = 0;
  long nr;

  /*
   * Do some preliminary argument and permissions checking before we
   * actually start allocating stuff
   */
  if (clone_flags & CLONE_NEWUSER) {          //clone and new user yes
      if (clone_flags & CLONE_THREAD)
          return -EINVAL;
      /* hopefully this check will go away when userns support is
       * complete
       */
      if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) ||
              !capable(CAP_SETGID))
          return -EPERM;
  }

  /*
   * We hope to recycle these flags after 2.6.26
   */
  if (unlikely(clone_flags & CLONE_STOPPED)) {
      static int __read_mostly count = 100;

      if (count > 0 && printk_ratelimit()) {
          char comm[TASK_COMM_LEN];

          count--;
          printk(KERN_INFO "fork(): process `%s' used deprecated "
                  "clone flags 0x%lx\n",
              get_task_comm(comm, current),
              clone_flags & CLONE_STOPPED);
      }
  }

  /*
   * When called from kernel_thread, don't do user tracing stuff.
   */
  if (likely(user_mode(regs)))
      trace = tracehook_prepare_clone(clone_flags);

  p = copy_process(clone_flags, stack_start, regs, stack_size,
           child_tidptr, NULL, trace);//把父进程 PCB 的内容复制到新进程的 PCB 中。
  /*通过copy_process()函数完成具体的进程创建工作,返回值类型为task_t类型
   * Do this prior waking up the new thread - the thread pointer
   * might get invalid after that point, if the thread exits quickly.
   */
  if (!IS_ERR(p)) {//函数 IS_ERR()分析copy_process()的返回值是否正确。
      struct completion vfork;//定义struct completion 类型的变量 vfork;

      trace_sched_process_fork(current, p);

      nr = task_pid_vnr(p);

      if (clone_flags & CLONE_PARENT_SETTID)
          put_user(nr, parent_tidptr);

      if (clone_flags & CLONE_VFORK) {//判断clone_flags中是否有CLONE_VFORK标志
          p->vfork_done = &vfork;
          init_completion(&vfork);/*这个函数的作用是在进程创建的最后阶段,父进程会将自己设置为不可中断状态,然后睡眠在
等待队列上(init_waitqueue_head()函数 就是将父进程加入到子进程的等待队列),等待子进程的唤醒。*/
      }

      audit_finish_fork(p);
      tracehook_report_clone(regs, clone_flags, nr, p);

      /*
       * We set PF_STARTING at creation in case tracing wants to
       * use this to distinguish a fully live task from one that
       * hasn't gotten to tracehook_report_clone() yet.  Now we
       * clear it and set the child going.
       */
      p->flags &= ~PF_STARTING;

      if (unlikely(clone_flags & CLONE_STOPPED)) {
          /*
           * We'll start up with an immediate SIGSTOP.
           */
          sigaddset(&p->pending.signal, SIGSTOP);
          set_tsk_thread_flag(p, TIF_SIGPENDING);
          __set_task_state(p, TASK_STOPPED);
      } else {
          wake_up_new_task(p, clone_flags);
      }

      tracehook_report_clone_complete(trace, regs,
                      clone_flags, nr, p);

      if (clone_flags & CLONE_VFORK) {
          freezer_do_not_count();
          wait_for_completion(&vfork);
          freezer_count();
          tracehook_report_vfork_done(p, nr);
      }
  } else {
      nr = PTR_ERR(p);
  }
  return nr;
}

参考 :https://blog.csdn.net/u012375924/article/details/87903620
参考 :https://blog.csdn.net/yiqiaoxihui/article/details/80385546

Linux 程序运行栈帧

阅读数 582

没有更多推荐了,返回首页