Linux最小系统实现

最小系统介绍

什么是最小系统?怎么实现?

使用printf实现打印hello world

有没有更简单的实现?
(1) 不使用外部任何库,头文件
(2) 单个文件
(3) 最简单的代码

打印:hello world
使用汇编语言实现
x86-64位机器实现打印hello world程序如下:

char* str = "Hello world!\n";
//static const char* str = "Hello world!\n";

//arch/x86/entry/syscalls/syscall_64.tbl

static void exit() {
    //void exit(int status);
        //param1: status
    asm("movq $42,%rdi \n\t"
            //syscall number exit = 60
            "movq $60,%rax \n\t"
            //enter system call
            "syscall \n\t");
}

static void printf() {
    //ssize_t write(int fd, const void *buf, size_t count);
    ¦   //param3: count
    asm("movq $13, %%rdx \n\t"
            //param2: buf
            "movq %0, %%rsi  \n\t"
            //param1: fd
            "movq $1, %%rdi  \n\t"
            //syscall number write = 1
            "movq $1, %%rax  \n\t"
            //enter system call
            //"syscall      \n\t" ::"r"(str));
            "syscall      \n\t" ::"r"("Hello world!\n"));
}

void nomain() {
    printf();
    exit();
}

aarch64位机器实现打印hello world程序如下:

  char* str = "Hello world!\n";                                                                                                                        
                                                                                                                                                       
  void exit() {     
    asm("ldr X0,=45 \n\t"     
      ¦ "ldr X8,=93 \n\t"     
      ¦ "svc 0x0 \n\t");
  #if 0     
    asm("mov X0,#45 \n\t"     
      ¦ "mov X8,#93 \n\t"     
      ¦ "svc 0x0 \n\t");     
  #endif     }void printf() {     
  #if 1     
    asm("ldr X2,=13 \n\t"     
      ¦ "mov X1,%0  \n\t"     
      ¦ "ldr X0,=1  \n\t"     
      ¦ "ldr X8,=64  \n\t"     
      ¦ "svc 0x0     \n\t" ::"r"(str));     
  #else     
    asm("mov X2,#13 \n\t"     
      ¦ "mov X1,%0  \n\t"     
      ¦ "mov X0,#1  \n\t"     
      ¦ "mov X8,#64  \n\t"     
      ¦ "svc 0x0     \n\t" ::"r"(str));     
  #endif     
  }     
      
  void nomain() {     
    printf();     
>>  exit();     
  }      

最小系统实现
正常C语言的包含glibc库的实现(x86架构(32bit,64bit))
不使用glibc的汇编语言实现(x86架构(32bit,64bit))

如果有机会的话把RK3308上面的ARM64架构的glibc实现和汇编语言实现也进行代码编写和反汇编介绍

X0-X7存放参数 X8存放系统调用编号
ARM64系统调用:
Sys_write ------ 64
Sys_exit ------ 93
SVC 进入系统调用

最小系统牵扯到的细节部分
同样是能打印hello world的程序,为什么一个程序这么大,另一个这么小?程序运行流程一样吗?程序怎么运行的?程序里面到底是什么?机器怎么执行我们写的程序?

系统调用过程介绍(x86架构(32bit,64bit))
(1) X86架构中老版本使用int 0x80软中断方式实现系统调用(用于程序跨越到内核程序中),后来使用fast system calls(sysenter进入系统调用,sysexit退出)
(2) X86-64bit系统中使用fast system calls(syscall进入系统调用,sysret返回用户程序)

整体调用流程(结合程序反汇编内容介绍):
(1) 从入口函数(初始化输入参数,读取系统环境,初始化堆,初始化IO,文件等)

(2) main(调用系统调用相关内容介绍)

(3) 退出函数exit等进行介绍

C语言程序编译流程
示例(打印hello world字符串):

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

Linux下,当我们使用gcc来编译Hello World程序时,只需使用最简单的命令:

gcc hello.c
./a.out
Hello World!

事实上,上述过程可以分解为4个步骤:分别为:预处理、编译、汇编、链接,如下图所示:
编译过程

预处理
首先源代码hello.c和相关头文件,如stdio.h等被预编译器cpp预编译成成一个.i文件。
如下命令:

Gcc –E hello.c –o hello.i

或者
cpp hello.c

预处理过程主要处理源代码中以#开头的预编译指令,如:“include”,“define”等,处理规则如下:
(1) 将所有#define删除,展开所有宏定义
(2) 处理所有预编译指令,如:#if、#ifdef、#else、#elif、#endif
(3) 处理#include,将头文件内容插入到该预编译指令位置(递归插入)。
(4) 删除所有注释“//”和“/**/”
(5) 添加行号和文件名标识,如:#2 “hello.c”,以便于编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时能够显示行号
(6) 保留所有#pragma编译器指令(编译器使用)
经过预编译后的.i文件不包含任何宏定义,因为所有宏均已展开,头文件也被展开。

编译
编译的过程是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成的相应汇编代码文件,这个过程是整个程序构建的核心部分,也是最复杂的部分(涉及到编译器原理)。
编译命令:

Gcc –S hello.i –o hello.s

预处理编译两步合成一步:

Gcc –S hello.c –o hello.s

或者使用cc1:

Cc1 hello.c

汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一个机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,只需要根据汇编指令和机器指令对照表一一翻译就可以了。
汇编器使用as:

as helloc.s –o hello.o

或者:

gcc –c hello.s -o hello.o

或者:

gcc –c hello.c –o hello.o

链接
链接器命令:

ld -static /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbeginT.o -L /usr/lib/gcc/x86_64-linux-gnu/11/ -L /usr/lib/x86_64-linux-gnu/ -  
L /usr/lib -L /lib hello.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o  /usr/lib/x86_64-linux-gnu/crtn.o  -o hello

如下图所示:
编译流程

编译器工作大致流程
编译器最繁重,最复杂的任务就是将源码翻译成汇编语言。
编译过程一般可以分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。如下图所示:
编译器工作流程

介绍ELF文件中的各个段
使用实际的例子来进行介绍
ELF文件类型
ELF文件类型
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一个程序包括:代码段,数据段,bss,堆栈等,如下一个简单的例子:
在这里插入图片描述

.text 代码段
.data 数据段
.bss 未初始化全局变量和局部静态变量
.rodata1 只读数据段,如:字符串常量,全局const变量,跟.rodata一样
.comment 存放的是编译器版本信息,如:GCC 11.4.0
.debug  调试信息
.dynamic  动态链接部分
.hash 符号哈希表
.line  调试时的行号表,即编译源代码行号与编译后指令对照表
.note  额外的编译器信息,如:公司名,发布版本号等
.strtab  string table字符串表,用于存储ELF中用到的各类字符串
.symtab  symbol table符号表
.shstrtab 段名字符串表
.plt
.got    动态链接跳转表和全局入口表
.init
.fini  程序初始化和终结代码段

ELF文件内容

ELF文件头
举例分析ELF文件头(/mnt/share/最小系统/ simpleSection.c)
ELF文件头

ELF头定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、程序入口地址、程序头入口和长度、段表位置和长度及段的数量等。
对应/usr/include/elf.h中如下结构:
ELF文件头结构

举例如下:

root@sc-VirtualBox:最小系统# readelf -h simpleSection.o 
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x0
  程序头起点:          0 (bytes into file)
  Start of section headers:          984 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         14
  Section header string table index: 13

各个节属性如下:
Magic:ELF魔数对应e_ident,16字节用来标识ELF文件平台属性,如:ELF字长(32位/64位)、字节序、ELF文件版本
在这里插入图片描述

最开始四个字节是所有ELF文件都必须相同的标识码,分别为0x7F,0x45,0x4c,0x46,第一个字节对应ASCII字符里面的DEL控制符,后面三个字节刚好就是ELF这3个字节的ASCII码。这四个字节为ELF文件魔术,用来确定文件类型。
接下来一个字节为ELF文件类型,第6个字节为字节序,第七个字节为ELF版本(ELF1.2以后没有更新)。后面9个字节ELF标准没有定义。
在这里插入图片描述

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

段表保存ELF文件中各个段的相关属性(段名,段长度,在文件中偏移,读写权限及段的其他属性)。编译器,、链接器和装载器都是依靠段表来定位和访问段的属性。段表偏移由e_shoff决定。

ELF段表结构
ELF段表描述符结构(数组形式存储每个段信息),段表结构如下:
在这里插入图片描述

各个成员含义如下:
在这里插入图片描述

例:

root@sc-VirtualBox:最小系统# readelf -S simpleSection.o 
There are 14 section headers, starting at offset 0x3d8:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000045  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000002e8
       0000000000000048  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000088
       000000000000000c  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  00000094
       0000000000000008  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  00000094
       0000000000000004  0000000000000000   A       0     0     4
  [ 6] .comment          PROGBITS         0000000000000000  00000098
       000000000000002c  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000c4
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.pr[...] NOTE             0000000000000000  000000c8
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000e8
       0000000000000058  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000330
       0000000000000030  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  00000140
       0000000000000138  0000000000000018          12     7     8
  [12] .strtab           STRTAB           0000000000000000  00000278
       000000000000006e  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  00000360
       0000000000000074  0000000000000000           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),
  D (mbind), l (large), p (processor specific)

段类型:
段类型
段类型

段的标志位:
段标志位

系统保留段:
段保留信息
在这里插入图片描述

段链接信息:
段链接信息

重定位表:
/rel.text段类型为重定位表。链接器在处理目标文件时,需要对目标文件中某些部分进行重定位,即代码段数据段中对绝对地址引用的位置。这些信息都记录在ELF文件的重定位表中,每个需要重定位的代码段或者数据段,都有一个对应的重定位表。

字符串表:
字符串表包含了各类字符串,如段名(.strtab或者.shstrtab),变量名等
字符串表

由上可知,ELF文件通过文件头就可以获知整个文件结构中各个段内容。

符号
符号包括:程序文件中包含的所有使用到的函数和变量。函数名和变量名就是符号名。每个目标文件都会有对应的符号表,记录了用到的所有符号,每个符号对应的数值就是符号值。
符号表中所有符号分类如下:
(1) 目标文件中全局符号(举例/mnt/share/最小系统/simpleSection.c),func1,main,global_init_var
(2) 引用的外部全局符号(未定义在本文件中),如:printf
(3) 段名,由编译器产生,值就是该段的起始地址,如:.text,.data等
(4) 局部符号,这类符号只在编译器内部可见,可用来分析程序崩溃
(5) 行号信息,即目标文件指令和源代码中的行号

ELF符号表(段名为.symtab)结构如下:
typedef struct
{
  Elf64_Word    st_name;        /* Symbol name (string tbl index) */
  unsigned char st_info;        /* Symbol type and binding */
  unsigned char st_other;       /* Symbol visibility */
  Elf64_Section st_shndx;       /* Section index */
  Elf64_Addr    st_value;       /* Symbol value */
  Elf64_Xword   st_size;        /* Symbol size */
} Elf64_Sym;

每个ELF符号都对应一个ELF符号表数组元素。
各成员对应如下:
在这里插入图片描述

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

st_shndx(符号所在段):
如果符号在本目标文件中,那么这个成员表示符号所在的段在段表中的下标,但是如果符号不是定义在本目标文件中,或者对于某些特殊符号,st_shndx有些特殊,如下:
在这里插入图片描述

符号值(st_value):每个符号对应值。大致分为如下几类:
(1) 符号不是“COMMON”块,则st_value表示符号在段中的偏移。即符号所对应的函数或者变量位于st_shndx制定的段,偏移st_value位置。
(2) 目标文件中,如果符号是“COMMON块”类型,则st_value表示该符号的对齐属性。
(3) 在可执行文件中,st_value表示符号的虚拟地址。这个虚拟地址对于动态链接器来说十分有用。
例(/mnt/share/最小系统/ simpleSection.o):
在这里插入图片描述
在这里插入图片描述

特殊符号
__executable_start – 程序开始地址
Etext - 代码段结束地址
Edata - 数据段结束地址
End - 程序结束地址
例:(/mnt/share/最小系统/specialSymbol.c)

强符号弱符号
attribute((weak))来指定全局变量为弱符号。

链接
链接过程分为两步:空间地址分配,符号解析与重定位
第一步 空间与地址分配
扫描所有输入文件,获取各个段长度,位置,属性,将所有输入目标文件中符号表定义和符号引用收集起来,同一放到全局符号表中。链接器ld获取所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,建立映射关系
第二步 符号解析与重定位(核心)
使用第一步搜集到的信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址等。

例(/mnt/share/最小系统/link/a.c b.c):
gcc -c a.c b.c
ld a.o b.o -o ab -e main

空间与地址分配
链接前后,目标文件各个段的分配,程序虚拟地址如下:

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

链接之后,各个段地址确定,各个符号地址由段地址加上偏移即可得到。

符号解析与重定位
反汇编a.o如下:
在这里插入图片描述

在这里插入图片描述

符号shared可直接确定地址404000(readelf -S ab可知),符号swap需要根据偏移量来进行计算。Call指令是一条近址相对位置调用指令,后面跟的是调用指令下一条指令的偏移量,call指令下一条指令是add,它的地址为0x40102e,相对于add指令偏移量为0x40102e+7=0x401035

重定位表
链接器如何知道哪些指令需要被调整,怎么调整?有一个重定位表结构专门用来保存重定位相关信息。

typedef struct
{
  Elf64_Addr    r_offset;       /* Address */
  Elf64_Xword   r_info;         /* Relocation type and symbol index */
} Elf64_Rel;

在这里插入图片描述

例:

root@sc-VirtualBox:link# objdump -r a.o

a.o:     文件格式 elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
000000000000001a R_X86_64_PC32     shared-0x0000000000000004
000000000000002a R_X86_64_PLT32    swap-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

在这里插入图片描述

符号解析
由于程序由多个文件组成,必然涉及到要使用到外部符号,当链接器需要对某个符号进行重定位的时候,首先就要从全局符号表中查找相关符号,然后进行重定位操作。如果找不到相关引用到的符号,则直接报符号未找到(也就是编译器编译源代码的时候打印出来的undefined xxx错误)。

在这里插入图片描述

介绍ELF文件程序启动调用流程
好了,上一节结束,我们已经知道了程序链接的流程,程序已经正常生成,下面就开始运行程序。
使用实际的例子来进行介绍(结合printf程序来看)
一个C/C++程序一般都从main开始,随着main结束而结束。实际中,main函数调用之前,为了使线程能够顺利执行,要先初始化执行环境,比如堆分配初始化(malloc,free)、线程子系统等(C++中构造函数在main调用之前被调用,析构函数在main调用之后被调用)。
Linux系统下一般程序入口为_start,这个函数是glibc的一部分。当我们的程序与Glibc库链接在一起形成最终可执行文件以后,这个函数就是程序的初始化部分入口,程序初始化部分完成一系列初始化过程,调用main函数执行程序主体。Main函数执行完成之后,返回到初始化部分,进行一些清理工作,然后结束进程(典型的例子:C++构造和析构函数)。ELF文件定义了两种特殊的段,如下:
(1) .init 包含了进程初始化代码。Main函数被调用之前,glibc初始化部分安排执行这个段中的代码。
(2) .fini 包含了进程终止代码。Main函数正常退出,glibc会安排执行这个段中的代码。

进程在内存中的整体布局如下所示:
在这里插入图片描述

使用具体例子来看进程在内存空间的实际内存分布图(cat /proc/self/maps)

举例说明,程序入口点是main函数吗?
1./mnt/share/最小系统/entry-c/1.c
当程序刚刚执行到main的时候,全局变量的初始化过程已经结束了(a值已经确定),main函数的两个参数(argc,argv)也都已经被传了过来。此外,在你不知道的时候,堆和栈的初始化已经悄悄完成,一些系统I/O也都已经初始化了,因此可以正常使用printf和malloc。

  1. /mnt/share/最小系统/entry-c/2.c
    atexit注册main结束或者exit函数调用结束程序之后要执行的函数。

3.c++里面的构造函数和析构函数

一个典型程序的运行步骤如下:
(1)	操作系统创建进程之后,把控制权交给程序入口(这个入口往往是运行库的某个入口函数)。
(2)	入口函数完成运行库和程序运行环境的初始化工作,包括:堆,I/O,线程,全局变量构造等。
(3)	入口函数初始化完成,调用main函数,正式开始执行程序主体部分。
(4)	Main函数执行完毕之后,返回到入口函数,进行清理工作,包括:全局变量析构,堆销毁,关闭I/O等,然后进行系统调用结束进程。

入口函数如何实现
GLIBC入口函数

示例(/mnt/share/最小系统/c/tinyHelloWorld.c):
Objdump –d tinyHelloWorld

在这里插入图片描述

Glibc入口函数为_start(入口点为ld链接器指定(可通过ld –verbose来查看默认链接器脚本),可以通过参数自行修改入口点)。_start由汇编实现,并且和平台相关,下面可以单独看x86_64的_start实现:

ENTRY (_start)
    /* Clearing frame pointer is insufficient, use CFI.  */  
    cfi_undefined (rip)
    /* Clear the frame pointer.  The ABI suggests this be done, to mark
    ¦  the outermost frame obviously.  */  
    xorl %ebp, %ebp

    /* Extract the arguments as encoded on the stack and set up
    ¦  the arguments for __libc_start_main (int (*main) (int, char **, char **),
        ¦  int argc, char *argv,
        ¦  void (*init) (void), void (*fini) (void),
        ¦  void (*rtld_fini) (void), void *stack_end).                                                                                                                        
    ¦  The arguments are passed via registers and on the stack:
    main:       %rdi
    argc:       %rsi
    argv:       %rdx
    init:       %rcx               /*  调用main前的初始化工作  */
    fini:       %r8               /*  调用main后的收尾工作  */
    rtld_fini:  %r9            /* 动态库加载收尾工作  */
    stack_end:  stack.  */    /* 栈底地址 */

    mov %RDX_LP, %R9_LP /* Address of the shared library termination
                ¦  function.  */  
#ifdef __ILP32__
    mov (%rsp), %esi    /* Simulate popping 4-byte argument count.  */  
    add $4, %esp
#else
    popq %rsi       /* Pop the argument count.  */  
#endif
    /* argv starts just at the current stack top.  */  
    mov %RSP_LP, %RDX_LP
    /* Align the stack to a 16 byte boundary to follow the ABI.  */  
    and  $~15, %RSP_LP

    /* Push garbage because we push 8 more bytes.  */  
    pushq %rax

    /* Provide the highest stack address to the user code (for stacks
    ¦  which grow downwards).  */  
    pushq %rsp

#ifdef SHARED
    /* Pass address of our own entry points to .fini and .init.  */  
    mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP
    mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP

    mov main@GOTPCREL(%rip), %RDI_LP
#else
    /* Pass address of our own entry points to .fini and .init.  */  
    mov $__libc_csu_fini, %R8_LP
    mov $__libc_csu_init, %RCX_LP

    mov $main, %RDI_LP
#endif
    /* Call the user's main function, and exit with its value.
    ¦  But let the libc call main.  Since __libc_start_main in
    ¦  libc.so is called very early, lazy binding isn't relevant
    ¦  here.  Use indirect branch via GOT to avoid extra branch
    ¦  to PLT slot.  In case of static executable, ld in binutils
    ¦  2.26 or above can convert indirect branch into direct
    ¦  branch.  */
    call *__libc_start_main@GOTPCREL(%rip)

    hlt         /* Crash if somehow `exit' does return.  */
END (_start)
_start -> __libc_start_main  -> generic_start_main 

generic_start_main:
	__pthread_initialize_minimal  线程库初始化
	__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL) 注册动态链接器初始化
	__libc_init_first (argc, argv, __environ);  libc初始化
	__cxa_atexit ((void (*) (void *)) fini, NULL, NULL) 注册程序收尾代码__libc_csu_fini
	(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM)   --__libc_csu_init
	result = main (argc, argv, __environ MAIN_AUXVEC_PARAM)  运行main函数
	exit (result)  程序退出
	调用注册的atexit函数
	根据平台架构调用对应的流程

Glibc组成
(1) 头文件/usr/include
(2) 库文件/usr/lib/libc.a libc.so
(3) 运行库/usr/lib/x86_64-linux-gnu/crt1.o crti.o crtn.o

Crt1.o包含程序入口函数_start,由他负责调用__libc_start_main初始化libc并调用main。由于C++的出现和对ELF文件的改进,出现了必须要在main函数之前执行的全局/静态对象构造和必须在main函数之后执行的全局/静态对象析构。为了满足类似需求,运行库在每个目标文件中引入两个与初始化相关的段“.init”和“.fini”。运行库保证这两个段里面的代码先于/后于main函数执行。链接器进行链接的时候,需要一些辅助代码,所以引入.crti.o和crtn.o。
Crti.o和crtn.o包含的代码实际上是_init和_finit()函数的开始和结尾部分,可通过反汇编objdump –dr crti.o和crtn.o查看。二进制文件中的init段和fini段都是通过合并Crti.o和crtn.o中的init和fini段代码而来。
举例:

objdump -dr crti.o
objdump -dr crtn.o
objdump –d /mnt/share/最小系统/c/tinyHelloWorld

剩余:crtbeginT.o, libgcc.a, libgcc_eh.a, crtend.o
crtbeginT.o, crtend.o实现C++全局构造和析构函数
__do_global_dtors_aux析构函数.fini中会调用

Libgcc.a处理平台差异性的东西(如,32位不支持64位long long类型的运算,libgcc.a中包含整数运算,浮点数运算等)
Libgcc_eh.a包含支持c++异常处理相关函数

gmon_start 用于生成程序执行的状态profile文件(包含程序运行时各部分运行时间),gcc加上编译选项-pg程序运行结束即可生成gmon.out文件,通过gprof工具可以分析。

程序加载过程(程序如何加载进内存中):
覆盖装入和页映射是两种典型的动态装载的方法。
(1) 覆盖装入
在这里插入图片描述

(2) 页映射(目前主流操作系统使用)
在这里插入图片描述

进程创建运行流程
三步:
(1) 创建一个独立的虚拟地址空间
I386的Linux下,创建虚拟空间实际上只是分配一个页目录,不设置映射关系(映射关系等到后面程序发生也错误的时候再进行设置)

(2) 读取可执行文件头,并且建立虚拟地址空间与可执行文件的映射关系

进程数据结构中保存如下信息:
代码在可执行文件中的位置,大小,对齐关系
虚拟空间的位置,大小
在这里插入图片描述

(3) 将CPU的指令存储器设置成可执行文件的入口地址,启动运行
包括:内核空间切换到用户空间,堆栈信息保存,CPU运行权限切换等

缺页:
CPU开始执行第一条指令时,发现虚拟内存对应页面没有实际对应的物理内存页面,此时触发缺页异常,操作系统会进行专门处理操作,找到空页面所在的VMA,计算出相应页面在可执行文件中的偏移,在物理内存中分配一个物理页面,将虚拟内存页与物理内存页之间进行映射,再返回到用户空间继续执行指令。
在这里插入图片描述

Linux内核装载ELF过程简介
首先,bash进程(每当开启一个虚拟终端的时候都会开一个bash进程等待解释用户输入命令,此处可举例演示)会调用fork()系统调用创建一个新的进程,然后新进程调用execve()系统调用执行制定的ELF文件,原先bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令,如下示例:

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

char buf[4096] = {0};
pid_t pid;
while(1) {
  printf(“minibash$ ”);
  scanf(%s”, buf);
pid = fork();
if (pid == 0) {
	execve(buf, NULL, NULL);
} else if (pid > 0) {
	Waitpid(pid, &status, 0);
} else {
	Printf(“fork error!\n”);
}
}

execve调用系统调用入口为sys_execve(),sys_execve进行一些参数的检查复制之后,调用do_execve。Do_execve()首先查找可执行文件,读取文件前128字节(a.out,Java,脚本程序等),判断文件格式,ELF可执行文件前四个字节为0x7F,’e’,’l’,’f’。
读取头部之后,调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程。Linux中每种可执行文件都有相应的装载处理过程,search_binary_handle通过判断文件头魔数确定文件格式,进而调用装载处理函数。比如ELF文件装载处理过程为load_elf_binary();a.out装载处理过程为load_aout_binary();可执行脚本处理过程为load_script()。

	load_elf_binary函数流程如下:
(1)	检查ELF可执行文件格式有效性,包括:魔数,程序表中段数量
(2)	寻找动态链接.interp段,设置动态链接器路径
(3)	根据ELF文件头表描述,对ELF文件进行映射,比如:代码,数据,只读数据
(4)	初始化ELF进程环境
(5)	系统调用返回地址设置为ELF入口文件点(有解释器,则设置为解释器entry)
加载完毕之后,返回至do_execve,返回至sys_execve,返回到用户空间,执行新程序。

此处可介绍一个实际例子,并且伴随着内核源代码进行分析即可。(ELF装载过程-strace-ls.txt,内核代码加载ELF分析)。

chongsun2@ubuntu:~$ strace pwd
**execve("/bin/pwd", ["pwd"], [/* 20 vars */]) = 0**  此处将校验ELF文件,并将ELF文件加载到内存中并设置PC知道到ELFentry入口处(具体可查看kernel/fs/exec.c中SYSCALL_DEFINE3(execve,
        const char __user *, filename,
        const char __user *const __user *, argv,
        const char __user *const __user *, envp))
brk(0)                                  = 0x77c000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f52aea71000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=63239, ...}) = 0
mmap(NULL, 63239, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f52aea61000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P \2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1840928, ...}) = 0
mmap(NULL, 3949248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f52ae48c000
mprotect(0x7f52ae646000, 2097152, PROT_NONE) = 0
mmap(0x7f52ae846000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ba000) = 0x7f52ae846000
mmap(0x7f52ae84c000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f52ae84c000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f52aea60000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f52aea5e000
arch_prctl(ARCH_SET_FS, 0x7f52aea5e740) = 0
mprotect(0x7f52ae846000, 16384, PROT_READ) = 0
mprotect(0x606000, 4096, PROT_READ)     = 0
mprotect(0x7f52aea73000, 4096, PROT_READ) = 0
munmap(0x7f52aea61000, 63239)           = 0
brk(0)                                  = 0x77c000
brk(0x79d000)                           = 0x79d000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=3571056, ...}) = 0
mmap(NULL, 3571056, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f52ae124000
close(3)                                = 0
getcwd("/home/chongsun2", 4096)         = 16
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 10), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f52aea70000
write(1, "/home/chongsun2\n", 16/home/chongsun2
)       = 16
close(1)                                = 0
munmap(0x7f52aea70000, 4096)            = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/98312.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL— 基础语法大全及操作演示!!!(事务)

MySQL—— 基础语法大全及操作演示&#xff08;事务&#xff09; 六、事务6.1 事务简介6.2 事务操作6.2.1 未控制事务6.2.2 控制事务一6.2.3 控制事务二 6.3 事务四大特性6.4 并发事务问题6.5 事务隔离级别 MySQL— 基础语法大全及操作演示&#xff01;&#xff01;&#xff01…

docker desktop安装es 并连接elasticsearch-head:5

首先要保证docker安装成功&#xff0c;打开cmd&#xff0c;输入docker -v&#xff0c;出现如下界面说明安装成功了 下面开始安装es 第一步&#xff1a;拉取es镜像 docker pull elasticsearch:7.6.2第二步&#xff1a;运行容器 docker run -d --namees7 --restartalways -p 9…

虚拟化技术原理

计算虚拟化 介绍 把物理主机上物理资源&#xff08;CPU&#xff0c;内存&#xff0c;IO外设&#xff09;&#xff0c;通过虚拟化层抽象成超量、等量的逻辑资源&#xff08;虚拟CPU&#xff0c;虚拟内存&#xff0c;虚拟IO设备&#xff09;&#xff0c;然后重新组合形成新的虚…

Krahets 笔面试精选 88 题——40. 组合总和 II

使用深度搜索的方法&#xff1a; 由于题目说候选数组中的每个数字在每个组合只能出现一次&#xff0c;所以&#xff0c;为了避免重复&#xff0c;在开始之前对候选数组进行升序排序&#xff0c;这样优先选择小的数&#xff0c;如果当前的数都小于目标值&#xff0c;则后面的数就…

MySQL的字符转义

表象 表结构如下: 其中 content 字段存放json之后的数据,这个json数据里面 extra 字段的内容又是一段json,如下: INSERT INTO future.test_escape_character( id, title, content, is_del )VALUES ( 2, 我的博客, {"web_id":31415,"name":"清澄秋…

正确进行自动化测试

前言&#xff1a; &#x1f4d5;作者简介&#xff1a;热爱编程的小七&#xff0c;致力于C、Java、Python等多编程语言&#xff0c;热爱编程和长板的运动少年&#xff01; &#x1f4d8;相关专栏Java基础语法&#xff0c;JavaEE初阶&#xff0c;数据库&#xff0c;数据结构和算法…

PDU+远控,企业如何应用工业级智能PDU远程赋能业务?

在很多企业级业务场景下&#xff0c;如何保障相关业务设备的稳定供电非常重要&#xff0c;插座也就成为了这些业务体系中的核心基建。 为了保证相关设备供电的稳定&#xff0c;并且实现高效的远程管理&#xff0c;很多企业级的业务场景会部署专业的智能PDU&#xff0c;而在众多…

200 套基于Java开发的Java毕业设计实战项目(含源码+说明文档)

文章目录 简介前言第一部分第二部分部分截图源码咨询 简介 博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 前言 对于java方向的毕业设计题目选题&#xf…

golang入门笔记——nginx

文章目录 Nginx介绍Nginx的安装Nginx文件Nginx反向代理负载均衡nginx动静分离URLRewrite防盗链nginx高可用配置安全性Nginx限流Nginx缓存集成Lua脚本OpenRestry Nginx介绍 Nginx是一个高性能的HTTP和反向代理服务器&#xff0c;特点是占用内存少&#xff0c;并发能力强&#x…

Cypress web自动化windows环境npm安装Cypress

前言 web技术已经进化了&#xff0c;web的测试技术最终还是跟上了脚步&#xff0c;新一代的web自动化技术出现了&#xff1f; Cypress可以对在浏览器中运行的任何东西进行快速、简单和可靠的测试。 官方地址https://www.cypress.io/,详细的文档介绍https://docs.cypress.io/g…

C# Linq源码分析之Take(四)

概要 本文主要对Take的优化方法进行源码分析&#xff0c;分析Take在配合Select&#xff0c;Where等常用的Linq扩展方法使用时候&#xff0c;如何实现优化处理。 本文涉及到Select, Where和Take和三个方法的源码分析&#xff0c;其中Select, Where, Take更详尽的源码分析&…

cpolar做一个内网穿透

因为不在公司&#xff0c;需要访问公司的数据库&#xff0c;所以做一个内网穿透 下载安装 下载地址&#xff1a; https://dashboard.cpolar.com/get-started 下载后是个压缩包&#xff0c;解压后傻瓜式安装 操作隧道 安装后打开Cpolar Web UI 登录账号&#xff0c;查看隧…

mfc140u.dll丢失如何修复?解析mfc140u.dll是什么文件跟修复方法分享

大家好&#xff01;今天&#xff0c;我将和大家分享一下关于计算机中mfc140u.dll丢失的6种解决方法。希望我的分享能对大家在计算机使用过程中遇到问题时提供一些帮助。 首先&#xff0c;我想请大家了解一下什么是mfc140u.dll文件。mfc140u.dll是一个动态链接库文件&#xff0…

kali的学习

网络配置 1.kali的网络设置 首先我们了解kali的网络设置 DHCP&#xff1a;动态主机配置协议 是一个局域网的协议 使用UDP 协议工作静态IP&#xff1a;用于大部分的中小型网络 通过网络管理员手动分配IP原理进程 /etc 系统大部分服务启动过程都要访问该目录 我们直接去看看…

(六)k8s实战-存储管理

一、Volumes 1、HostPath 【使用场景&#xff1a;容器目录 挂载到 主机目录】 【可以持久化到主机上】 将节点上的文件或目录挂载到 Pod 上&#xff0c;此时该目录会变成持久化存储目录&#xff0c;即使 Pod 被删除后重启&#xff0c;也可以重新加载到该目录&#xff0c;该目…

Matlab 基本教程

1 清空环境变量及命令 clear all % 清除Workspace 中的所有变量 clc % 清除Command Windows 中的所有命令 2 变量命令规则 &#xff08;1&#xff09;变量名长度不超过63位 &#xff08;2&#xff09;变量名以字母开头&#xff0c; 可以由字母、数字和下划线…

Web后端开发(请求响应)上

请求响应的概述 浏览器&#xff08;请求&#xff09;<--------------------------(HTTP协议)---------------------->&#xff08;响应&#xff09;Web服务器 请求&#xff1a;获取请求数据 响应&#xff1a;设置响应数据 BS架构&#xff1a;浏览器/服务器架构模式。…

《华为认证》二层EVPN的配置

步骤1&#xff1a;配置PE和P设备的IGP以及mpls、mpls ldp&#xff08;略&#xff09; 步骤2&#xff1a;配置evpn实例&#xff0c;并且绑定到BD中&#xff0c;配置evpn的源ip地址 PE1: evpn vpn-instance 1 bd-mode //指定创建BD模式EVPN实例 route-distinguisher 100:1 vpn-…

学乐多光屏 P90:打开儿童学习新视界

随着科技迅猛发展&#xff0c;儿童教育正在迎来一场前所未有的革命。在这个数字化时代的浪潮中&#xff0c;学乐多光屏P90凭借其卓越的特性和深远的教育理念&#xff0c;成为智能儿童学习领域的引领者&#xff0c;为孩子们创造了崭新的学习体验。 创新科技&#xff0c;引领学习…

Android片段

如果你希望应用根据不同的环境有不同的外观和行为&#xff0c;这种情况下就需要片段&#xff0c;片段是可以由不同活动重用的模块化代码组件。 片段&#xff08;Fragment&#xff09;是活动&#xff08;Activity&#xff09;的一种模块化部分&#xff0c;表示活动中的行为或界面…