目录
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 1190202421
班 级 1903005
学 生 黄守淞
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
本文分析了hello程序的整个生命周期。从预处理、编译、汇编、链接到最后的IO管理,讲述了各个阶段中计算机组织的原理和实现细节,分析了其中产生的各种信息及其作用。
关键词:预处理;编译;汇编;链接;进程;存储;I/O;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
程序员通过文本编辑器键入Hello程序的源码,将其保存为hello.c源文件,然后使用编译器驱动程序将其翻译为一个可执行目标程序。下面以GCC为例进行说明。hello.c首先经过预处理器cpp处理生成修改后的源程序hello.i;hello.i经过编译器ccl处理生成汇编程序hello.s;hello.s经过汇编器as处理生成可重定位目标程序hello.o;最后hello.o与其他的可重定位目标程序(如printf.o)经过链接器ld链接,生成可执行目标程序hello。
打开系统shell,cd进入hello所在目录,运行命令./hello 1190202421 黄守淞 n,shell会进行fork、execve等操作加载可执行目标文件hello至内存中名执行,为其设置虚拟内存,并在运行中映射物理内存空间。程序将相应各种信号,如CRTL+C或CRTL+Z等。这个过程中也会利用系统IO将相应的字符串显示到屏幕上。
1.2 环境与工具
软件环境:
Windows 10 x64;VMware WorkStation Pro 16;Ubuntu 16.04 LTS x64
硬件环境:
Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz;16.0GB RAM;1024GB SSD
开发调试工具:
Codeblocks;GCC;edb;readelf
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
名称 | 作用 |
hello.c | Hello程序源文件 |
hello.i | 经过预处理器处理后的源文件 |
hello.s | 经过编译器处理后的汇编源程序 |
hello.o | 经过汇编器处理后的可重定位目标文件 |
hello | 经过链接器处理后的可执行目标文件 |
1.4 本章小结
本章概括了hello的整个生命周期。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是预处理器(cpp)处理预处理命令,修改源文件的阶段,最终将生成.i文件(文本文件)。预处理命令在C语言中是以字符#开头的命令,ANSI标准定义的预处理命令和作用如下表所示。
名称 | 作用 |
#define | 宏定义 |
#undef | 撤销已定义过的宏名 |
#include | 使编译程序将另一源文件嵌入到带有#include的源文件中 |
#if | #if的一般含义是如果#if 后面的常量表达式为true,则编译它与#endif 之间的代码,否则跳过这些代码。命令#endif 标识一个#if 块的结束。#else命令的功能有点像C语言中的else,#else建立另一选择(在#if 失败的情况下)。#elif 命令意义与else if 相同,它形成一个if else-if 阶梯状语句,可进行多种编译选择。 |
#else | |
#elif | |
#endif | |
#ifdef | 用#ifdef与#ifndef命令分别表示“如果有定义”及“如果无定义”,是条件编译的另一种方法。 |
#ifndef | |
#line | 改变当前行数和文件名称,它们是在编译程序中预先定义的标识符。命令的基本形式如下: #line number["filename"] |
#error | 编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译。 |
#pragma | 为实现时定义的命令,它允许向编译程序传送各种指令例如,编译程序可能有一种选择,它支持对程序执行的跟踪。可用#pragma 语句指定一个跟踪选择。 |
2.2在Ubuntu下预处理的命令
首先利用cd指令进入hello.c所在目录。然后键入命令gcc -E hello.c -o hello.i
在当前目录就生成了预处理后的文件hello.i。
2.3 Hello的预处理结果解析
hello.c文件内容如上所示。
用Ubuntu内置的文本编辑器打开hello.i,将其与hello.c进行对比。
可以发现,预处理程序删除了hello.c中所有注释,将hello.c中#include的三个.h文件内容复制到了hello.i中。hello.c中的main函数被放到了hello.i的末尾处,没有作改变。
2.4 本章小结
第二章借助hello.c与hello.i分析了翻译过程的预处理操作,明确了预处理的定义和功能。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
在上一章中,通过预处理器cpp获得了修改了的源程序hello.i。这一章中调用编译器ccl将hello.i翻译为汇编程序hello.s。翻译为汇编语言的过程是很有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
3.2 在Ubuntu下编译的命令
在上一章的基础上,运行命令gcc -S hello.i -o hello.s
在当前目录就生成了经过编译器翻译好的汇编源程序hello.s
3.3 Hello的编译结果解析
首先分析hello.s文件,分析结果如下表。
行号 | 汇编源码 | 分析 |
2 | .text | 表示下面是代码段。 |
3 | .section .rodata | 表示下面是.rodata节,.rodata节中保存的是只读数据。 |
4 | .align 8 | 表示8字节对齐。 |
6 | .string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201" | .LC0存储的是字符串常量“用法: Hello 学号 姓名 秒数!\n”。 |
8 | .string "Hello %s %s\n" | .LC1存储的是字符串常量“Hello %s %s\n”。 |
9 | .text | 表示下面是代码段。 |
10 | .globl main | 全局变量main |
11 | .type main, @function | 表示main是一个函数。 |
12 | main: | 表示下面是main函数内容。 |
24-25 | cmpl $4, -20(%rbp) je .L2 | 对应于c源程序中的if(argc!=4),.L2标签指向的汇编程序就是argc==4时执行的c语句。如果argc!=4,那么执行后面的汇编代码。 可以判断出-20(%rbp)处保存着局部变量argc。 |
26-29 | leaq .LC0(%rip), %rdi call puts@PLT movl $1, %edi call exit@PLT | 对应于c源程序中的 printf("用法: Hello 学号 姓名 秒数!\n"); exit(1); |
31-32 | movl $0, -4(%rbp) jmp .L3 | 这里是汇编程序.L2的部分。可以判断出-4(%rbp)处保存着局部变量i,.L2的作用是给局部变量i赋初值0,然后转入.L3。 |
53-57 | cmpl $7, -4(%rbp) jle .L4 call getchar@PLT movl $0, %eax leave | 这里将i与7进行比较,如果i小于等于7(即i小于8),则转入.L4,否则执行call getchar。这里对应着c源程序中的for循环部分。可以判断出.L4是for循环成立时执行的语句。汇编码中55-57行的内容对应着 getchar(); return 0; |
43 | call printf@PLT | 对应着c源程序中的printf("Hello %s %s\n",argv[1],argv[2]); |
48 | call atoi@PLT | 对应着c源程序中sleep函数内的atoi(argv[3]) |
50 | call sleep@PLT | 对应着c源程序中的sleep函数。 |
51 | addl $1, -4(%rbp) | 对应着for循环中的i++ |
接下来,说明编译器处理C语言各个数据类型以及各类操作的方法。
3.3.1 数据
第一个printf中的字符串"用法: Hello 学号 姓名 秒数!\n"是一个经过初始化的字符串常量,保存在.rodata节中的.LC0标签处。
第二个printf中的字符串"Hello %s %s\n"也是一个经过初始化的字符串常量,保存在.rodata节中的.LC1标签处。
main函数内定义的局部变量i保存在栈-4(%rbp)处。
main函数参数,局部变量argc保存在栈-20(%rbp)处。
源程序中for、if中出现的数字等等,都在汇编程序中以$立即数的形式出现。
argv是一个指针数组,argv本身是一个局部变量,保存在栈中;其每个下标中的内容是一个字符串指针。
3.3.2 赋值
c源程序中只出现了对局部变量i的赋值,出现在语句for(i=0;i<8;i++)中。对应的汇编指令是:movl $0, -4(%rbp)
3.3.3 算数操作
在c源程序中出现的算数操作只有i++,对应的汇编指令是:addl $1, -4(%rbp)
在汇编程序中,除了addl,还出现了对栈帧的操作subq $32, %rsp,以及实际作用为做算数运算的命令leaq .LC1(%rip), %rdi。
3.3.4 关系操作
c源程序中第一个关系操作是argc!=4,对应的汇编指令是:cmpl $4, -20(%rbp)。第二个关系操作是i<8,对应的汇编指令是:cmpl $7, -4(%rbp)。这里用的并不是8和i比较,而是7,因为编译器i<8解析成为i<=7,效果上是一样的。
3.3.5 数组操作
c源程序中出现的数组操作都是对argv的。有argv[1]、argv[2]、argv[3]。
获取argv[1]对应的汇编程序:
movq -32(%rbp), %rax
addq $8, %rax
获取argv[2]对应的汇编程序:
movq -32(%rbp), %rax
addq $16, %rax
获取argv[3]对应的汇编程序:
movq -32(%rbp), %rax
addq $24, %rax
3.3.6 控制转移
对if(argc!=4)的处理:
cmpl $4, -20(%rbp)
je .L2
这里将argc与4进行比较,如果argc==4,那么将跳转到.L2,相当于执行if语句块后面的c语句。如果argc!=4,那么je不会被执行。
从.L2到.L3:
.L2的作用只有给for循环中的i赋初值0,然后交由.L3
movl $0, -4(%rbp)
jmp .L3
对for(i=0;i<8;i++)的处理:
cmpl $7, -4(%rbp)
jle .L4
这里将i与7比较而不是8,因为编译器将i<8解析为i<=7,如果i确实<=7,那么交由.L4。
3.3.7 函数操作
3.3.7.1 调用printf("用法: Hello 学号 姓名 秒数!\n");
leaq .LC0(%rip), %rdi
call puts@PLT
参数保存在rdi中,是字符串的地址。
3.3.7.2 调用exit(1);
movl $1, %edi
call exit@PLT
edi保存了参数1,调用exit()。
3.3.7.3 调用printf("Hello %s %s\n",argv[1],argv[2]);
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
这些语句将argv[2]字符串地址放入了rdx。
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
这些语句将argv[1]字符串地址放入了rsi。
leaq .LC1(%rip), %rdi
这条语句将printf中常量字符串地址放入rdi。
call printf@PLT
最后调用了printf,rdx、rsi、rdi保存了参数。
3.3.7.4 atoi(argv[3])
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
这些语句将argv[3]字符串地址放入了rdi,然后调用了atoi。从最后一句可以判断出atoi的返回值保存在了eax中。
3.3.7.5 sleep(atoi(argv[3]))
movl %eax, %edi
call sleep@PLT
atoi调用完成后,返回值保存在eax中。此时将eax值传入edi,说明sleep函数接受的参数需要保存在edi中。最后调用sleep。
3.4 本章小结
本章详细解析了编译器ccl将.i文件编译为.s文件的具体细节。分析了数据、赋值、算数操作、关系操作、数组、控制转移以及函数操作在汇编中的实现方式。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器as将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的字节是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。
4.2 在Ubuntu下汇编的命令
在上一章的基础上,运行命令gcc -c hello.s -o hello.o
在当前目录就生成了经过编译器翻译好的汇编源程序hello.s。
由于hello.s不是文本文件,而是二进制文件,所以无法用文本编辑器正常解析。
4.3 可重定位目标elf格式
4.3.1 ELF格式
名称 | 说明 |
ELF头 | 16字节序列描述生成该文件的系统的字大小和字节顺序,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。 |
.text | 已编译程序的机器代码。 |
.rodata | 只读数据。 |
.data | 已初始化的全局和静态变量。 |
.bss | 未初始化的全局和静态变量,以及所有被初始化为0的全局或静态变量。.bss在目标文件中不占实际空间,只是一个占位符。 |
.symtab | 符号表,存放在程序中定义和引用的函数和全局变量的信息。 |
.rel.text | 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或引用全局变量的指令都需要修改。 |
.rel.data | 被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或外部定义函数的地址,都需要被修改。 |
.debug | 一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始C源文件。 |
.line | 原始C源程序中的行号和.text节中机器指令间的映射。 |
.strtab | 一个字符串表,内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。 |
节头部表 | 包含不同节的位置和大小。 |
4.3.2 ELF头
16字节序列描述生成该文件的系统的字大小和字节顺序,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。上图中的Magic就是16字节序列,剩下的部分就是其他信息。
4.3.3 节头部表
节头部表记录了各个节的地址、尺寸、对齐信息等。
4.3.4 .rel.text
一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或引用全局变量的指令都需要修改。在hello.o中,.rel.text节的信息表明链接时外部函数puts、exit、printf、atoi、sleep、getchar,全局变量(之前汇编文件中.rodata的字符串)都需要修改位置。
4.3.5 .symtab
符号表,存放在程序中定义和引用的函数和全局变量的信息。这里面包含了main、puts、exit、printf、atoi、sleep、getchar等符号。
4.3.6 重定位条目
在可重定位目标文件中,重定位条目保存在两个位置中:.rel.text和.rel.data
.rel.text是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或引用全局变量的指令都需要修改。
.rel.data记录了被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或外部定义函数的地址,都需要被修改。
每个重定位条目的格式如上。ELF规定了32种不同的重定位类型,其中R_X86_64_PC32和R_X86_64_32是最基本的两种。前者重定位一个使用32位PC相对地址的引用,后者重定位一个使用32位绝对地址的引用。
重定位的算法伪代码如上。6-9行是对PC相对寻址的处理,12-14行是绝对地址的处理。
用readelf获取的hello.o重定位节.rel.text内容如上,hello.o没有全局变量,不需要.rel.data节。
其中每个条目的偏移量指出了在链接过程中需要修改的引用相对hello.o中.text节的偏移;类型指出了所有引用都是采用PC相对寻址的处理方式。后面显示的“+加数”就是addend的值。
4.4 Hello.o的结果解析
上图为hello.o的反汇编结果。在以下的图中,上方为hello.s中的汇编语言(以下简称为汇编语言),下方为hello.o经过命令objdump -d -r hello.o反汇编后的结果(以下简称为反汇编结果)。
汇编语言中的分支跳转语句是使用标签来表示的。在反汇编结果中是采用一个地址(相对地址或绝对地址)来表示的。
汇编语言中采用标签,如.LC0来表示位置,而反汇编结果中替换为一个偏移地址。hello.o由于还没有进行链接,所以这个偏移地址被暂时写为00 00 00 00。在链接时会使用PC相对寻址的计算方法重写这部分。
对于函数调用,在汇编语言中对于这些外部函数是用 函数名@PLT 来表示。在反汇编结果中是采用PC相对寻址的相对地址来表示的。hello.o还未链接,所以所有call指令都被暂时解析为e8 00 00 00 00。
在反汇编结果中,可以清楚地看到每一条汇编指令所对应的机器语言。汇编语言与机器语言有着一一对应的映射关系,汇编语言可以理解为机器语言的助记符。
4.5 本章小结
这一章详细分析了hello.o文件的ELF部分,分析了其中的ELF格式、ELF头、节头部表、.rel.text、.symtab以及重定位条目。并且分析了汇编语言与机器语言的关系。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
链接后就生成了一个可执行目标文件,可以直接通过shell运行。
5.2 在Ubuntu下链接的命令
运行命令gcc -m64 -no-pie -fno-PIC hello.c -o hello。或者使用ld命令如下:
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
这是采用动态链接库的方式将各个外部函数链接进hello。
在当前目录就生成了经过链接器链接好的可执行目标文件hello。
运行hello,能够正常运行。
5.3 可执行目标文件hello的格式
使用readelf -a hello命令列出hello的elf。
5.3.1 ELF格式
名称 | ||
ELF头 | 只读内存段 | |
将连续的文件节映射到运行时内存段 | 段头部表 | |
.init | ||
.text | ||
.rodata | ||
.data | 读/写内存段(数据段) | |
.bss | ||
.symtab | 不加载到内存的符号表和调试信息 | |
.debug | ||
.line | ||
.strtab | ||
描述目标文件的节 | 节头部表 |
hello相比于hello.o多了段头部表、.init节。ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片被映射到连续的内存段,程序头部表(段头部表)描述了这种映射关系。.init节中定义了一个_init函数,程序的初始化代码会调用它。
hello相比于hello.o少了.rel.text和.rel.data节,因为hello是已经重定位完毕的可执行文件,所以不需要重定位信息了。
5.3.2 ELF头
与hello.o的elf头相比,16字节序列没有发生改变,直到类型以前的数据也都未发生改变。hello.o的类型是REL(可重定位文件),而hello的类型是EXEC(可执行文件),说明经过链接之后hello是可以直接复制到内存中执行的机器代码。入口点地址出现不同,hello的入口点地址相比hello.o大了约0x400000,因为根据Linux运行时的内存映像,内核会从0x400000处开始加载。
5.3.3 节头部表
其中多出来的.init节大小为0x1b,地址是0x401000;.text段大小为0x1f5,地址是0x4010f0;其他段也同理。在hello中的地址是最终加载到虚拟内存中的地址,这和hello.o存在不同,hello.o中是相对于文件本身的偏移量,是从0开始计算的。
5.3.4 程序头部表(段头部表)
段头部表描述了可执行文件的连续的片被映射到连续内存段的映射关系。off是目标文件中的偏移;vaddr/paddr是内存地址;align是对齐要求;filesz是目标文件中的段大小,memsz是内存中的段大小;flags是运行时访问权限。
filesz和memsz大小可能不同,这是因为memsz大出filesz的部分是用来保存运行时被初始化为0的.bss数据。
5.3.5 .symtab
多出了许多符号,是在链接中产生的。
5.4 hello的虚拟地址空间
搜索程序入口点_start,发现.text段从0x4010f0开始,与elf中信息相同。
.rodata段从0x402000开始,与elf中信息相同。
其余节同理。
5.5 链接的重定位过程分析
hello反汇编结果如上。此处展示了main函数部分。
hello.o反汇编如上,此处展示main函数。
由图可知,在hello中main的地址从0x4011d6开始,而hello.o从0x0开始。这是由于hello.o是未经过链接的重定位目标文件,而hello是完全链接后的。hello中main的地址就是程序在运行时main在虚拟内存中的真实地址。
在hello.o中符号还没有经过重定位,对应的机器代码都是00 00 00 00。在hello中已经经过了重定位,相应的机器码已经经过了修改。
链接的过程首先是解析符号引用,将每个应用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。在解析符号引用结束后,要进行重定位。重定位分为两步,第一步是重定位节和符号定义,链接器将所有相同类型的节合并为同一类型的新的聚合节,完成后程序中的每条指令和全局变量都有唯一的运行时内存地址了。第二部是重定位节中的符号引用,链接器利用可重定位目标模块中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
运行edb –run hello指令。
名称 | 地址 |
ld-2.32.so!_dl_catch_exception@plt | 0x00007f7beb687010 |
ld-2.32.so!_dl_signal_exception | 0x00007f7beb687020 |
ld-2.32.so!_dl_signal_error@plt | 0x00007f7beb687030 |
ld-2.32.so!_dl_catch_error@plt | 0x00007f7beb687040 |
ld-2.32.so!_dl_rtld_di_serinfo | 0x00007f7beb691350 |
ld-2.32.so!_dl_debug_state | 0x00007f7beb698410 |
ld-2.32.so!_dl_fatal_printf | 0x00007f7beb698fc0 |
ld-2.32.so!_dl_mcount | 0x00007f7beb69a300 |
ld-2.32.so!_dl_get_tls_static_info | 0x00007f7beb69abc0 |
ld-2.32.so!_dl_allocate_tls_init | 0x00007f7beb69acc0 |
ld-2.32.so!_dl_allocate_tls | 0x00007f7beb69af00 |
ld-2.32.so!_dl_deallocate_tls | 0x00007f7beb69af80 |
ld-2.32.so!_dl_make_stack_executable | 0x00007f7beb69b6b0 |
ld-2.32.so!_dl_find_dso_for_object | 0x00007f7beb69b9e0 |
ld-2.32.so!_dl_exception_create | 0x00007f7beb69efd0 |
ld-2.32.so!_dl_exception_create_format | 0x00007f7beb69f0d0 |
ld-2.32.so!_dl_exception_free | 0x00007f7beb69f580 |
ld-2.32.so!__tunable_get_val | 0x00007f7beb6a0be0 |
ld-2.32.so!__tls_get_addr | 0x00007f7beb6a13a0 |
ld-2.32.so!__get_cpu_features | 0x00007f7beb6a13f0 |
ld-2.32.so!_dl_signal_exception | 0x00007f7beb6a4780 |
ld-2.32.so!_dl_signal_error | 0x00007f7beb6a47d0 |
ld-2.32.so!_dl_catch_exception | 0x00007f7beb6a4950 |
ld-2.32.so!_dl_catch_error | 0x00007f7beb6a4a40 |
hello!_init | 0x0000000000401000 |
hello!puts@plt | 0x0000000000401030 |
hello!printf@plt | 0x0000000000401040 |
hello!getchar@plt | 0x0000000000401050 |
hello!atoi@plt | 0x0000000000401060 |
hello!exit@plt | 0x0000000000401070 |
hello!sleep@plt | 0x0000000000401080 |
hello!_start | 0x00000000004010f0 |
hello!_dl_relocate_static_pie | 0x0000000000401120 |
hello!deregister_tm_clones | 0x0000000000401130 |
hello!register_tm_clones | 0x0000000000401160 |
hello!__do_global_dtors_aux | 0x00000000004011a0 |
hello!frame_dummy | 0x00000000004011d0 |
hello!main | 0x00000000004011d6 |
hello!__libc_csu_init | 0x0000000000401270 |
hello!__libc_csu_fini | 0x00000000004012e0 |
hello!_fini | 0x00000000004012e8 |
5.7 Hello的动态链接分析
在程序加载前,用edb查看数据段前的一小块内存空间。这里是保存全局偏移量表GOT的位置。
加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。从上图可以看出,这块内存区域已经被改变了,动态链接进来的符号真实地址被保存在了相应的GOT条目中。
在调用一个外部全局变量的时候,实际上是访问了GOT中的一个条目,这个条目中保存的是外部变量的真实地址,这个条目是在程序加载的时候修改的。
在调用一个动态链接库中的外部函数的时候,实际上调用的是过程连接表PLT中的内容。在第一次调用的时候,PLT中的代码会让动态链接器修改GOT中真正需要的函数的地址,然后调用真正需要的函数。在后续的调用中,由于GOT条目已经被修改,所以程序就能直接跳转到需要的函数中执行了。这个过程是延迟绑定。
5.8 本章小结
本章讲解了可执行目标文件的ELF细节,讨论了可执行目标文件的执行过程,并且分析了动态链接的原理,即利用全局偏移量表GOT和过程链接表PLT的PIC数据引用和PIC函数调用。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。关于操作系统如何实现进程的细节的讨论超出了本书的范围。反之,我们将关注进程提供给应用程序的关键抽象:1、一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
2、一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
述符的集合
Shell-bash是一个面向用户的进行进程管理的应用程序,其主要区别于内核。他的作用是根据用户输入的指令,创建、加载或终止进程,同时也可以让用户查看当前进程表。Shell本质是一个应用程序,他首先打印一个命令行提示符,指示用户可以输入命令,如果用户输入了一个命令,那么Shell将会读取这个命令并解析这个命令,命令的第一个参数要么是Shell内置的命令(例如quit、jobs、bg或fg),那么Shell就会直接执行此命令,要么是一个可执行程序的路径,Shell会创建(fork)出一个子进程,接下来在这个子进程的上下文中加载(execve)这个程序。如果用户要求后台进行此程序,则Shell返回到命令行提示符,否则等待(waitpid)进程终止,并进行进程回收。
(1)从终端读入用户输入的命令
(2)理解命令(字符串拆分)
(3)如果是内置命令则立即执行相应命令
(4)否则调用相应的程序为其分配子进程并运行
(5)shell 应该接受键盘输入信号(例如ctrl+c),并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
fork函数创建子进程,子进程有和父进程不同的pid但默认相同的pgid。
新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本(包括代码、数据段、堆、共享库以及用户栈),子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程最大的区别在于,子进程有不同于父进程的PID。fork函数是一个调用一次返回两次的特殊函数,子进程返回0,父进程返回子进程的PID。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件filename,且带参数列表 argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。参数列表是用图中的数据结构表示的。argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串。按照惯例,argv[0]是可执行目标文件的名字。环境变量的列表是由一个类似的数据结构表示的,如图8-21所示。envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如“name=value”的名字-值对。在execve加载了filename之后,它调用启动代码,设置栈,并将控制传递给新程序的主函数,该主函数有如下形式的原型:
int main(int argc, char **argv,char **envp);
或者等价的:
int main(int argc, char *argv[], char *envp[]);
6.5 Hello的进程执行
即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。一系列程序计数器PC的值的序列称为逻辑控制流,每个进程的执行过程都是一个逻辑流,每个进程都是在轮流使用处理器,一个进程执行它的流的一部分后,就会暂时挂起,让别的进程继续执行。
一个逻辑流的执行在时间上与另一个流重叠称为并发流,多个流并发执行的现象称为并发,一个进程执行它的控制流的一部分的每一时间段叫做时间片。
运行应用程序代码的进程初始时是在用户模式(不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据)中的。进程从用户模式变为内核模式(可以执行指令集中的任何指令,并可以访问系统中的任何内存位置)的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当发生异常时,处理器将模式从用户模式变为内核模式。异常处理程序运行在内核模式中,当它返回到应用程序时,处理器就把模式再变回用户模式。
不同进程之间交替运行靠的是称为上下文切换的异常控制流,内核保存当前进程的上下文,恢复某个先前被抢占的进程被保存的上下文,将控制传递给这个新恢复的进程。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 概述
异常就是控制流中的突变,用来响应处理器状态中的某些变化。状态变化被称为事件,事件可能与当前指令的执行直接相关。比如,发生虚拟内存缺页、算术溢出,或者一条指令试图除零。另一方面,事件也可能和当前指令的执行没有关系,比如,一个系统定时器产生的信号或者一个I/O请求完成。在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。当异常处理程序完成处理后,根据引起异常的事件的类型来决定是否返回,或返回到哪一条指令。共有三种情况:
1、处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令;
2、处理程序将控制返回给Inext,如果没有发生异常将会执行的下一条指令;
3、处理程序终止被中断的程序。
异常种类:中断、陷阱、故障和终止。
1、中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
2、陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
3、故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
4、终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
6.6.2 CRTL+C
内核发送一个SIGINT信号到前台进程组种的每个进程,默认结果是终止前台作业。
6.6.3 CRTL+Z
发送一个SIGTSTP信号到前台进程组中的每个进程,默认结果是停止(挂起)前台作业。再用ps和jobs检查。最后输入fg命令使hello继续执行。
6.6.4 乱输入
进程在前台运行时乱输入并回车,输出8行后程序结束,进程被回收。
6.7本章小结
本章讲解了进程的概念和作用、shell-bash的处理方式与作用,fork和execve和信号异常处理过程。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:CPU生成的地址。逻辑地址是内部和编程使用的、并不唯一。例如,在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址(偏移地址)。逻辑地址的格式在实模式下为“段地址:偏移地址”,在保护模式下为“段选择符:偏移量”。
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。物理地址是明确的、最终用在总线上的编号,不必转换,不必分页,也没有特权级检查。CPU访问内存的最自然的方式就是使用物理地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。
虚拟地址:保护模式下程序访问存储器所用的逻辑地址,是指向虚拟内存的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式存储管理中以段为单位分配内存,每段分配一个连续的内存区,但各段之间不要求连续,内存的分配和回收类似于动态分区分配,由于段式存储管理系统中作业的地址空间是二维的,因此地址结构包括两个部分:段号和段内位移。
段选择符:
TI=0:选择全局描述符表(GDT),TI=1:选择局部描述符表(LDT);RPL=00:第0级,位于最高级的内核态;RPL=11第3级,位于最低级的用户态。
段描述符:
B31~B0:32位基地址;L19~L0:20位限界,表示段中最大页号。
G:粒度。G=1以页(4KB)为单位;G=0以字节为单位。因为界限为20位,故当G=0时最大的段为1MB;当G=1时,最大段为4KB*2^20=4GB
D:D=1表示段内偏移量为32位宽,D=0表示段内偏移量为16位宽
P:P=1表示存在,P=0表示不存在。Linux总把P置1,不会以段为单位淘汰
DPL:访问段时对当前特权级的最低等级要求。因此,只有CPL之0(内核态)时才可访问DPL为0的段,任何进程都可访问DPL为3的段(0最高,3最低)
S:S=0系统控制描述符,S=1普通的代码段或数据段描述符
TYPE:段的访问权限或系统控制描述符类型。
A:A=1已被访问过,A=0未被访问过。(通常A包含在TYPE字段中)
逻辑地址(48位,“段选择符(16):偏移量(32)”)到线性地址的变换:
1、首先确定要访问的段,根据逻辑地址中的段选择符(高16位)中的低3位(TI和RPL),选择相应的描述符表(GDT或LDT)和状态,然后决定使用的段寄存器。
2、将段选择符中的索引值(高13位)乘8,然后加上GDT或LDT的首地址,就能得到对应段描述符在描述符表中的地址,并在GDT或LDT表中定位相应的段描述符,(GDT或LDT首地址则通过用户不可见的GDTR寄存器或LDTR寄存器来获得)。
3、利用段描述符校验段的访问权限和范围,以确保该段是可以访问的并且偏移量位于段界限内。
4、利用段描述符中取得的段基地址(32位)加上逻辑地址中的偏移量(低32位),得到一个32位线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
VM系统将虚拟内存分割成若干个大小固定的虚拟页(VP),把物理内存按同样大小分割成物理页(PP),然后把页式虚拟页与物理页建立一一对应页表,并用相应的地址翻译硬件,来将虚拟地址翻译为物理地址。
页表是一个页表条目的数组,将虚拟页地址映射到物理页地址。 每个条目由一个有效位和一个n位地址字段组成,有效位表示该虚拟页是否被缓存在DRAM中。若设置了有效位,则地址字段表示DRAM中相应的物理页的起始位置。若没有设置有效位,地址字段为空表示这个虚拟页还未分配,否则地址字段指向该虚拟页在磁盘上的起始位置。
地址翻译:1、利用虚拟地址的虚拟页号(VPN)找到页表中对应的条目。2、判断是否设置了有效位,若未设置有效位,则缺页异常,执行异常处理程序,之后再此执行到第1步,若已设置有效位,则直接进入第3步。3、将该条目中的物理页号(PPN)和虚拟页偏移量(VPO)分别做高低位组合成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,每一行都保存着一个由单一的PTE组成的块。MMU直接利用VPN从TLB中取出相应的PTE。可以用来加速地址翻译。
1、CPU将虚拟地址VA传递给MMU,MMU从TLB中取出相应的PTE,若TLB未命中,则MMU从访问内存中四级页表并取出PTE(若缺页则进行缺页异常处理),新取出的PTE存放到TLB中。若命中则直接进行下一步。2、将PTE中的PPN与VPO结合成PA,再利用PA来访问缓存了。
7.5 三级Cache支持下的物理内存访问
根据缓存的S、E、B、m的值来划分物理地址。CO的位数b对应块大小B = 2^b。CI的位数s对应组数S = 2^s。其余位CT的位数等于m-s-b。首先利用CI来选取相应的组,接着在选中的组中寻找标记位与CT相同的一行,然后根据是否设置有效位来判断是否命中(不命中则从下一级缓存中取出该行并替换),接着根据CO中的块偏移,取出块中相应位置的内容。
7.6 hello进程fork时的内存映射
内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程(shell进程)的mm_struct、区域结构和页表的原样副本。它将两个进程(新进程和shell进程)中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新的页面,因此,也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
1、删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。(即清除这个进程曾经运行过的程序遗留下来的痕迹,为新程序初始化区域)
2、映射私有区域:为新程序的代码、数据、bss和栈区创建新的区域结构。所有的这些区域都是私有的,写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆也是请求二进制零的,初始长度为0。
3、映射共享区域:如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4、设置程序计数器(PC):execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
MMU在试图翻译虚拟地址时。触发了一个缺页故障。(这里按照课本第八章的定义,故障处理后可能会返回当前指令,因此缺页触发的应该是故障。)这个异常导致控制转移到内核的缺页处理程序,处理程序执行以下步骤:
1、首先判断虚拟地址是否合法,缺页处理程序搜索区域结构的链表,把虚拟地址和每个区域结构中的vm_start和vm_end做比较。如果不合法,那么缺页处理程序就触发一个段错误,从而终止这个进程。对应于上图中的标号①。
2、接着判断内存访问是否合法,即进程是否有读、写或者执行这个区域内页面的权限。例如,试图对一个只读页面进行写操作,或者一个运行在用户模式中的进程试图从内核虚拟内存中读取字,等等。如果访问不合法,那么缺页处理程序会触发一个保护异常,从而终止这个进程。对应于上图中的标号②。
3、处理程序知道这个缺页是对合法的虚拟地址进行合法的操作造成的,即正常缺页。那么处理程序会选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表(如果使用TLB,那么当然也要更新TLB缓存)。处理程序返回到引起故障的指令,CPU再次执行这个指令,这次MMU可以正常翻译虚拟地址了,不会产生缺页故障了。对应于上图中的标号③。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
1、显式分配器:要求应用显式地释放任何已分配的块。
2、隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
堆中的块主要组织为两种形式:
1、隐式空闲链表(带边界标记):在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。
所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中header和footer中的block大小间接起到了前驱、后继指针的作用。(下图左是正常的块的结构,右是带边界标记的块的结构)。
隐式空闲链表的放置策略常见的有首次适配、下一次适配和最佳适配。首次适配每次都要从头开始搜索链表速度较慢且容易产生碎片,而下一次适配相较于首次适配速度更快但内存利用率低得多,最佳适配内存利用率比前两个都高但速度最慢。
2、显式空闲链表:在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。显示空闲链表将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,如下图:
维护链表的顺序有:后进先出(LIFO)和地址顺序法。
后进先出(LIFO):将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。
地址顺序法:按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章讲解了虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址、这些地址之间的变换,段、页式的管理模式、共享对象、fork和execve以及动态内存分配的原理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列:
B0, B1, …, Bk, …, Bm-1
所有的 IO 设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
8.2 简述Unix IO接口及其函数
-
-
- Unix IO 接口统一操作
-
1、打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2、Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
3、改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4、读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5、关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
8.2.2 Unix IO 函数
1、int open(char *filename, int flags, mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2、int close(int fd)
fd是需要关闭的文件的描述符,close返回操作结果。
3、 ssize_t read(int fd, void *buf, size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4、ssize_t wirte(int fd, const void *buf, size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。若成功则返回写的字节数,出错则返回-1。
5、int stat(const char *filename, struct stat *buf)
stat函数以一个文件名作为输入,填写stat数据结构中的各个成员。若成功则返回0,出错则返回-1。
8.3 printf的实现分析
1、va_list arg = (va_list)((char*)(&fmt) + 4);
首先,va_list的定义为:typedef char *va_list,也就是说他是字符指针类型,而(char*)(&fmt) + 4便是printf的第二个参数,通过对程序运行时的栈帧的了解我们能知道,第一个参数最后一个入栈,那么他的地址+4(32位系统)便是第二个参数的地址,也就是说arg是第二个参数的地址。
2、i = vsprintf(buf, fmt, arg);
vsprintf函数返回要打印出字符串的长度,vsprintf的作用是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
3、write(buf, i);
其汇编代码为:
int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call函数,这个函数是为了保存中断前进程的状态,简单来说他就实现了功能:显示格式化了的字符串。
sys_call:
;ecx中是要打印出的元素个数
;ebx中的是要打印的buf字符数组中的第一个元素
;这个函数的功能就是不断的打印出字符,直到遇到:'\0'
;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串
接下里便是通过字符显示驱动子程序,从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数:
调用getchar函数,读取键盘缓冲区中的一个字符(读取上一次被读取的字符的下一个字符),若没有字符可读,则等待用户键入并回车后再执行下一步(回车也算一个字符,因而getchar也会读缓冲区里的回车)。
异步异常-键盘中断的处理:read函数同样通过sys_call中断来调用内核中的系统函数。键盘中断处理子程序会接受按键扫描码并将其转换为ASCII码后保存在缓冲区。然后read函数调用的系统函数可以对缓冲区ASCII码进行读取,直到接受回车键返回。
8.5本章小结
本章介绍了Unix I/O设备模型化文件并统一处理的方法,Unix I/O函数,并对标准I/O函数printf和getchar的实现进行了分析。
(第8章1分)
结论
- 代码编辑:在文本编辑器或IDE中键入hello程序代码。
- 预处理:处理hello.c代码,如删除所有注释,将库函数导入。
- 编译:编译器将hello.i翻译成汇编码。
- 汇编:汇编器将汇编码转化成机器指令。
- 链接:将可重定位目标文件链接为可执行目标文件。
- 创建hello进程:shell调用fork、execve等函数创建hello进程并相应其他shell指令。
- 内存管理:虚拟地址、物理地址、缓存、主存、动态内存分配等内容。
- 信号:hello及shell对信号做出反应。
- 终止:hello自行终止或被信号终止。shell利用waitpid回收hello的一切数据。
hello的一生就像人的一生,轻轻地我走了,正如我轻轻地来,挥一挥手,不带走一片云彩。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
名称 | 作用 |
hello.c | Hello程序源文件 |
hello.i | 经过预处理器处理后的源文件 |
hello.s | 经过编译器处理后的汇编源程序 |
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分)