哈工大计算机系统大作业

计算机系统

大作业

​​​​​​​

计算机科学与技术学院

2024年5月

摘  要

本文详细介绍了hello.c程序的一生——从源代码到经过预处理、编译、汇编、链接最终生成可执行目标文件hello,再通过在shell 中键入启动命令后,shell 为其fork,产生子进程,内核为新进程创建数据结构, hello便从可执行程序(Program)变成为进程(Process)。之后为程序分配内存空间,包括读取内存。

关键词:编译;汇编;链接;异常;进程管理;存储;I/O……;                           

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

目  录

第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简介

P2P:程序员通过键盘输入hello.c程序后,hello.c源程序经过了cpp(预处理器)、ccl(编译器)、as(汇编器)、ld(链接器)后,生成了一个可执行的目标程序hello。在shell界面输入./hello指令,shell识别出这不是内置命令,而是一个文件名,故先调用fork函数创建一个子进程,然后子进程运行execve函数来加载hello。

020:shell加载hello,会映射虚拟内存。CPU引用一个被映射的虚拟页时,会将hello中的代码和数据都从磁盘复制到物理内存。在hello运行的过程中,TLB和四级页表提高了地址翻译速度,三级Cache使得CPU快速的搭配需要的字节。最后当程序员摁下Crtl-Z或hello运行到return 0 时,hello所有的进程都被杀死,进程结束。

1.2 环境与工具

硬件环境:12th Gen Inter(R) Core(TM) i5-12500H 2.50GHz 16.0GB RAM

软件环境:Windows11 64位;VMWare17 Ubuntu16.03

开发与调试工具:gcc edb gedit objdump readelf

1.3 中间结果

hello.c

hello源代码

hello.i

预处理之后的文本文件

hello.s

hello的汇编代码

hello.asm

hello反汇编得到的汇编代码

hello_o.asm

hello.o反汇编得到的汇编代码

elf.txt

hello.o的elf文件

elf1.txt

hello的elf文件

hello

hello的可执行文件

hello1

加入-g编译的hello的可执行文件

hello.o

hello的可重定位文件

1.4 本章小结

    本小节主要介绍了hello的P2P和020的过程,以及实验用到的硬件软件环境和调试开发工具,以及编写本论文产生的中间结果文件名。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:预处理是指在程序代码实际编译之前,对代码进行一系列的处理操作。预处理器是一种能够读取代码文件并执行预处理操作的程序,通常是由编译器提供的。

作用:对程序代码中的宏定义进行替换。宏定义是一种特殊的代码片段,它可以被多次调用,从而减少代码量并提高代码的可读性。预处理器会在程序编译之前将这些宏定义进行替换,使得程序代码更加简洁。除了宏定义替换之外,预处理器还可以执行其他一些操作。例如,它可以包含其他文件的内容,这样就可以将程序分成多个文件,从而提高代码的组织性和可维护性。预处理器还可以进行条件编译,根据程序中设置的条件决定是否包含某些代码。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。

2.2在Ubuntu下预处理的命令

linux> gcc -E hello.c -o hello.i

图1 预处理hello.c的命令

2.3 Hello的预处理结果解析

图2 hello.i文件内容

在hello.i文件中,仍然有源程序代码,但是对宏进行了展开,比如#include指令和#define指令。

2.4 本章小结

本章介绍了预处理的操作。预处理是将hello.c源程序进行拓展,插入#include里指定文件,拓展#define声明,产生了新文件hello.i。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:编译阶段是将预处理后的源文件(.i文件)转换成汇编语言代码(.s文件)的过程。编译器在这个阶段将高级语言的语法和结构转换成目标机器的汇编指令。

作用:编译器将高级语言的代码转换成汇编语言,这是机器语言的低级表示形式,更接近于机器能够理解的指令;在编译过程中,编译器会检查代码的语法和语义,确保代码符合语言规范,并且逻辑正确;编译器可能会在这个阶段进行一些优化,例如,去除冗余代码、优化循环结构等,以提高程序的执行效率; 编译器生成的.s文件包含了汇编代码,这些代码需要通过汇编器进一步转换成机器代码。    

3.2 在Ubuntu下编译的命令

linux>gcc -m64 -Og -S -no-pie -fno-PIC hello.i -o hello.s

图3 编译hello.i的命令

3.3 Hello的编译结果解析

   .file    "hello.c"(源文件名)

   .text           (代码段)   

   .section    .rodata.str1.8,"aMS",@progbits,1(.rodata节)

   .align 8         (对齐方式)

.LC0:

   .string    "\347\224\250\346\263\225: Hello 2022113371 \346\235\234\344\275\263\347\241\225 13831863679 4\357\274\201"(字符串)

   .section    .rodata.str1.1,"aMS",@progbits,1(.rodata节)

.LC1:

   .string    "Hello %s %s %s\n"(字符串)

   .text                      (代码段)

   .globl    main             (全局变量名)

   .type    main, @function    (指定是对象类型或是函数类型)

3.3.1数据

C语言中常用的数据有:常量、变量(全局/局部/静态)、表达式、类型、宏。

常量大多是以立即数的形式直接出现在汇编代码里。如:

图4 常量5

这个是if(argc!=5)中的5。

但是有的也会有变化,比如for(i=0;i<10;i++)中的10,在编译过后将i<10编译成了i<=9。

图5  i<=9

变量可以分成全局变量、局部变量、静态变量。已经初始化的全局变量和静态变量被存放在.data节,未初始化的全局变量和静态变量以及所有被初始化为0的全局或静态变量被存放在.bss节,而局部变量被放在栈中管理。

在hello.c的程序中是没有全局变量和静态变量的,所以在最开始的指令中是没有.data节和.bss节的。但是用到了.rodata节,是因为其中存放了字符串。例如:printf(“用法: Hello 学号 姓名 手机号 秒数!\n”)它的汇编代码为movl $.LC0,%edicall puts,可以看到.LC0是要输出的字符串。

但是在程序中是有局部变量的,比如for(i=0;i<10;i++)中的i就是一个局部变量。

图6 初始化i=0

可以看到先将i初始化为零。然后用cmpl和addl实现比较和加一操作。

图7 比较和加一操作

3.3.2赋值

hello.c文件中只有一个地方有赋值操作,即上面提到的i=0,通过movl指令来实现。

图8 用movl指令给i赋值(通过寄存器实现)

3.3.3算术操作

在hello.s中有用到了addl $1,%ebx,用以实现加一操作(i++)。

图9 用addl指令实现算术操作

3.3.4关系操作

在hello.s中主要用到cmp和test操作,它们只设置条件码但是不改变寄存器存储内容。我们用到了cmp指令来判断argc=5i<10,分别为cmpl $5,%edicmpl $9,%edx

3.3.5数组操作

C语言中的数组是一种将标量数据转变为更大的数据类型的方式。对于数据类型T和int类型常数N,声明一个A[N],起始位置表示为x,数组则意味着分配了一个L*N字节的连续区域,其中L的大小等于sizeof(T),并且引入了一个指针,其值为x,i(0~N-1)用于作为访问其中元素的索引。数组元素会被放在地址为x+L*i上。

在hello.s中,指针数组为argv,每个数组元素都是指向参数字符串的指针。C语言语句printf(“Hello %s %s %s \n”,argv[1],argv[2],argv[3])的汇编代码为:

图10 数组的汇编代码

其中%rcx,%rdx,%rsi分别向printf传递argv[3]、argv[2]、argv[1]。

3.3.6控制转移

控制转移里面经常用到jump指令。

if语句:在hello.c中if(argv!=5)它的汇编代码为:

图11 If语句的汇编代码

首先使用cmpl指令比较argc和5的大小,然后jne指令根据条件码比较argc和5是否相等,并根据结果进行跳转:如果不相等则跳转到.L6,如果相等则继续运行下一条指令。

for循环:在hello.c中是for(i=0;i<=10;i++)它的汇编代码为:

  

                               图12 给i赋值为0                                   图13 和9比较大小

       首先是对i赋初值为0,然后跳转到循环判断表达式是否满足循环条件,如果是的话就跳转到循环表达式。

3.3.7函数调用

hello.s中有以下几个函数调用:

printf(“用法:Hello 2022113371 杜佳硕 13831863679 4\n”),对应的汇编代码为:

图14 printf汇编代码

直接将参数.LC0传递到%edi中,然后调用puts函数。

exit(1),对应的汇编代码为:

图15 exit汇编代码

直接将参数1传递到%edi中,然后调用exit函数。

atoi(argv[4]),对应的汇编代码为:

图16 atoi汇编代码

首先将传递参数32(%rbp),也就是argv[3],然后调用atoi函数。

printf(“Hello %s %s %s\n”,argv[1],argv[2],argv[3]),对应的汇编代码为:

图17 printf汇编代码

这段代码传递了4个参数,第一个是%edi中的.LC1,第二个是%rsi中的argv[1],第三个是%rdx中的argv[2],第四个是%rcx中的argv[3]。

3.4 本章小结

本章介绍了编译的相关操作。编译是将hello.i文件转为hello.s文件。这是机器语言指令。通过理解汇编代码能够理解编译器的优化能力。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器将hello.s翻译成机器语言指令,并把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。

作用:汇编语言允许程序员对程序的性能进行精细控制,因为它提供了对硬件的直接访问;汇编语言通常用于编写与硬件紧密相关的代码,如设备驱动程序;操作系统内核和其他系统级软件常常使用汇编语言编写,以确保最高的效率和控制。

4.2 在Ubuntu下汇编的命令

    linux>gcc -m64 -Og -c -no-pie -fno-PIC hello.s -o hello.o

图18 汇编指令

4.3 可重定位目标elf格式

    可重定位目标文件的ELF格式如下:

图19 ELF格式

我们通过readelf工具,具体分析可重定位目标文件hello.o,看看这些节都做些什么。

首先是ELF,如下图:

图20 ELF头

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

图21 ELF头具体信息

重定位节.rela.text存放着代码的重定位条目。当链接器把这个目标文件和其他文件组合时,需要修改这些位置

图22 重定位节

每一个重定位条目的数据结构如下:

图23 同定位条目数据结构

它包括offset需要被修改的引用的节偏移、symbol被修改引用应该指向的符号、type告知链接器如何修改新的引用、以及偏移调整addend

在这里我们关心两种重定位类型中:R_X86_64_PC32和R_X86_64_32

图24 两种重定位类型

    CSAPP中也给出了重定位的计算方法:

图25 重定位的计算方法

重定位节.rela.eh_frameeh_frame 节的重定位信息

图26 重定位信息

符号表:用来存放程序中定义和引用的函数和全局变量的信息。注意,符号表不包含局部变量的条目。

图27 符号表

4.4 Hello.o的结果解析

图28 反汇编代码

4.4.1机器语言的构成

X86-64的指令长度在1到15个字节之间。常用指令以及操作数较少的指令需要的字节数较少,例如push %rbp只需一个字节55、pop %rbp只需一个字节5d;而那些不太常用的指令所需的字节数较多。

设计指令格式的方式是,从某个给定的位置开始,可以将字节唯一的解码成机器指令。

如果指令中有地址或者常数,要按小端存储方式存放。

4.4.2与汇编语言的映射关系

反汇编器是通过分析机器代码中的文件中的字节序列来确定汇编代码的,他不需要访问C语言源代码或者汇编代码,反汇编器使用的指令命名规则与GCC生成的汇编代码有细微差别。例如省略了很多指令结尾的q。

分支跳转:如判断argv!=5,然后跳转。在hello.s中是jump到.L6:

图29 hello.s代码中的jump

而在hello.o中,是先用指令cmp再跳转到地址19:

图30 hello.o中的跳转

函数调用:在调用printf函数时,printf(“Hello %s %s %s\n”,argv[1],argv[2],argv[3])在hello.s中是将参数传递到寄存器之后,call后面紧跟带用函数声明:

图31 hello.s中的printf调用

在hello.o中,call后面紧跟的是地址48:

图32 hello.o中的printf函数调用

在hello.o中,printf在.rodata格式串也不再表示为.LC1,而用mov$0x0,%edi表示。

4.5 本章小结

本章介绍了汇编及其过程。汇编器将汇编语言翻译成机器语言指令,把这些指令打包可重定位目标程序,并将结果保存在hello.o中,多个可重定位目标文件可以在连接时合并在一起。当进行反汇编时,反汇编器通过分析机器语言指令中的字节来生成汇编代码。

(第41分)

第5章 链接

5.1 链接的概念与作用

概念:链接是将多个目标文件(.o文件)以及它们依赖的库文件合并成一个单一的可执行文件的过程。在链接过程中,链接器(Linker)会解决目标文件之间的依赖关系,例如函数调用和变量引用,并将它们整合到一个可执行文件中。

作用:在编程时,我们经常需要调用其他模块或库中的函数。链接器会查找这些函数在目标文件或库文件中的定义,并在可执行文件中正确地引用它们;链接器可以进行一些代码优化,比如去除未被使用的代码和数据(称为死代码消除),以及合并相同代码片段等。

5.2 在Ubuntu下链接的命令

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

图33 Ubuntu下链接的命令

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

可执行目标文件的格式类似于可重定位目标文件,典型的ELF可执行文件的格式如下图所示:

图34 ELF可执行目标文件格式

图35 ELF头详细信息

ELF头描述了文件的总体格式。可执行目标文件中的入口点地址给出执行程序时的第一条指令的地址:0x4010f0,而在可重定位目标文件中此值没有意义。

可执行目标文件的ELF头中的程序头部有了实际意义,是一个结构体数组,记录着程序加载到主存中的重要信息;而且可执行目标文件还多了一个.init节,用于定义init函数(初始化);因为是完全链接故不需要重定位,也就少了.rel节。

图36 节头(1)

图37 节头(2)

可以查看hello各段的信息,例如:.init段在该目标文件的偏移是0x401000,大小是0x1b,Flags是AX,即可分配可执行。

图38 程序头

程序头部描述了可执行文件的连续的片被映射到连续的内存段的映射关系。每个表项提供了各段在虚拟地址空间大小和物理地址,标志,访问权限和对齐方式。我们可以以此读出隔断的起始地址。

5.4 hello的虚拟地址空间

用edb加载hello。

图39 edb加载hello 0x400000地址处

首先从0x400000处开始看,发现第一行与ELF头中的Magic是一样的。

图40 ELF头 Magic内容

再用edb查看.interp。发现进程中.interp的首地址与elf1.txt显示的0x4002e0相同。

图41 edb查看.interp

再用edb查看.text。发现.text的首地址与elf1.txt显示的0x4010f0相同。

图42 edb查看.text

再用edb看.rodata节。.rodata的首地址与elf1.txt显示的0x402000相同

图43 edb看.rodata节

5.5 链接的重定位过程分析

图44 main函数(1)

图45main函数(2)

通过简单对比首先可以得出,用objdump -d -r hello得到的反汇编代码,里面连接了_init、puts@plt、printf@plt、getchar@plt等外部函数。

其次,hello得到的反汇编代码有各个字节运行时的虚拟地址,而hello.o只有偏移的信息。

图46 hello.o得到的反汇编代码

最后,还有跳转指令的差异。hello.o跳转指令后加的是汇编代码块前的标号,而hello得到的反汇编代码的跳转指令后加的是具体的地址,但相对地址没有发生变化。

重定位过程:重定位过程分为重定位/符号定义和重定位节的符号引用。首先链接器将所有相同类型的节合并为同一类型的聚合节并将地址赋给新的聚合节,之后每个节和符号都被赋予一个地址;然后是符号引用,链接器对代码节和符号进行修改,使其指向正确的地址。这一过程依赖于重定位条目的type。

5.6 hello的执行流程

图47 edb执行hello

图48 函数列表(1)

图49 函数列表(2)

用edb运行hello,右键点击analyse here可以得到hello所有的函数列表信息。

5.7 Hello的动态链接分析

 程序调用有一个共享库定义的函数时,编译器无法预测函数的地址,因为这个函数的共享模块会被加载到任何位置。故编译系统采用延迟绑定的策略,将过程地址的绑定推迟到第一次调用该过程时。

以printf函数为例分析在dl_init前后,GOT和PLT中内容的变化。图中0x401049处的指令是跳转到动态链接器,动态链接器根据栈中的参数修改GOT中对应的一个条目的地址,导致下一次跳转到0x4010a4处的指令时就直接跳转到printf函数的地址了。

图50 edb查看PLT

调用动态链接器之前,0x404020处的八个字节为40 10 40 00 ,调用之后变为了0x7fea1e0606f0,恰好变为了printf的地址。

图51 调用动态链接器重定位之前

5.8 本章小结

本章介绍链接及其过程。不得不说,虽然执行链接的指令很简单,但是真的要分析链接内部的完整过程真的很麻烦。链接就是将各种代码和数据片段收集合并为一个单一文件的过程。链接可以自编译时由静态编译器完成,在加载和运行时可以由动态编译器完成。链接器处理三种文件:可重定位目标文件、可执行目标文件、共享文件。主要任务为符号解析和重定位。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

概念:一个执行中的程序的实例,同时也是系统进行资源分配和调度的基本单位。一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

作用:给应用程序提供两个关键抽象:一个独立的逻辑流,它提供一个假象,好像我们的程序独占地使用处理器;一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

6.2.1作用

命令解释:Shell 读取用户输入的命令,并将其转换为操作系统可以理解的格式,然后执行这些命令。

脚本编写:用户可以编写 Shell 脚本来自动化任务,Shell 脚本是一系列可以顺序执行的命令。

文件操作:Shell 提供了丰富的命令来创建、复制、移动和删除文件以及目录。

程序执行:Shell 可以启动和控制其他程序的执行。

管道和重定向:Shell 允许用户将一个命令的输出作为另一个命令的输入(管道),以及重定向命令的输入和输出。

环境变量管理:Shell 允许用户查看和设置环境变量,这些变量定义了用户的工作环境。

进程管理:Shell 提供了工具来查看、启动、停止和控制后台进程。

网络通信:Shell 支持网络命令,允许用户与远程服务器进行通信。

6.2.2处理流程

启动:当 Shell 启动时,它会加载用户的配置文件(如 .bashrc 或 .profile),初始化环境设置。

读取命令:Shell 等待用户输入命令,或者从脚本文件中读取命令。

命令解析:Shell 解析命令行,识别命令和参数。

命令查找:Shell 在环境变量 PATH 指定的目录中查找可执行文件。

执行命令:找到命令后,Shell 会执行该命令。

处理输入和输出:Shell 管理命令的输入和输出,包括标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。

等待命令完成:Shell 等待命令执行完成,然后返回结果。

错误处理:如果命令执行出错,Shell 会显示错误信息,并根据需要处理错误。

循环:Shell 继续读取和执行命令,直到用户退出 Shell 或关闭终端。

6.3 Hello的fork进程创建过程

父进程调用fork函数创建一个新的子进程。创建的子进程得到与父进程用户级虚拟地址空间相同的一份副本,并且获得了与父进程任何打开文件描述符相同的副本,这就意味着当父进程fork时,子进程可以读取父进程打开的任何文件。不过这两个进程的区别在于PID不相同。子进程fork返回0,返回值可以提供明确的方法区分父进程还是子进程。

6.4 Hello的execve过程

execve函数在当前进程的上下文加载并运行一个新程序。execve函数加载并运行可执行目标文件filename,并袋参数列表argv和环境变量envp。只有当出现错误,比如找不到filename,execve才会返回到调用程序,调用成功则不用返回。execve一次调用从不会返回。

6.5 Hello的进程执行

在操作系统中,每一时刻都会有许多程序在进行。但是通常认为每个进程都独立占用CPU的某些资源,如果单步调试程序可以发现在执行时一系列程序计数器的值,这个PC的值就是逻辑控制流。事实上程序的执行是采用并行的方式,都是交错的,它们轮流使用处理器,每一个进程执行它的流的一小部分然后被抢占,之后轮到其他进程。

图52 进程执行并行图

操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务:内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程一打开文件的信息的文件表。上下文切换的流程是:1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。

图53 进程切换流程

为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常使用某个控制寄存器的一个模式位提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

hello的进程调度:hello的上下文切换用到了一个sleep函数。当调用sleep函数时,hello显式地请求休眠,控制转移给另一个进程此时计时器开始计时,当计时器达到argc[4]时,即4s时,产生中断信号,中断当前正在进行的进程,上下文切换,恢复hello在休眠期前的上下文信息,控制权回到hello继续运行。循环结束后,hello调用getchar函数,之前hello运行在用户模式下,在调用getchar时进入内核模式,内核中的陷阱处理程序请求来自键盘缓冲区的 DMA传输,并执行上下文切换,并把控制转移给其他进程。当完成键盘缓冲区到内存的数据传输后,引发一个中断信号,此时内核从其他进程切换回 hello 进程,然后 hello执行 return,进程终止。

6.6 hello的异常与信号处理

hello在运行时会产生四种异常:中断、陷阱、故障、终止。

  1. 中断中断是来自 I/O 设备的信号,异步发生,中断处理程序对其进行处理,返 回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
  2. 陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指 令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
  3. 故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成 功,则将控制返回到引起故障的指令,否则将终止程序。
  4. 终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程 序会将控制返回给一个 abort 例程,该例程会终止这个应用程序。

下面进行对hello执行中各种信号和异常处理的分析。

  1. 正常运行

程序正常运行,需要经历十次循环,最后需要输入一个字符回车结束程序

图54正常运行

  1. 中途按下Ctrl-Z

内核向前台发送SIGSTP信号,前台进程被挂起,直到通知它继续的信号到来,当按下fg 1后,输出命令行后被挂起的程序从暂停处继续运行。

图55 按Ctrl-Z并用fg 1 恢复

  1. 中途按下Ctrl-C

内核向前台发送一个SIGINT信号,前台进程终止,内核再向父进程发送一个SIGCHLD信号,通知父进程回收子进程,此时子进程不再存在

图56 按Ctrl-C

  1. 运行中途乱按

运行中途乱按只是将乱按的内容输出,程序继续执行,但是输入的内容到第一个回车之前会被getchar缓冲掉,后面的输入只会被当作即将要执行的命令出现在下次要运行的命令行处。

图57 运行途中乱按

  1. 输入ps打印前台进程组(之前Ctrl-Z已经展示过了)
  2. 打印进程树

详细显示了从开机地各个进程的父子关系,以一颗树的形式展现

图58 进程树(1)

图59 进程树(2)

图60 进程树(3)

图61 进程树(4)

图62 进程树(5)

  1. 列出jobs当前的任务

图63 jobs打印进程状态信息

  1. 输入fg 1,继续执行前台程序

图64 继续执行前台程序

  1. Kill

跟据PID用kill将进程杀死

图65 kill进程

6.7本章小结

本章主要介绍了对于hello进程的管理。进程时一个执行中的程序的实例,它提供了两个关键抽象:好像我们在都在独占CPU,好像我们都在独自使用内存系统。

每个进程都有自己的上下文,也都处于别人的上下文中,用于操作系统通过上下文切换进行进程调度。用户通过shell与操作系统交互,shell通过fork函数和execve函数来执行可执行文件。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是由程序生成的地址,它是程序员编写代码时使用的地址。在现代操作系统中,逻辑地址通常指的是虚拟地址,因为操作系统会将这些地址映射到物理地址。逻辑地址允许程序员编写代码而不必担心物理内存的实际布局。

线性地址:线性地址是在虚拟内存和物理内存之间转换之前的地址。它是CPU在分页机制下看到的地址,即在虚拟地址转换为物理地址之前的地址。在没有启用分页机制的情况下,线性地址直接映射到物理地址。

虚拟地址:虚拟地址是现代操作系统中使用的一个概念,它允许每个进程拥有自己的地址空间。操作系统通过内存管理单元(MMU)将虚拟地址映射到物理地址,这个过程称为地址转换。虚拟地址空间使得每个进程都好像在独立的内存空间中运行,提高了内存使用的安全性和灵活性。

物理地址:物理地址是实际内存芯片(RAM)中存储数据的位置。它是CPU通过内存总线访问内存时使用的地址。物理地址是直接对应到硬件的地址,是实际存储数据的地方。

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

逻辑地址空间表示:段地址:偏移地址。段地址+偏移地址=线性地址。在实模式下:逻辑地址 CS:EA=CS*16+EA 物理地址在保护模式下:以段描述符作为下标,到 GDT/LDT 表查表获得段地址,段内偏移量是在链接后就已经得到的 32 位地址,因此要想由逻辑地址得到线性地址,需要根据逻辑地址的前 16 位获得段地址,这 16 位存放在段寄存器中。

段寄存器(16 位):用于存放段选择符。CS(代码段):程序代码所在段;SS(栈段):栈区所在段;DS(数据段):全局静态数据区所在段;其他三个段寄存器 ES、GS 和 FS 可指向任意数据段。

段选择符中字段的含义:

图66段选择符

其中 CS 寄存器中的 RPL 字段表示 CPU 的当前特权级。

TI=0,选择全局描述符表(GDT);TI=1,选择局部描述符表(LDT);RPL=00 为第 0 级,位于最高级的内核态;RPL=11 为第 3 级,位于最低级的用户态。高 13 位-8K 个索引用来确定当前使用的段描述符在描述符表中的位置。

图67 寄存器

段描述符是一种数据结构,等价于段表项,分为两类。一类是用户的代码段和数据段描述符,一类是系统控制段描述符。

描述符表:实际上为段表,由段描述符(段表项构成)分为三种类型:

全局描述符表 GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及 TSS(任务状态段)等都属于 GDT 中描述的段

局部描述符表 LDT:存放某任务(即用户进程)专用的描述符

中断描述符表 IDT:包含 256 个中断门、陷阱门和任务门描述符

下图展示了逻辑地址到线性地址的转化过程:

图68转化过程

首先根据段选择符的 TI 部分判断需要用到的段选择符表是全局描述符表还是局部描述符表,随后根据段选择符的高 13 位的索引(描述符表偏移)到对应的描述符表中找到对应的偏移量的段描述符,从中取出 32 位的段基址地址,将 32 位的段基址地址与 32 位的段内偏移量相加得到 32 位的线性地址。

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

Linux下,虚拟地址到物理地址的转化与翻译是依靠页式管理来实现的,虚拟内存作为内存管理的工具。概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。磁盘上数组的内容被缓存在物理内存中(DRAM cache)这些内存块被称为页 (每个页面的大小为P = 2p字节)。

   而分页机制的作用就是通过将虚拟和物理内存分页,并且通过MMU建立起相应的映射关系,可以充分利用内存资源,便于管理。一般来说一个页面的标准大小是4KB,有时可以达到4MB。而且虚拟页面作为磁盘内容的缓存,有以下的特点:DRAM缓存为全相联,任何虚拟页都可以放置在任何物理页中需要一个更大的映射函数,不同于硬件对SRAM缓存更复杂精密的替换算法太复杂且无限制以致无法在硬件上实现DRAM缓存总是使用写回,而不是直写。

      虚拟页面地集合被分为三个不相交的子集:已缓存、未缓存和未分配。

图69 虚拟内存与物理内存的映射

页表实现从虚拟页到物理页的映射,依靠的是页表,页表就是是一个页表条目 (Page Table Entry, PTE)的数组,将虚拟页地址映射到物理页地址。这个页表是常驻与主存中的。

图70 页表

下图展示了页式管理中虚拟地址到物理地址的转换:       

图71 虚拟地址到物理地址的转换

下图a展示了当页面命中时,CPU硬件执行的步骤:

第1步:处理器生成一个虚拟地址,并把它传送给MMU;

第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它;

第3步:高速缓存/主存向MMU返回PTE;

第4步:MMU构造物理地址,并把它传送给高速缓存/主存;

第5步:高速缓存/主存返回所请求的数据字给处理器

图72 页面命中和缺页的操作图

处理缺页如图b所示:

第1~3步:和图a中的第1步到第3步相同;

第4步:PTE中的有效位是零,所以MMU触发了一次异常,传给CPU中的控制到操作系统内核中的缺页异常处理程序;

第5步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘;

第6步:缺页处理程序页面调入新的页面,并更新内存中的PTE;

第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,在MMU执行了图b中的步骤之后,主存就会将所请求字返回给处理器。

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

为了消除每次 CPU 产生一个虚拟地址,MMU 就查阅一个 PTE 带来的时间开销,许多系统都在 MMU 中包括了一个关于 PTE 的小的缓存,称为翻译后被缓冲器(TLB),TLB 的速度快于 L1 cache。

图73 虚拟地址中用以访问TLB的组成部分

TLB 通过虚拟地址 VPN 部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU 在读取 PTE 时会直接通过 TLB,如果不命中再从内存中将PTE 复制到 TLB。同时,为了减少页表太大而造成的空间损失,可以使用层次结构的页表页压缩页表大小。core i7 使用的是四级页表。

图74 一个两级页表层次结构。注意地址是从上往下增加的

在四级页表层次结构的地址翻译中,虚拟地址被划分为 4 个 VPN 和 1 个 VPO。每个 VPNi 都是一个到第 i 级页表的索引,第 j 级页表中的每个 PTE 都指向第 j+1级某个页表的基址,第四级页表中的每个 PTE 包含某个物理页面的 PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定 PPN 之前,MMU 必须访问四个PTE。

图75 Core i7的地址翻译

综上,在四级页表下,MMU 根据虚拟地址不同段的数字通过 TLB 快速访问得到下一级页表的索引或者得到第四级页表中的物理页表然后与 VPO 组合,得到物理地址(PA)。

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

Core i7的内存系统如图所示。

图76 Core i7 内存系统

首先,根据物理地址的 s 位组索引索引到 L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为 1,若有,则说明命中,从这一行对应物理地址 b 位块偏移的位置取出一个字节,若不满足上面的条件,则说明不命中,需要继续访问下一级 cache,访问的原理与 L1 相同,若是三级 cache 都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。

图77 根据物理地址取数据的过程

7.6 hello进程fork时的内存映射

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

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

图78 一个私有的写时复刻对象

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 做的最后一件事情就是设置当前进程上下 文的程序计数器,使之指向代码区域的入口点。

图79 加载器是如何映射用户地址空间的区域的

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

DRAM缓存不命中称为缺页,即虚拟内存中的字不在物理内存中。CPU引用了虚拟页的一个字,地址翻译硬件从内存中读取了该虚拟页对应的页表条目,从有效位推断出该页未被缓存,这样就触发了一个缺页异常,缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,把要缓存的页缓存到牺牲 页的位置。如果这个牺牲页被修改过,就把它交换出去。当缺页处理程序返回时, CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。

下图对VP3的引用不命中,从而触发缺页。

图80 对VP3的引用不命中

缺页之后,缺页处理程序选择VP4作为牺牲页,并从磁盘上用VP3的副本取代它。在缺页处理程序重新启动导致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常

图81 VM缺页之后,VP4为牺牲页

7.9动态存储分配管理

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

分配器分为两种基本风格:显式分配器、隐式分配器。

1. 显式分配器:要求应用显式地释放任何已分配的块。

2. 隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这 个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

带边界标签的隐式空闲链表分配器原理:

图82 带边界标签的隐式空闲链表分配器原理

每个块增加四字节的头部和四字节的脚部保存块大小和是否分配信息,可以在 常数时间访问到每个块的下一个和前一个块,使空闲块的合并也变为常数时间,而且可以遍历整个链表。隐式空闲链表即为,利用边界标签区分已分配块和未分配块,根据不同的分配策略(首次适配、下一次适配、最佳适配),遍历整个链表,一旦找到符合要求的空闲块,就把它的已分配位设置为1,返回这个块的指针。隐式空闲链表并不是真正的链表,而是"隐式"地把空闲块连接了起来(中间夹杂着已分配块)。

显式空闲链表的基本原理:

图83 显式空闲链表的基本原理

因为隐式空闲链表每次查找空闲快都需要线性地遍历整个链表,而其中的已分配块显然是不需要遍历的,所以浪费了大量时间,一种更好的方式是把空闲块组织成一个双向链表,每个空闲块中包含一个 pred 和 succ 指针,指向它的前驱和后继,在申请空闲块时,就不需要遍历整个堆,只需要利用指针,在空闲链表中遍历空闲块即可。一旦空闲块被分配,它的前驱和后继指针就不再有效,变成了有效载荷的一部分。显式空闲链表的已分配块与隐式空闲链表的堆块的格式相同。

7.10本章小结

本章主要介绍了hello的存储管理机制,介绍了逻辑地址到线性地址的转化、线性地址与物理地址之间的转化、分页机制的原理和硬件优化、fork和execve有关虚拟地址的操作、缺页故障机器处理,最后还讲到了动态内存分配管理。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行.

一个 Liunx 文件就是一个 m 个字节的序列:B0,B1,…,Bm-1。所有的 I/O 设备都被模型化为文件。 而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅 的映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix I/O 接口的几种操作:

1. 打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描 述符),用于标识这个文件。程序在只要记录这个描述符便能记录打 开文件的所有信息。

2. shell 在进程的开始为其打开三个文件:标准输入、标准输出和标准错 误。

3. 改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位 置 k,初始为 0。这个文件位置是从文件开头起始的字节偏移量。应用 程序能够通过执行 seek 操作显式地设置文件的当前位置为 k。

4. 读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n。给定一个大小为 m 字节的文件, 当 k>=m 时执行读操作会出发一个称为 EOF 的条件,应用程序能检测 到这个条件,在文件结尾处并没有明确的 EOF 符号。

5. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源, 并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终 止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O 函数:

 1. int open(char *filename, int flags, mode_t mode);

open 函数将 filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。

 2. int close(int fd);

关闭一个打开的文件。

 3. ssize_t read(int fd, void *buf, size_t n);

read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际 传送的字节数量。

4. ssize_t write(int fd, const void *buf,size_t);

write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件 位置。

8.3 printf的实现分析

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.

先找到 printf 的函数定义:

图84 printf内部定义代码

其中 va_start()和 va_end 是获取可变长度参数的函数,任何可变长度的变元被访问之前,必须先用 va_start()初始化变元指针 argptr。初始化 argptr 后,经过对va_arg()的调用,以作为下一个参数类型的参数类型,返回参数。最后取完所有参数并从函数返回之前。必须调用 va_end()。由此确保堆栈的正确恢复。然后,printf 调用了 write 函数,这是 Unix I/O 函数,用以在屏幕输出长度为 i 的在 printbuf 位置的字节。这里 i = vsprintf(printbuf, fmt, args),,所以关键在于 vsprintf 函数。

图85 vsprintf内部代码

vsprintf 的功能就是将 printf 的参数按照各种各种格式进行分析,将要输出的字符串存在 buf 中,最终返回要输出的字符串的长度。

接着就轮到 write 系统函数了,在 Linux 下,write 函数的第一个参数为 fd,也就是描述符,而 1 代表的就是标准输出。查看 write 函数的汇编实现可以发现,它首先给寄存器传递了几个参数,然后执行 int INT_VECTOR_SYS_CALL,代表通过系统调用 syscall,syscall 将寄存器中的字节通过总线复制到显卡的显存中。显示芯片按照刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。由此 write 函数显示一个已格式化的字符串。

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

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

8.4 getchar的实现分析

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

再看 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;

}

可以看到,getchar 调用了 read 函数,read 函数也通过 sys_call 调用内核中的系统函数,将读取存储在键盘缓冲区中的 ASCII 码,直到读到回车符,然后返回整个字符串,getchar 函数只从中读取第一个字符,其他的字符被缓存在输入缓冲区。

8.5本章小结

本章主要讲述了linux的I/O设备管理方法,Unix I/O接口机器函数,以及printf和getchar函数的实现方法。

(第81分)

结论

没想到一个小小的hello.c,可以有这么多的考虑空间,涉及到这么多的知识点,它经历了许多,从出生到死亡。在表面看来只是简单地按下编译运行的按钮,但是里面真的是经历了很多啊,也只有学过CSAPP才能知道这些课。

一个程序的经历是这样的:程序员使用编程语言编写程序代码,设计程序的逻辑和功能。编写好的源代码需要通过编译器转换成机器可以执行的指令。编译过程通常包括预处理、编译、汇编和链接四个步骤。预处理:处理源代码中的预处理指令。编译:将源代码转换成汇编语言。汇编:将汇编语言转换成机器码。链接:将多个目标文件和库文件链接在一起,生成可执行文件。编译完成后,会生成一个可执行文件(在UNIX-like系统中通常是ELF格式)。当用户运行程序时,操作系统会将可执行文件加载到内存中,并分配必要的资源,如内存空间、文件描述符等。操作系统为程序创建一个进程,分配一个唯一的进程标识符(PID)。进程是程序运行的实例,拥有独立的内存空间和系统资源。程序开始执行,CPU开始按照程序的指令进行运算。操作系统负责进程的调度、内存管理、I/O操作等。程序执行完成后,会自动结束运行,操作系统回收资源。用户或系统管理员也可以通过发送信号(如SIGINT、SIGTERM)来请求终止进程。如果进程异常终止(如遇到未捕获的异常),操作系统会进行相应的处理,如清理资源。如果进程没有响应或占用过多资源,系统管理员可能会使用更强的信号(如SIGKILL)强制终止进程。操作系统也会在系统关闭或重启时,终止所有运行中的进程。进程结束后,操作系统会回收分配给该进程的所有资源,包括内存、文件描述符等。

这就是一个程序的一生。

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

附件

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

hello.c

hello源代码

hello.i

预处理之后的文本文件

hello.s

hello的汇编代码

hello.asm

hello反汇编得到的汇编代码

hello_o.asm

hello.o反汇编得到的汇编代码

elf.txt

hello.o的elf文件

elf1.txt

hello的elf文件

hello

hello的可执行文件

hello1

加入-g编译的hello的可执行文件

hello.o

hello的可重定位文件

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

参考文献

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

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

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

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值