就去射 你写的代码是何如跑起来的

另类图片亚洲

另类图片亚洲

  • 首页
  • www.11
  • 偷拍偷窥
  • 26uuu 电影
  • 亚洲AV
  • 5858p影音先锋
  • 第四播
  • 你的位置:另类图片亚洲 > 5858p影音先锋 > 就去射 你写的代码是何如跑起来的

    就去射 你写的代码是何如跑起来的

    发布日期:2024-11-02 13:01    点击次数:127

    就去射 你写的代码是何如跑起来的

    本文来自微信公众号:设备内功修都 (ID:kfngxl)就去射,作家:张彦飞 allen

    全国好,我是飞哥!

    今天咱们来想考一个简便的问题,一个法度是如安在 Linux 上践诺起来的?

    咱们就拿全天地最简便的 Hello World 法度来例如。

    #include <stdio.h>
    int main()
    {
       printf("Hello, World!\n");
       return 0;
    }

    咱们在写完代码后,进行简便的编译,然后在 shell 号召行下就不错把它启动起来。

    # gcc main.c -o helloworld
    # ./helloworld
    Hello, World!

    那么在编译启动运行的经过中都发生了哪些事情了呢?今天就让咱们来深远地了解一下。

    一、交融可践诺文献格式

    源代码在编译后会生成一个可践诺程小序件,咱们先来了解一下编译后的二进制文献是什么神态的。

    咱们领先使用 file 号召稽查一下这个文献的格式。

    # file helloworld
    helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), ...

    file 号召给出了这个二进制文献的摘抄信息,其中 ELF 64-bit LSB executable 暗示这个文献是一个 ELF 格式的 64 位的可践诺文献。x86-64 暗示该可践诺文献撑握的 cpu 架构。

    LSB 的全称是 Linux Standard Base,是 Linux 圭臬范例。其宗旨是制定一系列圭臬来增强 Linux 刊行版的兼容性。

    ELF 的全称是 Executable Linkable Format,是一种二进制文献格式。Linux 下的方针文献、可践诺文献和 CoreDump 都按照该格式进行存储。

    ELF 文献由四部分构成,分裂是 ELF 文献头 (ELF header)、Program header table、Section 和 Section header table。

    接下来咱们分几个末节挨个先容一下。

    1.1 ELF 文献头

    ELF 文献头记载了整个文献的属性信息。原始二进制特地未便于不雅察。不外咱们有趁手的器具 - readelf,这个器具不错帮咱们稽查 ELF 文献中的各式信息。

    咱们先来看一下编译出来的可践诺文献的 ELF 文献头,使用 --file-header (-h) 选项即可稽查。

    # readelf --file-header helloworld
    ELF Header:
      Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
      Class:                             ELF64
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              EXEC (Executable file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x401040
      Start of program headers:          64 (bytes into file)
      Start of section headers:          23264 (bytes into file)
      Flags:                             0x0
      Size of this header:               64 (bytes)
      Size of program headers:           56 (bytes)
      Number of program headers:         11
      Size of section headers:           64 (bytes)
      Number of section headers:         30
      Section header string table index: 29

    ELF 文献头包含了现时可践诺文献的摘抄信息,我把其中要津的几个拿出来给全国讲解一下。

    Magic:一串特等的识别码,主要用于外部法度快速地对这个文献进行识别,快速地判断文献类型是不是 ELF

    Class:暗示这是 ELF64 文献

    Type:为 EXEC 暗示是可践诺文献,其它文献类型还有 REL(可重定位的方针文献)、DYN(动态通顺库)、CORE(系统调试 coredump 文献)

    Entry point address:法度进口地址,这里显现进口在 0x401040 位置处

    Size of this header:ELF 文献头的大小,这里显现是占用了 64 字节

    以上几个字段是 ELF 头中对 ELF 的举座面目。另外 ELF 头中还关系于 program headers 和 section headers 的面目信息。

    Start of program headers:暗示 Program header 的位置

    Size of program headers:每一个 Program header 大小

    Number of program headers:统统有几许个 Program header

    Start of section headers: 暗示 Section header 的脱手位置。

    Size of section headers:每一个 Section header 的大小

    Number of section headers: 统统有几许个 Section header

    1.2 Program Header Table

    在先容 Program Header Table 之前咱们张开先容一下 ELF 文献中一双儿左近的见地 - Segment 和 Section。

    ELF 文献里面最宏大的构成单元是一个一个的 Section。每一个 Section 都是由编译通顺器生成的,都有不同的用途。例如编译器会将咱们写的代码编译后放到 .text Section 中,将全局变量放到 .data 无意是 .bss Section 中。

    但是关于操作系统来说,它不关心具体的 Section 是啥,它只关心这块内容应该以何种权限加载到内存中,例如读,写,践诺等权限属性。因此疏通权限的 Section 不错放在整个构成 Segment,以便捷操作系统更快速地加载。

    由于 Segment 和 Section 翻译成中语的话,理由太接近了,特地不利于交融。是以本文中我就平直使用 Segment 和 Section 原汁原味的见地,而不是将它们翻译成段无意是节,这样太容易让东谈主混浊了。

    Program headers table 即是算作通盘 Segments 的头信息,用来面目通盘的 Segments 的。。

    使用 readelf 器具的 --program-headers(-l)选项不错领会稽查到这块区域里存储的内容。

    # readelf --program-headers helloworld
    Elf file type is EXEC (Executable file)
    Entry point 0x401040
    There are 11 program headers, starting at offset 64
    
    Program Headers:
      Type           Offset             VirtAddr           PhysAddr
         FileSiz            MemSiz              Flags  Align
      PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
         0x0000000000000268 0x0000000000000268  R      0x8
      INTERP         0x00000000000002a8 0x00000000004002a8 0x00000000004002a8
         0x000000000000001c 0x000000000000001c  R      0x1
       [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
      LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
         0x0000000000000438 0x0000000000000438  R      0x1000
      LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
         0x00000000000001c5 0x00000000000001c5  R E    0x1000
      LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
         0x0000000000000138 0x0000000000000138  R      0x1000
      LOAD           0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
         0x0000000000000220 0x0000000000000228  RW     0x1000
      DYNAMIC        0x0000000000002e20 0x0000000000403e20 0x0000000000403e20
         0x00000000000001d0 0x00000000000001d0  RW     0x8
      NOTE           0x00000000000002c4 0x00000000004002c4 0x00000000004002c4
         0x0000000000000044 0x0000000000000044  R      0x4
      GNU_EH_FRAME   0x0000000000002014 0x0000000000402014 0x0000000000402014
         0x000000000000003c 0x000000000000003c  R      0x4
      GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
         0x0000000000000000 0x0000000000000000  RW     0x10
      GNU_RELRO      0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
         0x00000000000001f0 0x00000000000001f0  R      0x1
    
     Section to Segment ming:
      Segment Sections...
       00     
       01     .interp 
       02     .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
       03     .init .plt .text .fini 
       04     .rodata .eh_frame_hdr .eh_frame 
       05     .init_array .fini_array .dynamic .got .got.plt .data .bss 
       06     .dynamic 
       07     .note.gnu.build-id .note.ABI-tag 
       08     .eh_frame_hdr 
       09     
       10     .init_array .fini_array .dynamic .got

    上头的阻隔显现统统有 11 个 program headers。

    关于每一个段,输出了 Offset、VirtAddr 等面目现时段的信息。Offset 暗示现时段在二进制文献中的脱手位置,FileSiz 暗示现时段的大小。Flag 暗示现时的段的权限类型,R 暗示可读、E 暗示可践诺、W 暗示可写。

    在最底下,还把每个段是由哪几个 Section 构成的给展示了出来,比如 03 号段是由“.init .plt .text .fini” 四个 Section 构成的。

    1.3 Section Header Table

    和 Program Header Table 不相同的是,Section header table 平直面目每一个 Section。这二者面目的其实都是各式 Section ,只不外宗旨不同,一个针对加载,一个针对通顺。

    使用 readelf 器具的 --section-headers (-S)选项不错领会稽查到这块区域里存储的内容就去射。

    # readelf --section-headers helloworld
    There are 30 section headers, starting at offset 0x5b10:
    
    Section Headers:
      [Nr] Name              Type             Address           Offset
        Size              EntSize          Flags  Link  Info  Align
      ......
      [13] .text             PROGBITS         0000000000401040  00001040
        0000000000000175  0000000000000000  AX       0     0     16
      ......
      [23] .data             PROGBITS         0000000000404020  00003020
        0000000000000010  0000000000000000  WA       0     0     8
      [24] .bss              NOBITS           0000000000404030  00003030
        0000000000000008  0000000000000000  WA       0     0     1
      ......    
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
      L (link order), O (extra OS processing required), G (group), T (TLS),
      C (compressed), x (unknown), o (OS specific), E (exclude),
      l (large), p (processor specific)

    阻隔显现,该文献统统有 30 个 Sections,每一个 Section 在二进制文献中的位置通过 Offset 列暗示了出来。Section 的大小通过 Size 列体现。

    在这 30 个 Section 中,每一个都有独到的作用。咱们编写的代码在编译成二进制提醒后都会放到 .text 这个 Section 中。另外咱们看到 .text 段的 Address 列显现的地址是 0000000000401040。回忆前边咱们在 ELF 文献头中看到 Entry point address 显现的进口地址为 0x401040。这证据,法度的进口地址即是 .text 段的地址。

    另外还有两个值得关心的 Section 是 .data 和 .bss。代码中的全局变量数据在编译后将在在这两个 Section 中占据一些位置。如下简便代码所示。

    //未运动荡的内存区域位于 .bss 段
    int data1 ;     
    
    //还是运动荡的内存区域位于 .data 段
    int data2 = 100 ;  
    
    //代码位于 .text 段
    int main(void)
    {
     
    }
    1.4 进口进一步稽查

    接下来,咱们想再稽查一下咱们前边提到的法度进口 0x401040,望望它到底是啥。咱们此次再借助 nm 号召来进一步稽查一下可践诺文献中的标记过火地址信息。-n 选项的作用是显现的标记以地址排序,而不是称号排序。

    # nm -n helloworld
         w __gmon_start__
         U __libc_start_main&GLIBC_2.2.5
         U printf&GLIBC_2.2.5
                     
    0000000000401040 T _start
    
    0000000000401126 T main

    通过以上输出不错看到,法度进口 0x401040 指向的是 _start 函数的地址,在这个函数践诺一些运动荡的操作之后,咱们的进口函数 main 将会被调用到,它位于 0x401126 地址处。

    二、用户程度的创建经过综合

    在咱们编写的代码编译完生成可践诺法度之后,下一步即是使用 shell 把它加载起来并运行之。一般来说 shell 程度是通过 fork+execve 来加载并运行新程度的。一个简便加载 helloworld 号召的 shell 中枢逻辑是如下这个经过。

    // shell 代码示例
    int main(int argc, char * argv[])
    {
     
     pid = fork();
     if (pid==0){ // 要是是在程度中
      //使用 exec 系列函数加载并运行可践诺文献
      execve("helloworld", argv, envp);
     } else {
      
     }
     
    }

    shell 程度先通过 fork 系统调用创建一个程度出来。然后在子程度中调用 execve 将践诺的程小序件加载起来,然后就不错调到程小序件的运行进口处运行这个法度了。

    这个 fork 系统调用在内核进口是在 kernel / fork.c 下。

    //file:kernel/fork.c
    SYSCALL_DEFINE0(fork)
    {
     return do_fork(SIGCHLD, 0, 0, NULL, NULL);
    }

    在 do_fork 的罢了中,中枢是一个 copy_process 函数,它以拷贝父程度(线程)的方式来生成一个新的 task_struct 出来。

    //file:kernel/fork.c
    long do_fork()
    {
     //复制一个 task_struct 出来
     struct task_struct *p;
     p = copy_process(clone_flags, stack_start, stack_size,
        child_tidptr, NULL, trace);
    
     //子任务加入到就绪部队中去,恭候诊疗器诊疗
     wake_up_new_task(p);
     
    }

    在 copy_process 函数中为新程度恳求 task_struct,并用当远景度我方的地址空间、定名空间等对新程度进走运动荡,并为其恳求程度 pid。

    //file:kernel/fork.c
    static struct task_struct *copy_process()
    {
     //复制程度 task_struct 结构体
     struct task_struct *p;
     p = dup_task_struct(current);
     
    
     //程度中枢元素运动荡
     retval = copy_files(clone_flags, p);
     retval = copy_fs(clone_flags, p);
     retval = copy_mm(clone_flags, p);
     retval = copy_namespaces(clone_flags, p);
     
    
     //恳求 pid && 缔造程度号
     pid = alloc_pid(p-nsproxy-pid_ns);
     p-pid = pid_nr(pid);
     p-tgid = p-pid;
     
    }

    践诺完后,干预 wake_up_new_task 让新程度恭候诊疗器诊疗。

    不外 fork 系统调用只但是凭证当的 shell 程度再复制一个新的程度出来。这个新程度里的代码、数据都照旧和原本的 shell 程度的内容一模相同。

    要想罢了加载并运行另外一个法度,比如咱们编译出来的 helloworld 法度,那还需要使用到 execve 系统调用。

    三. Linux 可践诺文献加载器

    其实 Linux 不是写死只可加载 ELF 一种可践诺文献格式的。它在启动的时候,会把我方撑握的通盘可践诺文献的领会器都加载上。并使用一个 formats 双向链表来保存通盘的领会器。其中 formats 双向链表在内存中的结构如下图所示。

    咱们就以 ELF 的加载器 elf_format 为例,来望望这个加载器是何如注册的。在 Linux 中每一个加载器都用一个 linux_binfmt 结构来暗示。其中规矩了加载二进制可践诺文献的 load_binary 函数指针,以及加载崩溃文献 的 core_dump 函数等。其完好意思界说如下

    //file:include/linux/binfmts.h
    struct linux_binfmt {
     
     int (*load_binary)(struct linux_binprm *);
     int (*load_shlib)(struct file *);
     int (*core_dump)(struct coredump_params *cprm);
    };

    其中 ELF 的加载器 elf_format 中规矩了具体的加载函数,例如 load_binary 成员指向的即是具体的 load_elf_binary 函数。这即是 ELF 加载的进口。

    //file:fs/binfmt_elf.c
    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,
    };

    加载器 elf_format 会在运动荡的时候通过 register_binfmt 进行注册。

    //file:fs/binfmt_elf.c
    static int __init init_elf_binfmt(void)
    {
     register_binfmt(&elf_format);
     return 0;
    }

    而 register_binfmt 即是将加载器挂到全局加载器列表 - formats 全局链表中。

    //file:fs/exec.c
    static LIST_HEAD(formats);
    
    void __register_binfmt(struct linux_binfmt * fmt, int insert)
    {
     
     insert ? list_add(&fmt-lh, &formats) :
       list_add_tail(&fmt-lh, &formats);
    }

    Linux 中除了 elf 文献格式除外还撑握其它格式,在源码目次中搜索 register_binfmt,不错搜索到通盘 Linux 操作系统撑握的格式的加载法度。

    # grep -r "register_binfmt" *
    fs/binfmt_flat.c: register_binfmt(&flat_format);
    fs/binfmt_elf_fdpic.c: register_binfmt(&elf_fdpic_format);
    fs/binfmt_som.c: register_binfmt(&som_format);
    fs/binfmt_elf.c: register_binfmt(&elf_format);
    fs/binfmt_aout.c: register_binfmt(&aout_format);
    fs/binfmt_script.c: register_binfmt(&script_format);
    fs/binfmt_em86.c: register_binfmt(&em86_format);

    改日在 Linux 在加载二进制文献时会遍历 formats 链表,凭证要加载的文献格式来查询稳健的加载器。

    四、execve 加载用户法度

    具体加载可践诺文献的使命是由 execve 系统调用来完成的。

    该系统调用会读取用户输入的可践诺文献名,参数列表以及环境变量等脱手加载并运行用户指定的可践诺文献。该系统调用的位置在 fs / exec.c 文献中。

    //file:fs/exec.c
    SYSCALL_DEFINE3(execve, const char __user *, filename, )
    {
     struct filename *path = getname(filename);
     do_execve(path-name, argv, envp)
     
    }
    
    int do_execve()
    {
     
     return do_execve_common(filename, argv, envp);
    }

    execve 系统调用到了 do_execve_common 函数。咱们来看这个函数的罢了。

    //file:fs/exec.c
    static int do_execve_common(const char *filename, )
    {
     //linux_binprm 结构用于保存加载二进制文献时使用的参数
     struct linux_binprm *bprm;
    
     //1恳求并运动荡 brm 对象值
     bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
     bprm-file = ;
     bprm-filename = ;
     bprm_mm_init(bprm)
     bprm-argc = count(argv, MAX_ARG_STRINGS);
     bprm-envc = count(envp, MAX_ARG_STRINGS);
     prepare_binprm(bprm);
     
    
     //2遍历查找稳健的二进制加载器
     search_binary_handler(bprm);
    }

    这个函数中恳求并运动荡 brm 对象的具体使命不错用下图来暗示。

    在这个函数中,完成了一下三块使命。

    第一、使用 kzalloc 恳求 linux_binprm 内查对象。该内查对象用于保存加载二进制文献时使用的参数。在恳求完后,对该参数对象进行各式运动荡。

    第二、在 bprm_mm_init 中会恳求一个全新的 mm_struct 对象,准备留着给新程度使用。

    第三、给新程度的栈恳求一页的诬捏内存空间,偷拍偷窥并将栈指针记载下来。

    第四、读取二进制文献头 128 字节。

    咱们来看下运动荡栈的联系代码。

    //file:fs/exec.c
    static int __bprm_mm_init(struct linux_binprm *bprm)
    {
     bprm-vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
     vma-vm_end = STACK_TOP_MAX;
     vma-vm_start = vma-vm_end - PAGE_SIZE;
     
    
     bprm-p = vma-vm_end - sizeof(void *);
    }

    在上头这个函数中恳求了一个 vma 对象(暗示诬捏地址空间里的一段限度),vm_end 指向了 STACK_TOP_MAX(地址空间的顶部隔邻的位置),vm_start 和 vm_end 之间留了一个 Page 大小。也即是说默许给栈恳求了 4KB 的大小。临了把栈的指针记载到 bprm->p 中。

    另外再看下 prepare_binprm,在这个函数中,从文献头部读取了 128 字节。之是以这样干,是为了读取二进制文献头为了便捷后头判断其文献类型。

    //file:include/uapi/linux/binfmts.h
    #define BINPRM_BUF_SIZE 128
    
    //file:fs/exec.c
    int prepare_binprm(struct linux_binprm *bprm)
    {
     
     memset(bprm-buf, 0, BINPRM_BUF_SIZE);
     return kernel_read(bprm-file, 0, bprm-buf, BINPRM_BUF_SIZE);
    }

    在恳求并运动荡 brm 对象值完后,临了使用 search_binary_handler 函数遍历系统中已注册的加载器,尝试对现时可践诺文献进行领会并加载。

    在 3.1 节咱们先容了系统通盘的加载器都注册到了 formats 全局链内外了。函数 search_binary_handler 的使命经过即是遍历这个全局链表,凭证二进制文献头中佩戴的文献类型数据查找领会器。找到后调用领会器的函数对二进制文献进行加载。

    //file:fs/exec.c
    int search_binary_handler(struct linux_binprm *bprm)
    {
     
     for try=0; try2; try++ {
      list_for_each_entry(fmt, &formats, lh) {
       int (*fn)(struct linux_binprm *) = fmt-load_binary;
       
       retval = fn(bprm);
    
       //加载生效的话就复返了
       if (retval = 0) {
        
        return retval;
       }
       //加载失败链接轮回以尝试加载
       
      }
     }
    }

    在上述代码中的 list_for_each_entry 是在遍历 formats 这个全局链表,遍历时判断每一个链表元素是否有 load_binary 函数。有的话就调用它尝试加载。

    回忆一下 3.1 注册可践诺文献加载法度,关于 ELF 文献加载器 elf_format 来说,load_binary 函数指针指向的是 load_elf_binary。

    //file:fs/binfmt_elf.c
    static struct linux_binfmt elf_format = {
     .module  = THIS_MODULE,
     .load_binary = load_elf_binary,
     
    };

    那么加载使命就会干预到 load_elf_binary 函数中来进行。这个函数很长,不错说通盘的法度加载逻辑都在这个函数中体现了。我凭证这个函数的主要使命,分红以下 5 个小部分来给全国先容。

    在先容的经过中,为了抒发泄露,我会略微调一下源码的位置,可能和内核源码行数限定会有所不同。

    4.1 ELF 文献头读取

    在 load_elf_binary 中领先会读取 ELF 文献头。

    文献头中包含一些现时文献格式类型等数据,是以在读取完文献头后会进行一些正当性判断。要是分歧法,则退出复返。

    //file:fs/binfmt_elf.c
    static int load_elf_binary(struct linux_binprm *bprm)
    {
     //4.1 ELF 文献头领会
     //界说结构题并恳求内存用来保存 ELF 文献头
     struct {
      struct elfhdr elf_ex;
      struct elfhdr interp_elf_ex;
     } *loc;
     loc = kmalloc(sizeof(*loc), GFP_KERNEL);
    
     //得回二进制头
     loc-elf_ex = *((struct elfhdr *)bprm-buf);
    
     //寇仇部进行一系列的正当性判断,分歧法例平直退出
     if (loc-elf_ex.e_type != ET_EXEC && ){
      goto out;
     }
     
    }
    4.2 Program Header 读取

    在 ELF 文献头中记载着 Program Header 的数目,并且在 ELF 头之后紧接着即是 Program Header Tables。是以内核接下来不错将通盘的 Program Header 都读取出来。

    //file:fs/binfmt_elf.c
    static int load_elf_binary(struct linux_binprm *bprm)
    {
     //4.1 ELF 文献头领会
    
     //4.2 Program Header 读取
     // elf_ex.e_phnum 中保存的是 Programe Header 数目
     // 再凭证 Program Header 大小 sizeof(struct elf_phdr)
     // 整个经营出通盘的 Program Header 大小,并读取进来
     size = loc-elf_ex.e_phnum * sizeof(struct elf_phdr);
     elf_phdata = kmalloc(size, GFP_KERNEL);
     kernel_read(bprm-file, loc-elf_ex.e_phoff,
         (char *)elf_phdata, size);
     
     
    }
    4.3 清空父程度接受来的资源

    在 fork 系统调用创建出来的程度中,包含了不少原程度的信息,如老的地址空间,信号表等等。这些在新的法度运行时并莫得什么用,是以需要清空处置一下。

    具体使命包括运动荡新程度的信号表,欺骗新的地址空间对象等。

    //file:fs/binfmt_elf.c
    static int load_elf_binary(struct linux_binprm *bprm)
    {
     //4.1 ELF 文献头领会
     //4.2 Program Header 读取
    
     //4.3 清空父程度接受来的资源
     retval = flush_old_exec(bprm);
     
    
     current-mm-start_stack = bprm-p;
    }

    在清空完父程度接受来的资源后(天然也就使用上了新的 mm_struct 对象),这之后,平直将前边准备的程度栈的地址空间指针缔造到了 mm 对象上。这样改日栈就不错被使用了。

    4.4 践诺 Segment 加载

    接下来,加载器会将 ELF 文献中的 LOAD 类型的 Segment 都加载到内存里来。使用 elf_map 在诬捏地址空间中为其分派诬捏内存。临了稳健地缔造诬捏地址空间 mm_struct 中的 start_code、end_code、start_data、end_data 等各个地址空间联系指针。

    咱们来看下具体的代码:

    //file:fs/binfmt_elf.c
    static int load_elf_binary(struct linux_binprm *bprm)
    {
     //4.1 ELF 文献头领会
     //4.2 Program Header 读取
     //4.3 清空父程度接受来的资源
    
     //4.4 践诺 Segment 加载经过
     //遍历可践诺文献的 Program Header
     for(i = 0, elf_ppnt = elf_phdata;
      i < loc->elf_ex.e_phnum; i++, elf_ppnt++) 
    
      //只加载类型为 LOAD 的 Segment,不然跳过
      if (elf_ppnt-p_type != PT_LOAD)
       continue;
      
    
      //为 Segment 建筑内存 mmap, 将程小序件中的内容映射到诬捏内存空间中
      //这样改日法度中的代码、数据就都不错被考核了
      error = elf_map(bprm-file, load_bias + vaddr, elf_ppnt,
        elf_prot, elf_flags, 0);
    
      //经营 mm_struct 所需要的各个成员地址
      start_code = ;
      start_data = 
      end_code = ;
      end_data = ;
      
     }
    
     current-mm-end_code = end_code;
     current-mm-start_code = start_code;
     current-mm-start_data = start_data;
     current-mm-end_data = end_data;
     
    }

    其中 load_bias 是 Segment 要加载到内存里的基地址。这个参数有这样几种可能

    值为 0,即是平直按照 ELF 文献中的地址在内存中进行映射

    值为对都到整数页的脱手,物理文献中可能为了可践诺文献的大小弥漫紧凑,而不沟通对都的问题。但是操作系统在加载的时候为了运造孽果,需要将 Segment 加载到整数页的脱手位置处。

    4.5 数据内存恳求 & 堆运动荡

    因为程度的数据段需要写权限,是以需要使用 set_brk 系统调用专诚为数据段恳求诬捏内存。

    //file:fs/binfmt_elf.c
    static int load_elf_binary(struct linux_binprm *bprm)
    {
     //4.1 ELF 文献头领会
     //4.2 Program Header 读取
     //4.3 清空父程度接受来的资源
     //4.4 践诺 Segment 加载经过
     //4.5 数据内存恳求&运动荡
     retval = set_brk(elf_bss, elf_brk);
     
    }

    在 set_brk 函数中作念了两件事情:第一是为数据段恳求诬捏内存,第二是将程度堆的脱手指针和阻隔指针运动荡一下。

    //file:fs/binfmt_elf.c
    static int set_brk(unsigned long start, unsigned long end)
    {
     //1为数据段恳求诬捏内存
     start = ELF_PAGEALIGN(start);
     end = ELF_PAGEALIGN(end);
     if (end  start) {
      unsigned long addr;
      addr = vm_brk(start, end - start);
     }
    
     //2运动荡堆的指针
     current-mm-start_brk = current-mm-brk = end;
     return 0;
    }

    因为法度运动荡的时候,堆上照旧空的。是以堆指针运动荡的时候,堆的脱手地址 start_brk 和阻隔地址 brk 都缔造成了吞并个值。

    4.6 跳转到法度进口践诺

    在 ELF 文献头中记载了法度的进口地址。要是长短动态通顺加载的情况,进口地址即是这个。

    但是要是是动态通顺,也即是说存在 INTERP 类型的 Segment,由这个动态通顺器先来加载运行,然后再召回到法度的代码进口地址。

    # readelf --program-headers helloworld
    
    Program Headers:
      Type           Offset             VirtAddr           PhysAddr
         FileSiz            MemSiz              Flags  Align
      INTERP         0x00000000000002a8 0x00000000004002a8 0x00000000004002a8
         0x000000000000001c 0x000000000000001c  R      0x1
       [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

    关于是动态加载器类型的,需要先将动态加载器(本文示例中是 ld-linux-x86-64.so.2 文献)加载到地址空间中来。

    加载完成后再经营动态加载器的进口地址。这段代码我展示鄙人面了,莫得耐烦的同学不错跳过。归正独一知谈这里是经营了一个法度的进口地址就不错了。

    //file:fs/binfmt_elf.c
    static int load_elf_binary(struct linux_binprm *bprm)
    {
     //4.1 ELF 文献头领会
     //4.2 Program Header 读取
     //4.3 清空父程度接受来的资源
     //4.4 践诺 Segment 加载
     //4.5 数据内存恳求&堆运动荡
     //4.6 跳转到法度进口践诺
    
     //第一次遍历 program header table
     //只针对 PT_INTERP 类型的 segment 作念个预处置
     //这个 segment 中保存着动态加载器在文献系统中的旅途信息
     for (i = 0; i < loc->elf_ex.e_phnum; i++) {
      ...
     }
    
     //第二次遍历 program header table, 作念些特等处置
     elf_ppnt = elf_phdata;
     for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++){
      ...
     }
    
     //要是法度中指定了动态通顺器,就把动态通顺器法度读出来
     if (elf_interpreter) {
      //加载并复返动态通顺器代码段地址
      elf_entry = load_elf_interp(&loc->interp_elf_ex,
          interpreter,
          &interp_map_addr,
          load_bias);
      //经营动态通顺器进口地址
      elf_entry += loc->interp_elf_ex.e_entry;
     } else {
      elf_entry = loc->elf_ex.e_entry;
     }
    
     //跳转到进口脱手践诺
     start_thread(regs, elf_entry, bprm->p);
     ...
    }
    五、回来

    看起来简简便单的一瞥 helloworld 代码,但是要想把它运行经过交融了了可却需要特地深厚的内功的。

    本文领先指导全国刚烈和交融了二进制可运行 ELF 文献格式。在 ELF 文献中是由四部分构成,分裂是 ELF 文献头 (ELF header)、Program header table、Section 和 Section header table。

    Linux 在运动荡的时候,会将通盘撑握的加载器都注册到一个全局链表中。关于 ELF 文献来说,它的加载器在内核中的界说为 elf_format,其二进制加载进口是 load_elf_binary 函数。

    一般来说 shell 程度是通过 fork + execve 来加载并运行新程度的。践诺 fork 系统调用的作用是创建一个新程度出来。不外 fork 创建出来的新程度的代码、数据都照旧和原本的 shell 程度的内容一模相同。要想罢了加载并运行另外一个法度,那还需要使用到 execve 系统调用。

    在 execve 系统调用中,领先会恳求一个 linux_binprm 对象。在运动荡 linux_binprm 的经过中,会恳求一个全新的 mm_struct 对象,准备留着给新程度使用。还会给新程度的栈准备一页(4KB)的诬捏内存。还会读取可践诺文献的前 128 字节。

    接下来即是调用 ELF 加载器的 load_elf_binary 函数进行本色的加载。约莫会践诺如下几个顺序:

    ELF 文献头领会

    Program Header 读取

    妹妹自慰

    清空父程度接受来的资源,使用新的 mm_struct 以及新的栈

    践诺 Segment 加载,将 ELF 文献中的 LOAD 类型的 Segment 都加载到诬捏内存中

    为数据 Segment 恳求内存,并将堆的肇始指针进走运动荡

    临了经营并跳转到法度进口践诺

    当用户程度启动起来以后,咱们不错通过 proc 伪文献来稽查程度中的各个 Segment。

    # cat /proc/46276/maps
    00400000-00401000 r--p 00000000 fd:01 396999                             /root/work_temp/helloworld
    00401000-00402000 r-xp 00001000 fd:01 396999                             /root/work_temp/helloworld
    00402000-00403000 r--p 00002000 fd:01 396999                             /root/work_temp/helloworld
    00403000-00404000 r--p 00002000 fd:01 396999                             /root/work_temp/helloworld
    00404000-00405000 rw-p 00003000 fd:01 396999                             /root/work_temp/helloworld
    01dc9000-01dea000 rw-p 00000000 00:00 0                                  [heap]
    7f0122fbf000-7f0122fc1000 rw-p 00000000 00:00 0 
    7f0122fc1000-7f0122fe7000 r--p 00000000 fd:01 1182071                    /usr/lib64/libc-2.32.so
    7f0122fe7000-7f0123136000 r-xp 00026000 fd:01 1182071                    /usr/lib64/libc-2.32.so
    ......
    7f01231c0000-7f01231c1000 r--p 0002a000 fd:01 1182554                    /usr/lib64/ld-2.32.so
    7f01231c1000-7f01231c3000 rw-p 0002b000 fd:01 1182554                    /usr/lib64/ld-2.32.so
    7ffdf0590000-7ffdf05b1000 rw-p 00000000 00:00 0                          [stack]
    ......

    天然本文特地的长,但仍然其实只把大体的加载启动经过串了一下。要是你日后在使命学习中遭受想搞了了的问题,不错顺着本文的想路去到源码中寻找具体的问题,进而匡助你找到使命中的问题的解。

    临了提一下,夺宗旨读者可能发现了,本文的实例中加载新法度运行的经过中其实有一些滥用,fork 系统调用领先将父程度的许多信息拷贝了一遍,而 execve 加载可践诺法度的时候又是再行赋值的。是以在本色的 shell 法度中,一般使用的是 vfork。其使命旨趣基本和 fork 一致,但区别是会少拷贝一些在 execve 系统调用顶用不到的信息,进而擢升加载性能。

    告白声明:文内含有的对外跳转通顺(包括不限于超通顺、二维码、口令等模式)就去射,用于传递更多信息,节俭甄选期间,阻隔仅供参考,IT之家通盘著作均包含本声明。



    栏目分类