HIT 2024CSAPP大作业 程序人生-Hello’s P2P

摘  要

本文以简单的hello.c程序为例,介绍了Hello从被编写、预处理、编译、汇编和链接生成可执行文件,到其执行、加载,以及在运行过程中与计算机系统的MMU、TLB和页表、Cache和主存、I/O管理和信号处理的密切配合,最后进程终止和资源回收的完整的“一生”。

关键词:计算机系统;Helloc程序;预处理;编译;汇编;进程;存储;IO管理

目  录

第1章 概述................................................................................................................ - 4 -

1.1 Hello简介......................................................................................................... - 4 -

1.2 环境与工具........................................................................................................ - 4 -

1.3 中间结果............................................................................................................ - 4 -

1.4 本章小结............................................................................................................ - 4 -

第2章 预处理............................................................................................................ - 5 -

2.1 预处理的概念与作用........................................................................................ - 5 -

2.2在Ubuntu下预处理的命令............................................................................. - 5 -

2.3 Hello的预处理结果解析................................................................................. - 5 -

2.4 本章小结............................................................................................................ - 5 -

第3章 编译................................................................................................................ - 6 -

3.1 编译的概念与作用............................................................................................ - 6 -

3.2 在Ubuntu下编译的命令................................................................................ - 6 -

3.3 Hello的编译结果解析..................................................................................... - 6 -

3.4 本章小结............................................................................................................ - 6 -

第4章 汇编................................................................................................................ - 7 -

4.1 汇编的概念与作用............................................................................................ - 7 -

4.2 在Ubuntu下汇编的命令................................................................................ - 7 -

4.3 可重定位目标elf格式.................................................................................... - 7 -

4.4 Hello.o的结果解析.......................................................................................... - 7 -

4.5 本章小结............................................................................................................ - 7 -

第5章 链接................................................................................................................ - 8 -

5.1 链接的概念与作用............................................................................................ - 8 -

5.2 在Ubuntu下链接的命令................................................................................ - 8 -

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

5.4 hello的虚拟地址空间..................................................................................... - 8 -

5.5 链接的重定位过程分析.................................................................................... - 8 -

5.6 hello的执行流程............................................................................................. - 8 -

5.7 Hello的动态链接分析..................................................................................... - 8 -

5.8 本章小结............................................................................................................ - 9 -

第6章 hello进程管理....................................................................................... - 10 -

6.1 进程的概念与作用.......................................................................................... - 10 -

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

6.3 Hello的fork进程创建过程......................................................................... - 10 -

6.4 Hello的execve过程..................................................................................... - 10 -

6.5 Hello的进程执行........................................................................................... - 10 -

6.6 hello的异常与信号处理............................................................................... - 10 -

6.7本章小结.......................................................................................................... - 10 -

第7章 hello的存储管理................................................................................... - 11 -

7.1 hello的存储器地址空间................................................................................ - 11 -

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

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

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

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

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

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

7.8 缺页故障与缺页中断处理.............................................................................. - 11 -

7.9动态存储分配管理........................................................................................... - 11 -

7.10本章小结........................................................................................................ - 12 -

第8章 hello的IO管理.................................................................................... - 13 -

8.1 Linux的IO设备管理方法............................................................................. - 13 -

8.2 简述Unix IO接口及其函数.......................................................................... - 13 -

8.3 printf的实现分析........................................................................................... - 13 -

8.4 getchar的实现分析....................................................................................... - 13 -

8.5本章小结.......................................................................................................... - 13 -

结论............................................................................................................................ - 14 -

附件............................................................................................................................ - 15 -

参考文献.................................................................................................................... - 16 -

第1章 概述

1.1 Hello简介

1.1.1 P2P

P2P(From Program to Process),指hello.c文件从源程序(program)到计算机运行的进程(process)的过程。这一过程需要经历预处理(hello.i)、编译(hello.s)、汇编(hello.o)、链接,进而生成可执行文件hello;在Shell中输入./hello,Shell程序解析命令行并初始化环境变量,进而使用fork和execve创建与加载进程。

1.1.2 020

020(From Zero-0 to Zero-0),指程序从无到有,经历程序员编程、编译、链接等过程生成可执行文件,并通过Shell程序拥有自己的进程,最后在进程运行终止后资源被回收和释放,重新回到初始状态,从有到无。

1.2 环境与工具

1.2.1硬件环境

X64 CPU;3.2GHz;16G RAM;512G SSD

1.2.2软件环境

Windows11 64位;Vmware 17;Ubuntu 22.04 LTS 64位

1.2.3开发及调试工具

Visual Studio Code 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc

1.3 中间结果

hello.c

C语言程序的源文件

hello.i

预处理后的文本文件

hello.s

编译后的汇编文件

hello.o

汇编后的可重定位目标文件

hello

链接后的可执行目标文件

hello1.elf

hello.o的ELF文件

hello1.asm

hello.o的反汇编文件

hello2.elf

hello的ELF文件

hello2.asm

hello的反汇编文件

1.4 本章小结

本章主要介绍了Hello的一生(P2P、020过程),明确程序运行的软硬件环境和开发及调试工具,并列出生成的中间结果文件及其作用。

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。hello.c中第1行的#include <stdio.h>指令使预处理器读取系统头文件stdio.h的内容,并将它直接插入程序文本中,得到另一个C程序,通常以.i作为文件扩展名。

2.1.2预处理的作用

处理#define A B等宏定义指令,将程序中的所有A用B替换;处理#ifdef,#ifndef,#else,#elif,#endif等条件编译指令,过滤非必要代码;处理#include等头文件包含指令,将头文件中的宏和外部符号声明等定义加入目标文件中,以供编译程序对之进行处理;替换LINE、FILE等特殊符号。

预处理通过添加头文件,替换宏等处理,可以使源文件适应不同的计算机和操作系统环境的限制,提高灵活性。对于hello.c文件的预处理,gcc将stdio.h、unistd.h和stdlib.h文件中的代码包含进这段程序。

2.2在Ubuntu下预处理的命令

gcc -hello.c -o hello.i(或cpp hello.c > hello.i)

2.3 Hello的预处理结果解析

预处理结果保存在hello.i文件中,可以发现预处理过程添加了大量代码,源文件被扩展至3092行,其中原hello.c文件中的main函数代码位于hello.i文件末尾处,而前面为stdio.h、unistd.h和stdlib.h头文件的展开。

在main函数之前,预处理器读取stdio.h、unistd.h和stdlib.h文件中的内容,按照读入顺序递归地展开include包含文件,并    处理其中以“#”开头的语句(如#define替换宏)。

2.4 本章小结

本章主要介绍了预处理阶段的概念和作用,在Ubuntu对hello.c源文件进行预处理得到hello.i文件,并解析处理结果。

第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译是指从.i到.s即预处理后的文件到生成汇编语言程序的过程。编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,该程序包含函数main的定义。

3.1.2编译的作用

在编译阶段,gcc首先检查代码的规范性、是否有语法或语义错误等,并在检查无误后将C代码翻译成等价的汇编语言。

同时,编译器会对代码进行优化,以提高程序的执行效率和性能,如代码移动、简化复杂运算、共享共用子表达式、循环展开、减少过程调用和消除不必要的内存引用等。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

编译结果保存在hello.s文件中,可以发现汇编代码为85行

3.3.1常量

字符串:如图5-8行为两个字符串常量,对应C代码中的"用法: Hello 学号 姓名 手机号 秒数!\n"和"Hello %s %s %s\n",用.string声明并用 .LC0/.LC1标记。

3.3.2变量(全局/局部/静态)

无全局变量和静态变量,局部变量一般通过栈指针(rsp)和基址指针(rbp)访问,汇编代码中局部变量如下:

(1)main函数传参argc和argv:21-23行首先通过栈指针rsp为局部变量argc和argv申请了32字节空间,然后在rbp-20处存储edi(即argc的值),在rbp-32处存储rsi(即argv的值),可通过rbp寻址。

(2)循环计数变量i:32行初始化变量i为0,存储在rbp-4处,通过rbp寻址,56行为每次循环最后i加1,58-59行进行循环终止条件判断,若i小于10则循环继续。

3.3.3类型

(1)int类型:如argc和i,字段长度为4个字节,通过rbp与4的整数倍偏移量的运算寻址。

(2)char*类型(字符串首地址/指针):如argv,64位下字段长度为8个字节,通过rbp与8的整数倍偏移量的运算寻址。

3.3.4算术操作

i++:等价于“i = i + 1”,使用addl指令,将rbp-4(即i的值)加1。

3.3.5关系操作

(1)argc != 5:使用cmpl指令,比较5和rbp-20(即argc的值),若相等则进入循环,否则执行if语句内的代码。

(2)i < 10:使用cmpl指令,比较9和rbp-4(即i的值),若i小于等于9则循环继续。

3.3.6数组/指针/结构操作

argv[]:字符串数组,每个元素的字段长度为8个字节(地址),通过8的整数倍偏移量寻址,如rbp-32为argv[0],rcx=(rax)=(rbp-32+24)=(rbp-8)存储argv[3],同理rdx存储argv[2],rax存储argv[1]。

3.3.7控制转移

(1)if (argc != 5)条件分支:比较指令cmpl和跳转指令je相结合,若argc等于5则跳转至.L2跳过if语句,否则执行if语句内代码。

(2)for (i = 0; i < 10; i++)循环分支:32行初始化变量i为0,56行为每次循环最后i加1,58-59行进行循环终止条件判断,若i小于10则循环继续。

3.3.8函数操作

(1)main函数:10-12行声明main函数为全局函数并调用,22-23行通过edi和rsi传参(argc和argv),其内定义了局部变量i,62-64行函数返回0。

(2)printf(puts)函数:

第一个printf函数:传参为rdi=.LC0(rip),即.LC0的内容"用法: Hello 学号 姓名 手机号 秒数!\n",参数为字符串常量,故28行调用puts函数输出结果。

第二个printf函数:35-46行传参分别为rdi=.LC1(rip)为.LC1的内容"Hello %s %s %s\n",rsi=(rax)=(rbp-32+8)=(rbp-24)即argv[1],rdx=(rax)=(rbp-32+16)=(rbp-16)即agrv[2],rcx=(rax)=(rbp-32+24)=(rbp-8)即argv[3],48行调用printf函数,无返回值。

(3)exit函数:传参为edi=1,之后调用exit函数终止进程并退出程序,无返回值,exit(1)表示非正常运行而异常退出,原因是argc≠5,即用户传参数量错误。

(4)atoi函数:49-52行传参为rdi=rax=(rbp-32+32)=(rbp),即argv[4],53行调用atoi函数,54行是函数返回值。

(5)sleep函数:54行传参edi为调用atoi(argv[4])的返回值,55行调用sleep函数,使程序等待一段时间,无返回值。

(6)getchar函数:直接调用getchar函数,无返回值。

3.4 本章小结

本章主要介绍了编译阶段的概念和作用,在Ubuntu对预处理得到的hello.i文件进行编译得到hello.s汇编文件,并通过阅读汇编代码分析编译器如何处理C语言的各类数据与操作。

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编是指从.s到.o即编译后的文件到生成机器语言二进制程序的过程。汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序,并将结果保存在目标文件hello.o中。hello.o是一个二进制文件,包含main函数的指令编码。

4.1.2汇编的作用

汇编阶段生成的可重定位文件由代码段、数据段、符号表、重定位表和其他元数据组成,适合与其它目标文件链接而创建一个可执行的或共享的目标文件。

可重定位文件使得程序可以按模块的方式进行开发和管理,提高了代码的可维护性和复用性;通过将目标文件打包成库文件,可以在多个项目中共享和复用代码,提高了开发效率;可重定位目标文件中包含的调试信息可以帮助开发者快速定位问题并优化程序。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o(或as -o hello.o hello.s)

4.3 可重定位目标elf格式

ELF(Executable and Linkable Format)文件是一种常见的可执行文件和可链接文件格式,用于在Unix和类Unix系统上存储程序、库和其他相关数据,包括文件头、程序头表、节表、节、重定位表、符号表。

使用readelf -a hello.o > hello1.elf指令获得hello.o的ELF格式文件hello1.elf。

4.3.1 ELF文件头

·Magic:16字节的十六进制序列,用于识别文件格式,并提供字节序等信息,“7f 45 4c 46”是ELF文件标识符;

·类别:“ELF64”表示这是一个64位的ELF文件;

·数据:“2 补码,小端序 (little endian)”表示数据存储为小端模式;

·Version:文件版本号(目前为1);

·OS/ABI:“UNIX - System V” 表示该ELF文件是为UNIX System V ABI(应用程序二进制接口)设计的;

·ABI版本:ABI版本为0;

·类型:目标文件类型,REL(Relocatable file),可重定位文件,可通过链接器创建可执行文件或共享库;

·系统架构:“Advanced Micro Devices X86-64”表示这个文件是为AMD的64位x86架构(即x86_64或AMD64)设计的;

·入口点地址:可重定位文件无执行入口点,故入口点地址通常为0;

·程序头起点:程序头用于描述可执行段、加载段等,可重定位文件无程序头,故程序头起点为0;

·Start of section headers:节头用于描述各节的特性,如大小、类型、名称等,“1088 (bytes into file) ”表示其在文件中的偏移量是1088字节;

·标志:“0x0”表示没有设置任何特殊的ELF文件标志;

·Size of this header:ELF头大小为64字节;

·Size of program headers:程序头大小为0;

·Number of program headers:程序头数量为0;

·Size of section headers:节头大小为64字节;

·Number of section headers:节头数量为14个;

·Section header string table index:节头字符串表(存储节区名)的索引是13。

4.3.2节头

section header用来描述各个section(例如.text、.rela.text、.data、.bss、.rodata等)的特性,如大小、类型、名称等。节头有14个条目,表示该文件有14个section。

4.3.3重定位节

重定位节包含了链接器调整代码和数据的地址所需的信息。

(1).rela.text:.text节的重定位信息,包含8个条目;

(2).rela.eh_frame:.eh_frame节的重定位信息,包含1个条目。

4.3.4符号表(Symbol table)

即.symtab表,包含11个条目,用于存储在程序中定义或引用的各种符号以及与这些符号相关的信息,包括符号的名称、类型、绑定属性、所在节的索引等。

4.3.5 GNU属性注释:

.note.gnu.property节:GNU特有节,包含一些描述程序属性的信息,例如程序的代码模型、堆栈大小限制等。这些信息可以被链接器、调试器等工具用来进行优化和调试。

4.4 Hello.o的结果解析

使用objdump -d hello.o > hello1.asm指令生成hello.o的反汇编文件hello1.asm(或使用objdump -d -r hello.o指令分析hello.o的反汇编),并与hello.s对照分析。

4.4.1汇编指令对比

对比hello.s和hello1.asm,可以发现每条汇编指令前添加了一个十六进制序列串,为该汇编指令对应的机器语言指令(如ret对应c3),同时在前方添加了确切地址和相对偏移。

4.4.2操作数

所有立即数和参与运算的操作数均从十进制被转换成十六进制,如$32变为$0x20。

4.4.3分支转移

hello.s中跳转指令操作数为段名称,而hello1.asm中为相对于main函数的偏移量地址,以if (argc != 5)条件分支为例,跳转指令的操作数由段名称(.L2)变为确切地址(32 <main+0x32>)。

4.4.4函数调用

hello.s中函数调用采用函数名称,而hello1.asm中采用相对于main函数的偏移量确定函数地址,如call指令调用函数,由函数名(atoi/sleep)变为确切地址(86 <main+0x86/8d <main+0x8d>,重定位信息)。

4.5 本章小结

本章主要介绍了汇编的概念和作用,在Ubuntu对编译得到的hello.s文件进行汇编得到可重定位文件hello.o,然后分析hello.o的ELF格式文件hello1.elf各节的基本信息,最后通过与hello.s对照分析hello.o的反汇编文件hello1.asm,讨论机器语言的构成及其与汇编语言的映射关系。

5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

链接是指从hello.o到hello的生成过程。以printf函数为例,它是C编译器提供的标准C库中的函数,存在于一个名为printf.o的单独的已预编译完的目标文件中,该文件必须合并到hello.o程序中,而链接器(ld)负责处理这种合并,并生成一个可执行目标文件hello,可以被加载到内存中由系统执行。

5.1.2链接的作用

链接器在软件开发中扮演着重要角色,它们使得分离编译成为可能。通过将一个大型的应用程序组织分解为更小、更好管理的模块,可以独立地修改和编译这些模块,而无需将其组织为一个巨大的源文件。当修改其中一个模块时,只需简单地重新编译它,并重新连接应用,而不必重新编译其他文件。

在程序方面,它的功能包括符号解析、符号重定位、目标文件合并、库文件链接、地址空间分配等内容,最终生成可执行文件或共享库。

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的格式

使用readelf -a hello.o > hello2.elf指令获得hello的ELF格式文件hello2.elf。

5.3.1 ELF文件头

·Magic:16字节的十六进制序列,用于识别文件格式,并提供字节序等信息,“7f 45 4c 46”是ELF文件标识符;

·类别:“ELF64”表示这是一个64位的ELF文件;

·数据:“2 补码,小端序 (little endian)”表示数据存储为小端模式;

·Version:文件版本号(目前为1);

·OS/ABI:“UNIX - System V” 表示该ELF文件是为UNIX System V ABI(应用程序二进制接口)设计的;

·ABI版本:ABI版本为0;

·类型:目标文件类型,EXEC,可执行文件,可被加载到内存中由系统执行;

·系统架构:“Advanced Micro Devices X86-64”表示这个文件是为AMD的64位x86架构(即x86_64或AMD64)设计的;

·入口点地址:程序的入口点地址为0x4010f0,当操作系统加载并运行这个文件时,它会从该地址开始执行;

·程序头起点:“64 (bytes into file)”表示程序头在文件中的偏移量是64字节;

·Start of section headers:节头用于描述各节的特性,如大小、类型、名称等,“13560 (bytes into file)”表示其在文件中的偏移量是13560字节;

·标志:“0x0”表示没有设置任何特殊的ELF文件标志;

·Size of this header:ELF头大小为64字节;

·Size of program headers:程序头大小56字节;

·Number of program headers:程序头数量为12个;

·Size of section headers:节头大小为64字节;

·Number of section headers:节头数量为27个;

·Section header string table index:节头字符串表(存储节区名)的索引是26。

5.3.2节头

section header用来描述各个section(例如.init、.text、.data、.rodata、.symtab等,不包含.text和.data的可重定位信息节)的特性,如大小、类型、名称等。节头有27个条目,表示该文件有27个section。

5.3.3程序头

程序头是一个结构数组,描述各节到虚拟空间存储块的映射关系,即可执行文件在内存中的布局和加载信息。每个程序头条目描述一个段(Segment)的相关信息。

5.3.4段节(Section to Segment mapping)

节(sections)到段(segments)的映射关系,段节显示了哪些节被包含在每个段中,每个段都有一个编号,从 00 开始。

5.3.5动态节

包含程序的动态链接信息,即动态链接器在加载可执行文件或共享库时所需信息,每个条目包含一个tag标记、类型和相应名称/值。

5.3.6重定位节

重定位节包含了链接器调整代码和数据的地址所需的信息。

(1).rela.dyn:.dyn节的重定位信息,包含2个条目;

(2).rela.plt:.plt节的重定位信息,包含6个条目。

5.3.7符号表(Symbol table)

用于存储在程序中定义或引用的各种符号以及与这些符号相关的信息,包括符号的名称、类型、绑定属性、所在节的索引等。

(1).dynsym表:动态符号表,包含了动态链接中使用的符号,9个条目;

(2).symtab表:符号表,包含了程序中的所有符号,26个条目。

5.3.8桶列表长度直方图(Histogram for bucket list length)

动态链接器用来加速符号查找(定位动态符号表.dynsym中的符号)的哈希桶列表的长度分布。

5.3.9 GNU属性注释:

.note.gnu.property节:GNU特有节,包含一些描述程序属性的信息,例如程序的代码模型、堆栈大小限制等。这些信息可以被链接器、调试器等工具用来进行优化和调试。

5.3.10 ABI 标签注释

.note.ABI.tag节:包含应用程序二进制接口(ABI)的版本信息。

5.4 hello的虚拟地址空间

通过edb可查看到程序起始点为0x401000,本进程被载入至虚拟地址0x401000~0x402000。

对照5.3部分的节头表,可以查找各段起始地址,如.text段位于0x4010f0处。

5.5 链接的重定位过程分析

使用objdump -d hello > hello2.asm指令生成hello的反汇编文件hello2.asm(或使用objdump -d -r hello指令分析hello的反汇编),并与hello1.asm对照分析。

hello反汇编文件hello2.asm中,每行指令都有唯一的虚拟地址,而hello.o的反汇编文件hello1.asm没有,只是相对于代码段(通常是.text 段)的偏移地址。这是因为hello.o只是一个中间产物,还没有被链接到最终的内存地址空间,而hello经过链接,已经完成重定位,每条指令分配了唯一的虚拟地址,每条指令的地址关系已经确定。

5.5.1函数调用

函数增多,添加了包括源程序调用的库函数等多个函数,如_init、.plt、puts@plt、printf@plt、getchar@plt、atoi@plt、exit@plt、sleep@plt等函数,这是因为链接器将共享库中hello.c调用的库函数加入了可执行文件中。

函数调用地址发生变化,call指令操作数改变,并且十六进制机器代码中的操作数位置被链接器变为目标地址与下一条指令地址之差,从而得到完整的代码。

5.5.2跳转指令

与函数调用的call指令类似,jmp跳转指令在hello1.asm是相对main函数的偏移量,而hello2.asm是确切完整地址。

5.5.3重定位步骤

(1)重定位节和符号定义:链接器将输入目标文件的相同节合并成一个节,合并的节将作为可执行目标文件中此类型的节。随后,链接器确定每个合并节的运行时内存地址,并确定合并节中符号定义的运行时内存地址。

(2)重定位节中的符号引用:链接器修改所有的符号引用,使之指向符号定义的运行时内存地址。链接器要执行此步骤依赖于目标文件中的重定位信息。

5.6 Hello的执行流程

·开始:_start、_libe_start_main

·main执行:_main、printf、_exit、_sleep、getchar

·退出:exit

程序名

程序地址

<_init>

0000000000401000

<.plt>

0000000000401020

<puts@plt>

0000000000401090

<printf@plt>

00000000004010a0

<getchar@plt>

00000000004010b0

<atoi@plt>

00000000004010c0

<exit@plt>

00000000004010d0

<sleep@plt>

00000000004010e0

<_start>

00000000004010f0

<main>

0000000000401125

<_fini>

0000000000401238

5.7 Hello的动态链接分析

动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用 GOT 中地址跳转到目标函数。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。

.got.plt节保存着全局偏移量表GOT,其内容从地址0x404000开始。

使用gdb调试,查看未调用_init时0x404000处信息:

调用_init之后:

5.8 本章小结

本章主要介绍了链接的概念和作用,在Ubuntu对汇编得到的hello.o文件进行链接得到可执行文件hello,然后分析hello的ELF格式文件hello2.elf各节的基本信息,之后通过与hello1.asm对照分析hello的反汇编文件hello2.asm,得出连接的重定位过程,最后通过edb和gdb调试,分析Hello的执行流程和动态链接过程。

6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

进程是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合

6.1.2进程的作用

 在现代系统上运行一个程序时,会发生一个假象——程序时系统中当前运行的唯一的程序,独占地使用处理器和内存,处理器无间断地一条接一条的执行程序中的指令,程序中的代码和数据好像是系统内存中唯一的对象。这些假象便是进程提供的。

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

6.2.2壳Shell-bash的作用

Shell是一个交互型应用级程序,也被称为命令解析器,它为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。

6.2.3壳Shell-bash的处理流程

首先从终端读入用户输入的命令,对输入的命令进行解析,若该命令为Shell内置命令,则立即执行命令,否则调用fork创建一个新的子进程,在该子进程的上下文中执行指定的程序。判断该程序为前台程序还是后台程序,如果为前台程序则等待程序执行结束,若为后台程序则将其放回后台并返回。在过程中shell可以接受从键盘输入的信号并对其进行处理。

6.3 Hello的fork进程创建过程

首先,Shell带参执行当前目录下的可执行文件 hello,父进程会通过 fork 函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的 PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由 init 进程回收子进程。fork函数调用1次,返回2次。

6.4 Hello的execve过程

在子进程中,操作系统会调用 execve函数,将 hello 程序加载到子进程的地址空间中。execve系统调用需要提供三个参数:可执行文件的路径、命令行参数数组和环境变量数组。逻辑控制流交给要运行的程序,即操作系统根据路径加载 hello 程序的可执行文件到子进程的地址空间中。execve函数运行成功时不返回。

6.5 Hello的进程执行

上下文信息:描述进程当前状态的全套信息,包括各种寄存器的值、程序的指令和数据、打开的文件描述符等。这些信息在进程切换时需要被保存和恢复,以确保进程在重新获得CPU时能够继续执行。

进程时间片:CPU分配给各个程序的时间。每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。

6.5.1进程调度

在进程调度过程中,操作系统会先检查就绪队列中是否有进程等待执行,如果有,操作系统会根据一定的调度算法(如先来先服务、短作业优先、优先级调度等)选择一个进程进行执行。在选择进程时,操作系统会考虑进程的优先级、等待时间、资源需求等因素。

当进程获得CPU资源并开始执行时,它的上下文信息会被加载到CPU中,包括各种寄存器的值、程序的指令和数据等。进程会在用户态下执行,此时它只能访问受限的内存空间和资源。如果进程需要执行某些需要高权限的操作(如访问系统文件、调用系统服务等),它会通过系统调用或异常等机制陷入内核态。

6.5.2用户态与核心态转换

用户态和核心态(内核态)之间的转换主要通过以下方式进行:

·系统调用:用户态进程通过系统调用主动请求切换到内核态,以使用操作系统提供的服务。

·异常:当CPU执行用户态下的程序时,如果发生异常(如非法指令、缺页异常等),会触发异常处理程序,将进程从用户态切换到内核态。

·中断:当外部设备(如键盘、鼠标等)完成用户请求的操作后,会向CPU发出中断信号,导致CPU暂停当前进程的执行,并切换到核心态处理中断。

在内核态下,操作系统会保存当前进程的上下文信息(包括寄存器值、程序指令和数据等),然后根据需要执行相应的操作(如处理系统调用、处理异常或中断等)。操作完成后,操作系统会恢复被中断进程的上下文信息,并将其切换回用户态继续执行。

6.6 hello的异常与信号处理

(1)正常运行:在Shell终端输入./hello 2022113416 liuzikang 15620490510 0,执行hello程序。

*由于sleep秒数为0,不便于发送信号,故下述操作将秒数改为1。

(2)运行时随机输入字符和回车:程序继续运行,未受影响。

(3)输入Ctrl-C:程序终止并退出。会发送SIGINT信号,向子进程发送SIGKILL信号使进程终止并回收。

(4)输入Ctrl-Z:程序停止并显示暂停的进程。会产生SIGTSTP信号,使子进程被暂时挂起。

①ps命令:显示所有的进程及其状态,hello未终止。

②jobs命令:显示暂停的进程,发现hello被挂起。

③pstree命令:通过进程树显示所有进程的情况。

④fg命令:使第一个后台作业变成前台作业,这里hello是第一个后台作业,所以变为前台执行。

⑤kill命令:结合ps,输入“kill -9 13066”,杀死hello进程。

6.7本章小结

本章主要介绍了进程的概念和作用,壳Shell的作用和处理流程,Hello的fork进程创建过程、execve进程加载过程和进程执行过程,并在Ubunt下执行hello程序,对hello的异常与信号处理进行分析。

7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

机器语言指令中用来指定一个操作数或一条指令的地址。hello.c经过P2P过程,其包含的代码和数据会被组织成不同的段(如代码段、数据段等),每个段内的地址偏移量即为逻辑地址。

逻辑地址通常由两个部分组成:段基值和偏移量,段基值确定了该段在内存中的起始地址,而偏移量则确定了数据或指令在段内的具体位置。

7.1.2线性地址

线性地址是逻辑地址到物理地址变换之间的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。在hello程序的上下文中,当链接器将多个目标文件组合成一个可执行文件时,它会根据目标文件的段信息和链接脚本生成可执行文件的段布局,并为每个段分配一个线性地址范围。

线性地址是连续的,不受分段机制的影响。在程序运行时,处理器通过访问线性地址来访问内存中的数据或指令。

7.1.3虚拟地址

在Windows等操作系统中,为了保护系统安全和实现多任务处理,操作系统采用了虚拟内存管理机制。当程序运行时,操作系统会为每个进程分配一个独立的虚拟地址空间,并将程序中的逻辑地址转换为虚拟地址。

虚拟地址与物理地址之间的映射关系由操作系统的内存管理单元(MMU)维护。当处理器访问虚拟地址时,MMU会将其转换为对应的物理地址,然后访问内存中的数据或指令。

对于hello程序来说,它所使用的地址都是虚拟地址。当程序被加载到内存中并执行时,操作系统会负责将程序中的逻辑地址转换为虚拟地址,并维护虚拟地址与物理地址之间的映射关系。

7.1.3物理地址

物理地址是内存中物理单元的集合,是地址转换的最终地址,用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。处理器在执行指令和访问数据时,最终都是通过物理地址来存取主存的。物理地址是以字节为单位进行编址的,它是无符号整数,通常用十六进制数表示。在32位系统中,物理地址空间大小为4GB(即2^32字节);在64位系统中,物理地址空间大小则更大。

在hello程序的上下文中,当处理器访问虚拟地址时,操作系统的内存管理单元(MMU)会将其转换为物理地址,然后处理器通过物理地址来访问内存中的数据或指令。

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

段式管理,是一种将内存地址空间划分为多个段的管理方式,每个段可以具有不同的大小和属性。段式管理是通过段表进行的,包括段号(段名),段起点,装入位,段的长度等。程序通过分段划分为多个块,如代码段、数据段、共享段、等。

7.2.1检查段选择符

首先需要检查逻辑地址中的段选择符的TI字段,这个字段决定了段描述符是保存在全局描述符表还是局部描述符表中。

7.2.2计算段描述符地址

从段选择符的index字段计算段描述符的地址。index字段的值乘以描述符大小(在Intel架构中,描述符通常是8字节,即64位),然后将该结果与gdtr(全局描述符表寄存器)或ldtr(局部描述符表寄存器)的内容相加,可以找到对应的段描述符在内存中的位置。

7.2.3获取线性地址

将逻辑地址中的偏移量与段描述符的base字段的值相加,得到线性地址。这个base字段描述了段的开始位置的线性地址。

在这个过程中,段描述符的大小是固定的(通常是8字节),并且gdtr或ldtr寄存器的内容包含了描述符表的基地址。逻辑地址由段选择符和偏移量组成,段选择符用于定位描述符表(GDT或LDT)中的段描述符,偏移量则表示在段内的相对位置。线性地址是逻辑地址到物理地址转换过程中的一个中间步骤,是处理器直接寻址的地址空间中的地址。

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

页式管理,是将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

7.3.1创建页表

系统为每个进程维护一个页表,页表是内存中的一块固定存储区,用于记录虚拟页面与物理页面的映射关系。页表中的每个条目(PTE)对应一个物理页,由页表有效位和物理页号(PPN)。

7.3.2地址翻译

当CPU要访问某个线性地址时,会首先检查该地址对应虚拟页面是否在存储器中,若是则通过查找页表中对应的PTE,否则进行缺页处理。若页表有效位为1则获取到物理页号,再加上物理页偏移量就可以得到物理地址。

7.3.3 TLB加速

为了加速地址翻译过程,现代CPU通常包含一个TLB的硬件结构。TLB是一个高速缓存,用于缓存最近使用过的虚拟页面到物理页面的映射关系。当CPU访问某个线性地址时,会首先检查TLB中是否有对应的映射关系,若有则直接使用该映射关系计算物理地址,从而避免了访问内存中的页表。

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

Core i7采用四级页表的层次结构。当CPU需要访问某个虚拟地址时,首先会利用MMU在TLB中查找对应的PTE,若TLB命中,则直接使用PTE中的物理地址访问内存,无需进一步查找页表,从而提高了访问速度;若TLB未命中,则需要通过页表查找来获取PTE,然后将PTE加载到TLB中以备后续使用。

四级页表会将虚拟地址的VPN部分分为4部分,每部分对应一级页表,MMU会依次查询每一级页表,最后查询得到物理地址的偏移量PPN,和虚拟地址的低p位拼接得到完整的物理地址。

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

当CPU需要访问某个虚拟地址时,经过上述过程得到物理地址,分为CT(cache标记),CI(cache组索引)和CO(cache块偏移)三部分。

首先检查该地址是否在L1 Cache中,若L1 Cache命中(组索引找到组,有效位为1,标记位正确),则CPU直接从L1 Cache中读取数据;若L1 Cache不命中,则依次前往下级Cache直至主存判断,最终取出数据,同时根据替换策略将数据换入不命中的Cache。

7.6 hello进程fork时的内存映射

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

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数加载并运行hello需要以下几个步骤:

(1)删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。

(2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的. text和. data区;bss区域是请求二进制零的,映射到匿名文件,其大小包含在 a.out 中;栈和堆区域也是请求二进制零的,初始长度为零。

(3)映射共享区域:如果hello程序与共享对象(或目标)链接,例如标准 C 库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器(PC):execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

若CPU访问的线性地址对应的逻辑页面尚未装入内存,即发生缺页故障,则会触发缺页中断,调用缺页处理程序。缺页处理程序会选择一个牺牲页(若内存中没有空闲的页面,则需要采用页面置换算法选择一个页面框进行置换),并将所需的虚拟页面从磁盘等辅存中调入,然后更新页表以反映新的虚拟页面到物理页面的映射关系,并重新执行导致缺页中断的指令。

7.9动态存储分配管理

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

分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,已分配或是空闲的,已分配的块显式地保留为供应用程序使用,空闲块可以用来分配。一个已分配的块保持已分配状态,直到它被释放;一个空闲块保持空闲状态,直到它显式地被应用所分配。这种释放或是应用程序显式执行的,或是内存分配器自身隐式执行的。

7.9.1内存分配

(1)静态内存分配:在编译时预先分配固定的内存空间,适用于已知且不变的数据结构。

(2)动态内存分配:在运行时根据需要动态地分配和释放内存空间,是动态内存管理的核心,有以下两种方式。

①堆内存分配:通过调用系统提供的动态内存分配函数(如malloc、calloc、realloc等)来分配和释放内存。这些函数允许程序在运行时请求任意大小的内存块。

②栈内存分配:将变量存储在栈上,由系统自动管理其生命周期。局部变量通常在栈上分配,当函数返回时,栈上的内存会自动释放。

7.9.2内存回收

(1)标记清除法:标记出活跃对象,清除未被标记的对象

(2)复制法:将活跃对象复制到另一块内存区域,清除原始内存区域,在某些情况下可以提高效率,但可能会增加内存开销。

(3)分代收集法:根据对象的生命周期将内存划分为不同的代,不同代采用不同的回收策略,可以更高效地处理具有不同生命周期的对象。

7.10本章小结

本章主要介绍了hello的存储器地址空间,Intel的段式管理和hello的页式管理,以及进程中的execve的内存映射、缺页处理、动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.1.1设备的模型化

所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。

8.1.2设备管理

Linux内核有一个简单、低级的接口,称为Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

8.2.1 Unix IO接口

Unix I/O接口是Unix操作系统提供的一种用于文件和设备输入/输出(Input/Output)的接口。它允许用户程序与文件、管道、终端和网络设备等进行交互。

8.2.2 Unix IO函数

(1)打开文件(open)

·函数原型:int open(const char *pathname, int flags, ...);

·功能:打开一个文件,并返回一个文件描述符(file descriptor),用于后续的文件操作。

·参数:pathname—要打开的文件路径名;flags—打开文件的模式标志,如O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)等。

·可选参数:当flags中包含O_CREAT时,指定文件的访问权限(如0644表示文件所有者可读写,组用户和其他用户可读)。

(2)关闭文件(close)

·函数原型:int close(int fd);

·功能:关闭已打开的文件,释放与该文件描述符相关联的所有资源。

·参数:fd为要关闭的文件描述符。

(3)读取文件(read)

·函数原型:ssize_t read(int fd, void *buf, size_t count);

·功能:从文件中读取数据到内存缓冲区。

·参数:fd—要读取的文件描述符;buf—指向内存缓冲区的指针,用于存储读取的数据;count—要读取的字节数。

(4)写入文件(write)

·函数原型:ssize_t write(int fd, const void *buf, size_t count);

·功能:将内存缓冲区中的数据写入文件。

·参数:fd—要写入的文件描述符;buf—指向内存缓冲区的指针,包含要写入的数据;count—要写入的字节数。

(5)文件定位(lseek)

·函数原型:off_t lseek(int fd, off_t offset, int whence);

·功能:设置文件的读写位置。

·参数:fd—文件描述符;offset—相对于whence的偏移量;whence—定位基准,如SEEK_SET(从文件开头)、SEEK_CUR(从当前位置)、SEEK_END(从文件结尾)。

8.3 printf的实现分析

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;

    }

·vsprintf:作用是格式化。它接收确定输出格式的格式化字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从vsprintf生成显示信息到write系统函数,到陷阱-系统调用 int 0x80或syscall等。

·write:把buf中的i个元素的值写到终端。

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar函数包含在头文件<stdio.h>中,实际上调用的是Unix/IO函数getc:

/* Read a character from stdin.  */

__STDIO_INLINE int

getchar (void)

{

  return getc (stdin);

}

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了Linux系统的IO设备管理和Unix I/O接口,并对printf和getchar两个库函数的实现进行分析。

结论

hello经历的过程:

·编程:程序员使用文本编辑器编写C代码文件hello.c。

·预处理(cpp):预处理器展开hello.c中所有#include包含指令,替换宏定义,生成hello.i。

·编译(cc1):编译器将预处理得到的hello.i文件转换为汇编文件hello.s。

·汇编(as):汇编器将编译得到的hello.s文件转换为可重定位文件hello.o。

·链接(ld):链接器将汇编得到的hello.o文件与其他所需库和目标文件进行链接,生成可执行文件hello。

·运行:在Shell中输入./hello命令,程序开始运行。

·创建进程(fork):调用fork函数创建一个新/子进程。

·加载进程(execve):调用execve函数加载hello进程的代码和数据到子进程的虚拟内存空间。

·访存:MMU将虚拟地址翻译为物理地址,并以此访问存储空间。

·信号管理:操作系统调用信号处理函数处理用户发送的Ctrl-C或Ctrl-Z等信号。

·进程终止:子进程执行完毕,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为进程创建的所有数据结构,并释放内存。

通过学习hello程序从无到有再到无的完整过程,结合课程中多章的知识点,对程序在现代计算机内的实现、运行和回收过程有了更深入的理解,对计算机系统的整体框架有了更清晰的认知,认识到计算机系统的设计与实现的重要性,对未来的进一步学习有很大帮助。

附件

hello.c

C语言程序的源文件

hello.i

预处理后的文本文件

hello.s

编译后的汇编文件

hello.o

汇编后的可重定位目标文件

hello

链接后的可执行目标文件

hello1.elf

hello.o的ELF文件

hello1.asm

hello.o的反汇编文件

hello2.elf

hello的ELF文件

hello2.asm

hello的反汇编文件

参考文献

[1]  Randal E. Bryant, David R. O’Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.

[2]  ClassRoom706. 预处理、编译、汇编和链接[EB/OL].CSDN博客, 2020-03-30.https://blog.csdn.net/Zhubingge/article/details/105202116.

[3]  郑同学的笔记.【编译、链接、装载一】预处理、编译、汇编、链接[EB/OL].CSDN博客, 2024-04-19. https://blog.csdn.net/junxuezheng/article/details/130068109

[4]  饕子.深入解析计算机系统中的可重定位目标文件(.o文件)[EB/OL].CSDN博客, 2023-09-12. https://blog.csdn.net/m0_72410588/article/details/132841951

[5]  tanglinux.程序的本质之二ELF文件的文件头、section header和program header[EB/OL].CSDN博客, 2019-10-17.https://blog.csdn.net/npy_lp/article/details/102604380

[6]  csstormq.计算机系统篇之链接(5):静态链接(下)——重定位[EB/OL].CSDN博客,2020-04-19. https://blog.csdn.net/wohenfanjian/article/details/105618467

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

  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值