hello的一生

2018年12月31

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在Ubuntu下预处理的命令 - 5 -
2.3 Hello的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理 - 11 -
7.1 hello的存储器地址空间 - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级Cache支持下的物理内存访问 - 11 -
7.6 hello进程fork时的内存映射 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理 - 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
当将hello的代码写完,就得到了源程序Program。通过编译器驱动程序,hello.c经历预处理(cpp)、编译(cc1)、汇编(as)和链接(ld)生成存放在磁盘的可执行目标文件Hello。
在shell中,键入命令执行Hello。shell调用fork、execve函数来运行Hello,同时通过虚拟内存映射、分配空间、内核为其划分时间片,让Hello拥有自己的空间与时间,与其它程序并发地运行。至此,Hello完成从程序Program到进程Process的转变,即P2P。
通过段式管理与页式管理,各类存储器联动,让hello进程能够成功地完成对地址的访问;通过异常处理机制,让hello能够各类异常信号,使程序平稳地运行。同时,Unix I/O也提供了许多的函数与接口,让hello程序能够与I/O设备(文件)进行交互。
当Hello运行结束,父进程shell回收Hello程序,清理内存与和Hello有关的结构,hello又归于沉寂。
以上过程,Hello从无到有,再到被清空,完成一次020的过程。

1.2 环境与工具
1.硬件环境
Intel Core i5 X64 CPU;2.40GHz;8G RAM;

2.软件环境
Windows 10 64位; VMware 14;Ubuntu 16.04

3.开发与调试工具
GDB;EDB;OBJDUMP;READELF;CodeBlocks 64位;vim/gedit+gcc
Notepad++,Hexeditor

1.3 中间结果
hello.i: hello.c预处理的结果,预处理过程展开以#起始的行,试图解释为预处理指令。

hello.s: hello.i编译后的结果,得到了汇编语言代码。

hello.o: hello.s汇编后的结果,即可重定位目标程序,用于链接器或编译器链接生成最终可执行程序。

hello: hello.o链接后生成的可执行目标文件,可以hello.o加载到内存并执行。

helloo_objdump: hello.o文件的反汇编代码,用于查看汇编代码。

hello.objdump: hello文件的反汇编代码,用于查看机器指令的反汇编代码。

Helloelf:ELF格式下的Hello.o文件

helloelf: ELF格式下的hello.o文件

1.4 本章小结
hello程序经历了P2P( From Program to Process)以及O2O(From Zero-0 to Zero-0)两个过程,其中有许多的过程与机制。本章从宏观的过程角度,概括介绍了hello的一生。
同时本章提供了本次实验的环境与工具,以及本次实验过程中得到的中间结果。

第2章 预处理
2.1 预处理的概念与作用
预处理是由预处理器(cpp)实现的。
预处理过程会展开以#起始的行,试图解释为预处理指令。其主要有三个方面的内容:
1.宏定义,即进行#define定义的宏替换
2.文件包含,即根据#include <文件名>,用以包含其他文件。
3.条件编译,即解释希望在条件满足时才编译的语句。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

图2.2.1 预处理结果
成功生成了hello.i文件。

2.3 Hello的预处理结果解析
使用Notepad++打开hello.i文件。此时的hello.i文件具有3000多行代码,原因是扩展#include文件,并且在新的文件中扩展新的#include引用。
在代码中,我们可以看到大量的对结构的定义如typedef、struct、enum,变量申明如extern,并且每一部分的变量定义都有文件的引用目录。
引用完成后,最后是本身的源程序hello.c。应为没有对#define的宏定义,源程序代码无变化。值得注意的是程序的注释在预处理中被忽略。

图2.3.1 预处理结果代码
可以看到:
语句:# 817 “/usr/include/stdlib.h” 3 4标注了文件的引用目录。
其中有extern、typedef int 的数据申明。

图2.3.2 源程序位置
源程序无变化,仅仅缺少注释。
2.4 本章小结
预处理阶段扩展了大量头文件引用,让程序应该包含的内容全部包括在新的文件hello.i中,这步为之后的过程提供了基础。
对于源程序,主要是进行#define的宏替换对注释的删除。

第3章 编译
3.1 编译的概念与作用
概念:编译是用编译器(cc1)进行的。编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。可以说,编译是从源代码(通常为高级语言)到能直接被计算机或虚拟机执行的目标代码(通常为低级语言或机器语言)的翻译过程。
作用:这个阶段编译器主要做词法分析、语法分析、语义分析等,在检查无错误后后,把代码翻译成汇编语言。

3.2 在Ubuntu下编译的命令
使用命令:gcc -S hello.c -o hello.s

图3.2.1 编译结果
成功生成了hello.s文件。

3.3 Hello的编译结果解析
3.3.1 数据
1.常量
在此程序中涉及到的常量是字符串。

图3.3.1 字符串常量
第一个字符串为"Usage: Hello 学号 姓名!\n"。
第二个字符串为"Hello %s %s\n"。
查看hello.s:

图3.3.2 编译结果字符串常量
可以看到两个字符串均存放在.rodata(只读数据)中。同时中文被用\加三 位来进行编码。
2.变量
查看源代码,获取c定义的变量。

图3.3.3 变量位置
可以看到hello.c定义了四个变量:sleepsecs、argc、i、main
1.int sleepsecs:

图3.3.4 sleepsecs条目
在hello.i中,sleepsecs为一个.globl标识的全局变量。
在.data段(已初始化的全局变量)中,有着4字节对齐、类型为对象、大小为4个字节。

2.int argc:
argc作为传入参数,存放在寄存器%edi中,栈中使用。

图3.3.5 argc位置
3.int i:
i为函数局部变量,在栈中使用,地址为 -4(%rbp),其作用域是函数主体,因此分配在栈区,并且置初值是用运行时赋值来实现的。

图3.3.6 i变量位置
4.int main
定义主函数main。

图3.3.7 main信息
在hello.i中存放在.text节中,定义为全局变量,类型为函数。

3.数组
数组是定义在main函数传入参数中的 char *argv[] ,为一个存放字符串的二维数组。
Argv的地址存放在%rsi中,程序运行中,将%rsi入栈,通过%rsp找到argv的各个字符串的首地址来进行argv[]的调用。

hello.i截图:

图3.3.8 argv地址
Argv的地址存放在%rsi中。


图3.3.9 argv调用
Argv的各个调用。

3.3.2 赋值
程序中的赋值操作:

图3.3.10 赋值操作位置
可以看到有两个赋值操作。分别为sleepsecs=2.5和i=0。
对于sleepsecs=2.5,是在.data节进行声明的。
对于i = 0,置初值是用运行时赋值来实现的,
即命令:
图3.3.11 赋值操作
3.3.3 类型转换
程序中涉及到了隐式类型转换:

图3.3.12 隐式类型转换
查看hello.i中对sleepsecs的定义。

图3.3.13 sleepsecs的定义信息
可以看到将sleepsecs赋值为2.
2.5的二进制为10.1
根据向零舍入和向偶数舍入,最后的1应该去除,得到舍入结果2.

3.3.4 算术操作
在编译器的处理过程中,对源程序将进行词汇分析、语法分析与语义分析,最后生成中间代码并产生目标代码。

图3.3.14 词法分析器过程
当经历词汇分析后,得到一个拆分的源代码序列,在语法分析中,构造一个数结构,获得包含算术操作的语法树。

图3.3.15 语法分析器过程
根据语法树,生成包含对算术操作和操作数的中间代码。
根据包含算术操作的中间代码,经历代码优化、目标代码生成器解析, 最后得到对算术操作的解析。

图3.3.16 代码生成器过程
成功完成对算术操作的解析。
在此程序中,涉及的算术操作为对变量i的自增操作:i++
对应代码:

图3.3.17 自增操作代码
即在栈中用addl命令来实现

3.3.5 关系操作

图3.3.18 关系操作位置
此程序的关系操作有两个,argc!=3和i<10
1.argc!=3

图3.3.19
可以看到,编译器将!=符号解释为汇编指令:cmpl。
通过设置条件码来对变量argc和立即数3的进行比较,从而判断执行哪步分支语句。

2.i<10

图3.3.20
可以看到,编译器将<符号解释为汇编指令:cmpl。
通过设置条件码来对变量i和立即数9的进行比较,从而判断循环是否结束。

3.3.6 控制转移
在此程序中涉及的控制转移有两个:if(argc!=3)和for(i=0;i<10;i++)
1.if(argc!=3)

图3.3.21
可以看到,程序先使用指令cmpl $3, -20(%rbp)来比较argc和3的大小,同时设置标志位ZF。通过je .L2来判断标志位从而决定是否跳转。如果argc==3,ZF为0,则不执行if下的语句,直接进入L2。

2.for(i=0;i<10;i++)

图3.3.22
在for循环中,利用累计变量i来判断循环结束条件。
即命令addl $1, -4(%rbp) //i++
通过比较立即数9和i,判断是否退出循环。如果不退出,则跳入L4,即for循环体。

3.3.7 函数操作
此程序中涉及到的函数操作:
1.main函数:
1.参数传递:
main函数传入两个参数argc和argv。存于%edi与%rsi中

图3.3.23 两个参数argc和argv
2.函数调用:
main函数是在系统启动函数__libc_start_main中被调用。
3.函数返回:
main函数将%eax设置为0并返回。

图3.3.24 返回0

2.printf函数:
1.参数传递:
第一次调用将“Usage: Hello 学号 姓名!\n”字符串的首地址给%rdi。

图3.3.25 参数传递字符串
第二次调用将%rdi设置为“Hello %s %s\n”的首地址。
设置%rsi为argv[1],%rdx为argv[2]。

图3.3.26 参数传递字符串

2.函数调用:
第一次直接打印,使用call puts@PLT
第二次传入参数,使用call printf@PLT

3.exit函数:

图3.3.27 exit函数
1.参数传递:
将%edi设置为1。

2.函数调用:
使用call exit@PLT

4.sleep函数:

图3.3.28 sleep函数
1.参数传递:
将%edi设置为sleepsecs。

2.函数调用:
使用call sleep@PLT

5.getchar函数:
1.函数调用:
使用call getchar@PLT
3.4 本章小结
编译的过程,即让代码逐渐走向底层,慢慢由人易懂过度到机器易懂。
在编译阶段,编译器as对源代码进行词汇语法语义等的分析,在分析的过程中,逐层深入,利用了语法树等数据结构,完成了对c语言各种数据与操作的解析,对不同的数据、指令与操作,有着不同的语法与实现机制,这些信息依赖于对汇编代码的解析。
当完成对数据与操作的解析,结合源程序,进而得到中间代码,中间代码经过优化、化简,最终得到目标代码。

第4章 汇编
4.1 汇编的概念与作用
汇编阶段是由汇编器(as)完成的。
汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
4.2 在Ubuntu下汇编的命令
命令:gcc -c 1.s -o 1.o

图4.1 汇编指令
汇编成功,得到hello.o

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
使用命令readelf -a hello.o > Hello.elf,并用notepad来查看hello.o的ELF格式。
结果如下:

1.ELF头


图4.2 ELF头
可以看到,hello.o的ELF的头以一个16字节的序列开始,这个序列描述了生成该文件的的系统的字的大小与字节顺序。
同时,ELF头包含了ELF头的大小(64字节)、目标文件的类型(可重定位的)、机器类型(x86-64)、节头部表的文件偏移、节头部表中条目的大小和数量(13)。

2.节头部表

图4.2 节头部表

在节头部表中,描述了不同节的位置与大小,并且目标文件的每个节都有一个固定大小的条目,包含了名字、类型、地址、偏移和对齐等信息。

3…rela.text重定位信息

图4.3 重定位表
重定位节中包含了重定位条目的信息,用以完成对最终位置未知的目标引用。
由图,每个重定位条目有着固定的格式。
Offset 是需要被修改的引用的节偏移
Symbol 标识被修改引用应该指向的符号。
Type 告知连接器如何修改新的引用。
Addend 一些类型的重定位需使用它对被修改引用的值做偏移调整。
在类型中,使用了R_X86_64_PC32和R_X86_64_PLT32两种重定位类型。
以R_X86_64_PC32为例介绍sleepsecs的重定位过程。
首先,得到重定位条目信息,offset为000000000060、Addend为-4、类型为R_X86_64_PC32以及symbol为sleepsecs。同时得到代码段的地址ADDR(.text)和symbol的最终地址ADDR(symbol)。
由refaddr = ADDR(.text) + offset
*refptr = (unsigned) (ADDR(symbol)) + addend - refadde
得到最终的引用地址。
*refptr = (unsigned) (ADDR(symbol)) -4 - ( ADDR(.text)+ 0x60)
运行时,ADDR(.text)和(ADDR(symbol))是已知的,这样即可确定引用地址。

4…rela.eh_frame:eh_frame节的重定位信息。

5…symtab符号表
.symbol节包含了ELF符号表。符号表包含了可重定位目标模块定义和引用的符号的信息。每个符号都由一个条目来说明。
由下图,每个条目包含value、size、type、Bind、vis、Ndx、Name信息。
对于全局符号main的条目。我们看到它是一个位于.text节(Ndx为1)中偏移量为0(value值)处的129字节(size值)函数。

图4.4 符号表

4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello_objdump 用notepad查看

图4.5 hello_objdump截图 与 汇编语言代码
可以看到,机器语言与汇编语言在格式与语义上相差无几,但是在具体的指令构成上却由不同。
1.操作数
机器语言与汇编语言在操作数上有所不同。

图4.6 不同的操作数
可以看到在汇编语言中为20的数字在机器语言中为0x14,在汇编语言中为32的数字在机器语言中为0x32,为十进制与二进制的关系。
所以操作数在汇编语言为十进制,在机器语言为二进制。

2.分支转移

图4.7 分支转移语句
由图,可以清楚地看到,在汇编语言中,分支转移命令是由助记符来标识,通过用符号.L2等助记符,跳转到相应的位置。
在机器语言中,分支转移命令是直接跳转入目的地址,此处为6f <main+0x6f>,通过地址调用,直接进入相应的语句进行执行。

3.函数调用

图4.8 调用语句
由图,在汇编语言中,函数调用是对函数名的引用。
而在机器语言中,函数调用是通过对在.rela.text节中的重定位条目进行解析从而得到目的函数地址。

4.5 本章小结
由hello.s到hello.o,得到了由汇编指令翻译来的机器指令。
hello.o本身拥有着固定的ELF格式,用以包括各节,如ELF头、节头部表、重定位节、符号表等的基本信息。
在代码中的机器指令,与汇编指令在结构与逻辑上相差无几,也就是实现的机制与流程是差不多的。不过在具体的代码实现与指令细节方面存在差异,。

第5章 链接
5.1 链接的概念与作用
链接是由连接器(ld)来执行的。
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。

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

图5.1 链接结果

5.3 可执行目标文件hello的格式
可执行目标文件格式类似于可重定位目标文件格式。不过因为可执行目标文件是完全链接的(已被重定位),所以它不再需要rel节。
具体如下:
1.ELF头

图5.2 ELF头
2.段头部表

图5.3 段头部表

3.Program Headers:

图5.4 Program Headers

4.Segment Sections…

图5.5 Segment Sections

5.Dynamic section

图5.6 Dynamic section
6.Relocation section

图5.7 Relocation section

7.Symbol table

图5.8 Symbol table

图5.9 Symbol table
8.Version symbols section

图5.10 Version symbols section

9.Version needs section

图5.11 Version needs section

在段头部表中,包含了各个节的Name(名字)Type (类型) Address(地址)Offset(偏移)的信息。可以用HexEdit查看。

5.4 hello的虚拟地址空间

图5.12 程序头

ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片被映射到内存段。程序头部表描述了这种映射关系。程序头部表有如下信息:
Offset VirAddr PhysAddr FileSize MemSiz Flags Align
目标文件中的偏移 内存地址 物理地址 目标文件段大小 内存中的段大小 运行时访问权限 对齐要求

图5.13 edb虚拟地址空间

5.5 链接的重定位过程分析
使用objdump -d -r hello > hello.objdump得到hello的反汇编代码
对比hello.o的反汇编代码,与可执行目标文件格式与对照。
举例第一个字符串的重定位过程:
hello的反汇编代码 与 hello.o的反汇编代码

图5.14 hello的反汇编代码 与 hello.o的反汇编代码
重定位信息 与 程序头中显示的内存地址:

图5.15 重定位信息 与 程序头中显示的内存地址
由机器语言反汇编代码、重定位信息与程序头中显式的内存地址,得到:
ADDR(.text_main) = 0x400532
ADDR(.rodata) = 0x400644
Offset = 0x18
Addend = -4

可以得得到等式:
*refptr = (unsigned)(ADDR(.rodata)+addend- ADDR(.text_main)- Offset)
=(unsigned)(0x400644 +(-4) -(0x400532 + 0x18))
=(unsigned)(0xf6)
其与机器代码的反汇编结果中的0xf6一致:

图5.16 机器代码的反汇编结果中的0xf6

以上为重定位过程。

5.6 hello的执行流程
使用gdb进行查看:
_start
_libc_start_main
__GI___cxa_atexit
__internal_atexit
__new_exitfn
_setjmp ()
__sigsetjmp ()
__sigjmp_save
_init
_main
_printf
_exit
_sleep
_getchar
__GI_exit
__run_exit_handlers
_IO_cleanup ()
_IO_flush_all_lockp
_IO_unbuffer_all ()
_IO_new_file_setbuf
_IO_default_setbuf

5.7 Hello的动态链接分析

图5.17 _dl__init 代码

图5.18 _dl__init 前后设置断点

图5.19 变动信息

图5.20 变动信息
动态库运行时被加载到哪里是未知的,为了能使得代码段里对数据及函数的引用与具体地址无关,ELF 的做法是在动态库的数据段中加一个表项,叫作 GOT(global offset table), GOT 表格中放的是数据全局符号的地址,该表项在动态库被加载后由动态加载器进行初始化,动态库内所有对数据全局符号的访问都到该表中来取出相应的地址,即可做到与具体地址了,而该表作为动态库的一部分,访问起来与访问模块内的数据是一样的。
5.8 本章小结
连接器完成对可重定位目标文件的链接过程,得到了可执行目标文件hello。
在hello的可执行目标文件格式中,包含了链接后的各个信息,其中,段头部表包含了各个段的总体信息,程序头给出了hello的虚拟地址空间。
在可执行目标文件中,知道了各个段的内存地址,便可以对重定位条目进行解析,得到绝对的或相对的地址引用。此外,重定位信息还在动态链接时被用到,完成对动态链入的模块的地址引用。
hello的执行过程,调用的远远不止源代码中的几个函数,还有许多其他的或系统或链接的函数需要被调用。

第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
6.2 简述壳Shell-bash的作用与处理流程
shell是命令行界面,是系统内核的一层壳,作用是用来保护内核同时传递入与计算机交互的信息.它只是系统的一个工具,我们可以使用它来操作计算机。
Shell可以用来解释用户输入的命令并执行。Shell有大量的内置命令,如cat(查看文件)、cd(更改当前目录)、ls(列出信息)、dir等,能够对计算机进行一些基础的操作。同时也可以用./+可执行文件名来运行可执行文件。
处理流程:
第一步:用户输入命令。
第二步:Shell对用户输入命令进行解析,判断是否为内置命令。
第三步:若为内置命令,调用内置命令处理函数,否则调用execve函数创建一个子进程进行运行。
第四步:判断是否为前台运行程序,如果是,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令。

6.3 Hello的fork进程创建过程
Shell通过调用fork 函数创建一个新的运行的子进程。也就是Hello程序,Hello进程几乎但不完全与Shell相同。Hello进程得到与Shell用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。Hello进程还获得与Shell任何打开文件描述符相同的副本,这就意味着当Shell调用fork 时,Hello可以读写Shell中打开的任何文件。Shell和Hello进程之间最大的区别在于它们有不同的PID。

图6.1 fork过程

6.4 Hello的execve过程
execve函数的功能是在当前进程的上下文中加载并运行一个新的程序。
Shell的fork子程序调用execve函数,execve函数在当前子进程的上下文中加载运行新程序(hello),hello程序只是shell的一个复制品。它会覆盖子进程fork的地址空间,但新程序hello拥有相同的PID,并且继承了调用execve函数时已经打开的所有文件的描述符。
execve函数加载并运行可执行目标文件hello,它调用libc_start_main启动代码,启动代码设置栈,并将控制传递给新程序的主函数,且带参数列表argv和环境变量envp。
如果出现错误,例如找不到hello时,execve会返回到调用程序。Execve调用一次从不返回。

6.5 Hello的进程执行
内核为每个进程(包括hello)维持了一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。
当由一个进程转换为另一个进程,内核便进行了上下文切换,上下文切换主要有三个步骤:1保存当前进程的上下文 2恢复某个先前被抢占的进程被保存的上下文 3 将控制传递给这个恢复的进程。
当运行hello时,内核将控制转移给hello,同时为hello分时间片,上下文为hello的上下文。Hello正常运行,当遇到sleep函数时,系统会显式地请求让hello休眠,内核决定进行上下文切换,将控制转移到其它函数,直到hello的sleep函数结束,内核才会决定返回hello的上下文并运行hello。

图6.2 hello上下文切换
上图,进程A为hello,当运行至sleep,内核进行上下文切换,对应上方红圈,当sleep结束,内核进行上下文切换,继续执行hello,如下方红圈。

6.6 hello的异常与信号处理
1 正常执行:

图6.3 正常执行
程序将执行for循环体十次,之后键入字符串结束。
2 Ctrl-C

图6.4 Ctrl-C结果
在程序执行过程中,通过键入Ctrl+C,来中断子进程(hello)的执行。同时父进程(shell)收到SIGINT信号,通过信号处理程序,使用waitpid函数来回收子进程,清除子进程的状态信息。
通过观察前后两次PS的信息,可以看到hello被成功回收。

3 Ctrl-Z

图6.5 Ctrl-Z结果
在程序执行过程中,通过键入Ctrl+Z,来停止子进程(hello)的执行,子进程(hello)被挂起,成为后台挂起进程。同时父进程(shell)收到SIGTSTP信号,通过信号处理程序,打印停止信息,不等待子进程,直接开始接受下一条命令。
键入PS命令,可以看到hello在进程列表中。
键入fg 1将后台hello程序更改为前台,hello子进程继续刚刚的位置(打印了三条信息)执行,完成后7条信息的输出,之后,键入字符串结束程序。
使用PS命令,发现hello程序已运行完毕。
4 kill

图6.6 kill结果
当键入Ctrl+Z后,子进程(hello)被挂起,成为后台进程。Shell接受下一条命令。通过键入kill 3652(hello进程PID)来杀死子进程。此时,shell收到SIGCHLD信号,得到hello终止的信息,利用信号处理程序回收hello子进程。
当再用fg 1命令时,提示已经终止。
ps命令也无hello进程。

5 乱按

图6.7 乱按结果
在程序运行中途乱按不会阻碍程序的运行,乱按只是将屏幕的输入缓存到stdin,当for循环结束,运行getchar函数的时候读出一个以’\n’结尾的字串(end)作为一次输入,其他字串(ps、syl、out)会当做shell命令行输入。

6.7本章小结
程序是指令、数据及其组织形式的描述,进程是程序的实体。可以说,进程是运行的程序。
hello进程的执行依赖于shell,当键入运行hello的命令后,shell函数对命令进行解析,通过调用fork 函数创建一个新的运行的子进程,也就是hello程序,同时在fork函数中调用execve函数,其的功能是在当前进程(fork)的上下文中加载并运行一个新的程序——hello。
在hello运行过程中,内核有选择对其进行管理,决定何时进行上下文切换。
也同样是在hello的运行过程中,当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对不同的异常信号,hello也有不同的处理结果。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。
在hello中,有许多的相对地址引用,例如经过相对地址引用(R_X86_64_PC32)的重定位信息大多是逻辑地址:

图7.1 逻辑地址信息
可以看到R_X86_64_PC32重定位信息得到的地址是相对地址。

线性地址:
线性地址或也叫虚拟地址,跟逻辑地址类似,它也是一个不真实的地址,假设逻辑地址是相应的硬件平台段式管理转换前地址的话,那么线性地址则相应了硬件页式内存的转换前地址。

虚拟地址:
由线性地址的定义,可以知道虚拟地址即线性地址。虚拟地址是相对于虚拟内存而言的。每个进程的虚拟地址完成对物理地址的一个映射。
在hello中即可执行目标文件ELF格式中的程序头中的VirtAddr。

物理地址:
物理地址用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相应。CPU通过地址总线的寻址,找到真实的物理内存对应地址。

图7.2 四个地址关系
7.2 Intel逻辑地址到线性地址的变换-段式管理
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)。
一个逻辑地址由两部份组成,段标识符: 段内偏移量。
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再依据对应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,能够在这个数组中。查找到相应的段描写叙述符,这样。它了Base。即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。

Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样 线性地址=逻辑地址+ 0x00000000,也就是说逻辑地址等于线性地址。

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

图7.3 各符号基本信息
线性地址即虚拟地址,用VA来表示。由图,VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

图7.4 线性地址到物理地址

7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相连度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。

图7.5 TLB组成部分
因为所有的地址翻译都是在芯片上的MMU中进行的,因此非常快。
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。如下图,VPN被分为k个部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到最后确定对应的物理页号,与VPO结合,由7.3,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。

图7.6 多级页表
Core i7是四级页表进行的虚拟地址转物理地址。48位的虚拟地址的前36位被分为四级VPN区。结合存放在CR3的基址寄存器,由前面多级页表的知识,可以确定最终的PPN,与VPO结合得到物理地址。

图7.7 四级页表
7.5 三级Cache支持下的物理内存访问
对于一个虚拟地址请求,首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就先在结合多级页表,得到物理地址,去cache中找,到了L1里面以后,寻找物理地址又要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3。这里就是使用到CPU的高速缓存机制了,一级一级往下找,直到找到对应的内容。

图7.8 Core i7 的内存系统的三级cache结构


图7.9 物理地址寻找流程

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

图7.10 写时复制机制。

7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1删除已存在的用户区域。删除当前进程(shell)虚拟地址的用户部分中的已存在的区域结构。
2映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。下图概括了私有区域的不同映射。
3映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

图7.11 虚拟内存空间

7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中, DRAM 缓存不命中称为缺页(page fault) 。下图 展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。

图7.12 缺页
当遇到缺页时,会进入缺页异常处理程序:

图7.13 进入缺页异常处理程序

7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来进行。

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

动态内存分配主要有两种基本方法与策略:
1.带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
隐式空闲链表:

图7.15 隐式空闲链表:
在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。

图7.16 隐式空闲链表组织堆
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。

2.显示空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
显式空闲链表:

图7.17 双向显式空闲链表
在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
hello的地址种类众多,大体上的转换过程是由逻辑地址到线性地址,即虚拟地址,接着由线性地址再转换为内存地址。
由逻辑地址到线性地址的过程中,需要用到段式管理。
在线性地址到物理地址的过程中,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache同舟共济,利用页式管理完成虚拟地址到物理地址的转换。
加载hello时,少不了缺页故障,此时缺页中断处理保证了对页的成功请求。
当运行hello时,shell调用fork函数,为hello进行虚拟内存映射,创建虚拟内存,接着调用execve函数,加载并运行包含在可执行目标文件hello中的程序。
在请求动态内存时,需要用到动态存储分配管理来操作堆。动态内存分配主要有隐式空闲链表和显式空闲链表两种基本方法。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数
1.打开和关闭文件
打开文件:
进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
int open(char *filename, int flags, mode_t mode);
其中,open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。

关闭文件:
进程通过调用close 函数关闭一个打开的文件。
int close(int fd);

2.读和写文件
应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);

read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。

3.读取文件元数据
应用程序可以通过调用stat和fstat函数,检索到关于文件的信息(元数据)
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);

stat函数以一个文件名作为输入,并填写一个stat数据结构中的各个成员。Fstat函数是相似的,,只不过是以文件描述符而不是文件名作输入。

4.读取目录内容

应用程序可以用readdir系列函数来读取目录的内容。
DIR *opendir(const char *name);
函数opendir以路径名为参数,返回指向目录流的指针。流是对条目有序列表的抽象,在这里是指目录的列表。

Struct dirent *readdir(DIR *dirp);
每次对readdir的调用返回都是指向流dirp中下一个目录项的指针,或者,如果没有更多目录则返回NULL。

int closedir(DIR *dirp);
函数closdir关闭流并释放其所以资源。

8.3 printf的实现分析
printf函数:

图8.1 printf函数代码
对于参数:… 表示传递参数的个数不确定
之后arg是一个指针,表示的是…中的第一个参数。

图8.2 vsprintf代码
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
而write,接受buf与需要输出的参数个数,执行写操作,把buf中的i个元素的值写到终端。一下是write函数的汇编代码:

图8.3 write函数的汇编代码
其中INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。

图8.4 sys_call函数
以上是sys_call的汇编代码。其中ecx中是要打印出的元素个数 ,ebx中的是要打印的buf字符数组中的第一个元素,sys_call的功能就是不断的打印出字符,直到遇到:’\0’ 。其中call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析
getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结
所有的I/ O 设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种简单、低级的应用接口,称为Unix I/O。
Unix I/O定义了很多与文件进行交互的函数,如打开(open)、关闭(close)、读(read)、写(write)等函数并提供了调用这些函数的接口。本章同时大体上介绍了printf函数和getchar函数的实现与分析。

结论
hello所经历的过程:
1.hello最初是一个C语言源程序hello.c,即program。
2.hello.c经过预处理后,得到hello.i,解释预处理指令。
3.hello.i经历编译后,得到hello.s,即汇编语言级代码。
4.hello.s经历汇编后,得到hello.o,即机器语言级可重定位目标文件。
5.hello.o经历链接后,得到最终的可执行目标文件——hello。
6.在shell中,shell为hello程序fork,调用execve,将hello程序加载并运行,得到hello进程process。
7.在加载hello的过程中,通过虚拟内存映射,为hello分配虚拟内存空间,内核通过为hello划分时间片,让hello能够执行自己的控制流。
8.运行hello时,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache同舟共济,完成对地址的请求。
9.异常处理机制保证了hello对异常信号的处理,使程序平稳运行。
10.Unix I/O让程序能够与文件进行交互。
11.当hello运行完毕,shell父进程回收hello,hello也就彻底消失,From zero to zero。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

Hello的实现,看似只是在计算机界面打印出了结果,而其却经历了众多的过程,在操作系统的参与下,硬件与软件相结合,共同合作,完成对hello的执行。其中经历了很多阶段,从顶层一步步走向底层,每深入一部,就需要调动更多的资源,需要更多的处理流程。这些复杂却又固定的流程,是优化过的更适合计算机的处理流程。Hello虽然只是个例,但程序的经历却大体一致,麻雀虽小五脏俱全,我们的起点hello,平凡却也伟大着。

附件

各类中间文件

hello.i: hello.c预处理的结果,预处理过程展开以#起始的行,试图解释为预处理指令。

hello.s: hello.i编译后的结果,得到了汇编语言代码。

hello.o: hello.s汇编后的结果,即可重定位目标程序,用于链接器或编译器链接生成最终可执行程序。

hello: hello.o链接后生成的可执行目标文件,可以hello.o加载到内存并执行。

helloo_objdump: hello.o文件的反汇编代码,用于查看汇编代码。

hello.objdump: hello文件的反汇编代码,用于查看机器指令反汇编代码。

Helloelf:ELF格式下的Hello.o文件

helloelf: ELF格式下的hello.o文件
(附件0分,缺失 -1分)

参考文献
(美)布赖恩特(Bryant,R.E.)《深入理解计算机系统第三版》

百度百科:逻辑地址
https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80/3283849?fr=aladdin

地址转换:https://www.cnblogs.com/felixfang/p/3420462.html

Getchar函数https://baike.baidu.com/item/getchar%28%29/6876946?fr=aladdin

Printf函数分析https://www.cnblogs.com/pianist/p/3315801.html

编译原理:
https://wenku.baidu.com/view/9364b0b6d05abe23482fb4daa58da0116d171f03.html?from=search

计算机系统:程序Hello相关推荐

  1. 深入理解计算机系统---程序运行过程

    一个简单的C程序从编写到执行输出hello world!其中间经历的是诸多处理过程,而不仅仅是显示黑屏上的几个字符.这个过程透露着计算机系统的运行本质. 个人对该过程进行了一些分析和总结,如果有不对的 ...

  2. 《深入理解计算机系统-程序结构》读书笔记

    1.计算机系统漫游 计算机系统是由硬件和系统软件组成的,他们共同工作来运行应用程序.在<深入理解计算机系统>一书中将会学到很多实践的技巧.例如:了解编译器是如何实现过程调用的.避免缓冲区溢 ...

  3. 计算机系统程序和应用软程序的区别,系统软件和应用程序软件有什么区别?

    开放图书馆的成员最低需要0.27元才能查看全部内容> 原始发行者: zhangconhuan 系统软件是指控制和协调计算机和外部设备并支持应用程序软件的开发和操作的系统. 它是不需要用户干预的各 ...

  4. 计算机系统 程序人生-Hello’s P2P

    目录 摘  要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2在Ubuntu下预处理的命令 2.3 H ...

  5. 计算机系统 程序和指令

    文章目录 程序和指令 intel 处理器 高级语言中程序寻址举例 IA 32 机器指令格式 IA32 常用传送指令 常用定点运算指令 加分以运算的底层实现举例 加法指令和乘法指令举例 逻辑运算指令和按 ...

  6. 计算机系统-程序加载器

    本文将实现一个简单的程序加载器,首先要先了解一下实模式下的内存空间分配,这是固定好的 要实现一个程序加载器,需要实现下面4个步骤 1.将用户程序从硬盘中读取到10000处(当然,放到10000-9FF ...

  7. 计算机系统程序文件扩展名,怎么显示电脑文件扩展名

    有时候不知道文件的格式是一件很糟糕的事情,学习啦小编来教大家显示文件扩展名.欢迎阅读! 显示文件扩展名 你是否遇到过这种情况,在Windows中想把一个文件File.txt改为File.dat,改后发 ...

  8. 如何完成计算机的安装程序,如何安装计算机系统程序.doc

    目录 目录2 1.工具准备3 2.安装前确认3 3. 系统安装5 3.1安装前准备工作_______5 3.2ghost版安装__________6 3.3光盘安装版安装_______________ ...

  9. 百度一 29 岁程序员因使用CURL命令“篡改数据”被判有期徒刑一年九个月,并没收所有违法所得

    整理 | 王晓曼 出品 | 程序人生 (ID:coder _life) 近日,中国裁判文书网公布了一起非法控制计算机信息系统.给赌博网站"大开方便之门"的案件,涉及金额达374万元 ...

  10. 快速学习计算机系统编程

    全部内容基本就在这里了: C语言的语法,函数,指针,编译,调试 数据结构和算法: 数组,链表,树,图,排序,查找,插入,删除: Linux系统资源编程: 文件IO,进程,线程,信号,网络通信: 对于系 ...

最新文章

  1. Tensorflow源码解析5 -- 图的边 - Tensor
  2. ConfigurationClassPostProcessor设计与实现
  3. jQuery知识点笔记-常用方法
  4. C#处理微信json(将JSON转换为对象)
  5. JPA在MySQL中自动建表
  6. 编写程序,使用指针把一个 int 型数组的所有元素设置4.18: 为 0。
  7. java添加事件监听器_Java事件监听器的四种实现方式
  8. 【51nod - 1076】2条不相交的路径(Tarjan无向图判环)
  9. Asp.net MVC在Razor中输出Html的两种方式
  10. 大数据行业到底有多少种工作岗位,各自的技能需求是什么?
  11. Restrictions
  12. Process Hacker工具使用
  13. 【竞赛篇-国创(大创)申报书撰写(三大类别七千字总结建议)】国家级大学生创新创业训练计划申报书撰写经验分享
  14. 【基于贪心的树型动态规划】【NOI2007】追捕盗贼
  15. gpib-usb-hs linux,美国NI GPIB-USB-HS+连接线GPIB转USB接口控制器高速传输
  16. 什么是真正的架构设计?十年Java经验让我总结出了这些,不愧是我
  17. 十四五期间我国区块链技术趋势特征分析
  18. 链接预测(Link Prediction)
  19. spring整合jdbc配置文件
  20. outlook邮箱邮件内容乱码_邮件标题乱码问题解决一例

热门文章

  1. 线下门店互动营销产品浅析
  2. USACO05JAN「Naptime」
  3. 未报价快递损毁如何处理(网上信息整理)
  4. TCP的运输连接管理——TCP的连接建立
  5. 离线安装ffmpeg
  6. 微信小程序新闻详情页面效果实现
  7. 谷歌浏览器如何查css,谷歌浏览器查看编辑元素CSS样式_谷歌工具
  8. Linux内核优化(二):网络线程优化
  9. 通信协议基础知识总结二
  10. 联通没有4g显示无服务器,联通4G去哪了?解密“消失”的联通4G信号