哈工大计算机系统大作业——程序人生

 

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业    计算机科学与技术    

学     号      2021110509        

班    级        2103101         

学       生        崔佳奇      

指 导 教 师         刘宏伟      

计算机科学与技术学院

2022年5月

摘  要

本文介绍了一个C程序Hello是如何在Linux系统下完成它一生的使命的。

Hello程序源于简简单单的Hello.c,在经过GCC下预处理、编译、汇编、链接这一串多重考验,最终成为了伟大的Hello可执行目标文件。在命令行输入./Hello按下回车那一刻,Hello便开始了在计算机系统的历程,计算机系统对其进行进程管理、信号处理、存储管理等一系列操作,最终Hello在系统进程中消失殆尽。这篇文章我会结合我所学的知识对Hello的一生进行深入探索。

关键词: 预处理;汇编;编译;链接;信号;进程                            

 

目  录

第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的预处理结果解析 - 6 -

2.4 本章小结 - 7 -

第3章 编译 - 8 -

3.1 编译的概念与作用 - 8 -

3.2 在Ubuntu下编译的命令 - 8 -

3.3 Hello的编译结果解析 - 8 -

3.4 本章小结 - 13 -

第4章 汇编 - 14 -

4.1 汇编的概念与作用 - 14 -

4.2 在Ubuntu下汇编的命令 - 14 -

4.3 可重定位目标elf格式 - 14 -

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

4.5 本章小结 - 20 -

第5章 链接 - 21 -

5.1 链接的概念与作用 - 21 -

5.2 在Ubuntu下链接的命令 - 21 -

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

5.4 hello的虚拟地址空间 - 26 -

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

5.6 hello的执行流程 - 27 -

5.7 Hello的动态链接分析 - 28 -

5.8 本章小结 - 28 -

第6章 hello进程管理 - 29 -

6.1 进程的概念与作用 - 29 -

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

6.3 Hello的fork进程创建过程 - 29 -

6.4 Hello的execve过程 - 29 -

6.5 Hello的进程执行 - 30 -

6.6 hello的异常与信号处理 - 31 -

6.7本章小结 - 34 -

第7章 hello的存储管理 - 35 -

7.1 hello的存储器地址空间 - 35 -

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

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

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

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

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

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

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

7.9动态存储分配管理 - 40 -

7.10本章小结 - 41 -

第8章 hello的IO管理 - 43 -

8.1 Linux的IO设备管理方法 - 43 -

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

8.3 printf的实现分析 - 43 -

8.4 getchar的实现分析 - 44 -

8.5本章小结 - 45 -

结论 - 46 -

附件 - 47 -

参考文献 - 48 -


第1章 概述

1.1 Hello简介

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

Hello的P2P(From Program to Process)

Hello的源程序是hello.c,hello.c在经过预处理器cpp后,得到了修改后的源程序hello.i,经过编译器cc1后得到了汇编文件hello.s,经过汇编器as后得到了可重定位目标文件hello.o,最后,经过链接器ld得到了可执行目标文件hello。执行程序 “hello”,shell就会通过fork()创建一个子进程,子进程执行execve()在子进程的上下文中加载并执行hello。到这里P2P告一段落。

Hello的O2O(From Zero-0 to Zero-0)

shell在调用完execve()执行hello程序之后,系统内核就会为hello进程映射一份虚拟内存。在hello进入程序入口之后,hello的相关数据就会被系统内核加载到物理内存中,至此hello便开始了他的时间——hello正式被系统开始运行。为了保证正常执行,系统内核还要为hello做一系列工作,比如分配时间片、分配逻辑控制流等。hello运行结束后,它便成为了zombie进程,之后shell会负责回收它,最后,shell删除了一切和hello相关的内容。

1.2 环境与工具

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

使用的软件环境:VMware16虚拟机     Ubuntu22.04.01 LTS

硬件环境:AMD Ryzen7 5800H处理器   16 G RAM    512 G HD Disk

开发工具:gcc,edb,gdb

1.3 中间结果

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

文件名

作用

hello.c

Hello的源文件

hello.i

Hello的修改了的源程序

hello.s

Hello的汇编文件

hello.o

Hello的可重定位目标文件

hello

Hello的可执行目标文件

elfheader.txt

hello.o的ELF头

secheader.txt

hello.o的节头部表

relahello.txt

hello.o的重定位节

hellosymbol.txt

hello.o的符号表

disassembly.txt

hello.o的反汇编

helloelfhead.txt

hello的ELF 头

hellosection.txt

hello的节头部表

hellodiss.txt

hello的反汇编

hellorela.txt

hello的重定位节

hellosym.txt

hello的符号表

tree.txt

进程树

表1 文件的中间结果

1.4 本章小结

      这一章主要介绍了一下shell执行hello的全过程,简单说明了hello从hello.c变成hello的全过程,说明了进行实验的软硬件环境和测试工具。


第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理就是通过预处理器cpp操作hello.c,根据hello.c中所有以 “#”开头的命令,比如一些宏定义、条件编译、include<.h>等,修改原始的C程序,并将引用的所有库展开合并成为一个完整的文本文件。

预处理的作用:

预处理可以是编写的程序更便于阅读、修改、移植和调回,同时还有利于模块化程序设计。

比如hello.c变成hello.i的过程,cpp把#include  、#include 、#include这三个头文件插入到了程序的文本文件之中,方便编译器cc1进行下一步的操作,结果就是得到了hello.i。hello.i本质上也是一个C程序,只不过经过预处理后后缀通常为.i。

2.2在Ubuntu下预处理的命令

  gcc预处理的命令  Linux>gcc hello.c -E -o hello.i

 

图2.1 利用gcc对hello.c进行预处理

2.3 Hello的预处理结果解析

hello.i的图标

哈工大计算机系统大作业——程序人生_第1张图片

 

图2.2 产生的hello.i

hello.i的内容

哈工大计算机系统大作业——程序人生_第2张图片

 

图2.3 hello.i中的代码部分

  预处理过后生成的hello.i文件内的内容相比于hello.c多了不少,但是其中的代码内容被完整的保留了下来。hello.i中包含了很多宏展开,头文件中的内容。

2.4 本章小结

这一章介绍了C程序预处理的相关概念和作用,展现了Ubuntu下利用gcc预处理C文件的过程,并分析了.c和.i文件内容上的异同。


第3章 编译

3.1 编译的概念与作用

编译的概念:

简单的来说,编译就是将预处理文件(hello.i)生成汇编文件(hello.s)的过程。编译器cc1对hello.i经过一系列的语法分析、语义分析,并根据优化等级(-O)进行优化之后生成了相应的汇编代码文件,汇编代码文件是一个文本文件。

编译的作用:

编译可以将源程序(高级语言C语言)翻译成为优化后的汇编代码(低级语言),在这个阶段还可以对源程序进行语法检查、调试、修改、覆盖、优化等操作,以及一些嵌入式汇编的处理。

3.2 在Ubuntu下编译的命令

gcc编译的命令  Linux>gcc -S hello.i -o hello.s

 

图3.1 利用gcc对hello.i进行编译

3.3 Hello的编译结果解析

3.3.1 数据

1. 字符串常量

在汇编文件标注.rodata这个位置上写着printf()中的字符串常量以及格式串

 

图3.2 .rodata节的字符串

  1. 主函数参数int argc和char **argv

查看main函数的汇编代码,按照x86-64的参数传递规则,argc应该存放在寄存器%edi中,argv应该存放在寄存器%rsi中。

程序的最开始就是把%rbp压入栈中,为main函数创建栈帧。

哈工大计算机系统大作业——程序人生_第3张图片哈工大计算机系统大作业——程序人生_第4张图片

 

图3.3 main()函数的汇编代码

  1. printf的参数

  在调用printf之前寄存器%rdi和%rsi有所更新

 哈工大计算机系统大作业——程序人生_第5张图片

 

图3.4 参数传递的汇编代码

从汇编代码来看恰好是argv中的元素传入了printf

3.3.2 全局函数

哈工大计算机系统大作业——程序人生_第6张图片

 

图3.5 全局函数的在汇编代码中的标志

从汇编代码的声明中可以看出 int main(int argc, char **argv)是被定义成为了全局函数,main中的字符串常量被存在.rodata节

3.3.3 赋值操作

在hello.c程序中,主要的赋值操作就是在for循环之前的i = 0,这一条指令,是利用mov指令来实现的

哈工大计算机系统大作业——程序人生_第7张图片

 

图3.6 赋值操作的汇编代码

此外程序中还有通过lea指令来实现赋值操作,lea指令是用来进行地址传送的,它只计算地址的值

 

图3.7 赋值操作的汇编代码

3.3.4 关系操作

(1)hello程序中,有一处条件判断指令:argc != 4,这个条件分支用于判断程序是否需要分区,对应的汇编代码是这个

 

图3.8 分支跳转比较的汇编代码

  1. 在程序的for循环之中,还有一处循环条件判断指令i < 9,对应的汇编代码是

哈工大计算机系统大作业——程序人生_第8张图片

 

图3.9 分支跳转比较的汇编代码

这条代码的意思就是去比较8和-4(%rbp)中的值,设置对应的条件码,jle来根据条件码确定是否需要进行循环跳转,jle的条件码判断是(ZF ^ OF) | ZF,就是如果满足小于或等于就进行跳转

   

 3.3.5 算数操作

    (1)在有关栈的操作的时候,通常都会利用sub指令来对%rsp中的栈指针进行加减8(或者是8的整数倍)操作,对应的汇编代码有

 

图3.10 栈指针的减法操作

  1. 在for循环之中,会有i ++这一操作,会使用到add指令给i的值加一,对应的汇编代码为

 

图3.11 参数的加一操作

 3.3.6 控制转移指令

在汇编代码中,分支语句if - else以及各种循环都是通过条件跳转来实现的,对应的汇编代码有

 

 

图3.12 条件分支跳转的操作

3.3.7 类型转换

argv存放着参数字符串,但是sleep函数的参数类型是int型的,直接把参数传入是不可以的,所以程序调用了标准库中的函数atoi(),把字符串数字转换成int型的变量再传递给sleep使用,在汇编代码中就是call调用了atoi()函数进行类型转换的操作

 

图3.13 利用call调用atoi进行类型转换

3.3.8 函数操作

汇编代码中的函数调用通常都是使用call指令 + 函数名,在调用函数之前,汇编代码都会更新程序计数器%rip的值,确定函数调用结束后应该执行的指令。

在x86-64架构下,函数调用的时候前6个参数依次存放在%rdi,%rsi,%rdx,%rcx,%r8,%r9这6个通用寄存器中,其余的参数存放在栈。

函数的局部变量存放在栈中或者寄存器中。

函数的返回值存放在寄存器%rax中。

 

 

 

 

图3.14 利用call进行函数调用

函数调用结束后,会使用ret指令来结束调用

3.4 本章小结

     本章介绍了在编译过程中C语言转换成汇编代码的过程,同时练习了阅读汇编代码,明白了C语言和汇编代码之间指令的相关联系,知道了一些汇编指令的微操作。


第4章 汇编

4.1 汇编的概念与作用

汇编的概念:

汇编就是指汇编器as将汇编文件.s翻译成为机器语言,生成可重定位目标文件.o的过程,可重定位目标文件是二进制文件

汇编的作用:

汇编阶段可以生成可重定位目标文件,为下一步与其他程序相链接,和动态库,静态库相链接做好准备。

4.2 在Ubuntu下汇编的命令

gcc汇编的命令  Linux>gcc hello.s -c -o hello.o

 

图4.1 利用gcc汇编生成hello.o

生成的hello.o文件

哈工大计算机系统大作业——程序人生_第9张图片

 

图4.2 生成的hello.o文件

4.3 可重定位目标elf格式

  1. ELF header(ELF头)的相关信息

     查看指令 Linux> readelf -h hello.o > elfheader.txt

哈工大计算机系统大作业——程序人生_第10张图片

 

图4.3 hello.o文件的ELF头

ELF Header 文件包含了文件类别,数据类型,系统信息,链接器语法分析,程序入口地址等内容。

此外在文件靠前位置的Magic段主要是程序用来确认读入的是否是elf文件头,每次程序在读取elf头文件的时候,都会确认Magic是否正确,以防读入的不是elf文件。

  1. Section Header(节头部表)的相关信息

查看指令 Linux> readelf -S hello.o > secheader.txt

节头部表包含了hello.o文件中各个节的类型、地址、大小、偏移量等信息

相关内容

There are 14 section headers, starting at offset 0x428:

节头:

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

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

  [ 0]                   NULL             0000000000000000  00000000

       0000000000000000  0000000000000000           0     0     0

  [ 1] .text             PROGBITS         0000000000000000  00000040

       0000000000000098  0000000000000000  AX       0     0     1

  [ 2] .rela.text        RELA             0000000000000000  000002d8

       00000000000000c0  0000000000000018   I      11     1     8

  [ 3] .data             PROGBITS         0000000000000000  000000d8

       0000000000000000  0000000000000000  WA       0     0     1

  [ 4] .bss              NOBITS           0000000000000000  000000d8

       0000000000000000  0000000000000000  WA       0     0     1

  [ 5] .rodata           PROGBITS         0000000000000000  000000d8

       000000000000003a  0000000000000000   A       0     0     8

  [ 6] .comment          PROGBITS         0000000000000000  00000112

       000000000000002c  0000000000000001  MS       0     0     1

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

       0000000000000000  0000000000000000           0     0     1

  [ 8] .note.gnu.pr[...] NOTE             0000000000000000  00000140

       0000000000000020  0000000000000000   A       0     0     8

  [ 9] .eh_frame         PROGBITS         0000000000000000  00000160

       0000000000000038  0000000000000000   A       0     0     8

  [10] .rela.eh_frame    RELA             0000000000000000  00000398

       0000000000000018  0000000000000018   I      11     9     8

  [11] .symtab           SYMTAB           0000000000000000  00000198

       0000000000000108  0000000000000018          12     4     8

  [12] .strtab           STRTAB           0000000000000000  000002a0

       0000000000000032  0000000000000000           0     0     1

  [13] .shstrtab         STRTAB           0000000000000000  000003b0

       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)

表2  hello.o的节头部表

可以看到其属性分别有名称、类型、地址(没有重定位,这些都是0)、偏移量(节相对于文件开始的偏移)、节大小、全体大小、旗标(节属性)、链接(相对于其他节)、信息(附加节)、对齐(2的align次方的偏移)。

  1. .rela.txt (重定位节)的相关信息

     查看指令 Linux> readelf -r hello.o >  relahello.txt

     重定位节包含了各个段引用的外部符号等必要有效信息。在链接时需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断通过偏移量等信息应使用什么方法计算出正确地址值。

     相关内容

哈工大计算机系统大作业——程序人生_第11张图片

 

图4.4 hello.o的重定位节

     

      从中可以得到hello需要重定位的信息有:函数puts,exit,printf,atoi,sleep,getchar和rodata节中的两个字符串

  1. .symtab (符号表)的相关信息

查看指令 Linux> readelf -s hello.o >  hellosymbol.txt

符号表存放着hello程序中定义和引用的全局变量和函数的相关信息,其中:name代表的符号的名称,value是这个符号相对于目标节的起始地址的偏移量,在这里还没有重定位所以都是0,size是对应目标的大小,type代表着数据类型(包含:无类型、节、函数),bind则指明这个符号是本地/局部符号还是全局符号

哈工大计算机系统大作业——程序人生_第12张图片

 

图4.5 hello.o的符号表

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

查看指令 Linux> objdump -d -r hello.o > disassembly.txt

hello.o的反汇编

hello.c的汇编

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

Disassembly of section .text:

0000000000000000

:

   0: f3 0f 1e fa           endbr64 

   4: 55                    push   %rbp

   5: 48 89 e5              mov    %rsp,%rbp

   8: 48 83 ec 20           sub    $0x20,%rsp

   c: 89 7d ec              mov    %edi,-0x14(%rbp)

   f: 48 89 75 e0           mov    %rsi,-0x20(%rbp)

  13: 83 7d ec 04           cmpl   $0x4,-0x14(%rbp)

  17: 74 19                 je     32

  19: 48 8d 05 00 00 00 00 lea    0x0(%rip),%rax        # 20

1c: R_X86_64_PC32 .rodata-0x4

  20: 48 89 c7              mov    %rax,%rdi

  23: e8 00 00 00 00        call   28

24: R_X86_64_PLT32 puts-0x4

  28: bf 01 00 00 00        mov    $0x1,%edi

  2d: e8 00 00 00 00        call   32

2e: R_X86_64_PLT32 exit-0x4

  32: c7 45 fc 00 00 00 00 movl   $0x0,-0x4(%rbp)

  39: eb 4b                 jmp    86

  3b: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  3f: 48 83 c0 10           add    $0x10,%rax

  43: 48 8b 10              mov    (%rax),%rdx

  46: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  4a: 48 83 c0 08           add    $0x8,%rax

  4e: 48 8b 00              mov    (%rax),%rax

  51: 48 89 c6              mov    %rax,%rsi

  54: 48 8d 05 00 00 00 00 lea    0x0(%rip),%rax        # 5b

57: R_X86_64_PC32 .rodata+0x29

  5b: 48 89 c7              mov    %rax,%rdi

  5e: b8 00 00 00 00        mov    $0x0,%eax

  63: e8 00 00 00 00        call   68

64: R_X86_64_PLT32 printf-0x4

  68: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  6c: 48 83 c0 18           add    $0x18,%rax

  70: 48 8b 00              mov    (%rax),%rax

  73: 48 89 c7              mov    %rax,%rdi

  76: e8 00 00 00 00        call   7b

77: R_X86_64_PLT32 atoi-0x4

  7b: 89 c7                 mov    %eax,%edi

  7d: e8 00 00 00 00        call   82

7e: R_X86_64_PLT32 sleep-0x4

  82: 83 45 fc 01           addl   $0x1,-0x4(%rbp)

  86: 83 7d fc 08           cmpl   $0x8,-0x4(%rbp)

  8a: 7e af                 jle    3b

  8c: e8 00 00 00 00        call   91

8d: R_X86_64_PLT32 getchar-0x4

  91: b8 00 00 00 00        mov    $0x0,%eax

  96: c9                    leave  

  97: c3                    ret    

.file "hello.c"

.text

.section .rodata

.align 8

.LC0:

.string "\347\224\250\346\263\225: Hello 2021110509 \345\264\224\344\275\263\345\245\207 \347\247\222\346\225\260\357\274\201"

.LC1:

.string "Hello %s %s\n"

.text

.globl main

.type main, @function

main:

.LFB6:

.cfi_startproc

endbr64

pushq %rbp

.cfi_def_cfa_offset 16

.cfi_offset 6, -16

movq %rsp, %rbp

.cfi_def_cfa_register 6

subq $32, %rsp

movl %edi, -20(%rbp)

movq %rsi, -32(%rbp)

cmpl $4, -20(%rbp)

je .L2

leaq .LC0(%rip), %rax

movq %rax, %rdi

call puts@PLT

movl $1, %edi

call exit@PLT

.L2:

movl $0, -4(%rbp)

jmp .L3

.L4:

movq -32(%rbp), %rax

addq $16, %rax

movq (%rax), %rdx

movq -32(%rbp), %rax

addq $8, %rax

movq (%rax), %rax

movq %rax, %rsi

leaq .LC1(%rip), %rax

movq %rax, %rdi

movl $0, %eax

call printf@PLT

movq -32(%rbp), %rax

addq $24, %rax

movq (%rax), %rax

movq %rax, %rdi

call atoi@PLT

movl %eax, %edi

call sleep@PLT

addl $1, -4(%rbp)

.L3:

cmpl $8, -4(%rbp)

jle .L4

call getchar@PLT

movl $0, %eax

leave

.cfi_def_cfa 7, 8

ret

.cfi_endproc

.LFE6:

.size main, .-main

.ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"

.section .note.GNU-stack,"",@progbits

.section .note.gnu.property,"a"

.align 8

.long 1f - 0f

.long 4f - 1f

.long 5

0:

.string "GNU"

1:

.align 8

.long 0xc0000002

.long 3f - 2f

2:

.long 0x3

3:

.align 8

4:

表3 两种汇编的比较

  1. 机器语言的构成

     在计算机中,机器语言是由一系列的0、1代码构成的,在反汇编文件中可以看到机器代码由16进制数来描述

  1. 与汇编语言的映射关系

每一个机器指令序列都包含的操作码、操作数等信息,以此一一对应着每一种汇编指令,把汇编转换成为机器语言

  1. 机器语言中的操作数与汇编语言不一致分析

  首先,机器语言中使用的操作数均为16进制表示,原始的汇编语言中的操作数都是10进制表示的。此外,编译阶段没有保留符号的名字,所以函数调用均写成主函数 + 偏移量的形式。

汇编代码hello.s中函数调用的call指令使用的是函数名称,反汇编代码中的call指令使用的是main函数相对偏移地址。由于函数只有在链接后才能确定所运行执行的地址(可以看到call指令对应机器代码的后四个字节均为0),所以为其添加重定位条目。

对机器语言而言,给定文件开始位置就能够将合法字节序列唯一地翻译解释成有效指令;但是机器反汇编代码的操作数却会被映射为特定字节或“小/大端序”来表示的十六进制立即数。

4.5 本章小结

在这一章中我完成了汇编操作,将hello.s,转换成为了hello.o,并仔细的分析并阅读了可重定位目标文件的ELF内的相关内容,如ELF头、节头部表、符号表以及课程定位节。同时完成了对hello.o的反汇编操作,明白了汇编操作的原理和作用,分析比较了hello.s和反汇编文件之间的异同,明白了机器指令和汇编代码之间的一一对应关系,同时了解了之后链接对于一个C程序的作用。
5章 链接

5.1 链接的概念与作用

链接的概念:

链接就是指把各种代码和数据片段收集并组合成为一个文件的过程,这个文件就叫做可执行目标文件,它可以直接被加载到内存中执行。链接分为三种:

编译时链接(链接静态库,也称静态链接),加载时链接和运行时链接(动态链接,用于链接动态库)。

链接的作用:

可以使分块编程成为可能,有了链接,程序员不用去编写巨大的程序,同时也方便了程序的维护,更新和优化。

5.2 在Ubuntu下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

链接的指令 Linux> 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.1 利用gcc链接

生成的可执行目标文件的图标

哈工大计算机系统大作业——程序人生_第13张图片

 

图5.2 生成的可执行目标文件

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

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

5.3.1 可执行目标文件的ELF Header

查看指令 Linux> readelf -a hello > helloelfhead.txt

 

图5.3 生成ELF头

具体内容如下

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

  类型:                              EXEC (可执行文件)

  系统架构:                          Advanced Micro Devices X86-64

  版本:                              0x1

  入口点地址:               0x4010f0

  程序头起点:          64 (bytes into file)

  Start of section headers:          13560 (bytes into file)

  标志:             0x0

  Size of this header:               64 (bytes)

  Size of program headers:           56 (bytes)

  Number of program headers:         12

  Size of section headers:           64 (bytes)

  Number of section headers:         27

  Section header string table index: 26

表4 节头部表的内容

可以看到,相比于hello.o的ELF头,文件类型改变了,从REL变成了EXEC,入口点地址从0x0变成了0x4010f0(从不确定值变成了确定值),节头数量从14变成了27

5.3.2 可执行目标文件的Section Headers

利用指令  Linux> readelf -S hello > hellosection.txt

具体内容

There are 27 section headers, starting at offset 0x34f8:

节头:

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

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

  [ 0]                   NULL             0000000000000000  00000000

       0000000000000000  0000000000000000           0     0     0

  [ 1] .interp           PROGBITS         00000000004002e0  000002e0

       000000000000001c  0000000000000000   A       0     0     1

  [ 2] .note.gnu.pr[...] NOTE             0000000000400300  00000300

       0000000000000030  0000000000000000   A       0     0     8

  [ 3] .note.ABI-tag     NOTE             0000000000400330  00000330

       0000000000000020  0000000000000000   A       0     0     4

  [ 4] .hash             HASH             0000000000400350  00000350

       0000000000000038  0000000000000004   A       6     0     8

  [ 5] .gnu.hash         GNU_HASH         0000000000400388  00000388

       000000000000001c  0000000000000000   A       6     0     8

  [ 6] .dynsym           DYNSYM           00000000004003a8  000003a8

       00000000000000d8  0000000000000018   A       7     1     8

  [ 7] .dynstr           STRTAB           0000000000400480  00000480

       0000000000000067  0000000000000000   A       0     0     1

  [ 8] .gnu.version      VERSYM           00000000004004e8  000004e8

       0000000000000012  0000000000000002   A       6     0     2

  [ 9] .gnu.version_r    VERNEED          0000000000400500  00000500

       0000000000000030  0000000000000000   A       7     1     8

  [10] .rela.dyn         RELA             0000000000400530  00000530

       0000000000000030  0000000000000018   A       6     0     8

  [11] .rela.plt         RELA             0000000000400560  00000560

       0000000000000090  0000000000000018  AI       6    21     8

  [12] .init             PROGBITS         0000000000401000  00001000

       000000000000001b  0000000000000000  AX       0     0     4

  [13] .plt              PROGBITS         0000000000401020  00001020

       0000000000000070  0000000000000010  AX       0     0     16

  [14] .plt.sec          PROGBITS         0000000000401090  00001090

       0000000000000060  0000000000000010  AX       0     0     16

  [15] .text             PROGBITS         00000000004010f0  000010f0

       00000000000000cd  0000000000000000  AX       0     0     16

  [16] .fini             PROGBITS         00000000004011c0  000011c0

       000000000000000d  0000000000000000  AX       0     0     4

  [17] .rodata           PROGBITS         0000000000402000  00002000

       0000000000000042  0000000000000000   A       0     0     8

  [18] .eh_frame         PROGBITS         0000000000402048  00002048

       00000000000000a0  0000000000000000   A       0     0     8

  [19] .dynamic          DYNAMIC          0000000000403e50  00002e50

       00000000000001a0  0000000000000010  WA       7     0     8

  [20] .got              PROGBITS         0000000000403ff0  00002ff0

       0000000000000010  0000000000000008  WA       0     0     8

  [21] .got.plt          PROGBITS         0000000000404000  00003000

       0000000000000048  0000000000000008  WA       0     0     8

  [22] .data             PROGBITS         0000000000404048  00003048

       0000000000000004  0000000000000000  WA       0     0     1

  [23] .comment          PROGBITS         0000000000000000  0000304c

       000000000000002b  0000000000000001  MS       0     0     1

  [24] .symtab           SYMTAB           0000000000000000  00003078

       0000000000000270  0000000000000018          25     7     8

  [25] .strtab           STRTAB           0000000000000000  000032e8

       000000000000012e  0000000000000000           0     0     1

  [26] .shstrtab         STRTAB           0000000000000000  00003416

       00000000000000e1  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)

表5 节头部表的内容

这里和hello.o不同的地方是,在可执行目标文件中,每个节的地址不是0x0,而是变成了根据节自身的大小和对齐规则计算得出的偏移量。

5.3.3 可执行目标文件的重定位节.rela.txt

利用指令 Linux> readelf -r hello >  hellorela.txt

具体内容

重定位节 '.rela.dyn' at offset 0x530 contains 2 entries:

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

000000403ff0  000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0

000000403ff8  000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

重定位节 '.rela.plt' at offset 0x560 contains 6 entries:

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

000000404018  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0

000000404020  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0

000000404028  000400000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0

000000404030  000600000007 R_X86_64_JUMP_SLO 0000000000000000 atoi@GLIBC_2.2.5 + 0

000000404038  000700000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0

000000404040  000800000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0

表6 重定位节的内容

可以看到,相比于hello.o的重定位节,hello的重定位节被分成了两部分,包含动态链接的重定位内容,同时每一个部分都有了准确的偏移量,因而所有的加数都变成了0。

5.3.4 可执行目标文件的符号表

利用指令 Linux>  readelf -s hello >  hellosym.txt

具体内容如下

Symbol table '.dynsym' contains 9 entries:

   Num:    Value          Size Type    Bind   Vis      Ndx Name

     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 

     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _[...]@GLIBC_2.34 (2)

     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (3)

     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND [...]@GLIBC_2.2.5 (3)

     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND [...]@GLIBC_2.2.5 (3)

     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND atoi@GLIBC_2.2.5 (3)

     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (3)

     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@GLIBC_2.2.5 (3)

Symbol table '.symtab' contains 26 entries:

   Num:    Value          Size Type    Bind   Vis      Ndx Name

     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 

     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crt1.o

     2: 0000000000400330    32 OBJECT  LOCAL  DEFAULT    3 __abi_tag

     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c

     4: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 

     5: 0000000000403e50     0 OBJECT  LOCAL  DEFAULT   19 _DYNAMIC

     6: 0000000000404000     0 OBJECT  LOCAL  DEFAULT   21 _GLOBAL_OFFSET_TABLE_

     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_mai[...]

     8: 0000000000404048     0 NOTYPE  WEAK   DEFAULT   22 data_start

     9: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5

    10: 000000000040404c     0 NOTYPE  GLOBAL DEFAULT   22 _edata

    11: 00000000004011c0     0 FUNC    GLOBAL HIDDEN    16 _fini

    12: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5

    13: 0000000000404048     0 NOTYPE  GLOBAL DEFAULT   22 __data_start

    14: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getchar@GLIBC_2.2.5

    15: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

    16: 0000000000402000     4 OBJECT  GLOBAL DEFAULT   17 _IO_stdin_used

    17: 0000000000404050     0 NOTYPE  GLOBAL DEFAULT   22 _end

    18: 0000000000401120     5 FUNC    GLOBAL HIDDEN    15 _dl_relocate_sta[...]

    19: 00000000004010f0    38 FUNC    GLOBAL DEFAULT   15 _start

    20: 000000000040404c     0 NOTYPE  GLOBAL DEFAULT   22 __bss_start

    21: 0000000000401125   152 FUNC    GLOBAL DEFAULT   15 main

    22: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND atoi@GLIBC_2.2.5

    23: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5

    24: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@GLIBC_2.2.5

    25: 0000000000401000     0 FUNC    GLOBAL HIDDEN    12 _init

表7 符号表的内容

这部分相比于hello.o多出了动态链接解析出来的符号。同时其他的符号也多了不少。

5.4 hello的虚拟地址空间

使用edb来分析一下hello,查看一下虚拟空间的位置在哪里

哈工大计算机系统大作业——程序人生_第14张图片

 

图5.4 虚拟地址空间开头

可以看到,hello的虚拟是从0x400000开始的,根据hello的节头部表,我们得知.interp的偏移量是0x0002e0,所以它的位置应该就在0x4002e0,查看一下

 

图5.5 .interp节的内容

确实是这样

同样的,还可以找到.rodata节的位置在0x402000

[17] .rodata           PROGBITS         0000000000402000  00002000

       0000000000000042  0000000000000000   A       0     0     8

 

图5.6 .rodata节的内容

看到了固定的字符串!

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

首先对hello进行反汇编

指令 Linux>  objdump -d -r hello >  hellodiss.txt

hello:     文件格式 elf64-x86-64

Disassembly of section .init:

0000000000401000 <_init>:

  401000: f3 0f 1e fa           endbr64 

  401004: 48 83 ec 08           sub    $0x8,%rsp

  401008: 48 8b 05 e9 2f 00 00 mov    0x2fe9(%rip),%rax        # 403ff8 <__gmon_start__@Base>

  40100f: 48 85 c0              test   %rax,%rax

  401012: 74 02                 je     401016 <_init+0x16>

  401014: ff d0                 call   *%rax

  401016: 48 83 c4 08           add    $0x8,%rsp

  40101a: c3                    ret    

Disassembly of section .plt:

0000000000401020 <.plt>:

  401020: ff 35 e2 2f 00 00     push   0x2fe2(%rip)        # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>

  401026: f2 ff 25 e3 2f 00 00 bnd jmp *0x2fe3(%rip)        # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>

  40102d: 0f 1f 00              nopl   (%rax)

  401030: f3 0f 1e fa           endbr64 

  401034: 68 00 00 00 00        push   $0x0

  401039: f2 e9 e1 ff ff ff     bnd jmp 401020 <_init+0x20>

  40103f: 90                    nop

  401040: f3 0f 1e fa           endbr64 

  401044: 68 01 00 00 00        push   $0x1

  401049: f2 e9 d1 ff ff ff     bnd jmp 401020 <_init+0x20>

  40104f: 90                    nop

  401050: f3 0f 1e fa           endbr64 

  401054: 68 02 00 00 00        push   $0x2

  401059: f2 e9 c1 ff ff ff     bnd jmp 401020 <_init+0x20>

  40105f: 90                    nop

  401060: f3 0f 1e fa           endbr64 

  401064: 68 03 00 00 00        push   $0x3

  401069: f2 e9 b1 ff ff ff     bnd jmp 401020 <_init+0x20>

  40106f: 90                    nop

  401070: f3 0f 1e fa           endbr64 

  401074: 68 04 00 00 00        push   $0x4

  401079: f2 e9 a1 ff ff ff     bnd jmp 401020 <_init+0x20>

  40107f: 90                    nop

  401080: f3 0f 1e fa           endbr64 

  401084: 68 05 00 00 00        push   $0x5

  401089: f2 e9 91 ff ff ff     bnd jmp 401020 <_init+0x20>

  40108f: 90                    nop

Disassembly of section .plt.sec:

0000000000401090 :

  401090: f3 0f 1e fa           endbr64 

  401094: f2 ff 25 7d 2f 00 00 bnd jmp *0x2f7d(%rip)        # 404018

  40109b: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010a0 :

  4010a0: f3 0f 1e fa           endbr64 

  4010a4: f2 ff 25 75 2f 00 00 bnd jmp *0x2f75(%rip)        # 404020

  4010ab: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010b0 :

  4010b0: f3 0f 1e fa           endbr64 

  4010b4: f2 ff 25 6d 2f 00 00 bnd jmp *0x2f6d(%rip)        # 404028

  4010bb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010c0 :

  4010c0: f3 0f 1e fa           endbr64 

  4010c4: f2 ff 25 65 2f 00 00 bnd jmp *0x2f65(%rip)        # 404030

  4010cb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010d0 :

  4010d0: f3 0f 1e fa           endbr64 

  4010d4: f2 ff 25 5d 2f 00 00 bnd jmp *0x2f5d(%rip)        # 404038

  4010db: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

00000000004010e0 :

  4010e0: f3 0f 1e fa           endbr64 

  4010e4: f2 ff 25 55 2f 00 00 bnd jmp *0x2f55(%rip)        # 404040

  4010eb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)

Disassembly of section .text:

00000000004010f0 <_start>:

  4010f0: f3 0f 1e fa           endbr64 

  4010f4: 31 ed                 xor    %ebp,%ebp

  4010f6: 49 89 d1              mov    %rdx,%r9

  4010f9: 5e                    pop    %rsi

  4010fa: 48 89 e2              mov    %rsp,%rdx

  4010fd: 48 83 e4 f0           and    $0xfffffffffffffff0,%rsp

  401101: 50                    push   %rax

  401102: 54                    push   %rsp

  401103: 45 31 c0              xor    %r8d,%r8d

  401106: 31 c9                 xor    %ecx,%ecx

  401108: 48 c7 c7 25 11 40 00 mov    $0x401125,%rdi

  40110f: ff 15 db 2e 00 00     call   *0x2edb(%rip)        # 403ff0 <__libc_start_main@GLIBC_2.34>

  401115: f4                    hlt    

  401116: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)

  40111d: 00 00 00 

0000000000401120 <_dl_relocate_static_pie>:

  401120: f3 0f 1e fa           endbr64 

  401124: c3                    ret    

0000000000401125

:

  401125: f3 0f 1e fa           endbr64 

  401129: 55                    push   %rbp

  40112a: 48 89 e5              mov    %rsp,%rbp

  40112d: 48 83 ec 20           sub    $0x20,%rsp

  401131: 89 7d ec              mov    %edi,-0x14(%rbp)

  401134: 48 89 75 e0           mov    %rsi,-0x20(%rbp)

  401138: 83 7d ec 04           cmpl   $0x4,-0x14(%rbp)

  40113c: 74 19                 je     401157

  40113e: 48 8d 05 c3 0e 00 00 lea    0xec3(%rip),%rax        # 402008 <_IO_stdin_used+0x8>

  401145: 48 89 c7              mov    %rax,%rdi

  401148: e8 43 ff ff ff        call   401090

  40114d: bf 01 00 00 00        mov    $0x1,%edi

  401152: e8 79 ff ff ff        call   4010d0

  401157: c7 45 fc 00 00 00 00 movl   $0x0,-0x4(%rbp)

  40115e: eb 4b                 jmp    4011ab

  401160: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  401164: 48 83 c0 10           add    $0x10,%rax

  401168: 48 8b 10              mov    (%rax),%rdx

  40116b: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  40116f: 48 83 c0 08           add    $0x8,%rax

  401173: 48 8b 00              mov    (%rax),%rax

  401176: 48 89 c6              mov    %rax,%rsi

  401179: 48 8d 05 b5 0e 00 00 lea    0xeb5(%rip),%rax        # 402035 <_IO_stdin_used+0x35>

  401180: 48 89 c7              mov    %rax,%rdi

  401183: b8 00 00 00 00        mov    $0x0,%eax

  401188: e8 13 ff ff ff        call   4010a0

  40118d: 48 8b 45 e0           mov    -0x20(%rbp),%rax

  401191: 48 83 c0 18           add    $0x18,%rax

  401195: 48 8b 00              mov    (%rax),%rax

  401198: 48 89 c7              mov    %rax,%rdi

  40119b: e8 20 ff ff ff        call   4010c0

  4011a0: 89 c7                 mov    %eax,%edi

  4011a2: e8 39 ff ff ff        call   4010e0

  4011a7: 83 45 fc 01           addl   $0x1,-0x4(%rbp)

  4011ab: 83 7d fc 08           cmpl   $0x8,-0x4(%rbp)

  4011af: 7e af                 jle    401160

  4011b1: e8 fa fe ff ff        call   4010b0

  4011b6: b8 00 00 00 00        mov    $0x0,%eax

  4011bb: c9                    leave  

  4011bc: c3                    ret    

Disassembly of section .fini:

00000000004011c0 <_fini>:

  4011c0: f3 0f 1e fa           endbr64 

  4011c4: 48 83 ec 08           sub    $0x8,%rsp

  4011c8: 48 83 c4 08           add    $0x8,%rsp

  4011cc: c3                    ret    

表8 反汇编的内容

分析:

  1. hello的反汇编文件中可以看出,相比于hello.o,里面所有的不确定的地址值都变得确定了,这说明链接器完成了对于hello的重定位工作。
  2. hello中增加了很多hello.o中没有,但是定义在了动态库并被hello直接使用的函数。在链接过程中,链接器ld完成了对它们的符号解析和重定位。
  3. 相比于hello.o程序中大量使用了间接跳转。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

利用edb执行hello

哈工大计算机系统大作业——程序人生_第15张图片

 

图5.7 利用edb执行hello

调用的子程序名和程序地址

子程序名

程序地址

_start

0x4010f0

_init

0x401000

main

0x40112d

puts@plt

0x401090

exit@plt

0x4010d0

_fini

0x4011c0

sleep@plt

0x4010e0

atoi@plt

0x4010c0

printf@plt

0x4010a0

getchar@plt

0x4010b0

表9 子程序地址

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

动态链接采用了延迟加载的策略,只有在调用函数的时候才会进行符号的映射。动态链接通过使用偏移量表GOT和过程链表PLT的协同工作来实现。

GOT表中存放着函数的目标地址,PLT表则使用GOT中的地址来跳转到目标函数,在程序执行的过程中,dl_init负责修改PLT和GOT。

首先先找到GOT的地址

  1. .got              PROGBITS         0000000000403ff0  00002ff0 0000000000000010  0000000000000008  WA       0     0     8   ->起始地址为403ff8

调用edb查看调用dl_init前后的变化

调用前

 

调用后

 

图5.8 调用前后地址的变化

5.8 本章小结

这一章我介绍了hello.o的链接过程,分析比较了hello和hello.o的ELF文件的异同,仔细分析了动态链接过程中的各种变化,理解了链接的原理过程。


6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

经典的定义就是一个执行中程序的实例,每一个进程都有它自己的地址空间,一般情 况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数 据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动 过程调用的指令和本地变量。

进程的作用:

进程为用户提供了以下假象:

(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。

(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

哈工大计算机系统大作业——程序人生_第16张图片

 

图6.1 进程的逻辑控制流

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

Shell-bash的作用:

shell是一个交互型的应用程序,代表用户运行其他程序,执行一系列的读、求值步骤,然后终止。

Shell-bash的处理流程:

shell的基本功能是解释并运行用户的指令,重复如下处理过程:

(1)终端进程读取用户由键盘输入的命令行。

(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量

(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令

(4)如果不是内部命令,调用fork( )创建新进程/子进程

(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。

(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid()(或wait…等待作业终止后返回。

7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;

6.3 Hello的fork进程创建过程

创建过程:

进程的创建采用fork()函数:pid_t fork();创建的子进程得到与父进程用户级虚拟地址空间相同的(但是物理空间是独立的,所以两个进程中的变量值是不相关联的)一份副本,包括代码和数据段、堆、共享库以及用户栈。

子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork()函数时,子进程可以读取父进程中打开的任何文件。

父进程与创建的子进程之间最大的区别在于它们有不同的PID。子进程中,fork()返回0。父进程中fork()会返回子进程的PID,因为子进程的PID总是为非零,返回值就提供一个明确的方法分辨程序是在父进程还是在子进程中。还可以进行多进程处理任务。

利用进程图就可以很形象的描述fork()的作用

哈工大计算机系统大作业——程序人生_第17张图片

 

图6.2 进程图示意

6.4 Hello的execve过程

execve()函数在当前进程的上下文中加载并运行一个新程序。execve()函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。只有当出现错误时,例如:找不到文件名,调用发生错误,execve()才会返回到调用程序,调用成功不会返回。

与fork()不同,fork()一次调用两次返回,execve()一次调用从不返回(必须是成功调用)。

哈工大计算机系统大作业——程序人生_第18张图片

 

图6.3 用户栈的结构

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

  1. 上下文信息

     上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成(不包含代码段和数据段)。

  1. 进程的时间片

     一个进程执行它的控制流的一部分的每一时间段叫做时间片。

  1. 用户模式和内核模式

     处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。上下文切换的时候,进程就处于内核模式。

哈工大计算机系统大作业——程序人生_第19张图片

 

图6.3 上下文切换

对于hello程序,程序在调用了sleep()后就会进入内核模式,并会进行上下文切换。程序运行到getchar()的时候,内核也会进行上下文切换,让其他进程运行。

除此之外,系统还会为hello程序分配时间片,只要hello时间片被用完,即使没有执行到getchar()或者sleep()函数,系统就会判断当前程序的运行时间已经足够久了,从而进行上下文切换,将处理器让给其他进程,这样可以提升程序运行的效率。

6.6 hello的异常与信号处理

  1. Linux环境下异常的种类

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

  1. hello运行过程中会出现哪些异常
  1. 中断:在执行hello程序的时候,由处理器外部的I/O设备的信号引起的。I/O设备通过像处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。这个异常号标识了引起中断的设备。在当前指令完成执行后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。在处理程序返回前,将控制返回给下一条指令。
  2. 陷阱:hello在执行sleep()函数的时候就会发生陷阱异常。
  3. 故障:hello在执行的时候可能会出现缺页的情况,导致缺页故障。
  4. 终止:终止大多是由于不可恢复的致命错误而导致的,通常都是一些硬件上的错误。
  1. 故障处理的方式

哈工大计算机系统大作业——程序人生_第20张图片

 

图1 异常处理

哈工大计算机系统大作业——程序人生_第21张图片

 

图2 陷阱处理

哈工大计算机系统大作业——程序人生_第22张图片

 

图3 故障处理

哈工大计算机系统大作业——程序人生_第23张图片

 

图4 终止处理

  1. 运行时输入

1.  运行时键入ctrl + c,向hello发送SIGINT信号

哈工大计算机系统大作业——程序人生_第24张图片

 

图6.4 发送信号

  1. 运行时输入ctrl + z,向hello发送SIGTSTP信号

哈工大计算机系统大作业——程序人生_第25张图片

 

图6.5 发送信号

此时输入ps ,监视后台程序

 哈工大计算机系统大作业——程序人生_第26张图片

图6.5 进程监视

可以看到hello在后台被挂起了

输入jobs来查看当前被暂停的进程

 

图6.6 进程状态查看

输入pstree,查看进程树

systemd-+-ModemManager---2*[{ModemManager}]

        |-NetworkManager---2*[{NetworkManager}]

        |-VGAuthService

        |-accounts-daemon---2*[{accounts-daemon}]

        |-acpid

        |-avahi-daemon---avahi-daemon

        |-bluetoothd

        |-colord---2*[{colord}]

        |-cron

        |-cups-browsed---2*[{cups-browsed}]

        |-cupsd

        |-dbus-daemon

        |-gdm3-+-gdm-session-wor-+-gdm-wayland-ses-+-gnome-session-b---2*[{gnome-session-b}]

        |      |                 |                 `-2*[{gdm-wayland-ses}]

        |      |                 `-2*[{gdm-session-wor}]

        |      `-2*[{gdm3}]

        |-gnome-keyring-d---3*[{gnome-keyring-d}]

        |-irqbalance---{irqbalance}

        |-2*[kerneloops]

        |-networkd-dispat

        |-packagekitd---2*[{packagekitd}]

        |-polkitd---2*[{polkitd}]

        |-power-profiles----2*[{power-profiles-}]

        |-rsyslogd---3*[{rsyslogd}]

        |-rtkit-daemon---2*[{rtkit-daemon}]

        |-snapd---13*[{snapd}]

        |-switcheroo-cont---2*[{switcheroo-cont}]

        |-systemd-+-(sd-pam)

        |         |-at-spi2-registr---2*[{at-spi2-registr}]

        |         |-2*[dbus-daemon]

        |         |-dconf-service---2*[{dconf-service}]

        |         |-evolution-addre---5*[{evolution-addre}]

        |         |-evolution-calen---8*[{evolution-calen}]

        |         |-evolution-sourc---3*[{evolution-sourc}]

        |         |-fcitx---2*[{fcitx}]

        |         |-fcitx-dbus-watc

        |         |-gedit---3*[{gedit}]

        |         |-2*[gjs---6*[{gjs}]]

        |         |-gnome-session-b-+-at-spi-bus-laun-+-dbus-daemon

        |         |                 |                 `-3*[{at-spi-bus-laun}]

        |         |                 |-evolution-alarm---5*[{evolution-alarm}]

        |         |                 |-gsd-disk-utilit---2*[{gsd-disk-utilit}]

        |         |                 |-update-notifier---3*[{update-notifier}]

        |         |                 `-3*[{gnome-session-b}]

        |         |-gnome-session-c---{gnome-session-c}

        |         |-gnome-shell-+-Xwayland

        |         |             |-gjs---7*[{gjs}]

        |         |             `-10*[{gnome-shell}]

        |         |-gnome-shell-cal---5*[{gnome-shell-cal}]

        |         |-gnome-terminal--+-3*[bash]

        |         |                 |-bash-+-hello

        |         |                 |      `-pstree

        |         |                 `-3*[{gnome-terminal-}]

        |         |-goa-daemon---3*[{goa-daemon}]

        |         |-goa-identity-se---2*[{goa-identity-se}]

        |         |-gsd-a11y-settin---3*[{gsd-a11y-settin}]

        |         |-gsd-color---3*[{gsd-color}]

        |         |-gsd-datetime---3*[{gsd-datetime}]

        |         |-gsd-housekeepin---3*[{gsd-housekeepin}]

        |         |-gsd-keyboard---3*[{gsd-keyboard}]

        |         |-gsd-media-keys---3*[{gsd-media-keys}]

        |         |-gsd-power---3*[{gsd-power}]

        |         |-gsd-print-notif---2*[{gsd-print-notif}]

        |         |-gsd-printer---2*[{gsd-printer}]

        |         |-gsd-rfkill---2*[{gsd-rfkill}]

        |         |-gsd-screensaver---2*[{gsd-screensaver}]

        |         |-gsd-sharing---3*[{gsd-sharing}]

        |         |-gsd-smartcard---3*[{gsd-smartcard}]

        |         |-gsd-sound---3*[{gsd-sound}]

        |         |-gsd-wacom---3*[{gsd-wacom}]

        |         |-gsd-xsettings---3*[{gsd-xsettings}]

        |         |-gvfs-afc-volume---3*[{gvfs-afc-volume}]

        |         |-gvfs-goa-volume---2*[{gvfs-goa-volume}]

        |         |-gvfs-gphoto2-vo---2*[{gvfs-gphoto2-vo}]

        |         |-gvfs-mtp-volume---2*[{gvfs-mtp-volume}]

        |         |-gvfs-udisks2-vo---3*[{gvfs-udisks2-vo}]

        |         |-gvfsd-+-gvfsd-dnssd---2*[{gvfsd-dnssd}]

        |         |       |-gvfsd-network---3*[{gvfsd-network}]

        |         |       |-gvfsd-smb-brows---4*[{gvfsd-smb-brows}]

        |         |       |-gvfsd-trash---2*[{gvfsd-trash}]

        |         |       `-2*[{gvfsd}]

        |         |-gvfsd-fuse---5*[{gvfsd-fuse}]

        |         |-gvfsd-metadata---2*[{gvfsd-metadata}]

        |         |-nautilus---5*[{nautilus}]

        |         |-pipewire---{pipewire}

        |         |-pipewire-media----{pipewire-media-}

        |         |-pulseaudio---3*[{pulseaudio}]

        |         |-snap-store---4*[{snap-store}]

        |         |-snapd-desktop-i---3*[{snapd-desktop-i}]

        |         |-sogoupinyin-ser---9*[{sogoupinyin-ser}]

        |         |-sogoupinyin-wat---5*[{sogoupinyin-wat}]

        |         |-tracker-miner-f---5*[{tracker-miner-f}]

        |         |-vmtoolsd---3*[{vmtoolsd}]

        |-systemd-journal

        |-systemd-logind

        |-systemd-oomd

        |-systemd-resolve

        |-systemd-timesyn---{systemd-timesyn}

        |-systemd-udevd

        |-udisksd---4*[{udisksd}]

        |-unattended-upgr---{unattended-upgr}

        |-upowerd---2*[{upowerd}]

        |-vmtoolsd---3*[{vmtoolsd}]

        |-vmware-vmblock----2*[{vmware-vmblock-}]

        `-wpa_supplicant

表11 进程树

可以从进程树中看到hello这个进程被挂起了

输入fg,让hello重新到前台执行

 

 

图6.7 程序运行

运行kill指令,让hello终止运行

先查看一下hello的PID

 

 

 

图6.8 程序运行

输入 kill -9 3993就可以了

图6.9 杀死进程

再查看一下进程

哈工大计算机系统大作业——程序人生_第27张图片

 

图6.10 查看进程

hello已经被杀死了

  1. 运行时输入回车

哈工大计算机系统大作业——程序人生_第28张图片

 

图6.11 运行时按回车

并没有什么异常

  1. 在键盘上乱按

哈工大计算机系统大作业——程序人生_第29张图片

 

图6.12 运行时乱按

可以看到随便乱按并不会影响到当前进程的运行,不仅如此,shell会依次记录你的输入的命令,在当前进程运行结束之后去依次处理的输入的内容。

6.7本章小结

这一章我详细介绍了进程的概念和作用,并仔细分析了fork()和execve()的调用方法,过程和作用,上下文切换的原理和过程,还有对于hello的信号异常的处理与分析。

第7章 hello的存储管理

7.1 hello的存储器地址空间

各种地址空间的概念:

逻辑地址:逻辑地址指的是在汇编代码中通过偏移量+段基址得到的地址,所以汇编代码中所有显示的地址都是逻辑地址。

物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。第一个字节的地址为0,写下来的字节地址为1,再下一个为2,以此类推。所以物理地址就像是一个大的数组。

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

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

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

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

在Intel下,对于cache的访问时通过物理地址来实现的,一个物理地址可以被分成三个部分:标记位,组索引,块偏移

 

图7.1 物理地址的构成

Intel i7芯片中的cache都是以组相联的方式构成的,也就是说cache被分成了很多块,块分成了很多组,组内有分成了很多行。在访问cache的时候,首先通过组索引来找到我们的地址在Cache中所对应的组号,再通过标记和Cache的有效位来判断我们的内容是否在Cache中。若命中则通过块偏移读取我们要的数据,若不命中则从下一级Cache(准确的来说是下一层存储空间,因为L3 cache不命中会从主存DRAM中寻找)中寻找。

讨论一个例子,这个是我的电脑L1 D-cache 的信息

 

图7.2 L1D-cache的信息

     可以知道每一个L1 D-cache的大小为32KB,每一组cache中有8行,查询我的CPU的配置,可以知道每一块是64B,物理地址位数是52位,经过公式

C=S *E*B,可以知道我的电脑中组索引为6位,块偏移为6位,标志位40位。

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

   这一章我简单介绍了计算机系统中的逻辑地址和物理地址这两个地址空间的概念,并且详细分析了物理地址的构成,含义,寻址方式,结合自己电脑的配置完成了对于cache寻址方式的了解。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

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;

}

printf函数是一个变参数函数,也就是说它在使用的过程中参数的个数是不确定的。

   函数中的va_list就是typedef后的char*字符串。va_list arg = (va_list)((char*)(&fmt) + 4);就是得到了后面所有参数(就是...)中的第一个量。

   之后printf调用vsprintf函数。vsprintf函数将我们需要输出的字符串格式化并把内容存放在buf(缓冲区)中。并返回要输出的字符个数i。然后调用系统函数write来在屏幕上打印buf中的前i个字符,也就是我们要输出的格式串。

    调用write系统函数后,程序进入到陷阱,系统调用 int 0x80或syscall指令等,通过字符驱动子程序打印printf中的内容。

最后程序返回我们实际输出的字符数量i,也就是说printf是由返回值的,只是我们很少去使用。

8.4 getchar的实现分析

查看一下getchar()函数的定义

int getchar (void)

 {

 static char buf [BUFSIZ];

 static char *bb = buf;

 static int n = 0;
 if (n == 0)
 n = read(0, buf, BUFSIZ);
  bb = buf;

 }

 return(--n >= 0)? (unsigned char) *bb++ : EOF;

 }

当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII 码,保存到系统的键盘buf缓冲区之中。

getchar() 函数落实到底层调用了系统函数 read(),通过系统调用 read() 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar() 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

    根据我的所学知识,这一章我总结介绍了printf()和getchar()这两个I/O函数的具体实现过程,理解了计算机系统是如何实现内部和外部相互联系并且同时运作的。

结论

 Hello最初只是一个简简单单的hello.c文件,只是一些简简单单的文本,它能够在屏幕上显示出 “Hello World!”,真的经历了许多考验。

一开始,hello.c只是默默的存在磁盘当中,终于有一刻,有一位程序员使用了gcc,让hello.c经过了预处理,其中的头文件被展开,宏常量被存入了hello.i中。接着,编译器cc1又对它进行了一番处理,hello.c变成了较为低级的存在——hello.s汇编代码,但是这远远不够,只有经历了编译器的考验,变成机器能够认识的语言——机器语言,才能够继续它的使命。俗话说,一木不成林,仅仅靠hello.o单个程序还是难以运行,只有经过了链接器的处理,有了动态库的合作,hello.c才最终成为了伟大的hello——可执行目标文件。

当在命令行中输入了./hello之后,“Hello World!”完美的出现在了屏幕上,这个过程还有这shell的功劳,shell解析了输入命令行的命令,调用fork()创建子进程,并用execve()在子进程的上下文中加载程序,经过一系列操作,hello终于进入了CPU的内部。

最后,hello运行完成,它完成了它伟大的使命,默默的消失在了茫茫的计算机系统中,被回收,再次回到了孤单的磁盘之中。

计算机系统,知识很多,难度很大,但是内容真的很有趣,我很喜欢,我一定会在未来继续深入探究。


附件

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

文件名

作用

hello.c

Hello的源文件

hello.i

Hello的修改了的源程序

hello.s

Hello的汇编文件

hello.o

Hello的可重定位目标文件

hello

Hello的可执行目标文件

elfheader.txt

hello.o的ELF头

secheader.txt

hello.o的节头部表

relahello.txt

hello.o的重定位节

hellosymbol.txt

hello.o的符号表

disassembly.txt

hello.o的反汇编

helloelfhead.txt

hello的ELF 头

hellosection.txt

hello的节头部表

hellodiss.txt

hello的反汇编

hellorela.txt

hello的重定位节

hellosym.txt

hello的符号表

tree.txt

进程树

表12 中间文件


参考文献

  1.  兰德尔.E.大卫.R. 深入理解计算机系统[M]. 北京:机械工业出版社,2017:7.
  2. objdump(Linux)反汇编命令使用指南[W].CSDN

你可能感兴趣的:(程序人生)