计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 1180300327
班 级 11803003
学 生 黄锦洋
指 导 教 师 史先俊
附件:https://download.csdn.net/download/wyp237/12082603
计算机科学与技术学院 2019 年 12 月
计算机系统基础课程报告
- 1 -
摘 要
本文通过 hello.cP2P 和 020 的整体介绍,回顾了对计算机系统的学习,并稍做 拓展和深入,对相关存储与相关函数做了些许介绍。
关键词:计算机系统;P2P;020;存储;I/O
(摘要 0 分,缺失-1 分,根据内容精彩称都酌情加分 0-1 分)
计算机系统基础课程报告
- 2 -
目 录
第 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 本章小结 … - 11 - 第 4 章 汇编 … - 12 - 4.1 汇编的概念与作用 … - 12 - 4.2 在 UBUNTU 下汇编的命令 … - 12 - 4.3 可重定位目标 ELF 格式 … - 12 - 4.4 HELLO.O 的结果解析 … - 14 - 4.5 本章小结 … - 16 - 第 5 章 链接 … - 17 - 5.1 链接的概念与作用 … - 17 - 5.2 在 UBUNTU 下链接的命令 … - 17 - 5.3 可执行目标文件 HELLO 的格式 … - 17 - 5.4 HELLO 的虚拟地址空间 … - 18 - 5.5 链接的重定位过程分析 … - 19 - 5.6 HELLO 的执行流程 … - 20 - 5.7 HELLO 的动态链接分析 … - 20 - 5.8 本章小结 … - 22 - 第 6 章 HELLO 进程管理 … - 23 - 6.1 进程的概念与作用 … - 23 -
计算机系统基础课程报告
- 3 -
6.2 简述壳 SHELL-BASH 的作用与处理流程 … - 23 - 6.3 HELLO 的 FORK 进程创建过程 … - 24 - 6.4 HELLO 的 EXECVE 过程 … - 24 - 6.5 HELLO 的进程执行 … - 25 - 6.6 HELLO 的异常与信号处理 … - 26 - 6.7 本章小结 … - 28 - 第 7 章 HELLO 的存储管理 … - 29 - 7.1 HELLO 的存储器地址空间 … - 29 - 7.2 INTEL 逻辑地址到线性地址的变换-段式管理 … - 29 - 7.3 HELLO 的线性地址到物理地址的变换-页式管理 … - 29 - 7.4 TLB 与四级页表支持下的 VA 到 PA 的变换 … - 32 - 7.5 三级 CACHE 支持下的物理内存访问 … - 33 - 7.6 HELLO 进程 FORK 时的内存映射 … - 34 - 7.7 HELLO 进程 EXECVE 时的内存映射 … - 34 - 7.8 缺页故障与缺页中断处理 … - 35 - 7.9 动态存储分配管理 … - 35 - 7.10 本章小结 … - 37 - 第 8 章 HELLO 的 IO 管理 … - 38 - 8.1 LINUX 的 IO 设备管理方法 … - 38 - 8.2 简述 UNIX IO 接口及其函数 … - 38 - 8.3 PRINTF 的实现分析 … - 38 - 8.4 GETCHAR 的实现分析 … - 49 - 8.5 本章小结 … - 50 - 结论 … - 50 - 附件 … - 52 - 参考文献 … - 53 -
计算机系统基础课程报告
- 4 -
第 1 章 概述
1.1 Hello 简介
根据 Hello 的自白,利用计算机系统的术语,简述 Hello 的 P2P,020 的 整个过程。 P2P:From Program to Process,即 从程序到过程。在 linux 中,我们的 hello.c 文件经过编译器 cpp 的预处理成为 hello.i、再经过编译器 ccl 的编译成为 hello.s、接着被汇编器 as 汇编成 hello.o、最终经过链接器 ld 的链接最终成为 可执行目标程序 hello,执行此文件,操作系统会为其 fork 产生子进程,再调 用 execve 函数加载进程。至此,P2P 结束。 020:From Zero-0 to Zero-0,shell 通过 execve 加载并执行 hello,映射虚 拟内存。先删除当前虚拟地址的数据结构并为 hello 文件创建新的区域结构。 进入程序入口后,程序开始载入物理内存,然后进入 main 函数执行目标代码, CPU 为运行的 hello 分配时间片执行逻辑控制流。当程序运行结束后,shell 父进程负责回收 hello 进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU;2.30GHz;8G RAM;64GB SSD ;2TB HDD ;2G RAM; 20GB SCSI。 软件环境:Windows10 64 位;Ubuntu 16.04 LTS 64 位;VMware Workstation Pro。
开发工具:gcc + gedit , Visual Studio 2019, Codeblocks , gdb edb。
1.3 中间结果
hello.c :hello 源代码
hello.i:预处理生成的文本文件
hello.s:编译后得到的汇编语言文件
hello.o:汇编后得到的可重定位目标文件
hello:链接生成的可执行目标文件
计算机系统基础课程报告 - 5 -
objdump :hello 的反汇编代码
objdump_o :hello.o 的反汇编代码
1.4 本章小结
本章分析了 hello.c 的 P2P 和 020 的过程,列出了本次大作业的环境和工具,并且 大致阐明了任务过程中出现的中间结果及其作用。 (第 1 章 0.5 分)
计算机系统基础课程报告
- 6 -
第 2 章 预处理
2.1 预处理的概念与作用
预处理是预处理器(cpp)所进行的操作,具体为读取源程序中#开头的命令, 修改程序文本,如:hello.c 第六行的#include<stdio.h>就是读取系统头文件 stdio.h 的内容,将之直接插入程序文本。 本操作完成后,就得到了另一个程序文本,一般以.i 作为文件扩展名。
2.2 在 Ubuntu 下预处理的命令
命令:gcc -E hello.c -o hello.i
图 2.2
2.3 Hello 的预处理结果解析
得到的 C 程序文本 hello.i 已经从原来 hello.c 的 527 字节增加到 63,946 字节, 并且增加到 3043 行。再用 Visual Studio 2019 打开 hello.i,发现 main 函数在文件 的最后部分。
计算机系统基础课程报告 - 7 -
图 2.3 在 main 函数之前,是预处理器(cpp)读取头文件 stdio.h 、stdlib.h 、和 unistd.h 后插入的内容,三个头文件依次展开。比如 stdio.h 的展开,打开 usr/include/stdlib.h 发现了其中还含有#开头的宏定义等。 预处理器会对此继续递归展开,最终的.i 程序中没有#define 等#开头的语句。
2.4 本章小结
本阶段完成并分析了对 hello.c 的预处理工作结合结果进行了基础的介绍。
(第 2 章 0.5 分)
计算机系统基础课程报告
- 8 -
第 3 章 编译
3.1 编译的概念与作用
首先,编译器(ccl)检查代码是否存在语法错误等,并确认代码的实际功能。 检查无误后,在此编译器会将 hello.i 翻译成 hello.s,生成汇编语言程序。这包 含函数 main 的定义,语句以一种文本格式描述了一条低级机器语言指令。
汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在 Ubuntu 下编译的命令
gcc -S hello.i -o hello.s
图 3.2
3.3 Hello 的编译结果解析
以下将依照先后顺序逐步进行解析。
3.3.1 开头部分:
计算机系统基础课程报告
- 9 -
图 3.3.1.1
.string:字符串 .globl:全局变量 .type:指定是对象类型或是函数类型
图 3.3.1.2
这
3
条指令我们一般称之为函数序言,基本上每个函数都以函数序言开始, 其主要作用在于保存调用者的 rbp 寄存器以及为当前函数分配栈空间 。
3.3.2 数据: hello.s 中 C 语言的数据类型主要有:局部变量、指针数组。 1.int argc:argc 是函数传入的第一个 int 型参数,存储在%edi 中。 2.char* argv:argv 是函数获知的指针数组。 3. int i;局部变量,通常保存在寄存器或是栈中。根据 movl $0, -4(%rbp)操作 可知 i 的数据类型占用了 4 字节的栈空间。 4.常量,hello.s 中一些如同 4、1 等常量以立即数形式($4、$1……)出现。
3.3.3 赋值:
图 3.3.3 对局部变量 i 的赋值:使用 movl 语句,对应于 C 程序中 i=0。
3.3.4 算术操作: 编译器将 i++编译为:
即每次运行到此处时,令对应地址处的数据+1。
计算机系统基础课程报告
- 10 -
3.3.5 关系操作: 1. i<8 的关系操作编译为:
图 3.3.5.1
(即 i-7 小于等于零时跳往.L4) 2. argc!=4 的关系操作编译为:
图 3.3.5.2
(即 argc-4=0 时跳往.L2)
3.3.6 数组/指针/结构操作: 指针数组 char argv[]:在 argv 数组中,argv[0]指向输入程序的路径和名称, argv[1]和 argv[2]分别表示两个字符串。 因为 char 数据类型占 8 个字节,根据.L4 部分:
图 3.3.6.1
与.LC1 部分:
图 3.3.6.2
计算机系统基础课程报告
- 11 -
对比原函数可知通过(%rax)和%rax+8,分别得到 argv[1]和 argc[2]两个字符串。
3.3.7 控制转移: 如 3.3.5 中所描述,cmpl 语句比较后设置条件码,判断 ZF 标志,满足 jmp 要 求则进入相关部分。
3.3.8 函数操作: 1.main 函数: 参数传递:传入参数 argc 和 argv[],分别用寄存器%rdi 和%rsi 存储。 函数调用:被系统启动函数调用。 函数返回:设置%eax 为 0 并且返回,对应 return 0 。 2.printf 函数: 参数传递:call puts 时只传入了字符串参数首地址;for 循环中 call printf 时 传入了 argv[1]和 argc[2]的地址。 函数调用:if 判断满足条件后调用,与 for 循环中被调用。 3.exit 函数: 参数传递:传入的参数为 1,再执行退出命令 函数调用:if 判断条件满足后被调用. 4.sleep 函数: 参数传递:传入参数 atoi(argv[3]), 函数调用:for 循环下被调用,call sleep 5.getchar 函数: 函数调用:在 main 中被调用,call getchar
3.4 本章小结
编译程序所做的工作,就是通过词法分析和语法分析,在确认所有的指令都 符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码表示。 而此番,包括之前对编译的结果进行解析,都令我更深刻地理解了 C 语言的 数据与操作,对 C 语言翻译成汇编语言有了更好的掌握。因为汇编语言的通用性, 这也相当于掌握了语言间的一些共性。 (第 3 章 2 分)
计算机系统基础课程报告
- 12 -
第 4 章 汇编
4.1 汇编的概念与作用
汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成可重定位目标程 序的格式,并将结果保存在目标文件 hello.o 中。 此时的 hello.o 文件不再是一个文本文件,而是一个二进制文件,包含的是程序的 指令编码。
4.2 在 Ubuntu 下汇编的命令
gcc -c hello.s -o hello.o
图 4.2
4.3 可重定位目标 elf 格式
分析 hello.o 的 ELF 格式,用 readelf 等列出其各节的基本信息,特别是重定位 项目分析。
计算机系统基础课程报告
- 13 -
图 4.3.0
- ELF 头描述了生成该文件的系统的字的大小和字节顺序,并且包含帮助链接 器语法分析和解释目标文件的信息。
图 4.3.1 - 节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定 大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。
图 4.3.2 - 当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存何处, 也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。 所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重 定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。 代码的重定位条目放在.rel.text 中,已初始化数据的重定位条目放在.rel.data 中。
计算机系统基础课程报告
- 14 -
图 4.3.3 从图中可以看出八条重定位信息的详细情况。 4. .symtab 是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
图 4.3.4
4.4 Hello.o 的结果解析
objdump -d -r hello.o
计算机系统基础课程报告
- 15 -
图 4.4 分析 hello.o 的反汇编,并请与第 3 章的 hello.s 进行对照分析: 1. 数的表示:hello.s 中的操作数时十进制,hello.o 反汇编代码中的操作数是 十六进制。 2. 分支转移:跳转语句之后,hello.s 中是.L2 和.LC1 等段名称,而反汇编代 码中跳转指令之后是相对偏移的地址,也即间接地址。 3. 函数调用:hello.s 中,call 指令使用的是函数名称,而反汇编代码中 call
计算机系统基础课程报告 - 16 -
指令使用的是 main 函数的相对偏移地址。因为函数只有在链接之后才能 确定运行执行的地址,因此在.rela.text 节中为其添加了重定位条目。
4.5 本章小结
经过汇编器的操作,汇编语言转化为机器语言,hello.o 可重定位目标文件的 生成为后面的链接做了准备。 通过对比 hello.s 和 hello.o 反汇编代码的区别,令人更深刻地理解了汇编语言 到机器语言实现地转变,和这过程中为链接做出的准备(设置重定位条目等)。 (第 4 章 1 分)
计算机系统基础课程报告 - 17 -
第 5 章 链接
5.1 链接的概念与作用
概念:链接是将各种不同文件的代码和数据部分收集(符号解析和重定位) 起来并组合成一个单一文件的过程。
作用:令源程序节省空间而未编入的常用函数文件(如 printf.o)进行合并, 生成可以正常工作的可执行文件。这令分离编译成为可能,节省了大量的工 作空间。
5.2 在 Ubuntu 下链接的命令
使用 ld 的 链 接 命 令 : 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
图 5.2.1
图 5.2.2
5.3 可执行目标文件 hello 的格式
可执行目标文件 hello 的格式类似于可重定位目标文件的格式。 而 ELF 头描述文件的总体格式,它还包括程序的入口点,也就是当程序运行 时要执行的第一条指令的地址。 .text、 .rodata 和.data 节与可重定位目标文件中的节是类似的,除了这些节已经 被重定位到它们最终的运行时的内存地址外。.init 节定义了一个小函数_init,程序 初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel 节。
计算机系统基础课程报告 - 18 -
图 5.3.1
图 5.3.2
5.4 hello 的虚拟地址空间
在edb中打开hello,通过Data Dump查看hello程序的虚拟地址空间各段信息。 在 Memory Regions 选择 View in Dump 可以分别在 Data Dump 中看到只读内存段 和读写内存段的信息。 结合 5.3 中获得的指令与地址的关系表,就可以很轻易地查看。
计算机系统基础课程报告 - 19 -
图 5.4
5.5 链接的重定位过程分析
objdump -d -r hello
分析 hello 与 hello.o 的不同: 1.链接增加新的函数: 在 hello 中链接加入了在 hello.c 中用到的库函数,如 exit、printf、sleep、getchar 等函数。 2.增加的节: hello 中增加了.init 和.plt 节,和一些节中定义的函数。 3.函数调用: hello 中无 hello.o 中的重定位条目,并且跳转和函数调用的地址在 hello 中都 变成了虚拟内存地址。对于 hello.o 的反汇编代码,函数只有在链接之后才能 确定运行执行的地址,因此在.rela.text 节中为其添加了重定位条目。 4.地址访问: hello.o 中的相对偏移地址变成了 hello 中的虚拟内存地址。而 hello.o 文件中对 于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重 定位,在汇编成机器语言时,将操作数全部置为 0,并且添加重定位条目。
计算机系统基础课程报告
- 20 -
说明链接的过程: 根据 hello 和 hello.o 的不同,分析出链接的过程为: 链接就是链接器(ld)将各个目标文件(各种.o 文件)组装在一起,文件中的 各个函数段按照一定规则累积在一起。
结合 hello.o 的重定位项目,分析 hello 中对其怎么重定位的: 反汇编见附件 objdump.txt 与 objdump_o.txt。 重定位过程合并输入模块,并为每个符号分配运行时地址,主要有以下两步:
1.重定位节和符号的定义。 在这一步中,链接器将所有相同类型的节合并成同一类型的新聚合节。包括 hello.o 在内的所有可重定位目标文件中的.data 节全被合并成一个节——输出的可 执行目标文件 hello 中的.data 节。 然后,连接器将运行时的内存地址赋入新节,赋给输入模块定义的每个节, 以及赋给输入模块定义的每个符号。当这一步完成时,hello 中每条指令和变量都 有唯一的运行时内存地址了。 2.重定位节中的符号引用。链接器依赖于 hello.o 中的重定位条目,修改代码 节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。
5.6 hello 的执行流程
使用 edb 执行 hello,说明从加载 hello 到_start,到 call main,以及程序终止的 所有过程。请列出其调用与跳转的各个子程序名或程序地址。 DEBUG [Analyzer] adding: hello!_init <0x0000000000401000> DEBUG [Analyzer] adding: hello!puts@plt <0x0000000000401030> DEBUG [Analyzer] adding: hello!printf@plt <0x0000000000401040> DEBUG [Analyzer] adding: hello!getchar@plt <0x0000000000401050> DEBUG [Analyzer] adding: hello!atoi@plt <0x0000000000401060> DEBUG [Analyzer] adding: hello!exit@plt <0x0000000000401070> DEBUG [Analyzer] adding: hello!sleep@plt <0x0000000000401080> DEBUG [Analyzer] adding: hello!_start <0x0000000000401090> DEBUG [Analyzer] adding: hello!_dl_relocate_static_pie <0x00000000004010c0> DEBUG [Analyzer] adding: hello!main <0x00000000004010c1> DEBUG [Analyzer] adding: hello!__libc_csu_init <0x0000000000401150> DEBUG [Analyzer] adding: hello!__libc_csu_fini <0x00000000004011b0> DEBUG [Analyzer] adding: hello!_fini <0x00000000004011b4>
5.7 Hello 的动态链接分析
计算机系统基础课程报告
- 21 -
分析 hello 程序的动态链接项目,通过 edb 调试,分析在 dl_init 前后,这些项 目的内容变化。要截图标识说明。 首先在节头部表中找到 GOT 的首地址——0x00403ff0,在 edb 中查看其变化
图 5.7.1
图 5.7.2 发现其从起始地址开始 16 字节全为 0,调用_dl_init 函数后,GOT 中内容就发 生了变化。
计算机系统基础课程报告 - 22 -
图 5.7.3 其中,GOT[[2]]是动态链接器在 ld-linux.so 模块中的入口点,也就是对应地址。
(因为 hello 程序需要调用由共享库定义的函数 printf 等。) 这就是动态链接的过程。
5.8 本章小结
本章主要了解温习了在 linux 中链接的过程。 通过查看 hello 的虚拟地址空间,并且对比 hello 与 hello.o 的反汇编代码,更 好地掌握了链接与之中重定位的过程。 不过,链接远不止本章所涉及的这么简单,就像是 hello 会在它运行时要求动 态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。 (第 5 章 1 分)
计算机系统基础课程报告
- 23 -
第 6 章 hello 进程管理
6.1 进程的概念与作用
概念:进程是执行中程序的抽象。 作用: 1.每次运行程序时,shell 创建一新进程,在这个进程的上下文切换中运行这个 可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它 们自己的代码或其他应用程序。 2.进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处 理器;一个私有的地址空间,如同程序独占内存系统。
6.2 简述壳 Shell-bash 的作用与处理流程
一.shell 的作用: Shell 字面理解就是个“壳”,是操作系统(内核)与用户之间的桥梁,充当 命令解释器的作用,将用户输入的命令翻译给系统执行。Linux 中的 shell 与 Windows 下的 DOS 一样,提供一些内建命令(shell 命令)供用户使用,可以用这 些命令编写 shell 脚本来完成复杂重复性的工作。 二.shell 的处理流程 1.从脚本或终端或 bash -c 选项后的字符串中获取输入 2.将获取的输入分解成词元(token),此步骤会执行别名(alisa)展开 1)shell 识别出的一个字符序列单元称为词元,shell 一般通过元字符 (metacharacter)将得到的输入进行分割,得到若干个词元,再对词元进行处理。
2)shell 的元字符有:space,tab, newline,‘|’, ‘&’, ‘;’, ‘(’, ‘)’, ‘<’, or ‘>’.元字符用于词元分割符; 3)shell 中的词(word):不包含非转义元字符的字符序列; 4)shell 中的操作符(operator):newline,‘||’, ‘&&’, ‘&’, ‘;’, ‘;;’, ‘;&’, ‘;;&’, ‘|’, ‘|&’, ‘(’, or ‘)’. 5)词和操作符都是词元 3.将词解析为简单命令或复合命令 1)简单命令是由空格进行分割的词组成的序列 2)复合命令包括循环结构,条件结构,命令组
计算机系统基础课程报告 - 24 -
4.执行各种 shell 展开 1)shell 主要有七大展开:大括号展开,波浪符展开,参数展开,命令替换, 算术展开,分词,文件名展开; 2)展开执行完后,没有转义的,’,”会被移除。 5.执行必要的重定向, 6.执行命令 如果命令中包含/,则执行制定路径的程序;如果命令中不包含/,会检查是否是 shell 函数,shell 内建命令,如果都不是,则在 PATH 环境变量中的路径进行查找。
7.等待命令结束获取命令执行状态。
6.3 Hello 的 fork 进程创建过程
根据 shell 的处理流程,可以推断,输入命令执行 hello 后,父进程如果判断不 是内部指令,即会通过 fork 函数创建子进程。子进程与父进程近似,并得到一份 与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆 和用户栈。 父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于 PID 的 不同。 Fork 函数只会被调用一次,但会返回两次,在父进程中,fork 返回子进程的 PID,在子进程中,fork 返回 0.
6.4 Hello 的 execve 过程
execve 函数在加载并运行可执行目标文件 Hello,且带列表 argv 和环境变量列 表 envp。该函数的作用就是在当前进程的上下文中加载并运行一个新的程序。 只有当出现错误时,例如找不到 Hello 时,execve 才会返回到调用程序,这里 与一次调用两次返回的 fork 不同。 在 execve 加载了 Hello 之后,它调用启动代码。启动代码设置栈,并将控制 传递给新程序的主函数,该主函数有如下的原型: int main(int argc , char **argv , char *envp); 结合虚拟内存和内存映射过程,可以更详细地说明 exceve 函数实际上是如何 加载和执行程序 Hello: 1.删除已存在的用户区域(自父进程独立)。 2.映射私有区:为 Hello 的代码、数据、.bss 和栈区域创建新的区域结构,所 有这些区域都是私有的、写时才复制的。
计算机系统基础课程报告 - 25 -
3.映射共享区:比如 Hello 程序与标准 C 库 libc.so 链接,这些对象都是动态链 接到 Hello 的,然后再用户虚拟地址空间中的共享区域内。 4.设置 PC:exceve 做的最后一件事就是设置当前进程的上下文中的程序计数 器,使之指向代码区域的入口点。
6.5 Hello 的进程执行
程序执行:
图 6.5
进程上下文信息: 进程的上下文由程序正确运行所需的状态组成,这个状态包括存放在内存中 的程序的代码和数据,还包括它的栈、通用目的寄存器的内容、程序计数器、环 境变量以及打开文件描述符的集合。 进程时间片: 分时操作系统分配给正在运行的进程的一段 CPU 时间。 调度的过程: 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前 被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。 当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个 新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转 移到新的进程。 以执行 sleep 函数为例,sleep 函数请求调用休眠进程,sleep 将内核抢占,进 入倒计时,当倒计时结束后,hello 程序重新抢占内核,继续执行。 用户态与核心态转换: 为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可 执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核 心态可以说是“创世模式”,拥有最高的访问权限,处理器以一个寄存器当做模 式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内 核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
计算机系统基础课程报告 - 26 -
6.6 hello 的异常与信号处理
hello 执行过程中会出现的异常: 中断:信号 SIGTSTP,默认行为是 停止直到下一个 SIGCONT; 终止:信号 SIGINT,默认行为是 终止程序运行。
过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C 等,Ctrl-z 后可以 运行 ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异 常与信号的处理。 1.乱按:
图 6.6.1 运行时的乱按并不会影响程序的运行,但因为程序中存在 getchar 函数,按到 回车键时,getchar 会读入回车符及之后字符串,导致在程序运行完成后,将这些 字符串作为 shell 的命令行输入。 2.ctrl+c: 运行时键入 Ctrl+c,将使得程序终止并被回收。
图 6.6.2
3.Ctrl+z 这将导致程序挂起,暂停运行。
计算机系统基础课程报告
- 27 -
图 6.6.3
4.ctrl+z 后 ps 挂起后,ps 命令列出当前系统中的进程(包括僵死进程)。
图 6.6.4
5.ctrl+z 后 pstree 与 ps 相似,但 pstree 命令是以树状图显示进程间的关系,这里只展示图的部 分。
图 6.6.5
6.ctrl+z,jobs jobs 命令列出 当前 shell 环境中已启动的任务状态。
图 6.6.6
7.crtl+z 后,fg 将挂起程序调回前台继续运行。
计算机系统基础课程报告 - 28 -
8.ctrl+z 与 ps 后,kill 输入 kill -9 PID,杀死进程。
图 6.6.8
6.7 本章小结
本章了解了 hello 进程的执行过程,主要是 hello 的创建、加载和终止,通过 键盘输入,对 hello 执行过程中产生信号和信号的处理过程有了更多的认识,对使 用 linux 调试运行程序也有了更多的新得。 (第 6 章 1 分)
计算机系统基础课程报告
- 29 -
第 7 章 hello 的存储管理
7.1 hello 的存储器地址空间
结合 hello 说明逻辑地址、线性地址、虚拟地址、物理地址的概念。 逻辑地址:包含在机器语言中用来指定操作数或指令的地址。每一个逻辑地 址都包含一个段和一个偏移量,偏移量指明了从段开始的地方到实际地址之间的 距离。就是 hello.o 里相对偏移地址(间接寻址)。 线性地址:逻辑地址到物理地址变换之间的中间层。程序内部代码会产生间 接寻址的逻辑地址,加上相应段的基地址就生成了一个线性地址。放在 hello 中就 是 hello 中的虚拟内存地址。其与虚拟地址容易搞混,其要求连续。 虚拟地址:一个带虚拟内存的系统中,CPU 从一个有 N=2^n 个地址空间中生 成虚拟地址,与物理地址相对应。 物理地址:用于内存芯片级的单元寻址,与处理器和 CPU 连接的地址总线相 对应。地址翻译器会将虚拟地址转化为物理地址,放在 hello 中也是这样的。
7.2 Intel 逻辑地址到线性地址的变换-段式管理
这是指把一个程序分成若干段进行存储,每一段都是一个逻辑实体。段式管 理的产生与程序的模块化有极大关系。段式管理通过段表进行,包括段号或段名、 段起点、装入位、段的长度等。 此外,段式管理还需要主存占用区域表、主存可用区域表。 在段式存储管理系统中,除了为每个段分配一个连续的分区便于找寻外,进 程中的各个段可以不连续地存放在内存的不同分区中。 之后在程序加载时,操作系统为所有段分配其所需内存,物理内存的管理采 用动态分区的管理方法,这能够利用碎片化的空间。 段式管理是不连续分配内存技术中的一种,最大特点在于非常直观,按程序 段、数据段等有明确逻辑含义的“段”来分配内存空间。克服了页式的、硬性的、 非逻辑划分给保护和共享与支态伸缩带来的不自然性。 段另一个好处就是可以充分实现共享和保护,便于动态申请内存,管理和使 用统一化,便于动态链接,其缺点是有碎片问题。
7.3 Hello 的线性地址到物理地址的变换-页式管理[8]
计算机系统基础课程报告 - 30 -
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。
将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间 按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建 立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页 式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。 优点 1、由于它不要求作业或进程的程序段和数据在内存中连续存放,从而有效地 解决了碎片问题。 2、动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利 用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。
缺点 1、要求有相应的硬件支持。例如地址变换机构,缺页中断的产生和选择淘汰 页面等都要求有相应的硬件支持。这增加了机器成本。 2、增加了系统开销,例如缺页中断处理机, 3、请求调页的算法如选择不当,有可能产生抖动现象。 4、虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到 利用果页面较大,则这一部分的损失仍然较大。
计算机系统基础课程报告 - 31 -
图 7.3 动态页式管理流程图
计算机系统基础课程报告 - 32 -
7.4 TLB 与四级页表支持下的 VA 到 PA 的变换
TLB,即转译后备缓冲器,是用于缩短虚拟寻址时间的小缓存,其每一行都保 存着 PTE 块,这就使得如果请求的虚拟地址在 TLB 中存在,它将很快地返回匹配 结果。 如果没有 TLB,对线性地址的访问,就需要首先从 PGD 中获取 PTE(第一次 内存访问),在 PTE 中获取页框地址(第二次内存访问),最后访问物理地址, 总共需要 3 次 RAM 的访问,远比一次访问要麻烦得多。 变换过程可以分成以下几步[9]: 首先将 CPU 内核发送过来的 32 位 VA[31:0]分成三段,前两段 VA[31:20]和 VA[19:12]作为两次查表的索引,第三段 VA[11:0]作为页内的偏移,查表的步骤如 下: ⑴从协处理器 CP15 的寄存器 2(TTB 寄存器,translation table base register)中 取出保存在其中的第一级页表(translation table)的基地址,这个基地址指的是 PA, 也就是说页表是直接按照这个地址保存在物理内存中的。 ⑵以 TTB 中的内容为基地址,以 VA[31:20]为索引值在一级页表中查找出一项 (2^12=4096 项),这个页表项(也称为一个描述符,descriptor)保存着第二级页表 (coarse page table)的基地址,这同样是物理地址,也就是说第二级页表也是直接按 这个地址存储在物理内存中的。 ⑶以 VA[19:12]为索引值在第二级页表中查出一项(2^8=256),这个表项中就保 存着物理页面的基地址,我们知道虚拟内存管理是以页为单位的,一个虚拟内存 的页映射到一个物理内存的页框,从这里就可以得到印证,因为查表是以页为单 位来查的。 ⑷有了物理页面的基地址之后,加上 VA[11:0]这个偏移量(2^12=4KB)就可以 取出相应地址上的数据了。 这个过程称为 Translation Table Walk,Walk 这个词用得非常形象。从 TTB 走 到一级页表,又走到二级页表,又走到物理页面,一次寻址其实是三次访问物理 内存。注意这个“走”的过程完全是硬件做的,每次 CPU 寻址时 MMU 就自动完 成以上四步,不需要编写指令指示 MMU 去做,前提是操作系统要维护页表项的 正确性,每次分配内存时填写相应的页表项,每次释放内存时清除相应的页表项, 在必要的时候分配或释放整个页表。
计算机系统基础课程报告 - 33 -
图 7.4 直接映射的 TLB 变换原理
7.5 三级 Cache 支持下的物理内存访问
现代 MMU 一般使用四级页表来将虚拟地址翻译成物理地址。经过先前的翻 译以后,我们就得到了物理地址 PA。 现分析三级 cache 支持下的物理内存访问: 如图 7-5,以 L1 d-cache 的介绍为例,L2 和 L3 同理。 L1 Cache 是 8 路 64 组相联。块大小为 64B。因此 CO 和 CI 都是 6 位,CT 是 40 位。根据物理地址(PA),首先使用组索引 CI,每组 8 路,分别匹配标记 CT。 如果匹配成功且块的有效位是 1,则命中,根据块偏移 CO 返回数据。 如果没有匹配成功,或者匹配成功但是标志位是 0,则不命中,向下一级缓存 申请请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。 一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个 现存的块,一般采用最近最少被使用策略 LRU 进行替换。
计算机系统基础课程报告
- 34 -
图 7-5
7.6 hello 进程 fork 时的内存映射
当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,同时为这个新进程创建虚拟内存。 它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程 中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写 时复制。 当 fork 在新进程中返回时,新进程现在的虚拟内存刚好和调用 fork 时存在的 虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会 创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello 进程 execve 时的内存映射
execve 函数加载并执行 hello.程序的步骤如下: 1. 删除已存在的用户区域:删除当前进程虚拟地址用户部分的区域结构。 2. 映射私有区域:为新程序的代码、数据、.bss 和栈区域创建新的区域结构, 同时标记为私有的写时复制。代码和数据段映射到 hello 的.text 及.data 节。.bss 请求二进制零,映射到匿名文件,其大小在程序头部表中,堆栈 也是请求二进制零,初始长度为零。
计算机系统基础课程报告
- 35 -
- 映射共享区:hello 与系统执行文件链接,如 lib.so,这部分映射到共享区 域。 4. 设置程序计数器 PC:设置当前进程上下文中的 PC,指向 entry point。
7.8 缺页故障与缺页中断处理
在虚拟内存相关的习惯性说法中,DRAM 缓存不命中称为缺页(page fault) 。 例如书中的示例图,CPU 引用了 VP3 中的一个字,VP3 并未缓存在 DRAM 中。 地址翻译器从内存中读取 PTE3,从有效位推断出某文件未被缓存,并且触发一个缺 页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页, 在此例中就是存放在 PP3 中的 VP4。如果 VP4 已经被修改了,那么内核就会将它 复制回磁盘。无论哪种情况,内核都会修改 VP4 的页表条目,反映出 VP4 不再缓 存在主存中这一事实。 缺页处理程序从磁盘上用 VP3 的副本取代 VP4,在缺页处理程序重新启动导 致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常。
7.9 动态存储分配管理[10]
动态存储分配,即指在目标程序或操作系统运行阶段动态地为源程序中的量 分配存储空间,动态存储分配包括栈式或堆两种分配方式。需要主要的是,采用 动态存储分配进行处理的量,并非所有的工作全部放在运行时刻做,编译程序在 编译阶段要为其设计好运行阶段存储组织形式,并为每一个数据项安排好它在数 据区中的相对位置。 存储分配所要解决的问题是多道程序之间如何共享主存的存储空间。解决存 储分配问题有三种方式:直接存储分配方式、静态存储分配方式、动态存储分配 方式。 直接存储分配方式 直接存储分配方式要求存储器的可用空间已经确定,且确保各程序所用的地 址之间互不重叠。缺点是用户感到不方便,存储器的利用率也不高。 静态存储分配方式 静态存储分配方式中。在程序被装入、连接时,才确定它们在主存中的相应 位置(物理地址)。系统必须分配其要求的全部存储空间.否则不能装入该用户程序。 程序将占据着分配给它的存储空间直到程序结束。该存储空间的位置固定不变, 也不能动态地申请存储空间。这种方式无法实现用户对存储空间的动态扩展,而 且也不能有效地实现存储器资源的共享。
计算机系统基础课程报告
- 36 -
动态存储分配方式 动态存储分配方式是不一次性将整个程序装入到主存中。可根据执行的需要, 部分地动态装入。同时,在装入主存的程序不执行时,系统可以收回该程序所占 据的主存空间。再者,用户程序装入主存后的位置,在运行期间可根据系统需要 而发生改变。此外,用户程序在运行期间也可动态地申请存储空间以满足程序需 求。由此可见,动态存储分配方式在存储空间的分配和释放上,表现得十分灵活, 现代的操作系统常采用这种存储方式。 重定位: 为了实现静态、动态存储分配方式.必须把逻辑地址和物理地址分开井将逻 辑地址定位为物理地址。为此,首先耍弄清地址空间和存储空间这两个概念。编 译系统总是从零号地址单元开始,为目标程序指令顺序分配地址。这些地址故称 为相对地址,相对地址的集合称为逻辑地址空间,简称地址空间。存储空间是指 主存中一系列存储信息的物理单元的集合,这些物理单元的编号称为物理地址或 绝对地址。由于用户程序的装入而引起地址空间中的相对地址转化为存储空间中 的绝对地址的地址变换过程,称为地址重定位。实现地址重定位的方法有两种: 静态地址重定位和动态地址重定位。 静态地址重定位 静态地址重定位是指用户程序在装入时由装配程序一次完成。即地址变换只 是在装入时一次完成,以后不再改变。这种重定位方式实现起来比较简单,在早 期多道程序设计中大多采用这种方案:它的缺点是用户程序必须分配一个连续的 存储空间,难以实现程序和数据的共享。 动态地址重定位 动态地址重定位是在程序执行的过程中,当 CPU 要对存储器进行访问时,通 过硬件地址变换机构,将要访问的程序和数据地址转换成主存地址。 动态地址重定位的优点是: (1)程序执行时可以在主存中浮动.有利于提高主存的利用率和存储空间使用 的灵活性。 (2)有利于程序段的共享实现。当系统提供多个重定位寄存器时,规定某些或 某个重定位寄存器作为共享程序段使用,就可实现主存中的相应程序段为多个程 序所共享。 (3)为实现虚拟存储管理提供了基础。有了动态地址重定位的概念和技术,程 序中的信息块可根据执行时的需要分配在主存中的任何区域,还可以覆盖或交换 不再使用的区域,使得程序的逻辑地址空间可比实际的物理存储空间大.从而实
计算机系统基础课程报告 - 37 -
现了虚拟存储管理功能。 动态地址重定位的缺点是实现存储器管理的软件比较复杂以及需要附加更多 的硬件支持。
7.10 本章小结
在本章中,对虚拟内存相关的知识进行了回顾,对 hello 程序在执行过程中对 存储空间的影响做了分析,并做节归纳了段式与页式管理的内容,对 TLB 与四级 页表支持下的 VA 到 PA 的变换,三级 Cache 支持下的物理内存访问等内容也做出 了讨论。最后还列出了动态存储分配管理的内容。 (第 7 章 2 分)
计算机系统基础课程报告 - 38 -
第 8 章 hello 的 IO 管理
8.1 Linux 的 IO 设备管理方法
设备的模型化:文件 设备管理:unix io 接口 所有的 I/O 设备都被抽象为文件(例如网络、磁盘和终端),而所有的输入和 输出都被抽象为相应文件的读和写来完成,这种将设备映射为文件的方式,允许 Linux 内核引出一个简单的,低级的应用接口,成为 Unix I/O,这样做可以使所有 的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述 Unix IO 接口及其函数
Linux/unix I/O:将设备映射为文件的方式,允许 Unix 内核引出一个简单、低 级的应用接口。 1.打开文件,内核记录文件的信息 2.shell 会有三个打开的文件:标准输入,标准输出,标准错误。 3.改变文件位置 4.读写文件 5.关闭文件 Unix I/O 接口函数: open 函数:调用 open 函数打开或创建一个文件。 create 函数:创建一个文件,也可通过以特定参数使用 open 来实现。 close 函数:对读文件进行关闭。 Iseek 函数:为一个打开的文件设置其偏移量。 read 函数:从打开的文件中读数据到 buf。 write 函数:写入文件。 pread,prwrite 函数:主要用于解决文件共享问题。 dup 函数:复制一个现存的文件描述符。 syns 函数:用于解决延迟写问题,保证磁盘上实际文件系统和缓冲区高速缓 存中内容的一致性。
8.3 printf 的实现分析[11]
计算机系统基础课程报告 - 39 -
https://www.cnblogs.com/pianist/p/3315801.html 从 vsprintf 生成显示信息,到 write 系统函数,到陷阱-系统调用 int 0x80 或 syscall. 字符显示驱动子程序:从 ASCII 到字模库到显示 vram(存储每一个点的 RGB 颜色信息)。 显示芯片按照刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一 个点(RGB 分量)。
研究 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; } 代码位置:D:/~/funny/kernel/printf.c 在形参列表里有这么一个 token:… 这个是可变形参的一种写法。 当传递参数的个数不确定时,就可以用这种方式来表示。 很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数。 先来看 printf 函数的内容: 这句: va_list arg = (va_list)((char*)(&fmt) + 4); va_list 的定义: typedef char va_list 这说明它是一个字符指针。 其中的: (char)(&fmt) + 4) 表示的是…中的第一个参数。 如果不懂,我再慢慢的解释: C 语言中,参数压栈的方向是从右往左。 也就是说,当调用 printf 函数的适合,先是最右边的参数入栈。 fmt 是一个指针,这个指针指向第一个 const 参数(const char *fmt)中的第一个元素。 fmt 也是个变量,它的位置,是在栈上分配的,它也有地址。 对于一个 char *类型的变量,它入栈的是指针,而不是这个 char *型变量。 换句话说: 你 sizeof§ (p 是一个指针,假设 p=&i,i 为任何类型的变量都可以) 得到的都是一个固定的值。(我的计算机中都是得到的 4)
计算机系统基础课程报告
- 40 -
当然,我还要补充的一点是:栈是从高地址向低地址方向增长的。 ok! 现在我想你该明白了:为什么说(char*)(&fmt) + 4) 表示的是…中的第一个参数的地 址。 下面我们来看看下一句: i = vsprintf(buf, fmt, arg); 让我们来看看 vsprintf(buf, fmt, arg)是什么函数。
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 要左什么吧 它接受一个格式化的命令,并把指定的匹配的参数格式化输出。 ok,看看 i = vsprintf(buf, fmt, arg); vsprintf 返回的是一个长度,我想你已经猜到了:是的,返回的是要打印出来的字符串 的长度
计算机系统基础课程报告 - 41 -
其实看看 printf 中后面的一句:write(buf, i);你也该猜出来了。 write,顾名思义:写操作,把 buf 中的 i 个元素的值写到终端。 所以说:vsprintf 的作用就是格式化。它接受确定输出格式的格式字符串 fmt。用格式 字符串对个数变化的参数进行格式化,产生格式化输出。 我代码中的 vsprintf 只实现了对 16 进制的格式化。 你只要明白 vsprintf 的功能是什么,就会很容易弄懂上面的代码。 下面的 write(buf, i);的实现就有点复杂了 如果你是 os,一个用户程序需要你打印一些数据。很显然:打印的最底层操作肯定和硬 件有关。 所以你就必须得对程序的权限进行一些限制: 让我们假设个情景: 一个应用程序对你说:os 先生,我需要把存在 buf 中的 i 个数据打印出来,可以帮我么? os 说:好的,咱俩谁跟谁,没问题啦!把 buf 给我吧。 然后,os 就把 buf 拿过来。交给自己的小弟(和硬件操作的函数)来完成。 只好通知这个应用程序:兄弟,你的事我办的妥妥当当!(os 果然大大的狡猾 _) 这样 应用程序就不会取得一些超级权限,防止它做一些违法的事。(安全啊安全) 让我们追踪下 write 吧: write: mov eax, _NR_write mov ebx, [esp + 4] mov ecx, [esp + 8] int INT_VECTOR_SYS_CALL 位置:d:~/kernel/syscall.asm 这里是给几个寄存器传递了几个参数,然后一个 int 结束 想想我们汇编里面学的,比如返回到 dos 状态: 我们这样用的 mov ax,4c00h
计算机系统基础课程报告 - 42 -
int 21h 为什么用后面的 int 21h 呢? 这是为了告诉编译器:号外,号外,我要按照给你的方式(传递的各个寄存器的值)变形 了。 编译器一查表:哦,你是要变成这个样子啊。no problem! 其实这么说并不严紧,如果你看了一些关于保护模式编程的书,你就会知道,这样的 int 表示要调用中断门了。通过中断门,来实现特定的系统服务。 我们可以找到 INT_VECTOR_SYS_CALL 的实现: init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER); 位置:d:~/kernel/protect.c 如果你不懂,没关系,你只需要知道一个 int INT_VECTOR_SYS_CALL 表示要通过系 统来调用 sys_call 这个函数。(从上面的参数列表中也该能够猜出大概) 好了,再来看看 sys_call 的实现: sys_call: call save push dword [p_proc_ready] sti push ecx push ebx call [sys_call_table + eax * 4] add esp, 4 * 3 mov [esi + EAXREG - P_STACKBASE], eax cli ret 位置:~/kernel/kernel.asm
计算机系统基础课程报告 - 43 -
一个 call save,是为了保存中断前进程的状态。 靠! 太复杂了,如果详细的讲,设计到的东西实在太多了。 我只在乎我所在乎的东西。sys_call 实现很麻烦,我们不妨不分析 funny os 这个操作 系统了 先假设这个 sys_call 就一单纯的小女孩。她只有实现一个功能:显示格式化了的字符串。 这样,如果只是理解 printf 的实现的话,我们完全可以这样写 sys_call: sys_call: ;ecx 中是要打印出的元素个数 ;ebx 中的是要打印的 buf 字符数组中的第一个元素 ;这个函数的功能就是不断的打印出字符,直到遇到:’\0’ ;[gs:edi]对应的是 0x80000h:0 采用直接写显存的方法显示字符串 xor si,si mov ah,0Fh mov al,[ebx+si] cmp al,’\0’ je .end mov [gs:edi],ax inc si loop: sys_call .end: ret ok!就这么简单! 恭喜你,重要弄明白了 printf 的最最底层的实现! 如果你有机会看 linux 的源代码的话,你会发现,其实它的实现也是这种思路。 freedos 的实现也是这样 比如在 linux 里,printf 是这样表示的: static int printf(const char *fmt, …) { va_list args;
计算机系统基础课程报告 - 44 -
int i; va_start(args, fmt); write(1,printbuf,i=vsprintf(printbuf, fmt, args)); va_end(args); return i; } va_start va_end 这两个函数在我的 blog 里有解释,这里就不多说了 它里面的 vsprintf 和我们的 vsprintf 是一样的功能。 不过它的 write 和我们的不同,它还有个参数:1 这里我可以告诉你:1 表示的是 tty 所对应的一个文件句柄。 在 linux 里,所有设备都是被当作文件来看待的。你只需要知道这个 1 就是表示往当前 显示器里写入数据 在 freedos 里面,printf 是这样的: int VA_CDECL printf(const char *fmt, …) { va_list arg; va_start(arg, fmt); charp = 0; do_printf(fmt, arg); return 0; } 看起来似乎是 do_printf 实现了格式化和输出。 我们来看看 do_printf 的实现: STATIC void do_printf(CONST BYTE * fmt, va_list arg) { int base; BYTE s[11], FAR * p; int size; unsigned char flags; for (;*fmt != ‘\0’; fmt++) { if (*fmt != ‘%’)
计算机系统基础课程报告 - 45 -
{ handle_char(*fmt); continue; } fmt++; flags = RIGHT; if (*fmt == ‘-’) { flags = LEFT; fmt++; } if (*fmt == ‘0’) { flags |= ZEROSFILL; fmt++; } size = 0; while (1) { unsigned c = (unsigned char)(*fmt - ‘0’); if (c > 9) break; fmt++; size = size * 10 + c; } if (*fmt == ‘l’) { flags |= LONGARG; fmt++; } switch (*fmt) { case ‘\0’: va_end(arg); return;
计算机系统基础课程报告 - 46 -
case ‘c’: handle_char(va_arg(arg, int)); continue; case ‘p’: { UWORD w0 = va_arg(arg, unsigned); char *tmp = charp; sprintf(s, “%04x:%04x”, va_arg(arg, unsigned), w0); p = s; charp = tmp; break; } case ‘s’: p = va_arg(arg, char ); break; case ‘F’: fmt++; / we assume %Fs here */ case ‘S’: p = va_arg(arg, char FAR *); break; case ‘i’: case ‘d’: base = -10; goto lprt; case ‘o’: base = 8; goto lprt; case ‘u’: base = 10; goto lprt; case ‘X’: case ‘x’:
计算机系统基础课程报告 - 47 -
base = 16; lprt: { long currentArg; if (flags & LONGARG) currentArg = va_arg(arg, long); else { currentArg = va_arg(arg, int); if (base >= 0) currentArg = (long)(unsigned)currentArg; } ltob(currentArg, s, base); p = s; } break; default: handle_char(’?’); handle_char(*fmt); continue; } { size_t i = 0; while(p[i]) i++; size -= i; } if (flags & RIGHT) { int ch = ’ '; if (flags & ZEROSFILL) ch = ‘0’; for (; size > 0; size–) handle_char(ch); } for (; *p != ‘\0’; p++) handle_char(*p);
计算机系统基础课程报告 - 48 -
for (; size > 0; size–) handle_char(’ '); } va_end(arg); } 这个就是比较完整的格式化函数 里面多次调用一个函数:handle_char 来看看它的定义: STATIC VOID handle_char(COUNT c) { if (charp == 0) put_console©; else *charp++ = c; } 里面又调用了 put_console 显然,从函数名就可以看出来:它是用来显示的 void put_console(int c) { if (buff_offset >= MAX_BUFSIZE) { buff_offset = 0; printf(“Printf buffer overflow!\n”); } if (c == ‘\n’) { buff[buff_offset] = 0; buff_offset = 0; #ifdef TURBOC _ES = FP_SEG(buff); _DX = FP_OFF(buff); _AX = 0x13; int(0xe6); #elif defined(I86) asm { push ds;
计算机系统基础课程报告 - 49 -
pop es; mov dx, offset buff; mov ax, 0x13; int 0xe6; } #endif } else { buff[buff_offset] = c; buff_offset++; } } 注意:这里用递规调用了 printf,不过这次没有格式化,所以不会出现死循环。 好了,现在你该更清楚的知道:printf 的实现了 现在再说另一个问题: 无论如何 printf()函数都不能确定参数…究竟在什么地方结束,也就是说,它不知 道参数的个数。它只会根据 format 中的打印格式的数目依次打印堆栈中参数 format 后 面地址的内容。 这样就存在一个可能的缓冲区溢出问题。。。
8.4 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;
计算机系统基础课程报告
- 50 -
} return(–n>=0)?(unsigned char)*bb++:EOF; } 异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成 ascii 码,保存到系统的键盘缓冲区。 getchar 等调用 read 系统函数,通过系统调用读取按键 ascii 码,直到接受到回 车键才返回。
8.5 本章小结
本章主要讨论了有关 hello 程序的 IO 管理方面的内容,介绍了 Linux 下的 IO 设备管理,UNIX 的 IO 接口以及相关的函数。并对 printf 函数、getchar 函数的具 体实现进行了分析。 (第 8 章 1 分)
结论
用计算机系统的语言,逐条总结 hello 所经历的过程: hello 所经历的过程: 1. IO 设备编写 hello,以文件的方式储存在主存中。 2.hello.c 被预处理 hello.i 文件 3.hello.i 被编译为 hello.s 汇编文件 4.hello.s 被汇编成可重定位目标文件 hello.o 5.链接器将 hello.o 和外部文件链接成可执行文件 hello 6.在 shell 中键入命令后,通过 exceve、fork 加载并运行 hello 7.在一个时间片中,hello 程序以进程形式存在,有自己的 CPU 资源,顺序执 行逻辑控制流 8.hello 的虚拟地址通过 TLB 和页表翻译为物理地址 9.三级 cache 支持下的 hello 物理地址访问 10.hello 在运行过程中会有异常和信号等 11.printf 会调用 malloc 通过动态内存分配器申请堆中的内存 12.shell 父进程回收 hello 子进程,内核删除为 hello 创建的所有数据结构 你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实 现方法。 深切感悟: 通过对计算机系统的深入理解,我理解了计算机的运行原理,在编写代码时, 逐渐能从计算机的底层语言思考问题,对程序进行优化,编写对编译器友好的代 码。
计算机系统基础课程报告 - 51 -
创新理念: 就我个人而言关于计算机系统的创新理念,应该就是与生物存储信息相同, 令计算机在存储信息时,同样的一段 01,以不同的读法与不同的始端,能够读出 不同的有用信息,这也能让 VR 与完全潜入更快地走进我们的生活。 (结论 0 分,缺失 -1 分,根据内容酌情加分)
计算机系统基础课程报告 - 52 -
附件
hello.c :hello 源代码
hello.i:预处理生成的文本文件
hello.s:编译后得到的汇编语言文件
hello.o:汇编后得到的可重定位目标文件
hello:链接生成的可执行目标文件
objdump :hello 的反汇编代码
objdump_o :hello.o 的反汇编代码
(附件 0 分,缺失 -1 分)
计算机系统基础课程报告
- 53 -
参考文献
为完成本次大作业你翻阅的书籍与网站等 [1] 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出 版社 [2] 博客园 printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html [3] CSDN 博客 Ubuntu 系统预处理、编译、汇编、链接指令 https://blog.csdn.net/spfLinux/article/details/54427494?utm_source=blogxgwz4 [4] 博客园 从汇编层面看函数调用的实现原理 https://www.cnblogs.com/abozhang/p/10788396.html [5] CSDN 博客 ELF 可重定位目标文件格式 https://blog.csdn.net/qq_15099443/article/details/102160175 [6] 百度知道 如何理解 bash shell https://zhidao.baidu.com/question/203159301.html [7] 博客园 shell 命令执行过程 https://www.cnblogs.com/buddy916/p/10291845.html [8] 百度百科 页式管理 https://baike.baidu.com/item/页式管理/6984316?fr=aladdin [9] 《步步惊芯——软核处理器内部设计分析》 TLB 的作用及工作过程 https://www.cnblogs.com/alantu2018/p/9000777.html [10] 百度百科 动态存储分配 https://baike.baidu.com/item/动态存储分配/21306011?fr=aladdin [11] 博客园 [转]printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html [12] 百度百科 getchar 计算机语言函数 https://baike.baidu.com/item/getchar/919709?fr=aladdin
(参考文献 0 分,缺失 -1 分)