Hello 的一生

作者: zsh2517 分类: 未分类 发布时间: 2021-06-27 18:34

 

摘 要

Hello world 是大多数人写的第一个程序,本文以 hello.c 为线索,从生成(预处理、编译、汇编、链接)到运行(进程、存储、IO),多个角度解读 Linux 下程序的开发运行过程。从而更深层次的理解Linux系统下的动态链接机制、存储层次结构、异常控制流、虚拟内存及UnixI/O等相关内容。从而更好地了解程序的生命周期,深入理解 HIT-ICS 这门课程。

关键词:程序生命周期,Unix,编译,汇编,进程

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 – 4 –

1.1 Hello简介 – 4 –

1.2 环境与工具 – 4 –

1.3 中间结果 – 4 –

1.4 本章小结 – 5 –

第2章 预处理 – 6 –

2.1 预处理的概念与作用 – 6 –

2.2在Ubuntu下预处理的命令 – 6 –

2.3 Hello的预处理结果解析 – 7 –

2.4 本章小结 – 8 –

第3章 编译 – 9 –

3.1 编译的概念与作用 – 9 –

3.2 在Ubuntu下编译的命令 – 9 –

3.3 Hello的编译结果解析 – 10 –

3.4 本章小结 – 10 –

第4章 汇编 – 11 –

4.1 汇编的概念与作用 – 11 –

4.2 在Ubuntu下汇编的命令 – 11 –

4.3 可重定位目标elf格式 – 11 –

4.4 Hello.o的结果解析 – 12 –

4.5 本章小结 – 1 –

第5章 链接 – 3 –

5.1 链接的概念与作用 – 3 –

5.2 在Ubuntu下链接的命令 – 3 –

5.3 可执行目标文件hello的格式 – 3 –

5.4 hello的虚拟地址空间 – 3 –

5.5 链接的重定位过程分析 – 4 –

5.6 hello的执行流程 – 4 –

5.7 Hello的动态链接分析 – 4 –

5.8 本章小结 – 4 –

第6章 hello进程管理 – 5 –

6.1 进程的概念与作用 – 5 –

6.2 简述壳Shell-bash的作用与处理流程 – 5 –

6.3 Hello的fork进程创建过程 – 5 –

6.4 Hello的execve过程 – 5 –

6.5 Hello的进程执行 – 6 –

6.6 hello的异常与信号处理 – 6 –

6.7本章小结 – 8 –

第7章 hello的存储管理 – 9 –

7.1 hello的存储器地址空间 – 9 –

7.2 Intel逻辑地址到线性地址的变换-段式管理 – 9 –

7.3 Hello的线性地址到物理地址的变换-页式管理 – 9 –

7.4 TLB与四级页表支持下的VA到PA的变换 – 10 –

7.5 三级Cache支持下的物理内存访问 – 10 –

7.6 hello进程fork时的内存映射 – 11 –

7.7 hello进程execve时的内存映射 – 11 –

7.8 缺页故障与缺页中断处理 – 12 –

7.9动态存储分配管理 – 12 –

7.10本章小结 – 13 –

第8章 hello的IO管理 – 14 –

8.1 Linux的IO设备管理方法 – 14 –

8.2 简述Unix IO接口及其函数 – 14 –

8.3 printf的实现分析 – 14 –

8.4 getchar的实现分析 – 15 –

8.5本章小结 – 16 –

结论 – 16 –

附件 – 17 –

参考文献 – 18 –

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。


P2P:

gcc编译器先读取hello.c文件,处理“#”,将#include包含的头文件直接拷贝到hello.c当中,同时将代码中没用的注释部分删除等。添加行号和文件标示,并保留#pragma编译器指令。进行预处理修改该文件获得hello.i文件,再通过编译器将文本文件翻译成汇编程序hello.s,再通过汇编器将hello.s翻译成机器语言指令hello.o文件。最后将程序与函数库中需要使用的二进制文件进行链接,形成可执行目标程序ELF二进制文件。执行该文件,操作系统会使用fork函数形成一个子进程,分配相应的内存资源,使用execve函数加载进程。完成P2P过程。

020:

hello对数据进行处理时,其空间在内存上申请。计算机存储结构层层递进,下一级作为上一级的缓存,让hello的数据能够从磁盘传输到CPU寄存器。而TLB、分级页表等机制又为数据在内存中的高效访问提供了支持,合理的信号机制又让hello程序能够应对执行中的各种情况。操作系统将I/O设备都抽象为了文件,将底层与应用层隔离,将用户态与内核态隔离,通过描述符与接口,让hello程序能够间接调用硬件进行输入输出,完成程序从键盘、主板、显卡,再到屏幕的工作。 hello执行完成后shell会回收hello进程,并且内核会从系统中删除hello所有痕迹,实现O2O:From Zero-0 to Zero-0。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:AMD 3900XT,8 Core(VMWare),8G RAM,100G SSD

软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位

开发工具:GDB/OBJDUMP;EDB;gedit+gcc;CodeBlocks 64位等。

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c

源代码文件

hello.i

gcc -E 预处理之后展开的源文件

hello.s

gcc -S 编译后的汇编代码文件

hello.o

gcc -c 汇编之后的可重定位目标文件

hello

gcc 编译到最终的可执行文件

hello.elf

readelf -a hello > hello.elf

hello.objdump

objdump -dr hello.o > hello.o.objdump

hello.o.elf

readelf -a hello.o > hello.o.elf

hello.o.objdump

objdump -dr hello > hello.objdump

1.4 本章小结

了解了hello.c的生命周期,从编写、预处理、编译、汇编、链接再到执行,体现了计算机系统系统各部分的具体功能,介绍了各个部分文件的基本作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:在编译之前进行的处理。预处理主要有三个方面的内容:宏定义,文件包含和条件编译。宏定义预处理(预编译)工作也叫做宏展开:将宏名替换为文本。即在对相关命令或语句的含义和功能作具体分析之前就要换。文件包含预处理变译时以包含处理以后的文件为编译单位,被包含的文件是源文件的一部分。编译以后只得到一个目标文件.obj,被包含的文件又被称为“标题文件”或“头部文件”、“头文件”,并且常用.h作扩展名。条件编译预处理使用条件编译可以使目标程序变小,运行时间变短。此外,还有布局控制:#pragma,这也是我们应用预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i


2.3 Hello的预处理结果解析

可见hello.i与hello.c相比,代码大大增多,而源程序hello.c中除注释和头文件部分位于hello.i的最后。

文件一共扩展为 3060 行,其中前面大部分是被包含的文件等等的内容(如 stdio.h )等的展开。

通过结果分析可以发现,预处理实现了在编译前对代码的初步处理,对源代码进行了某些转换。另外,如果代码中有#define命令还会对相应的符号进行替换。

2.4 本章小结

本章通过了解预处理的概念及作用,进行Ubuntu下预处理操作,同时解析了预处理文本文件内容,更直观的展现预处理的结果,为代码的下一步编译做好了准备。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译是利用编译程序从源语言编写的源程序产生目标程序的过程。用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:

  1. 扫描(词法分析):将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号。
  2. 语法分析:基于词法分析得到的一系列记号,生成语法树。
  3. 语义分析:由语义分析器完成,指示判断是否合法,并不判断对错。
  4. 源代码优化(中间语言生成):中间代码(语言)使得编译器分为前端和后端,前端产生与机器(或环境)无关的中间代码,编译器的后端将中间代码转换为目标机器代码。
  5. 代码生成,目标代码优化:编译器后端主要包括:代码生成器:依赖于目标机器,依赖目标机器的不同字长,寄存器,数据类型等。目标代码优化器:选择合适的寻址方式,左移右移代替乘除,删除多余指令。

3.2 在Ubuntu下编译的命令

gcc -S hello. i -o hello.s

3.3 Hello的编译结果解析

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

int main(int argc, char *argv[]) {

    int i;

    if (argc != 4) {

        printf(“用法: Hello 学号 姓名 秒数!\n”);

        exit(1);

    }

    for (i = 0; i < 8; i++) {

        printf(“Hello %s %s\n”, argv[1], argv[2]);

        sleep(atoi(argv[3]));

    }

    getchar();

    return 0;

}

3.3.1 函数调用

main 函数返回

将 $0 传送到 %eax

printf 多参数调用

如图所示,分别

sleep 函数调用


3.3.2 != 运算

3.3 for 循环(包含<. ++ 运算,局部变量 int i;)

3.4 本章小结

本阶段完成了对hello.i的编译工作。使用编译指令可以将其转换为.s汇编语言文件。完成该阶段转换后,可以进行下一阶段的汇编处理。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

把汇编语言翻译成机器语言的过程称为汇编。作用:汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式

.text

已编译程序的机器代码

.rodata

只读数据

.data

已初始化的全局和静态C变量

.bss

未初始化的全局和静态C遍历

.symtab

存放程序中定义和引用的函数和全局变量信息

.rel.text

一个.text节中位置的列表,链接时修改

.rel.data

被模块引用或定义的所有全局变量的重定位信息

.debug

条目是局部变量、类型定义、全局变量及C源文件

.line

C源程序中行号和.text节机器指令的映射

.strtab

.debug

4.3.1 ELF 头

ELF 头 以一个16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF 头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64) 、节头部表(section header table) 的文件偏移,以及节头部表中条目的大小和数量

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: 1160 (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: 13

Section header string table index: 12

4.3.2节头部表

节头部表描述了不同节的位置和大小。

其中[Nr]是节的序号,Name是节的名称,Type是节的类型,Address描述偏移的起始位置,可以看到,从0开始。off是偏移地址,也就是说,每一节都是从off所指的位置作为起始。Size则记录了每一节的大小,Flg是每一节的标识,记录了每一节的一些特性。可以查询下面Key to Flags部分查询每个符号的信息。

节头:

[号] 名称 类型 地址 偏移量

大小 全体大小 旗标 链接 信息 对齐

[ 0] NULL 0000000000000000 00000000

0000000000000000 0000000000000000 0 0 0

[ 1] .text PROGBITS 0000000000000000 00000040

000000000000008e 0000000000000000 AX 0 0 1

[ 2] .rela.text RELA 0000000000000000 00000348

00000000000000c0 0000000000000018 I 10 1 8

[ 3] .data PROGBITS 0000000000000000 000000ce

0000000000000000 0000000000000000 WA 0 0 1

[ 4] .bss NOBITS 0000000000000000 000000ce

0000000000000000 0000000000000000 WA 0 0 1

[ 5] .rodata PROGBITS 0000000000000000 000000d0

0000000000000033 0000000000000000 A 0 0 8

[ 6] .comment PROGBITS 0000000000000000 00000103

000000000000002b 0000000000000001 MS 0 0 1

[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000012e

0000000000000000 0000000000000000 0 0 1

[ 8] .eh_frame PROGBITS 0000000000000000 00000130

0000000000000038 0000000000000000 A 0 0 8

[ 9] .rela.eh_frame RELA 0000000000000000 00000408

0000000000000018 0000000000000018 I 10 8 8

[10] .symtab SYMTAB 0000000000000000 00000168

0000000000000198 0000000000000018 11 9 8

[11] .strtab STRTAB 0000000000000000 00000300

0000000000000048 0000000000000000 0 0 1

[12] .shstrtab STRTAB 0000000000000000 00000420

0000000000000061 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),

l (large), p (processor specific)

4.3.3重定位信息

重定位节 ‘.rela.text’ at offset 0x348 contains 8 entries:

偏移量 信息 类型 符号值 符号名称 + 加数

000000000018 000500000002 R_X86_64_PC32 0000000000000000 .rodata – 4

00000000001d 000b00000004 R_X86_64_PLT32 0000000000000000 puts – 4

000000000027 000c00000004 R_X86_64_PLT32 0000000000000000 exit – 4

000000000050 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 22

00000000005a 000d00000004 R_X86_64_PLT32 0000000000000000 printf – 4

00000000006d 000e00000004 R_X86_64_PLT32 0000000000000000 atoi – 4

000000000074 000f00000004 R_X86_64_PLT32 0000000000000000 sleep – 4

000000000083 001000000004 R_X86_64_PLT32 0000000000000000 getchar – 4

重定位节 ‘.rela.eh_frame’ at offset 0x408 contains 1 entry:

偏移量 信息 类型 符号值 符号名称 + 加数

000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.

4.3.4 .symtab

Symbol table ‘.symtab’ contains 17 entries:

Num: Value Size Type Bind Vis Ndx Name

0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND

1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.i

2: 0000000000000000 0 SECTION LOCAL DEFAULT 1

3: 0000000000000000 0 SECTION LOCAL DEFAULT 3

4: 0000000000000000 0 SECTION LOCAL DEFAULT 4

5: 0000000000000000 0 SECTION LOCAL DEFAULT 5

6: 0000000000000000 0 SECTION LOCAL DEFAULT 7

7: 0000000000000000 0 SECTION LOCAL DEFAULT 8

8: 0000000000000000 0 SECTION LOCAL DEFAULT 6

9: 0000000000000000 142 FUNC GLOBAL DEFAULT 1 main

10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_

11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts

12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit

13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf

14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi

15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep

16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar

No version information found in this file.

4.4 Hello.o的结果解析

objdump -d -r hello.o > hello_objdump.s

分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。

全局变量访问:在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。

4.5 本章小结

本章介绍了程序生成过程中编译器汇编的相关内容。汇编过程将汇编语言转换为机器代码,生成可重定位的目标文件,使机器能够直接处理与执行。可以通过readelf读取其elf信息与重定位信息,得到其符号表的相关信息。另外,可以通过objdump反汇编目标文件,从中可以得到机器代码与汇编代码的对照。作为机器可以直接执行的语言,机器语言与汇编语言存在映射关系,能够反映机器执行程序的逻辑。当然,生成最终的可执行文件还需要经过链接这一步。

(第4章1分)

第5章 链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。

作用:链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到存储器并执行时;甚至执行于运行时,由应用程序来执行。

5.2 在Ubuntu下链接的命令

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

5.3 可执行目标文件hello的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.4 hello的虚拟地址空间

 


在0x400000~0x401000段中,程序被载入,自虚拟地址0x400000开始,自0x400fff结束,这之间每个节(开始 ~ .eh_frame节)的排列即开始结束同上一页中Address中声明。

查看ELF格式文件中的Program Headers,程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。在下面可以看出,程序包含8个段:

PHDR保存程序头表。

INTERP指定在程序已经从可执行文件映射到内存之后,必须调用的解释器(如动态链接器)。

LOAD表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。

DYNAMIC保存了由动态链接器使用的信息。

NOTE保存辅助信息。

GNU_STACK:权限标志,标志栈是否是可执行的。

GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。

5.5 链接的重定位过程分析

使用objdump -dr hello > hello.objdump 获得hello的反汇编代码。

通过比较hello.objdump和helloo.objdump了解链接器。

1)函数个数:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。

2)函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。

3).rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。这里以计算第一条字符串相对地址为例说明计算相对地址的算法refptr = s + r.offset = Pointer to 0x40054A

refaddr = ADDR(s) + r.offset= ADDR(main)+r.offset=0x400532+0x18=0x40054A

*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr) = ADDR(str1)+r.addend-refaddr=0x400644+(-0x4)-0x40054A=(unsigned) 0xF6

5.6 hello的执行流程


1. 调用start函数,地址0x4010f0

2. 调用__libc_start_main函数,地址0x7f32e0c0ffc0

3. 调用libc-2.27.so! cxa_atexit ,地址0x7faf e65cef60

4. 调用libc-2.31.so! setjmp函数,地址0x7f2033be0e00

5. 调用libc-2.31.so! sigsetjmp函数,地址0x7f2033be0d30

6. 调用main函数,地址 0x401125

7. 调用printf函数,地址 0x4010a0

8. 调用atoi函数,地址 0x4010c0

9. 调用sleep函数,地址 0x4010e0

10. 调用getchar函数,地址 0x4010b0

11. 调用libc-2.27.so!exit函数,地址0x7f18d89eebc0

5.7 Hello的动态链接分析

在加载可执行文件时,加载器发现在可执行文件的程序头表中有.interp段,其中包含了动态连接器路径ld-linux.so . 加载器加载动态链接器,动态链接器完成相应的重定位工作后,再将控制权交给hello。

hello有自己的GOT 和PLT 。GOT 是数据段的一部分,而PLT 是代码段的一部分。

PLT 是一个数组,其中每个条目是16 字节代码。PLT[0] 是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT 条目。每个条目都负责调用一个具体的函数。下图是hello的PLT数组。

PLT数组

GOT 是一个数组,其中每个条目是8 字节地址。和PLT 联合使用时, GOT[O] 和GOT[l] 包含动态链接器在解析函数地址时会使用的信息。GOT[2] 是动态链接器在ld-linux.so 模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT 条目。

下面分析printf函数的动态链接。第一次调用printf时,程序进入PLT表,跳转到GOT表的对应条目。因为每个GOT 条目初始时都指向它对应的PLT 条目的第二条指令,这个间接跳转只是简单地把控制传送回PLT中的下一条指令。在把printf的ID压入栈之后,跳转到PLT[0]。再通过动态链接器重写对应printf的GOT条目信息,最后把控制传递给printf。

由图5-3-3动态链接库的重定位信息可知,printf的GOT条目地址为0x404020。在调用printf之前,其对应GOT条目信息为0x404010,指向PLT表。当调用printf后,对应条目修改为0x7fc29994ee10,为共享的printf函数的地址。

printf调用前GOT表

printf调用后GOT表

5.8 本章小结

本章通过解释链接的概念及作用,和分析hello的ELF格式,以及hello的虚拟导致空间,重定位过程,执行流程,和动态连接过程,深入学习了hello.o 可重定位文件到hello可执行文件的流程,和链接的各个过程。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

作用:在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

6.2 简述壳Shell-bash的作用与处理流程

1. 作用

shell 是一种交互式的应用程序,接收并解析用户命令,然后如果是内置命令,则自行处理,如果是外部命令,则调用相应的程序,即代表用户运行对应程序。

2. 处理流程

读取输入字符串;解析字符串;判断是否为内置命令

若为内置命令:运行

若非内置命令,fork自身,然后 execve 对应程序

6.3 Hello的fork进程创建过程

shell 进程作为父进程,调用 fork 生成新的子进程,父进程返回子进程 pid,子进程返回 0,然后通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本。

6.4 Hello的execve过程

根据 fork 的返回结果,可以判断是父进程还是子进程,子进程通过 execve 函数运行新的程序, execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。

6.5 Hello的进程执行

多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。

hello程序执行过程中同样存储时间分片,与操作系统的其他进行并发运行。并发执行涉及到操作系统内核采取的上下文交换策略。内核为每个进程维持一个上下文,上下文就是内核重新启动一个先前被抢占的进程所需的状态。

在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。

在此基础上,hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。

程序在涉及到一些操作时,例如调用一些系统函数,内核需要将当前状态从用户态切换到核心态,执行结束后再及时改用户态,从而保证系统的安全与稳定。

6.6 hello的异常与信号处理

hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。

中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。

陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。

故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。

终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。

hello执行过程中,可能会遇到各种异常,信号则是一种通知用户异常发送的机制。例如较为底层的硬件异常以及较高层的软件事件,比如Ctrl-Z和Ctrl-C,分别触发SIGCHLD和SIGINT信号。

收到信号后进程会调用相应的信号处理程序对其进行处理。



最左侧 systemd

乱按等(符号)会写入缓冲区,如果没有读取操作则无影响

Ctrl+Z 向程序发送 SIGTSTP 信号,该信号无法被阻塞,会使程序进入挂起状态。

Ctrl+C 向程序发送 SIGINT 信号,该信号可以被阻塞。被挂起后可以通过 fg bg 分别回到前台和后台运行。

kill -9 发送 SIGKILL 信号,强制终止程序。此信号无法被阻塞

后续运行过程中,在挂起的时候 ps 可以看到该进程,pstree 可以看到进程关系为 system – sshd – sshd – sshd – zsh – hello(通过 ssh 接入虚拟机,使用 shell 为 zsh),而 kill -9 {pid} 后,ps pstree 该进程消失。

6.7本章小结

本章介绍了程序在shell执行及进程的相关概念。程序在shell中执行是通过fork函数及execve创建新的进程并执行程序。进程拥有着与父进程相同却又独立的环境,与其他系统进并发执行,拥有各自的时间片,在内核的调度下有条不紊的执行着各自的指令。

程序运行中难免遇到异常,异常分为中断、陷阱、故障和终止四类,均有对应的处理方法。操作系统提供了信号这一机制,实现了异常的反馈。这样,程序能够对不同的信号调用信号处理子程序进行处理。

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

物理地址是用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。现代操作系统都提供了一种内存管理的抽像,即虚拟内存。进程使用虚拟内存中的地址,即虚拟地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。hello.s中使用的就是虚拟空间的虚拟地址。线性地址指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。而逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。它是Intel为了兼容,而将段式内存管理方式保留下来的产物。

逻辑(虚拟)地址经过分段(查询段表)转化为线性地址。线性地址经过分页(查询页表)转为物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

Intel处理器从逻辑地址到线性地址的变换通过段式管理,段寄存器对应着内存不同的段,有栈段寄存器(SS)、数据段寄存器(DS)、代码段寄存器(CS)和辅助段寄存器(ES/GS/FS)。

7.3 Hello的线性地址到物理地址的变换-页式管理

CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。

7.4 TLB与四级页表支持下的VA到PA的变换

如图给出了Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。36位的VPN划分为4个9位的片,每个片对应一个页表的偏移量。CR3寄存器存有L1页表的物理地址。VPN1提供一个到L1PET的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个到L2PET的偏移量,以此类推。

通过四级页表,我们可以查找到PPN,与VPO组合成PA,或者说VPO直接对应了PPO,它们组成PA并且向TLB中添加条目。

7.5 三级Cache支持下的物理内存访问

处理器对物理内存中数据的访问,同样需要经过缓存,即Cache,主流的处理器通常采用三级Cache。层与层之间按照以下原则进行读与写:

读取数据时,首先在高速缓存中查找所需字w的副本。如果命中,立即返回字w给CPU。如果不命中,从存储器层次结构中较低层次中取出包含字w的块,将这个块存储到某个高速缓存行中(可能会驱逐一个有效的行),然后返回字w。

下面具体三类Cache进行分析:

(1)直接映射高速缓存

直接映射高速缓存每个组只有一行,当CPU执行一条读内存字w的指令,它会向L1高速缓存请求这个字。如果L1高速缓存中有w的一个缓存副本,那么就会得到L1高速缓存命中,高速缓存会很快抽取出w,并将它返回给CPU。否则就是缓存不命中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求块最终从内存到达时,L1高速缓存将这个块存放在它的一个高速缓存行里,从被存储的块中抽取出字w,然后将它返回给CPU。确定是否命中然后抽取的过程分为三步:1)组选择;2)行匹配;3)字抽取。

2)组相联高速缓存

组相联高速缓存每个组内可以多于一个缓存行,总体逻辑类似于直接映射高速缓存,不同之处在于行匹配时每组有更多的行可以尝试匹配,遍历每一行。如果不命中,有空行时也就是冷不命中则直接存储在空行;如果没有空行也就是冲突不命中,则替换已有行,通常有LFU(最不常使用)、LRU(最近最少使用)两者替换策略。

3)全相联高速缓存

全相联高速缓存只有一个组,且这个组包含所有的高速缓存行(即E =

C/B)。对于全相联高速缓存,因为只有一个组,组选择变的十分简单。地址中不存在索引位,地址只被划分为一个标记位和一个块偏移。行匹配和字选择同组相联高速缓存。

写入数据时,假设我们要写一个已经缓存了的字w,在高速缓存中更新了它的w的副本之后,有两种方法来更新w在层次结构中紧接着低一层中的副本。分别是直写和写回:

(1)直写

立即将w的高速缓存块写回到紧挨着的低一层中。优点是简单,缺点则是每次写都会引起总线流量。其处理不命中的方法是非写分配,即避开高速缓存,直接将这个字写到低一层去。

(2)写回

尽可能地推迟更新,只有当替换算法要驱逐这个更新过的块时,才把它写到紧接着的低一层中。优点是能显著地减少总线流量,缺点是增加了复杂性,必须为每个高速缓存行增加一个额外的修改位,表明是否被修改过。写回处理不命中的方法是写分配,加载相应低一层中的块到高速缓存中,然后更新这个高速缓存块,利用了写的空间局部性,但会导致每次不命中都会有一个块从低一层传到高速缓存。

通过这样的Cache读写机制,实现了从CPU寄存器到L1高速缓存,再到L2高速缓存,再到L3高速缓存,再到物理内存的访问,有效的提高了CPU访问物理内存的速度。

7.6 hello进程fork时的内存映射

shell通过fork为hello创建新进程。当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给hello进程唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和样表的原样副本。它将两个进程中的每个页面都标记为只读,并将每个进程中的每个区域结构都标记为写时复制。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

  1. 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  2. 映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
  3. 映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理

缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。其处理流程遵循图 所示的故障处理流程。

缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆 (heap) 。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。

配器将堆视为一组不同大小的块 (block) 的集合来维护。每个块就是一个连续的虚拟内存片 (chunk) ,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它被应用所分配。一个已分配的块保持已分配状态,直到它被释放。

基本方法:这里指的基本方法应该是在合并块的时候使用到的方法,有三种分别是:首次适配,下一次适配和最佳适配。首次适配是从开始处往后搜索,下一次适配是从上一次适配发生处开始搜索,最佳适配依次检查所有块,性能要比首次适配和下一次适配都要高。

策略:分为隐式空闲链表和显示空闲链表。任何实际的分配器都需要一些数据结构,允许他来区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身。

隐式空闲链表:通过头部中的大小字段隐含的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。

显示空闲链表:因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构地指针可以存放在这些空闲块的主体里。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这就是Unix I/O接口。

8.2 简述Unix IO接口及其函数

read 和 write

最简单的读写函数

readn 和 writen

原子性读写操作

recvfrom 和 sendto

增加了目标地址和地址结构长度的参数

recv 和 send

允许从进程到内核传递标志

readv 和 writev

允许指定往其中输入数据或从其中输出数据的缓冲区

recvmsg 和 sendmsg

结合了其他IO函数的所有特性,并具备接受和发送辅助数据的能力

8.3 printf的实现分析

代码如下:

int printf(const char fmt, …) {

int i;

char buf[256];

va_list arg = (va_list)((char)(&fmt) + 4);

i = vsprintf(buf, fmt, arg);

write(buf, i);

return i;

}

(char*)(&fmt) + 4) 表示的是…可变参数中的第一个参数的地址。而vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从vsprintf生成显示信息,到write系统函数,直到陷阱系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

int getchar(void) {

char c;

return (read(0,&c,1)==1)?(unsigned char)c:EOF;

}

getchar函数通过调用read函数返回字符。其中read函数的第一个参数是描述符fd,0代表标准输入。第二个参数输入内容的指针,这里也就是字符c的地址,最后一个参数是1,代表读入一个字符,符号getchar函数读一个字符的设定。read函数的返回值是读入的字符数,如果为1说明读入成功,那么直接返回字符,否则说明读到了buf的最后。

read函数同样通过sys_call中断来调用内核中的系统函数。键盘中断处理子程序会接受按键扫描码并将其转换为ASCII码后保存在缓冲区。然后read函数调用的系统函数可以对缓冲区ASCII码进行读取,直到接受回车键返回。

这样,getchar函数通过read函数返回字符,实现了读取一个字符的功能。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。

(第8章1分)

结论

  1. 预处理,初步处理hello.c将外部库合并到hello.i文件中。
  2. 编译,将hello.i编译成hello.s
  3. 汇编,将hello.s汇编成hello.o
  4. 链接,将hello.o与可重定位目标文件以及动态链接库链接称为可执行程序hello
  5. 运行,在shell输入./hello 1170300110 lzy运行
  6. 创建子进程,shell调用fork
  7. 运行程序,shell调用execve
  8. 执行指令,CPU为hello分配时间片,hello在一个时间片中执行自己的逻辑控制流。
  9. 访问内存,MMU将虚拟内存映射成物理地址
  10. 动态内存申请,malloc
  11. 信号,如果遇到ctrl+c或ctrl+z,则分别停止、挂起
  12. 结束,shell父进程回收子进程。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.i

gcc -E 预处理之后展开的源文件

hello.s

gcc -S 编译后的汇编代码文件

hello.o

gcc -c 汇编之后的可重定位目标文件

非中间产物,以及 objdump 等文件未列举。(可见开头部分有列举)

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1] 深入理解计算机系统(原书第三版)

[2] [转]printf 函数实现的深入剖析 – Pianistx – 博客园 (cnblogs.com) https://www.cnblogs.com/pianist/p/3315801.html

[3] Unix文件IO相关函数 – 魏传柳 https://langzi989.github.io/2017/05/22/Unix%E6%96%87%E4%BB%B6IO%E7%9B%B8%E5%85%B3%E5%87%BD%E6%95%B0/

(参考文献0分,缺失 -1分)

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注