NormanZyq
发布于 2018-12-30 / 330 阅读
0
0

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

因开始使用博客框架,原图片资源尚未导入,图片显示将不正常

大作业要求

Hello 的自白
我是Hello,我是每一个程序猿的初恋(羞羞……)
却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜……),他们很快喜欢上sum、sort、matrix、PR、AI、IOT、BD、MIS……,从不回头。
只有我自己知道,我的出身有多么高贵,我的一生多么坎坷!
多年后,那些真懂我的大佬(也是曾经的菜鸟一枚),才恍然感悟我的伟大!
……………………想当年:

俺才是第一个玩 P2P的: From Program to Process
懵懵懂懂的你笨笨磕磕的将我一字一键敲进电脑存成hello.c(Program),无意识中将我预处理、编译、汇编、链接,历经艰辛,我-Hello一个完美的生命诞生了。
你知道吗?在壳(Bash)里,伟大的OS(进程管理)为我fork(Process),为我execve,为我mmap,分我时间片,让我得以在Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等);
你知道吗?OS(存储管理)与MMU为VA到PA操碎了心;TLB、4级页表、3级Cache,Pagefile等等各显神通为我加速;IO管理与信号处理使尽了浑身解数,软硬结合,才使我能在键盘、主板、显卡、屏幕间游刃有余, 虽然我在台上的表演只是一瞬间、演技看起来很Low、效果很惨白。
感谢 OS!感谢 Bash!在我完美谢幕后回收了我。 我赤条条来去无牵挂!
我朝 CS(计算机系统-Editor+Cpp+Compiler+AS+LD + OS + CPU/RAM/IO等)挥一挥手,不带走一片云彩! 俺也是 O2O: From Zero-0 to Zero-0。
历史长河中一个个菜鸟与我擦肩而过,只有CS知道我的生、我的死,我的坎坷,“只有 CS 知道……我曾经……来…………过……”————未来一首关于Hello的歌曲绕梁千日不绝 !!

摘要

  计算机伴随我们每个人,在生活中无处不见,对于程序员更是如此。每个程序员第一个接触到的程序便是hello——这个打印出"hello, world"的简单程序。它虽然简单,但是包含了无穷的奥秘,本文以此以此贯穿始终,从程序猿的角度来理解,完整讲述了hello程序的一生的每个重大阶段,分析了每个阶段的过程和意义。

  关键词:计算机系统;编译;异常控制流;虚拟内存

第1章 概述

1.1 Hello 简介

  当一个文本文件被敲上了那段代码,储存成了hello.c文件后,经过预处理变成了hello.i,经过编译变成了hello.s,经过汇编器变成了hello.o,最后由链接器链接成为二进制的可执行文件——hello。自此,Program诞生了。当我们在shell中输入./hello时,shell将字符逐一读入寄存器,再把它放到内存中,敲下回车时,这个program hello被运行了!它从0开始,随后内核为其分配空间,加载到内存,分配处理器资源后,它就成了一个进程Process,这时它有了许多资源!在短暂的时间之后,它执行了main函数返回或exit。它,终止了,在被shell回收之后,还被内核清楚了痕迹!它又变成了0。

1.2 环境与工具

硬件组成:
处理器:2.5 GHz Intel Core i7-7660U
内存:16 GB 2133 MHz LPDDR3
硬盘:512GB SSD

软件和环境:
VMware Fusion 专业版10.1.3
Ubuntu 18.04.1

开发调试工具:
Xcode
CLion
Sublime Text 3
GDB
Hexedit

1.3 中间结果

名称 类型
hello.c 源文件
hello.i 修改了的源程序
hello.s 汇编程序
hello.o 可重定位目标程序
hello 可执行目标程序

1.4 本章小结

  hello虽然简单,但是产生的过程是十分伟大的,理解hello执行的过程能够帮助我们理解程序的一生。

第2章 预处理

2.1 预处理的概念与作用

  预处理器(cpp) 根据以字符#开头的命令,修改原始的C程序。主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

2.2 在Ubuntu 下预处理的命令

命令:

gcc -E hello.c > hello.i

生成的hello.i如图(节选):

helloi


图2-1 hello.i节选

2.3 Hello 的预处理结果解析

  预处理之后生成了.i文件,执行了这些操作:将引用的头文件的内容复制进来、进行宏替换,定义和替换了由#define指令定义的符号、删掉注释的内容、条件编译。

2.4 本章小结

  预处理是一个程序能够执行的前提,它让我们写的简单的程序更加完整,正价严谨,增加了必要的内容,也删去了不需要的内容,为下一步操作做好了铺垫。

第3章 编译

3.1 编译的概念与作用

  编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,是一个将高级语言翻译成低级语言的过程。

3.2 在Ubuntu下的编译命令

命令:

gcc -S hello.c -o hello.s

compile


图3-1 编译命令

hellos


图3-2 hello.s节选

3.3 hello的编译结果解析

  1. 数据解析:

    • int sleepsecs: 在C程序中声明为int类型,但赋值为2.5,小数点后内容被舍去,所以实际编译后为:

      _sleepsecs:
      .long   2                       ## 0x2
      
    • 字符串"Usage: Hello 1173710229 xxx!\n":

      L_.str:                                         ## @.str
      .asciz  "Usage: Hello 1173710229 \xxx\xxx\xxx\xxx\xxx\n"
      
    • int i: 程序为它保留了空间,是-20(%rbp),在为其赋值时能够体现:

      LBB0_2:
      movl    $0, -20(%rbp)
      
    • int argc: 被储存到-8(%rbp)

  2. 运算符和操作解析:

    • i++: 程序先将变量i(即-20(%rbp))放入寄存器%eax,然后执行addl $1, %eax

      ## %bb.5:                   ## in Loop: Header=BB0_3 Depth=1
      movl    -20(%rbp), %eax     
      addl $1, %eax ## i++操作
      movl %eax, -20(%rbp)
      jmp LBB0_3
    • i=0:

      LBB0_2:
      movl    $0, -20(%rbp)   ## i=0
      
    • if (argc!=3): !=被翻译为cmpl xx, xxje,是一个条件跳转,它修改了“不等于”为“等于则跳转”:

      ## %bb.0:
      pushq   %rbp
      .cfi_def_cfa_offset 16
      .cfi_offset %rbp, -16
      movq %rsp, %rbp
      .cfi_def_cfa_register %rbp
      subq $48, %rsp
      movl $0, -4(%rbp)
      movl %edi, -8(%rbp)
      movq %rsi, -16(%rbp)
      cmpl $3, -8(%rbp) ## != 操作
      je LBB0_2
    • i<10: 此处的小于号被编译为compl xxx, xxxjge

      LBB0_3:                  ## =>This Inner Loop Header: Depth=1
      cmpl    $10, -20(%rbp)      ## i和10比较
      jge LBB0_6
    • for 语句: 循环语句靠比较和控制转移来实现,例如:

      LBB0_3:
      cmpl    $10, -20(%rbp)  
      jge LBB0_6 ## 上面将10与i比较,此条指令执行跳转

3.4 本章小结

  编译是很关键的一步,它能够将高级语言翻译成低级语言,编译器的出现使得用高级编写程序成为可能,除此以外,编译时编译器还会执行一系列的优化操作,让程序更加高效的执行。

第4章 汇编

4.1 汇编的概念与作用

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

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

disas-cmd


图4-1 汇编命令

helloo


图4-2 hello.o节选

4.3 可重定位目标elf格式

使用

readelf -S hello.o

可以得到如下内容:

  [号] 名称              类型                 地址               偏移量       大小              全体大小      旗标   链接   信息   对齐
[ 0]                   NULL             0000000000000000  00000000  0000000000000000  0000000000000000           0     0     0
[ 1] .text             PROGBITS         0000000000000000  00000040  0000000000000081  0000000000000000  AX       0     0     1
[ 2] .rela.text        RELA             0000000000000000  00000348  00000000000000c0  0000000000000018   I      10     1     8
[ 3] .data             PROGBITS         0000000000000000  000000c4  0000000000000004  0000000000000000  WA       0     0     4
[ 4] .bss              NOBITS           0000000000000000  000000c8  0000000000000000  0000000000000000  WA       0     0     1
[ 5] .rodata           PROGBITS         0000000000000000  000000c8  0000000000000032  0000000000000000   A       0     0     8
[ 6] .comment          PROGBITS         0000000000000000  000000fa  000000000000002b  0000000000000001  MS       0     0     1
[ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000125  0000000000000000  0000000000000000           0     0     1
[ 8] .eh_frame         PROGBITS         0000000000000000  00000128  0000000000000038  0000000000000000   A       0     0     8
[ 9] .rela.eh_frame    RELA             0000000000000000  00000408  0000000000000018  0000000000000018   I      10     8     8
[10] .symtab           SYMTAB           0000000000000000  00000160  0000000000000198  0000000000000018          11     9     8
[11] .strtab           STRTAB           0000000000000000  000002f8  000000000000004d  0000000000000000           0     0     1
[12] .shstrtab         STRTAB           0000000000000000  00000420  0000000000000061  0000000000000000           0     0     1

作用
.text 已编译程序的机器代码。
.rodata 只读数据。
.data 已初始化的全局和静态C变量。
.bss 未初始化对的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
.symtab 符号表,存放在程序中定义和引用的函数和全局变量的信息。
.rel.text text节中位置的列表。
.rel.data 被模块引用或定义的所有全局变量的重定位信息。
.debug 调试符号表。
.line 原始C源程序中的行号和.text节中机器指令之间的映射。
.strtab 字符串表,包括.symtab和.debug节中的符号表,以及节头部中的节名字。

4.4 hello.o的结果解析

hello.o文件内容如图所示:

helloo


图4-3 hello.o节选

  hello.o的反汇编文件内容包含十六进制代码和汇编代码,movsubcmpl等与跳转、调用无关的操作与hello.s相同,并且十六制代码和汇编代码存在唯一(或多种)映射,但是与地址相关的内容会存在区别,例如原来的je LBB0_2变为了je 2b <main+0x2b>,对应十六进制代码74 16

  总结起来,有如下不同:

  1. .o反汇编文件中的每条指令都对应了一个地址;
  2. .s文件中的跳转指令和call指令后面跟随了偏移地址;
  3. .o文件中的数都是十进制的,.s反汇编文件中都是十六进制的。

4.5 本章小结

  汇编将低级语言翻译为机器语言并且将原文件打包成可重定位目标程序,为链接做好准备。

第5章 链接

5.1 链接的概念与作用

链接器将库函数或者其他引用与当前C程序合并到一起,然后保存为可执行目标文件。例如我们的hello程序就使用了printf()函数,这是标准C库的函数,链接器将它们链接起来,这样hello程序就能够使用printf()函数了。

5.2 在Ubuntu下链接的命令

ld /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/7 hello.o -lc -lgcc -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello

ld /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/7 hello.o -lc -lgcc -dynamic-linker /lib64/ld-linux-x86-64.so.2  /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello

linkcmd


图5-1 链接操作

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

使用

readelf -S hello

可以得到如下内容:

There are 28 section headers, starting at offset 0x19c0:

节头:
[号] 名称              类型             地址              偏移量        大小              全体大小          旗标   链接   信息   对齐
[ 0]                   NULL             0000000000000000  00000000  0000000000000000  0000000000000000           0     0     0
[ 1] .interp           PROGBITS         0000000000400200  00000200  000000000000001e  0000000000000000   A       0     0     1
[ 2] .note.ABI-tag     NOTE             0000000000400220  00000220  0000000000000020  0000000000000000   A       0     0     4
[ 3] .hash             HASH             0000000000400240  00000240  0000000000000034  0000000000000004   A       5     0     8
[ 4] .gnu.hash         GNU_HASH         0000000000400278  00000278  000000000000001c  0000000000000000   A       5     0     8
[ 5] .dynsym           DYNSYM           0000000000400298  00000298  00000000000000c0  0000000000000018   A       6     1     8
[ 6] .dynstr           STRTAB           0000000000400358  00000358  0000000000000057  0000000000000000   A       0     0     1
[ 7] .gnu.version      VERSYM           00000000004003b0  000003b0  0000000000000010  0000000000000002   A       5     0     2
[ 8] .gnu.version_r    VERNEED          00000000004003c0  000003c0  0000000000000020  0000000000000000   A       6     1     8
[ 9] .rela.dyn         RELA             00000000004003e0  000003e0  0000000000000030  0000000000000018   A       5     0     8
[10] .rela.plt         RELA             0000000000400410  00000410  0000000000000078  0000000000000018  AI       5    21     8
[11] .init             PROGBITS         0000000000400488  00000488  0000000000000017  0000000000000000  AX       0     0     4
[12] .plt              PROGBITS         00000000004004a0  000004a0  0000000000000060  0000000000000010  AX       0     0     16
[13] .text             PROGBITS         0000000000400500  00000500  0000000000000212  0000000000000000  AX       0     0     16
[14] .fini             PROGBITS         0000000000400714  00000714  0000000000000009  0000000000000000  AX       0     0     4
[15] .rodata           PROGBITS         0000000000400720  00000720  000000000000003a  0000000000000000   A       0     0     8
[16] .eh_frame         PROGBITS         0000000000400760  00000760  0000000000000100  0000000000000000   A       0     0     8
[17] .init_array       INIT_ARRAY       0000000000600e00  00000e00  0000000000000008  0000000000000008  WA       0     0     8
[18] .fini_array       FINI_ARRAY       0000000000600e08  00000e08  0000000000000008  0000000000000008  WA       0     0     8
[19] .dynamic          DYNAMIC          0000000000600e10  00000e10  00000000000001e0  0000000000000010  WA       6     0     8
[20] .got              PROGBITS         0000000000600ff0  00000ff0  0000000000000010  0000000000000008  WA       0     0     8
[21] .got.plt          PROGBITS         0000000000601000  00001000  0000000000000040  0000000000000008  WA       0     0     8
[22] .data             PROGBITS         0000000000601040  00001040  0000000000000014  0000000000000000  WA       0     0     8
[23] .bss              NOBITS           0000000000601060  00001054  0000000000000050  0000000000000000  WA       0     0     32
[24] .comment          PROGBITS         0000000000000000  00001054  000000000000002a  0000000000000001  MS       0     0     1
[25] .symtab           SYMTAB           0000000000000000  00001080  0000000000000630  0000000000000018          26    43     8
[26] .strtab           STRTAB           0000000000000000  000016b0  000000000000022d  0000000000000000           0     0     1
[27] .shstrtab         STRTAB           0000000000000000  000018dd  00000000000000e2  0000000000000000           0     0     1

5.4 hello的虚拟地址空间

vituralstuff


图5-2 hello虚拟地址空间展示

5.5 链接的重定位过程分析

  1. hello.ohello对比和分析:

    helloodisas

    图5-3 hello.o反汇编内容

    hellodisas


    图5-4 hello反汇编内容

    可见有如下不同:

    • hello中指令的地址已经不再是hello.o中的偏移量了,已经变成了虚拟地址;
    • jmpcall指令后面的地址也变成了虚拟地址;
    • hello中多了许多外部函数,例如puts@pltprintf@pltgetchar@plt,链接之后,hello才是一个完整的程序。
  2. 重定位:
    重定位包含两个操作,首先重定位节和符号定义,然后重定位节中的符号引用。

    relocation


    图5-5 重定位展示

5.6 hello的执行流程

1.  _init
2.  __libc_csu_init ()
3.  _dl_elf_hash ()
4.  __vdso_platform_setup ()
5.  _dl_vdso_vsym ()
6.  _dl_lookup_symbol_x ()
7.  dl_new_hash ()
8.  do_lookup_x ()
9.  check_match ()
10. strcmp ()
11. dl_symbol_visibility_binds_local_p ()
12. _dl_vdso_vsym ()
13. __vdso_platform_setup ()
14. __init_misc ()
15. __strrchr_avx2 ()
16. __GI___ctype_init ()
17. call_init ()
18. init_cacheinfo ()
19. handle_intel ()
20. intel_check_word ()
21. __GI_bsearch ()
22. intel_02_known_compare ()
23. handle_intel ()
24. init_cacheinfo ()
25. _dl_init ()
26. __libc_start_main ()
27. __GI___cxa_atexit ()
28. __new_exitfn ()
29. _init()
30. _setjmp ()
31. __sigsetjmp__sigjmp_save ()
32. _IO_puts ()
33. *ABS*+0x9dc70@plt ()
34. __strlen_avx2 ()
35. IO_validate_vtable ()
36. _IO_new_file_xsputn ()
37. _IO_new_file_overflow ()
38. __GI__IO_doallocbuf ()
39. __GI__IO_file_doallocate ()
40. __GI___fxstat ()
41. main()
42. puts@plt ()
43. exit@plt ()

5.8 本章小结

  链接在编译时由静态编译器完成,也可以在加载和运行时由动态连接起来完成。链接的主要任务是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。链接的机制使得程序的规模能够越来越大,同时还能方便调试、解决问题。

第6章 hello进程管理

6.1 进程的概念与作用

进程是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中。
进程总是处于下面三种状态之一:
•运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
•停止。进程的执行被挂起,且不会被调度。当收到SIGSTOPSIGTSTPSIGTTOU信号时,进程就会停止,并且保持停止直到收到SIGCONT信号。
•终止。进程永远地停止了。

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

  1. 作用:shell是一个交互型的应用级程序,它代表用户运行其他程序。
  2. 处理流程:shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并代表用户运行程序。

6.3 Hello的fork进程创建过程

①
--->printf--->exit
	|               |
	shell--->fork------------------->

②
--->printf--->sleep--->printf--->
	|
	shell--->fork----------------------------------->

6.4 hello的execve过程

           --->execve--->Hello--->exit
		|                         |
		shell--->fork--------------------waitpid--->exit

6.5 hello对的进程执行

  Linux系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell运行一个程序时,会fork出一个子进程,子进程通过execve调用加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。
  其中用户模式和内核模式之间的转换成为上下文切换。

6.6 hello的异常与信号处理

  1. hello在运行中会遇到的信号: SIGTSTP:挂起 SIGINT:终止 SIGCONT:继续运行被挂起的进程
  2. 挂起hello

    sus-hello
    图6-1 挂起hello

  3. ps

    ps
    图6-2 ps命令

  4. jobs

    jobs
    图6-3 jobs命令

  5. pstree

    pstree
    图6-4 pstree命令

  6. 发送SIGCONT

    sigcont
    图6-5 发送SIGCONT

  7. 杀死进程

    kill
    图6-6 杀死进程

6.7本章小结
  异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。
  在硬件层,异常是由处理器中的时间出发的控制流中的突变。控制流传递给一个软件处理程序,该处理程序进行一些处理,然后返回控制给被中断的控制流。
  在操作系统层,内核用异常控制流提供进程的基本概念。进程提供给应用两个抽象:1)逻辑控制流,它提供给每个程序一个假象,好像它是在独自占用处理器,2)私有地址空间,它给每个应用一个假象,好像它在独自占用主存。

第7章 hello的存储管理

7.1 hello的存储器地址空间

首先明白地址空间的概念,地址空间是一个非负整数地址的有序集合。

逻辑地址:一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
虚拟地址:在一个带虚拟内存的系统中,CPU从有一个N=2^n 个虚拟地址的空间生成虚拟地址,虚拟地址的概念和逻辑地址没有明显区别,都在一个虚拟环境下的地址。例如,在已经链接好的可执行目标文件hello中,指令已经全部被赋予了虚拟地址,所有控制转移语句和常量的引用都依靠虚拟地址。
物理地址:计算机系统的主存被组织成一个由M个连续字节大小的单元组成的数组。每个字节都有唯一的物理地址。物理地址用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相相应。

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

  逻辑地址必须加上隐含的DS 数据段的基地址,才能构成线性地址。

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

  线性地址转物理地址通过分页机制,在保护模式下,控制寄存器©0的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。如果PG=0,则分页机制无效,线性地址就直接做为物理地址。

32位的线性地址被分成3个部分:
  最高10位Directory页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。
  页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。
  页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。
  以我们程序中遇到的callq 4004c0 <printf@plt>为例,内核先将0x4004c0转换为二进制,即0000 0000 0000 0100 0000 0000 1100 0000,最高10位是0,查看页表目录第0项,里面存放的是页表的物理地址。线性地址中间十位是00 0100 0000是十进制64,查看页表的第64项,那里存放的是最终物理页的物理起始地址。物理页基地址加上线性地址中最低12位的偏移量,CPU就找到了线性地址最终对应的物理内存单元。

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

下图给出了Core i7 页表翻译过程:

i7pte-trans


图7-1 Core i7 页表翻译

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

i7find-addr


图7-2 Core i7 Cache支持下的物理内存访问

core-i7


图7-3 Core i7 处理器封装

7.6 hello进程fork时的内存映射

  当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有时写复制。
  当fork在新进程中返回时,新进程在现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

执行execve时需要以下几个步骤:

  • 删除已存在的用户区域。
  • 映射私有区域。
  • 映射共享区域。
  • 设置程序计数器。

下图展示了加载器是如何映射用户地址空间的区域的:

vituralstuff


图7-4 加载器映射用户地址空间区域

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

  DRAM缓存不命中时称为缺页,当缺页发生后,会触发一个缺页异常,需要调用缺页处理程序,该程序会选择一个牺牲页(如果所选的牺牲页已经被修改了,内核会先将其复制回磁盘),接下来,内核会从磁盘复制请求的页到物理内存,然后更新PTE随后返回。返回后,会重新启动刚才导致出现缺页异常的指令,但这时请求的页已经缓存在主存中了。

7.9动态存储分配管理

  1. 概念
      动态内存分配器维护着一个进程的虚拟内存区域,称为堆,它进阶在未初始化的数据区域后开始,内核维护着一个变量brk指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,要么已分配,要么空闲。
  2. 分配器,分配器有两种基本风格:
    • 隐式分配器,要求显式地分配,但是释放可以交给垃圾收集器自动完成。
    • 显式分配器,要求应用显式地分配和释放内存,C语言就是使用这样的分配器,而printf()函数调用的malloc()函数就是分配过程。
  3. 实现问题,动态存储分配需要考虑如下问题:
    1. 空闲块组织。
    2. 放置策略。
    3. 分割空闲块。
    4. 合并空闲块。

  通常可以采用组织方式有:隐式空闲链表、带边界标记的隐式空闲链表、显式空闲链表、分离的空闲链表。隐式空闲链表使用头部来标记这个块的大小和是否空闲,由它的大小可以计算出下一个块的位置;带边界标记的隐式空闲链表是对隐式空闲链表合并效率低的一个改进方式,为每个块加入脚部,这样就可以从中间开始查找上一个块是否空闲了;显式空闲链表的块中有前驱和后继块的指针,可以直接找到前一个块和后一个块,使得首次适配和释放的速度更加快;分离的空闲链表是维护多个空闲链表,其中每个链表的块有大致相等的大小。
  当请求块时,分配器搜索空闲链表,找到一个足够大的可以放置所请求的空闲块,执行这种搜索的方式是由放置策略决定的,常见的策略是首次适配、下一次适配、最佳适配。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块,下一次适配从刚才搜索结束的地方开始继续搜索;最佳适配会检查每个块,选择合适所需请求大小的最小空闲块。
  在释放内存时会执行空闲块合并,来节省内存空间,当所有块都被搜索过并且块已经最大程度合并了,仍然没有找到足够大小的块来放置时,分配器会调用sbrk函数,向内核请求额外的堆内存。

7.10本章小结

  虚拟内存是对主存的一个抽象,支持虚拟内存的处理器通过虚拟寻址间接引用主存。虚拟内存提供三个重要的功能,第一,它在主存中自动缓存最近使用的存放在磁盘上的虚拟地址空间的内容;第二,它简化了内存管理,进而简化了链接、在进程之间共享数据、进程的内存分配以及程序的加载。最后虚拟内存通过在每条页表条目中加入保护位,从而简化了内存保护。
  现代系统通过将虚拟内存片和磁盘上的文件片关联起来,来初始化虚拟内存片,这个过程即内存映射,它为共享数据、创建新的进程、加载程序提供了高效的机制。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:Unix IO接口
一个Linux文件就是一个m个字节的序列,所有的I/O设备都被模型化为文件,而所有的输入和输出都被大动作对相应文件的读和写来执行,Linux内核引出的接口为Unix I/O,使得所有的输入和输出都能以一种统一的方式来执行:

  • 打开文件。
  • Linux shell创建的每个进程开始都有三个打开的文件:标准输入、标准输出、标准错误
  • 改变当前文件的位置,对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量。应用程序能通过执行seek操作显式地设置文件当前的位置为k
  • 读写文件。
  • 关闭文件,当应用完成了对文件的访问后就通知内核关闭这个文件。

8.2 简述Unix IO接口及其函数

  • int open(char *filename, int flags. mode_t mode) 打开一个文件或者创建一个新文件。
  • int close(int fd) 关闭一个已打开的文件
  • ssize_t read(int fd, void *buf, size_t n) 输入
  • ssize_t write(int fd, const void *buf, size_t n) 输出

8.3 printf的实现分析

首先看Linux下的printf的实现:


static int printf(const char *fmt, ...) {
	va_list args;
	int i;

	va_start(args, fmt);
	write(1,printbuf,i=vsprintf(printbuf, fmt, args));
	va_end(args);
	return i;
}

这里fmt指向字符串的第一个字符,其中,调用write函数时,第一个参数1表示输出设备,由上面的内容可知,Linux下所有设备都是以文件形式看待的。里面的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);
}

下面是write函数的实现:

write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL

最后一条语句表示要通过系统来调用sys_call这个函数。sys_call的内容较复杂,但是它实现了一个功能:显示格式化了的字符串。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ASCII码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。

8.5本章小结

Linux提供了少量基于Unix I/O模型的系统级函数,它们允许应用程序打开、关闭、读和写文件,提取文件的元数据,以及执行I/O重定向,为程序设计带来更大一步发展空间。

结论

hello经历了些什么?

  1. 被写成hello.c
  2. 被预处理后变成hello.i
  3. 被编译器编译为hello.s
  4. 被汇编成了hello.o
  5. 由链接器链接成为可执行目标文件hello
  6. shell为hello程序fork,然后execve
  7. 内核通过上下文切换,使hello程序运行起来像独占CPU,通过虚拟内存,让hello程序看起来像独占主存
  8. MMU将虚拟地址翻译为物理地址完成访存
  9. hello调用printf函数,printf调用malloc,执行动态内存分配
  10. shell回收hello,内核删除hello的所有痕迹,hello运行完毕

附件

hello.c——C语言源文件
hello.i——修改了的源程序
hello.s——汇编程序
hello.o——可重定位目标程序
hello——可执行目标程序

参考文献

[1]兰德尔.深入理解计算机系统[M]. 北京:机械工业出版社, 2016.
[2]百度百科.逻辑地址[EB/OL]. https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80/3283849?fr=aladdin#3.2018-2018.
[3]phlsheji.[EB/OL]. https://www.cnblogs.com/bhlsheji/p/4868964.html.2015-2018.
[4]佚名.[EB/OL]. https://zhidao.baidu.com/question/1835973961391339380.html.2017-2018.
[5]斐然成章.[EB/OL]. https://blog.csdn.net/l_nan/article/details/51188290.2016-2018.

评论