趣谈Linux操作系统随笔——5.0 进程:公司接这么多项目,如何管

进程:公司接这么多项目,如何管


1、编译—程序的二进制格式(.o ELF文件的第一种格式)

1.1 生成方式

执行gcc -o xxx.o xxx.c,最终生成的.o文件——这就是ELF的第一种类型可重定位文件(Relocatable File)。

1.2 文件格式(存储方式)

如下图所示:由ELF头,多个节(Section)与节头部表(Section Header Table)构成

img

  • 对于一开始的**ELF Header文件头,其用于描述整个文件**,在内核中struct elf32_hdrstruct elf64_hdr结构体描述,形式如下:

    • 位置:include\uapi\linux\elf.h

    • 源码:可以看到,32位机与64位机在文字描述上无任何区别,由于位数的增加,结构体中每项变量的大小的有所区别

      有一项e_entry,也是个虚拟地址,是程序运行的入口

      /* 32-bit ELF base types. */
      typedef unsigned int 	Elf32_Addr;
      typedef unsigned short 	Elf32_Half;
      typedef unsigned int 	Elf32_Off;
      typedef signed int 		Elf32_Sword;
      typedef unsigned int 	Elf32_Word;
      
      #define EI_NIDENT    16
      
      typedef struct elf32_hdr{
        unsigned char	e_ident[EI_NIDENT];  //开始的16个字节
        Elf32_Half	e_type;  		//文件类型
        Elf32_Half    e_machine;  	//运行的机器类型
        Elf32_Word    e_version;  	//版本
        Elf32_Addr    e_entry;  		//程序入口地址
        Elf32_Off    	e_phoff;  		//程序头表在文件中的偏移
        Elf32_Off    	e_shoff;  		//节头表在文件中的偏移
        Elf32_Word    e_flags;  		//标记
        Elf32_Half    e_ehsize;  		//elf文件头大小
        Elf32_Half    e_phentsize;  	//程序头表项的大小
        Elf32_Half    e_phnum;  		//程序头表中表项项的个数
        Elf32_Half    e_shentsize;  	//节头表项大小
        Elf32_Half    e_shnum;  		//节头表中表项的个数
        Elf32_Half    e_shstrndx;  	//节头表的字符串节所在节头表中下标
      } Elf32_Ehdr;
      
      /* 64-bit ELF base types. */
      typedef unsigned long long 	Elf64_Addr;
      typedef unsigned short 		Elf64_Half;
      typedef signed short 		Elf64_SHalf;
      typedef unsigned long long 	Elf64_Off;
      typedef signed int 			Elf64_Sword;
      typedef unsigned int 		Elf64_Word;
      typedef unsigned long long 	Elf64_Xword;
      typedef signed long long 	Elf64_Sxword;
      
      typedef struct elf64_hdr {
        unsigned char	e_ident[EI_NIDENT];	//开始的16个字节
        Elf64_Half 	e_type;			//文件类型
        Elf64_Half 	e_machine;		//运行的机器类型
        Elf64_Word 	e_version;		//版本
        Elf64_Addr 	e_entry;		//程序入口地址
        Elf64_Off 	e_phoff;		//程序头表在文件中的偏移
        Elf64_Off 	e_shoff;		//节头表在文件中的偏移
        Elf64_Word 	e_flags;		//标记
        Elf64_Half 	e_ehsize;		//elf文件头大小
        Elf64_Half 	e_phentsize;  	//程序头表项的大小
        Elf64_Half 	e_phnum;      	//程序头表中表项项的个数
        Elf64_Half 	e_shentsize;  	//节头表项大小
        Elf64_Half 	e_shnum;      	//节头表中表项的个数
        Elf64_Half 	e_shstrndx;   	//节头表的字符串节所在节头表中下标
      } Elf64_Ehdr;
      
  • 对于其他的项,我们称其为节(Section),可以看到其有多个节,那么怎么定位到每个节呢?由最底下的节头部表(Section Header Table)来定位,在这个表里面,每一个节都有一项,其在内核中由struct elf32_shdrstruct elf64_shdr结构体描述。(在上述的ELF头有描述这个文件的节头部表的位置,有多少个表项等等信息)。

    • 位置:include\uapi\linux\elf.h

    • 源码:32位机与64位机在文字描述上无任何区别,由于位数的增加,结构体中每项变量的大小的有所区别

      typedef struct elf32_shdr {
        Elf32_Word    sh_name;  		/* 节的名字,在符号表中的下标 */
        Elf32_Word    sh_type;  		/* 节的类型,描述符号,代码,数据,重定位等 */
        Elf32_Word    sh_flags;  		/* 读写执行标记 */
        Elf32_Addr    sh_addr;  		/* 节在执行时的虚拟地址 */
        Elf32_Off		sh_offset;  	/* 节在文件中的偏移量 */
        Elf32_Word    sh_size;  		/* 节的大小 */
        Elf32_Word    sh_link;  		/* 其它节的索引 */
        Elf32_Word    sh_info;  		/* 节的其它信息 */
        Elf32_Word    sh_addralign;	/* 节对齐 */
        Elf32_Word    sh_entsize;		/* 节拥有固定大小项的大小 */
      } Elf32_Shdr;
      
      typedef struct elf64_shdr {
        Elf64_Word 	sh_name;		/* 节的名字,在符号表中的下标 */
        Elf64_Word 	sh_type;		/* 节的类型,描述符号,代码,数据,重定位等 */
        Elf64_Xword 	sh_flags;		/* 读写执行标记 */
        Elf64_Addr 	sh_addr;		/* 节在执行时的虚拟地址 */
        Elf64_Off 	sh_offset;		/* 节在文件中的偏移量 */
        Elf64_Xword 	sh_size;		/* 节的大小 */
        Elf64_Word 	sh_link;		/* 其它节的索引 */
        Elf64_Word 	sh_info;		/* 节的其它信息 */
        Elf64_Xword 	sh_addralign;	/* 节对齐 */
        Elf64_Xword 	sh_entsize;     /* 节拥有固定大小项的大小 */
      } Elf64_Shdr;
      
1.3 每个节的含义

刚刚介绍了ELF头表与节头部表,下面来介绍下每个节/段中具体存储的信息

img

  • .text:放编译好的二进制可执行代码
  • .data:已经初始化好的全局变量与静态变量
  • .rodata:只读数据,例如字符串常量、const的变量
  • .bss未初始化全局变量与静态变量运行时会置0
  • .symtab(符号表):记录的则是函数和变量
  • .rel.text.text节中需要重定位的信息。一般,任何调用外部函数或者引用全局变量的指令都需要被修改,但是,如果指令调用本地函数,则不需要被修改。重定位信息在可执行文件中可以不需要。
  • .rel.data重定位任何被这个模块定义和引用的全局变量信息。通常在一个全局变量的值是另一个全局变量地址或外部函数地址的情况下需要重新修改这个值。
  • .strtab(字符串表)字符串常量和变量名

2、静态链接生成的可执行文件(ELF第二种格式)

2.1 生成方式
  • 静态链接库生成方式

    执行ar -rc xxx.a xxx.o xxx.o,可以生成一个静态链接库xxx.a,这个静态链接库可以由一个或多个.o文件组织起来

  • 可执行文件生成方式

    执行gcc -o xxx xxx.o -L. -lxxx,可以根据命令,调用静态链接库,与.o文件生成一个可执行的文件

    其中**-L参数跟着的是库文件所在的目录名**;-l参数用来指定程序要链接的库,即库名,例如:-lstaticprocess,即静态链接库为libstaticprocess.a,其到一个自动补全的功能。

2.2 文件格式(存储方式)

如下图所示,由ELF头、段头部表(Segment Header Table)与每个节组成的段构成

img

  • 对于ELF头,其组成方式如上面结构的一样

  • 对于段头部表(Segment Header Table)

    因为此时文件已经是马上就可以加载到内存里面执行的文件了,因而这些section被分成了需要加载到内存里面的代码段、数据段和不需要加载到内存里面的部分,将小的section合成了大的段segment,并且在最前面加一个段头表(Segment Header Table)

    代码段.textrodata组成,会被加载到内存中

    数据段.data.bss组成,会被加载到内存中

    不加载到内存的段.symtab。strtabSection Header Table组成

    其在内核中struct elf32_phdrstruct elf64_phdr结构体表示

    • 位置:include\uapi\linux\elf.h

    • 源码:32位机与64位机在文字描述上有顺序的区别,内容无区别,由于位数的增加,结构体中每项变量的大小的有所区别

      最重要的是**p_vaddr,这个是这个段加载到内存的虚拟地址**。

      typedef struct elf32_phdr{
        Elf32_Word    p_type;		/* 段的类型,LOAD,DYNAMIC等 */
        Elf32_Off    	p_offset;  	/* 段在文件中的偏移量 */
        Elf32_Addr    p_vaddr;  	/* 段的物理地址 */
        Elf32_Addr    p_paddr;  	/* 段的虚拟地址 */
        Elf32_Word    p_filesz;  	/* 段在文件中的大小 */
        Elf32_Word    p_memsz;  	/* 段在内存中的大小 */
        Elf32_Word    p_flags;  	/* 读写执行标记 */
        Elf32_Word    p_align;  	/* 段的对齐 */
      } Elf32_Phdr;
      
      typedef struct elf64_phdr {
        Elf64_Word 	p_type;     /* 段的类型,LOAD,DYNAMIC等 */
        Elf64_Word 	p_flags;    /* 读写执行标记 */
        Elf64_Off 	p_offset;	/* 段在文件中的偏移量 */
        Elf64_Addr 	p_vaddr;	/* 段的物理地址 */
        Elf64_Addr 	p_paddr;	/* 段的虚拟地址 */
        Elf64_Xword 	p_filesz;	/* 段在文件中的大小 */
        Elf64_Xword 	p_memsz;	/* 段在内存中的大小 */
        Elf64_Xword 	p_align;	/* 段的对齐 */
      } Elf64_Phdr;
      
  • 对于其他节,其介绍也和上述说明一致。

2.3 静态链接库的缺点

根据上面可以知道,由于静态链接库,会根据调用关系,把代码重新组织构成可执行文件,其中会把静态链接库的源代码“内嵌”到内存中

多个文件使用同一个静态链接库时,会重复的加载相同的代码到内存中,若静态链接库更新后,需要重新编译链接代码否则使用的可执行文件中调用的代码为之前加载的静态链接库的代码

image-20201124174606991

3、动态链接库——共享对象文件(Shared Object,ELF文件的第三种类型)

对于动态链接库,是通过一定的方式把所需要的段的地址存储到可执行文件中,在运行时才会跳到地址的代码中执行,类似下图。

image-20201124174843001

3.1 生成方式

执行gcc -fpic -shared xxx.c -o libxxx.so,生成libxxx.so动态链接库

注意,此时若需要调用该动态链接库,在链接时候需要指定动态链接库的路径或把动态链接库放入到指定目录中,否则在运行时会报错,这是为什么呢?

原因:当一个动态链接库被链接到一个程序文件中的时候,最后的程序文件并不包括动态链接库中的代码,而仅仅包括对动态链接库的引用,并且不保存动态链接库的全路径,仅仅保存动态链接库的名称

3.2 文件格式(存储方式)

在上述可执行文件的格式基础上,有以下的不同:

  1. 多了一个**.interpSegment**,这里面是ld-linux.so,这是动态链接器,也就是说,运行时的链接动作都是它做的。

  2. 多了两个section,一个是**.plt**,过程链接表(Procedure Linkage Table,PLT),一个是**.got.plt**,全局偏移量表(Global Offset Table,GOT)

3.3 如何加载动态链接库到进程空间

这个时候就需要上述的**.plt.got.plt**,下面会举例子说明,

假设如下场景:

  • 可执行程序:createprocess
  • 动态库:libcprocess.so
  • 执行情况:createprocess执行时,需要调用libcprocess.so中的create_process()

由于在程序编译时,create_process()所在的位置是不知道,在程序运行时会有如下步骤:

  1. 建立本地代理

    PLT里面建立一项PLT[x],在二进制程序里面,而是调用PLT[x]里面的代理代码,这个代理代码会在运行的时候找真正的create_process函数

  2. 获取代理代码

    GOT里面也会为create_process函数创建一项GOT[y]。这一项是运行时create_process函数在内存中真正的地址。

  3. 获取真正函数地址

    一开始,GOT[y]会回调PLT(PLT[x]里面的代理代码来找我要create_process函数的真实地址)

    然后,PLT会转而调用PLT[0],也即第一项

    之后,PLT[0]转而调用GOT[2],这里面ld-linux.so的入口函数,这个函数会找到加载到内存中的libdynamicprocess.so里面的create_process函数的地址

    最后,把这个地址放在GOT[y]里面。下次,PLT[x]的代理函数就能够直接调用了。

4、可执行文件如何加载到内存

  • include\linux\binfmts.h中有这样一个结构体linux_binfmt,其中的load_binary()函数是用于加载二进制文件到内存的定义。

    /*
     * This structure defines the functions that are used to load the binary formats that
     * linux accepts.
     */
    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 */
    } __randomize_layout;
    
    
  • fs\binfmt_elf.c文件中,对于elf文件格式,有如下结构体实现elf_format

    static struct linux_binfmt elf_format = {
    	.module		 	= THIS_MODULE,
    	.load_binary	= load_elf_binary,
    	.load_shlib	 	= load_elf_library,
    	.core_dump	 	= elf_core_dump,
    	.min_coredump	= ELF_EXEC_PAGESIZE,
    };
    

    其中load_elf_binary就是把elf格式文件加载到内存中的函数,在之前其调用顺序为:

    do_execve --> do_execveat_common --> __do_execve_file --> exec_binprm – > search_binary_handler --> search_binary_handler --> fmt->load_binary(bprm);

  • 那么谁调用do_execve 呢?通过source insight的搜索可以知道:

    fs\exec.c文件中有如下定义,通过对系统调用的理解,可以知道exec这个系统调用最终调用的load_elf_binary

    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);
    }
    

5、exec系统调用简单介绍

exec比较特殊,它是一组函数:

  • 包含p的函数(execvp, execlp)会在PATH路径下面寻找程序
  • 不包含p的函数需要输入程序的全路径
  • 包含v的函数(execv, execvp, execve以数组的形式接收参数
  • 包含l的函数(execl, execlp, execle以列表的形式接收参数
  • 包含e的函数(execve, execle以数组的形式接收环境变量

img

6、对于Linux操作系统,其进程树的大致结构

7、总结

用图表示一个进程从代码到二进制到运行时的一个过程

  1. 通过图右边的文件编译过程生成so文件和可执行文件,放在硬盘上
  2. 左边的用户态的进程A执行fork,创建进程B,在进程B的处理逻辑中,执行exec系列系统调用
  3. 这个系统调用会通过load_elf_binary方法,将刚才生成的可执行文件,加载到进程B的内存中执行

img

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值