摘 要
本论文将CSAPP课程所学内容通过hello小程序的一生,对我们所学进行全面的梳理与回首。主要在Ubuntu下进行相关使用,对hello程序从.c文件到可实行文件再到被系统接纳的过程进行了全面的梳理与剖析。使用了许多Ubuntu下的使用工具,进行细致的历程分析,目的是加深对计算机系统的了解,同时进一步对所学知识进行复习。
关键词:hello;程序调用;计算机系统;Ubuntu ;
(摘要0分,缺失-1分,根据内容出色称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
1.2 环境与工具
1.3 中心结果
1.4 本章小结
第2章 预处置惩罚
2.1 预处置惩罚的概念与作用
2.2在Ubuntu下预处置惩罚的下令
2.3 Hello的预处置惩罚结果解析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的下令
3.3 Hello的编译结果解析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的下令
4.3 可重定位目的elf格式
4.4 Hello.o的结果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的下令
5.3 可实行目的文件hello的格式
5.4 hello的虚拟地址空间
5.5 链接的重定位过程分析
5.6 hello的实行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 hello进程管理
6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处置惩罚流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程实行
6.6 hello的异常与信号处置惩罚
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处置惩罚
7.9动态存储分配管理
7.10本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献
第1章 概述
1.1 Hello简介
P2P:
- Program: 在 editor 中键入代码得到 hello.c 程序
- Process: hello.c(在 Linux 中),颠末过 cpp 的预处置惩罚、 ccl 的编译、 as 的汇编、 ld 的链接最终成为可执目的程序 hello。 在 shell 中键入启动下令后, shell 为其 fork,产生子进程。
020:
- shell 为 hello 进程 execve,映射虚拟内存,进入程序入口后程序开始载入物理内存。
- 进入 main 函数实行目的代码, CPU 为运行的 hello 分配时间片实行逻辑控制流。
- 当程序运行竣事后, shell 父进程负责接纳 hello 进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:处置惩罚器:AMD® Ryzen 7 6800H with radeon graphics × 2
RAM:16.00GB 系统类型:64位使用系统,基于x64的处置惩罚器
软件环境:Windows11 64位;Ubuntu 22.04.4 LTS
开发与调试工具:gcc,as,ld,vim,edb,gdb,readelf,VScode
1.3 中心结果
文件的作用
| 文件名
| 预处置惩罚后的文件
| hello.i
| 编译之后的汇编文件
| hello.s
| 汇编之后的可重定位目的文件
| hello.o
| 链接之后的可实行目的文件
| Hello
| Hello.o 的 ELF 格式
| Elf.txt
| Hello.o 的反汇编代码
| Disas_hello.s
| hello的ELF 格式
| hello1.elf
| hello 的反汇编代码
| hello1_objdump.s
|
1.4 本章小结
本章对hello进行了一个总体的概括,起首介绍了P2P、020的意义和过程,介绍了作业中的硬件环境、软件环境和开发工具,末了简述了从.c文件到可实行文件中心经历的过程。
(第1章0.5分)
第2章 预处置惩罚
2.1 预处置惩罚的概念与作用
预处置惩罚的概念:
程序计划领域中,预处置惩罚一般是指在程序源代码被翻译为目的代码的过程中,生成二进制代码之前的过程。典范地,由预处置惩罚器(preprocessor) 对程序源代码文本进行处置惩罚,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处置惩罚成为特定的单位——(用C/C++的术语来说是)预处置惩罚记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
预处置惩罚的作用:
- 将源文件中用#include 形式声明的文件复制到新的程序中。好比 hello.c 第 7-9 行中的#include<stdio.h> 等下令告诉预处置惩罚器读取系统头文件 <stdio.h> <unistd.h> <stdlib.h> 的内容,并把它直接插入到程序文本中。
- 宏界说与更换。预处置惩罚器允许界说带参数的宏,在编译前会用实际值更换用#define 界说的字符串。
- 特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用符合的值进行更换。
- 注释去除。在预处置惩罚阶段,所有注释(// 和 /* ... */)都会被移除,确保它们不会干扰后续的编译过程。
2.2在Ubuntu下预处置惩罚的下令
指令:cpp hello.c > hello.i
图1 ubuntu中的预处置惩罚过程
2.3 Hello的预处置惩罚结果解析
查看编译结果hello.i显示,文本内容已经达到3092行;原来hello.c的主函数被放到了文末(3078~3092行)。
图2 hello.i末端的hello.c主函数
在这之前出现的是头文件<stdio.h> <unistd.h> <stdlib.h> 的内容依次展开,如果头文件中仍然有以字符“#”开头的内容,则预处置惩罚器继续对其进行处置惩罚,最终的hello.i文件中没有宏界说、文件包罗及条件解析等内容。以stdio.h的展开为例:
图3 hello.i中的库文件展开
图4 库中预置函数(部分)
2.4 本章小结
本章主要介绍了预处置惩罚(包罗头文件的展开、宏更换、去掉注释、条件编译)的概念和作用,以及Ubuntu下预处置惩罚的cpp指令,同时针对hello.c文件的预处置惩罚结果hello.i进行了文本文件解析,详细了解了预处置惩罚的内涵。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译:1、使用编译程序从源语言编写的源程序产生目的程序的过程。 2、用编译程序产生目的程序的动作。编译就是把高级语言变成计算机可以识别的二进制语言,计算机只熟悉1和0,编译程序把人们熟悉的语言换成二进制的。 编译程序把一个源程序翻译成目的程序的工作过程分为五个阶段:词法分析;语法分析;语义查抄和中心代码生成;代码优化;目的代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
编译的作用:
1.编译器起首会查抄源代码的语法是否正确。如果发现语法错误,编译器会生成错误信息并制止编译过程。
2.在语法查抄之后,编译器会进行语义分析,确保代码的逻辑和意义是正确的。比方,查抄变量是否在使用前已声明,函数调用时参数的类型和数量是否匹配等。
3. 编译器会尝试优化代码,以进步生成的目的代码的性能。比方,移除不必要的计算、简化表达式、在适当的时间使用寄存器而不是内存等。
4. 编译器会将颠末优化的中心表现生成目的代码。目的代码可以是呆板码,或者是某种中心代码(如字节码)。
5. 链接器将多个目的文件和库文件链接在一起,生成最终的可实行文件。链接阶段可以解决符号引用问题,即将函数调用和变量引用解析到实际的内存地址。
6. 在编译过程中,编译器不仅会进行语法和语义查抄,还会检测其他类型的错误,如类型不匹配、未界说的变量或函数等。
(留意:这儿的编译是指从 .i 到 .s 即预处置惩罚后的文件到生成汇编语言程序)
3.2 在Ubuntu下编译的下令
指令:gcc -S hello.i -o hello.s
图5 编译指令
3.3 Hello的编译结果解析
3.3.1.1常量
在if语句
if(argc!=5)
中,常量5的值保存在.text中,作为指令的一部分:
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $5, -20(%rbp)
je .L2
同理,语句
for(i=0;i<10;i++){
printf("Hello %s %s %s\n",argv[1],argv[2],argv[3]);
sleep(atoi(argv[4]));
}
中的常量0,10,1,2,3,4也被保存在.text节中。
在
printf(" Hello 2022113291 毛瑞鑫 18691707113 3!\n");
中出现的常量则被保存在.rodata中作为只读常量。
.LC0:
.string " Hello 2022113291 \346\257\233\347\221\236\351\221\253 18691707113 \347\247\222\346\225\260\357\274\201"
3.3.1.2 变量
全局变量:
初始化的全局变量储存在.data节,它的初始化不需要汇编语句,而是直接完成的。
局部变量:
局部变量存储在寄存器或栈中。程序中的局部变量i界说句:
int i;
在汇编代码中
.L2:
movl $0, -4(%rbp)
jmp .L3
此处是循环前i=0的使用,i被保存在栈当中、%rsp-4的位置上。
3.3.2 算术使用
在for循环语句中,使用了自增使用符(++):
for(i=0;i<10;i++)
在每次循环的竣事时实行,对i进行一次增加,栈上储存i的值加1。
call sleep@PLT
addl $1, -4(%rbp)
3.3.3 控制转移
程序在第14使用用if语句来判断传入参数是否等于5:
if(argc!=5)
{
printf(" Hello 2022113291 毛瑞鑫 18691707113 3 \n");
exit(1);
}
实现该if语句的汇编代码为
cmpl $5, -20(%rbp)
je .L2
je用于判断cmpl产生的条件码,若两个使用数的值不相称则跳转到指定地址。
程序在第18使用用了for循环语句:
for(i=0;i<10;i++)
{
printf("Hello %s %s %s\n",argv[1],argv[2],argv[3]);
sleep(atoi(argv[4]));
}
实现for循环的汇编代码为:
L3:
cmpl $9, -4(%rbp)
jle .L4
jle用于判断cmpl产生的条件码,若后一个使用数的值小于等于前一个使用数的值(9)时则跳转到指定地址。
3.3.4 函数使用
main ( ):
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数调用:被系统启动函数startproc调用。
函数返回:将%eax的值置为0
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 $5, -20(%rbp)
je .L2
leaq .LC0(%rip), %rax
movq %rax, %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
printf ( ):
参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:if判断满足条件后调用,for循环中被调用。
源代码1:
printf(" Hello 2022113291 毛瑞鑫 18691707113 3 \n");
汇编代码1:
.LC0:
.string " Hello 2022113291 \346\257\233\347\221\236\351\221\253 18691707113 \347\247\222\346\225\260\357\274\201"
.LFB6:
cmpl $5, -20(%rbp)
je .L2
leaq .LC0(%rip), %rax
call puts@PLT
源代码2:
printf("Hello %s %s %s\n",argv[1],argv[2],argv[3]);
汇编代码2:
.L4:
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rcx
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
exit ( ):
参数传递:传入的参数为1,再实行退出下令
函数调用:if判断条件满足后被调用.
源代码:
exit(1);
汇编代码:
movl $1, %edi
call exit@PLT
sleep():
参数传递:传入参数atoi(argv[4]),
函数调用:for循环下被调用,call sleep
源代码:
sleep(atoi(argv[4]));
汇编代码:
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
addl $1, -4(%rbp)
getchar ( ) :
函数调用:在main中被调用,call getchar
源代码:
getchar();
汇编代码:
.L3:
call getchar@PLT
movl $0, %eax
Leave
3.3.5 数组/指针/结构使用
main函数所传递的参数中含有指针数组char *argv[]
int main(int argc,char *argv[])
在argv数组中,argv[0]指向输入程序的路径和名称,argv[1],argv[2]和argv[3]分别表现3个字符串。
由于char* 数据类型占8个字节,由
LFB6:
subq $32, %rsp
movl %edi, -20(%rbp)//argc存储于%edi
movq %rsi, -32(%rbp)//argv存储于%rsi
.L4:
leaq .LC1(%rip), %rax
movq %rax, %rdi
movl $0, %eax
call printf@PLT
.LC1:
.string "Hello %s %s %s\n"
.text
.globl main
.type main, @function
对比原函数可知通过%rax-8,%rax-16,%rax-24,分别得到argv[1],argv[2]和argv[3]三个字符串。
3.4 本章小结
本章主要介绍了编译的概念以及过程。同时通过分析代码表现了c语言怎样转换成为汇编代码。介绍了汇编代码怎样实现常量、变量、传递参数以及分支和循环。编译器所做的工作,就是通过语法查抄和代码优化,在确认所有的指令都符合语法规则之后,将其翻译成等价的中心代码表现或使用汇编代码表现。通过分析汇编代码,可以了解程序的运行过程与参数的传递转换,进而从计算机的角度明白代码的运行过程。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
驱动程序运行汇编器,将汇编语言翻译成可实行的呆板语言的过程称为汇编,同时这个呆板语言文件也是可重定位目的文件。
汇编的作用:
汇编就是将高级语言转化为呆板可直接识别实行的代码文件的过程,汇编器将.s 汇编程序翻译成呆板语言指令,把这些指令打包成可重定位目的程序的格式,并将结果保存在.o 目的文件中。.o 文件是一个二进制文件,它包罗程序的指令编码。
(留意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成呆板语言二进制程序的过程。)
4.2 在Ubuntu下汇编的下令
指令:as hello.s -o hello.o
图6 汇编指令
4.3 可重定位目的elf格式
指令:readelf -a hello.o > ./elf.txt
图7 生成可重定位目的elf格式文件
分析:
1.ELF 头:
包罗了系统架构,编码方式,ELF头大小,节的大小和数量等一系列信息。Elf头内容如下:
图8 ELF头
2.节头:
描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
图9 节头
3.重定位节:
表述了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,atoi,sleep,getchar这些符号。
图10 重定位节
4.符号表:
.symtab是一个符号表,它存放在程序中界说和引用的函数和全局变量的信息。
图11 符号表
4.4 Hello.o的结果解析
指令:objdump -d -r hello.o > disas_hello.s
图12 反汇编指令
分析呆板语言的构成,与汇编语言的映射关系。特殊是呆板语言中的使用数与汇编语言不一致,特殊是分支转移函数调用等。
图13 反汇编文件(部分)
分析hello.o的反汇编,并与第3章的 hello.s进行对照分析:
- 数的表现:hello.s中的使用数为十进制,hello.o反汇编代码中的使用数是十六进制。
- 分支转移:跳转语句之后,hello.s中是.L2和.LC1等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,也即间接地址。
- 函数调用:hello.s中,call指令使用的是函数名称,而反汇编代码中call指令使用的是main函数的相对偏移地址。由于函数只有在链接之后才气确定运行实行的地址,因此在.rela.text节中为其添加了重定位条目。
4.5 本章小结
本章对汇编结果进行了详细的介绍。颠末汇编器的使用,汇编语言转化为呆板语言,hello.o可重定位目的文件的生成为后面的链接做了准备。通过对可重定位目的elf格式进行了详细的分析,和对比hello.s和hello.o反汇编代码的区别,能够更深刻地明白汇编语言到呆板语言实现的转变。同时对hello.o文件进行反汇编,将disas_hello.s与之宿世成的hello.s文件进行了对比。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是一个程序,将一个或多个由编译器或汇编器生成的代码和数据部分外加库收集(符号解析和重定位)起来并组合成一个可实行文件的过程。
链接的作用:
令源程序节流空间而未编入的常用函数文件(如printf.o)进行归并,生成可以正常工作的可实行文件。这令分离编译成为大概,节流了大量的工作空间。
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
图14 链接指令
5.3 可实行目的文件hello的格式
指令:readelf -a hello > hello1.elf
1.ELF 头:
图15 ELF头
2.节头:
展示了各个节的大小、类型、地址、偏移量和其他属性。链接器链接时,会将各个文件的相同段归并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
图16 节头
3.程序头
程序头表(Program Header)显示在运行时使用的段(Segments),而节头表(Section Header)则列出了二进制文件的所有节(Sections)的聚集。程序头表主要用于运行时加载和链接。
图17 程序头
5.4 hello的虚拟地址空间
使用edb加载hello,数据转储窗口可以查看加载到虚拟地址中的 hello 程序。查看 ELF 格式文件中的程序头部分,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。在下面可以看出,程序包罗PHDR,INTERP,LOAD ,DYNAMIC,NOTE ,GNU_STACK,GNU_RELRO几个部分,如下图所示。
图18 edb加载后的数据转储部分
此中PHDR 保存程序头表。INTERP 指定在程序已经从可实行文件映射到内存之后,必须调用的解释器。LOAD 表现一个需要从二进制文件映射到虚拟地址空间的段。此中保存了常量数据、程序的目的代码等。DYNAMIC 保存了由动态链接器使用的信息。NOTE 保存辅助信息。GNU_STACK是权限标志,用于标志栈是否是可实行。GNU_RELRO为指定在重定位竣事之后哪些内存区域是需要设置只读。
5.5 链接的重定位过程分析
指令:objdump -d -r hello > hello_objdump.s
图19 反汇编指令
5.5.1分析hello与hello.o的差别,分析链接的过程。
1.链接增加新的函数:在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
2.增加的节:hello中增加了.init和.plt节,和一些节中界说的函数。
图20新增.init和.plt节
3.函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才气确定运行实行的地址,因此在.rela.text节中为其添加了重定位条目。
4.地址访问:hello反汇编的代码有明白的虚拟地址,完成了重定位。
5.5.2结合hello.o的重定位项目,分析hello中对其怎么重定位的。
重定位的过程分为两大步:
1.重定位节和符号界说:在这一步中,毗连器将所有相同类型的节归并成为同一类型的新的聚合节。比方,来自所有输入模块的.data节全部被归并成一个节,这个节成为输出的可实行目的文件的.data节。
2.重定位节中的符号引用:在这一步中,毗连器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。要实行这一步,毗连器依赖于可重定位条目,及5.3节中分析的那些数据。
5.6 hello的实行流程
使用edb实行hello的结果:
图21使用edb实行hello的结果
结合hello的反汇编结果,可以得到hello的实行流程:(地址仅列出后6位)
401000 <_init>
401020 <.plt>
401090 <puts@plt>
4010a0 <printf@plt>
4010b0 <getchar@plt>
4010c0 <atoi@plt>
4010d0 <exit@plt>
4010e0 <sleep@plt>
4010f0 <_start>
401120 <_dl_relocate_static_pie>
401125 <main>
402008 <_IO_stdin_used+0x8>
40203a <_IO_stdin_used+0x3a>
4011c8 <_fini>
5.7 Hello的动态链接分析
在elf中查看与动态链接相关的段:
图22 elf中与动态链接相关的段
.got:GOT是一个数组,此中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包罗动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。别的的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
通过调试可以看出共享链接库代码是动态的目的模块,对动态链接的重定位过程就是在程序开始运行或者调用程序加载时,自动加载该代码到任意的一个内存地址,并和一个在目的模块内存中的应用程序链接起来。在plt和got中分别存放着链接器的目的变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现函数的动态过程链接,这样它就包罗了正确的绝对运行时地址。
5.8 本章小结
本章主要了解了在hello程序在linux中链接的过程。通过查看hello的虚拟地址空间,并且对比hello与hello.o的反汇编代码,得到了hello的实行流程,更好地掌握了链接与之中重定位的过程。同时知道了hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
狭义上的进程指的就是一个实行中程序的实例。广义上的进程是一个具有一定独立功能的程序关于某个数据聚集的一次运行活动。它是使用系统动态实行的基本单位,在传统的使用系统中,进程既是基本的分配单位,也是基本的实行单位。
进程的作用:
进程提供给应用程序的关键抽象:一个独立的逻辑控制流,犹如程序独占处置惩罚器;一个私有的地址空间,犹如程序独占内存系统。
6.2 简述壳Shell-bash的作用与处置惩罚流程
作用:Shell 是一个下令解释器,它解释由用户输入的下令并且把它们送到内核。Shell 有本身的编程语言用于对下令的编辑,它允许用户编写由 shell 下令构成的程序。同时Shell还负责毗连用户和使用系统以及内核。
bash下令的实行分为四大步骤:输入、解析、扩展和实行。
1.输入:在交互模式下,输入来自终端。bash使用GNU Readline库处置惩罚用户下令输入。
2.解析:解析阶段的主要工作为词法分析和语法解析。词法分析指分析器从Readline或其他输入获取字符行,根据元字符将它们分割成word,并根据上下文环境标记这些word(确定单词的类型)。语法解析指解析器和分析器互助,根据各个单词的类型以及它们的位置,判断下令是否正当以及确定下令类型。
3.扩展:扩展阶段对应于单词的各种变换,最终得到可用于实行的下令。
4.实行。
6.3 Hello的fork进程创建过程
根据shell的处置惩罚流程,可以推断,输入下令实行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包罗数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的差别大概在于PID的差别。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4 Hello的execve过程
execve函数用于装载一个可实行文件以进程为单位加载到内存中。只有当出现错误时,比方找不到Hello时,execve才会返回到调用程序,这里与一次调用两次返回的fork差别。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(intargc , char **argv , char *envp);
execve系统调用的实行过程:
- 删除已存在的用户区域(自父进程独立)。
- 映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
- 映射共享区:好比Hello程序与尺度C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
- 设置PC:exceve做的末了一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程实行
上下文信息:使用系统使用一种称为上下文切换的较高条理的异常控制流来实现多任务。由于每个CPU只能同时处置惩罚一个进程,而许多时间系统中有许多进程都要去运行,因此处置惩罚器只能一段时间就要切换新的进程去运行,这时就需要先行存储现在进程的状态,再将欲实行的进程之状态读回CPU中。而实现差别进程中指令交替实行的机制称为进程的上下文切换。
时间片:一个进程实行它的控制流的一部分的每一时间段叫做时间片,它是分时使用系统分配给每个正在运行的进程微观上的一段CPU时间。
示例:printf进程的调度过程
图23 进程的调度过程
初始时,控制流在hello内,处于用户模式
调用系统函数printf后,进入内核态,此时间片制止。
2s后,发送中断信号,转回用户模式,继续实行指令。
调度的过程:
在进程实行的某些时间,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决议就叫做调度,是由内核中称为调度器的代码处置惩罚的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以实行printf函数为例,printf函数请求调用打印进程,printf将内核抢占,进入倒计时,当倒计时竣事后,hello程序重新抢占内核,继续实行。
用户态与核心态转换:
从用户态到内核态切换可以通过三种方式:
系统调用:用户态进程自动切换到内核态的方式.用户态进程通过系统调用申请使用使用系统的提供的程序完成使用.系统调用本身就是中断.
异常:当CPU在实行运行在用户态下的程序时.发生了某些事先不可知的异常.这时会触发由当前运行进程切换到处置惩罚此异常的内核相关程序中.也就转到了内核态.好比缺页异常.
外设中断:当外设完成用户的请求时.会向CPU发送中断信号.
6.6 hello的异常与信号处置惩罚
正常运行:
图24 正常运行
异常类型:
类别
| 原因
| 异步/同步
| 返回行为
| 中断
| 来自I/O设备的信号
| 异步
| 总是返回到下一条指令
| 陷阱
| 故意的异常
| 同步
| 总是返回到下一条指令
| 故障
| 潜在可恢复的错误
| 同步
| 大概返回到当前指令
| 制止
| 不可恢复的错误
| 同步
| 不会返回
|
处置惩罚方式:
图 25 中断处置惩罚方式
图 26 陷阱处置惩罚方式
图 27 故障处置惩罚方式
图 28 制止处置惩罚方式
按下CTRL+Z:进程收到 SIGSTP 信号, hello 进程挂起。
用ps查看其进程PID,可以发现hello的PID是13146;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台。
图29 按下CTRL+Z
按下CTRL+C:进程收到 SIGINT 信号,竣事 hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底竣事。
图30 按下CTRL+C
按下回车:程序换行,运行情况稳固。
图31 按下回车
乱按:只是将屏幕的输入缓存到缓冲区。乱码被以为是下令。
图32 乱按
Kill下令:挂起的进程被制止,在ps中无法查到到其PID。显示程序被逼迫中断,
图33 挂起状态下输入kill下令
6.7本章小结
本章了解了hello进程的实行过程。在hello运行过程中,内核有选择对其进行管理,决定何时进行上下文切换。何时进行内核态与用户态的切换。在hello的运行过程中,当担当到差别的异常信号时,异常处置惩罚程序将对异常信号做出回应,实行相应的代码,每种信号都有差别的处置惩罚机制,对差别的异常信号,hello也有差别的处置惩罚结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址:程序颠末编译后产生的与段相关的偏移地址部分(hello.o)。
- 线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址颠末段机制后转化为线性地址。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
- 虚拟地址:有时我们也把逻辑地址称为虚拟地址。由于与虚拟内存空间的概念雷同,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
- 物理地址:CPU通过地址总线的寻址,对应地址物理内存的地址信号,是地址变换的最终结果地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式存储管理中以段为单位分配内存,每段分配一个连续的内存区,但各段之间不要求连续,内存的分配和接纳雷同于动态分区分配,由于段式存储管理系统中作业的地址空间是二维的,因此地址结构包罗两个部分:段号和段内位移。
段式管理地址变换过程:在段式管理地址变换过程中,和页式变换基本相同,先要为运行的进程建立一个段表。段表包罗:段号、段长、存储权限、状态、起始地址、修改位、增补位。在段式系统中,分段的共享是通过两个作业的段表中相应表目都指向被共享部分的同一个物理副本来实现的,由于段是一个完备的逻辑信息,所以可以共享,但是页不完备,难以实现共享。 不能修改的过程称为纯过程或可重入过程。这样的过程和不能修改的数据是可以共享的,而可修改的程序和数据则不能共享。
图34 段式管理
段式管理的特点:
优点: 1、提供了表里存统一管理的虚拟实现方案
2、段式虚存每次交换的是一个程序段或数据段
3、在段式管理中,段长可以根据需要动态扩充
不足: 1、要求更多的硬件支持,进步了呆板的成本
2、每段的长度受内存可用空闲区大小的限定
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技能,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间分别成若干个长度相称的页(page),页式管理把内存空间按页的大小分别成片或者页面(page frame),然后把页式虚拟地址与内存地址建立逐一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理接纳请求调页或预调页技能实现了表里存存储器的统一管理。
图35 页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
MMU把虚拟地址(VA)转化成物理地址(PA)是通过查询页表(PTE)实现的,PTE中存储了虚拟页到物理页的映射关系,PTE是常驻内存的一个表,如果CPU每次查询都去访问内存访问PTE的话速度太慢,所以引入了TLB(与Cache雷同),TLB是MMU中一个小的具有较高相联度的缓存,其运行机理雷同于Cache,只不过存储的只是PTE而已,通过这样的缓存极大的进步了PTE的访问效率,但对于地址空间位64位的系统来讲,PTE占用了非常多的内存,于是引入了多级页表,第一级页表常驻内存,而别的的级数只在用到的时间创建放入内存中,这样极大的减少了内存的需要。
在访问时,MMU通过把根据虚拟地址查表一级PTE,PTE根据虚拟地址指向下一级页表,下一级页表又根据虚拟地址指向下下级页表,到第四级页表时查询得到详细的物理页号(PPN),根据PPN和VPO(虚拟页面偏移量与物理页面偏移量PPO相同),就可以访问到详细的物理内存了。
图36 使用k级页表进行翻译
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照上述使用得到了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CS(组号),CO(偏移量)。根据CS探求到正确的组,比较每一个cacheline是否标记位有用以及CT是否相称。如果掷中就直接返追念要的数据,如果不掷中,就依次去L2,L3,主存判断是否掷中,当掷中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要接纳换入换出策略)。
图37 3级Cache
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。
它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个厥后进行写使用时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包罗在可实行目的文件hello中的程序,用hello程序有用地更换了当前程序。
加载并运行 hello 需要以下几个步骤:
删除当前进程虚拟地址中已存在的用户区域
映射私有区域,为新程序的代码、数据、bss和栈创建新的区域结构,所有这些新的区域都是私有的、写时复制的
映射共享区域,将hello与libc.so动态链接,然后再映射到虚拟地址空间中的共享区域
设置当前进程上下文程序计数器(PC),使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处置惩罚
如果程序实行过程中发生了缺页故障,则内核调用缺页处置惩罚程序。处置惩罚程序实行如下步骤:.
1.查抄虚拟地址是否正当,如果不正当则触发一个段错误,程序制止
2.查抄进程是否有读、写或实行该区域页面的权限,如果不具有则触发保护异常,程序制止
3.两步查抄都无误后,内核选择一个捐躯页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表
4.将控制转移给hello进程,再次实行触发缺页故障的指令。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组差别大小的块的聚集来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保存为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式实行的,要么是内存分配器自身隐式实行的。
动态内存分配主要有两种基本方法与策略:隐式空闲链表和显式空闲链表。
(1)隐式空闲链表的空闲块是通过头部中的大小字段隐含地毗连着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的聚集。我们需要某种特殊标记的竣事块,即一个设置了已分配位而大小为零的制止头部。
用隐式空闲链表来构造堆。阴影部分是已分配块,没有引用的部分是空闲块。头部标记为(大小(字节)/已分配位)
(2)显式空闲链表是将空闲块构造为某种形式的显式数据结构。堆被构造为一个双向空闲链表,在每个空闲块中,都包罗一个前驱和后继的指针。使用双向链表而不是隐式空闲链表使得首次适配的时间从块数的线性时间减小到空闲块数的线性时间。
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、 intel 的段式管理、 hello 的页式管理,在指定环境下介绍了 VA 到 PA 的变换、物理内存访问,还介绍 hello 进程 fork 时的内存映射、 execve 时的内存映射、缺页故障与缺页中断处置惩罚、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模子化:文件(Linux 把这些设备当作一种特殊文件整合到文件系统中,一般通常位于 /dev 目录下。可以使用与平凡文件相同的方式来对待这些特殊文件。)
设备管理:unix io接口。
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
我们可以对文件的使用有:打开关闭使用open和close;读写使用read和write;改变当前文件位置lseek等。
8.2 简述Unix IO接口及其函数
Unix IO接口:
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有使用中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux Shell创建的每个进程都有三个打开的文件:尺度输入、尺度输出、尺度错误。
改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置 k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过实行 seek,显式地将改变当前文件位置 k。
读写文件。一个读使用就是从文件复制 n > 0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k + n。给定一个大小为 m 字节的文件,当 k >= m 时,实行读使用会触发 EOF,应用程序能检测到它。雷同地,写使用就是从内存中复制 n > 0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
关闭文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix IO函数:
1.open:一个应用程序通过此方法来要求内核打开相应的文件,内核返回一个非负整数,叫做文件描述符,后续所有的使用都基于这个文件描述符。内核记录了对应文件描述符的所有信息,应用程序只需要记住这个描述符。
2.close:用于关闭一个被打开的的文件,描述符为fd,0成功,-1出错.
3.read 读取文件会从当前文件位置复制字节到内存,然后更新文件位置(前提是文件支持seeking),返回从文件fd读取到buf的字节数。
4.write 写入文件会将字节从内存复制到当前文件位置,然后更新当前文件位置(前提是文件支持seeking)
5.seek:对于每个打开的文件,内核保存着一个文件位置k,表现从文件开头起始字节的偏移量,默以为0.应用程序可以通过seek显示的设置k的值。
8.3 printf的实现分析
printf函数的函数体为:
图38 printf函数的函数体
使用的vsprintf函数:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
char tmp[256];
va_list p_next_arg = args;
for (p=buf;*fmt;fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
printf要做的是担当一个格式化的下令,并把指定的匹配的参数格式化输出。
vsprintf的作用就是格式化。它担当确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。之后调用write函数将buf的前i个字符输出到终端,调用了unix I/O。之后系统调用sys_call函数:syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息并将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),于是我们的打印字符串就显示在了屏幕上。
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;
}
getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回EOF(end of file,即-1),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保存在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
异步异常-键盘中断的处置惩罚:键盘中断处置惩罚子程序。担当按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到担当到回车键才返回。
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。末了分析了printf 函数和 getchar 函数的工作过程。
(第8章1分)
结论
hello在程序员通过键盘输入保存在磁盘上以.c文件存储,之后颠末预处置惩罚,拓展得到hello.i文本文件;颠末编译得到汇编代码hello.s汇编文件;通过汇编又得到二进制可重定位目的文件hello.o;再颠末链接得到了hello可实行文件。通过这一系列过程,它从人能看懂的文本文件变成了呆板能够看懂的二进制文件。
之后,在shell输入./hello,shell根据输入判断,不是内置指令,于是先实行fork,创建了子进程,此时复制了一份虚拟内存并且都映射到物理内存中相同的地址空间中,并把他们标记成为写复制,之后在子进程中调用execve加载运行当前进程的上下文中加载并运行新程序hello的程序,至此hello成为了独立的进程。hello加载进入内存之后,起首会进行动态链接,动态链接器会根据hello的需要构建一个查表函数,而hello通过这个查表函数来进行对共享库函数的调用。hello再运行时会调用一些函数,好比printf函数,这些函数与linux I/O的设备模仿化密切相关。
在hello实行完毕之后调用exit退出,在退出后会给shell父进程发送一个SIGCHLD信号,shell父进程收到这个信息后会释放之前用于存储hello信息的一些内存空间,这就是hello从Zero到Zero的一生。
通过大作业,我发现一个小小的Hello进程实现起来需要考虑的方面是如此之多,调用的函数和系统使用涉及如此之广,令我不禁感叹于计算机系统的神妙。从一个hello开始对它的方方面面进行梳理,可以让我系统性地熟悉整个计算机软硬件系统结构。我们基于实践在学习计算机,然而却也要基于理论;我们不应该只盯着顶层的实现,而忽视底层的构造。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中心产物的文件名,并予以分析起作用。
文件的作用
| 文件名
| 预处置惩罚后的文件
| hello.i
| 编译之后的汇编文件
| hello.s
| 汇编之后的可重定位目的文件
| hello.o
| 链接之后的可实行目的文件
| Hello
| Hello.o 的 ELF 格式
| Elf.txt
| Hello.o 的反汇编代码
| disas_hello.s
| hello的ELF 格式
| hello1.elf
| hello 的反汇编代码
| hello1_objdump.s
|
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的册本与网站等
[1] 《深入明白计算机系统》 Randal E.Bryant David R.O’Hallaron 呆板工业出版社.
[2] C/C++预处置惩罚过程详细梳理(预处置惩罚步骤+宏界说#define/#include+inline函数+宏展开顺序+条件预处置惩罚+别的预处置惩罚界说)https://blog.csdn.net/luolaihua2018/article/details/124067982
[3] 你真的懂Hello World!吗?(编译与链接,静态链接与动态链接)你真的懂Hello World!吗?(编译与链接,静态链接与动态链接)-CSDN博客
[4] Executable and Linkable Format(ELF)https://blog.csdn.net/Kongxiangyunltj/article/details/136391027
[5] Linux下shell脚本:bash的介绍和使用(详细)https://blog.csdn.net/weixin_42432281/article/details/88392219
[6] Linux内核分析(七)系统调用execve处置惩罚过程https://blog.csdn.net/yubo112002/article/details/82527157
[7] UNIX IO 简介https://blog.csdn.net/weixin_42695485/article/details/110248969
[8] 博客园 [转]printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |