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 +++
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
线程概念 什么是线程 LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下) 进程:独立地址空间,拥有PCB 线程:也有PCB,但没有独立的地址空间(共享) 区别:在于是否共享地址空间。 独居(进程);合租(线程)。 Linux下: 线程:最小的执行单位 进程:最小分配资源单位,可看成是只有一个线程的进程。 Linux内核线程实现原理 类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。 1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone 2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的 3. 进程可以蜕变成线程 4. 线程可看做寄存器和栈的集合 5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位 察看LWP号:ps –Lf pid 查看指定线程的lwp号。 三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元 参考:《Linux内核源代码情景分析》 ----毛德操 对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。 但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。 实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。 如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。 因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。 线程共享资源 1.文件描述符表 2.每种信号的处理方式 3.当前工作目录 4.用户ID和组ID 5.内存地址空间 (.text/.data/.bss/heap/共享库) 线程非共享资源 1.线程id 2.处理器现场和栈指针(内核栈) 3.独立的栈空间(用户空间栈) 4.errno变量 5.信号屏蔽字 6.调度优先级 线程优、缺点 优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便 缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好 优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。 线程控制原语 pthread_self函数 获取线程ID。其作用对应进程中 getpid() 函数。 pthread_t pthread_self(void); 返回值:成功:0; 失败:无! 线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现 线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同) 注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。 pthread_create函数 创建一个新线程。 其作用,对应进程中fork() 函数。 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 返回值:成功:0; 失败:错误号 -----Linux环境下,所有线程特点,失败均直接返回错误号。 参数: pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t; 参数1:传出参数,保存系统为我们分配好的线程ID 参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。 参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。 参数4:线程主函数执行期间所使用的参数。 在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。star
### 回答1: Linux 操作系统中管理内存空间的最小单位是 "页面"(page)。在大多数的 Linux 发行版中,一个页面通常为 4KB。Linux 通过分页机制将物理内存划分为固定大小的页面,这些页面被映射到虚拟内存中,以便进程可以使用它们。页表维护了虚拟地址和物理地址之间的映射关系。 ### 回答2: Linux操作系统中管理内存的最小单位是页面(Page)。页面是一块固定大小的内存块,通常为4KB或者2MB大小。Linux将所有的物理内存和虚拟内存都划分为大小相等的页面,便于内存管理和页表管理。 页面是内存分配和访问的基本单位。当程序需要内存时,内核会按照页面大小为其分配相应的页面。而当程序访问内存时,也是以页面为单位进行操作。页面的大小可以在系统自行设置或在编译内核时进行调整。 Linux采用分页机制来实现内存管理。内存被划分为大小相同的页面,每个页面都有唯一的页面编号和虚拟地址。内核通过维护一个页表来管理页面,将虚拟地址映射到物理地址。 通过页面的分配和释放,Linux能够灵活地管理内存,避免过多的内存浪费。页面的分配可以根据程序的需要动态进行调整,而页面的释放则可以在程序不再使用时进行回收。 总之,Linux操作系统中管理内存的最小单位是页面。页面是一块固定大小的内存块,可以灵活地进行分配和管理,保证了内存资源的高效利用。 ### 回答3: Linux操作系统中管理内存的最小单位是页面(page)。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行走在软件开发路上的人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值