2021春计算机系统大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业      计算学部         

学     号      1190201420       

班     级      1903004          

学       生      马立凡         

指 导 教 师      史先俊           

计算机科学与技术学院

2021年5月

摘  要

本文简述了hello.c源程序的预处理、编译、汇编、链接、运行的主要过程,以及hello程序的进程管理、存储管理与I/O管理,包括异常处理和寻址方法。通过对hello.c这一程序的程序周期的描述,深入介绍了程序的编译、加载、运行过程。

关键词:预处理;编译;汇编;链接;进程;加载;I/O管理    

目  录

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

1.2 环境与工具..................................................................................................... - 4 -

1.3 中间结果......................................................................................................... - 5 -

1.4 本章小结......................................................................................................... - 5 -

第2章 预处理......................................................................................................... - 6 -

2.1 预处理的概念与作用..................................................................................... - 6 -

2.2在Ubuntu下预处理的命令.......................................................................... - 6 -

2.3 Hello的预处理结果解析.............................................................................. - 7 -

2.4 本章小结......................................................................................................... - 8 -

第3章 编译............................................................................................................. - 9 -

3.1 编译的概念与作用......................................................................................... - 9 -

3.2 在Ubuntu下编译的命令............................................................................. - 9 -

3.3 Hello的编译结果解析.................................................................................. - 9 -

3.4 本章小结....................................................................................................... - 14 -

第4章 汇编........................................................................................................... - 15 -

4.1 汇编的概念与作用....................................................................................... - 15 -

4.2 在Ubuntu下汇编的命令........................................................................... - 15 -

4.3 可重定位目标elf格式............................................................................... - 16 -

4.4 Hello.o的结果解析.................................................................................... - 20 -

4.5 本章小结....................................................................................................... - 22 -

第5章 链接........................................................................................................... - 23 -

5.1 链接的概念与作用....................................................................................... - 23 -

5.2 在Ubuntu下链接的命令........................................................................... - 23 -

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

5.4 hello的虚拟地址空间................................................................................ - 30 -

5.5 链接的重定位过程分析............................................................................... - 32 -

5.6 hello的执行流程........................................................................................ - 33 -

5.7 Hello的动态链接分析................................................................................ - 34 -

5.8 本章小结....................................................................................................... - 35 -

第6章 hello进程管理................................................................................... - 36 -

6.1 进程的概念与作用....................................................................................... - 36 -

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

6.3 Hello的fork进程创建过程..................................................................... - 36 -

6.4 Hello的execve过程................................................................................. - 37 -

6.5 Hello的进程执行........................................................................................ - 38 -

6.6 hello的异常与信号处理............................................................................ - 39 -

6.7本章小结....................................................................................................... - 42 -

第7章 hello的存储管理............................................................................... - 43 -

7.1 hello的存储器地址空间............................................................................ - 43 -

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

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

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

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

7.6 hello进程fork时的内存映射.................................................................. - 46 -

7.7 hello进程execve时的内存映射.............................................................. - 47 -

7.8 缺页故障与缺页中断处理........................................................................... - 48 -

7.9动态存储分配管理....................................................................................... - 48 -

7.10本章小结..................................................................................................... - 49 -

第8章 hello的IO管理................................................................................. - 50 -

8.1 Linux的IO设备管理方法.......................................................................... - 50 -

8.2 简述Unix IO接口及其函数....................................................................... - 50 -

8.3 printf的实现分析........................................................................................ - 51 -

8.4 getchar的实现分析.................................................................................... - 52 -

8.5本章小结....................................................................................................... - 53 -

结论......................................................................................................................... - 54 -

附件......................................................................................................................... - 55 -

参考文献................................................................................................................. - 56 -

第1章 概述

1.1 Hello简介

在shell命令行中,输入:

./hello 学号 姓名 秒数

以执行hello程序。

shell解析命令行参数,并初始化环境变量等内容,获取argc、argc、envp,解析命令行参数发现该命令非内置命令,将其视为可执行文件,并以(学号)、(姓名)(秒数)作为argv[1]、argv[2]、argv[3]的内容。加载和运行过程调用fork函数创建进程、execve函数运行函数,通过内存映射、分配空间等手段让hello拥有自己的空间和时间,与其他程序并发地运行。由于没有’&’等符号,于是程序不在后台运行,hello在前台运行。至此程序Program转换为进程Process,即P2P。

O2O(From Zero-0 to Zero-0):

Linux加载器execve()将程序计数器置为程序入口点,也就是_start函数的地址。这个函数是在系统目标文件ctrl.o中定义的。_start函数调用系统启动函数__libc_start_main,该函数定义在libc.so,它初始化执行函数,调用用户层的main函数,处理main函数的返回值,并在需要的时候把控制返回给内核。

通过段式管理、页式管理,各存储器联动,CPU为执行文件hello分配时间周期,执行逻辑控制流,每条指令在流水线上取值、译码、执行、访存、写回、更新PC。内存管理单元和CPU处理器在执行过程中通过L1、L2、L3高速缓存(SRAM)和TLB(翻译后备缓冲器)、多级页表在物理内存中存取数据、指令,通过I/O系统输入输出。当程序运行结束时(例如通过键盘中断Ctrl+C手段),shell回收进程,释放hello的内存并且删除有关进程上下文。hello从无倒有再到0的过程就是From Zero-0 to Zero-0,即O2O。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2GHz;4G RAM;512GHD Disk

1.2.2 软件环境

Windows10 64位;Vmware 11;Ubuntu 20.04

1.2.3 开发工具

GCC;EDB;Objdump;readelf;Visual Studio Code

1.3 中间结果

文件名

作用

hello

可执行目标文件

hello.c

源程序

hello.i

预处理完的程序

hello.o

可重定位目标程序

hello.o.txt

hello.o的反汇编文件

hello.s

汇编程序

hello1elf.txt

ELF格式的hello.o

hello2.txt

hello的反汇编文件

hello2elf.txt

ELF格式的hello

1.4 本章小结

       本章简述了hello.c源程序的程序周期:P2P和O2O的过程。介绍了实验的硬软件环境、开发和调试工具,以及中间所生成的相关文件及其作用。

第2章 预处理

2.1 预处理的概念与作用

程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。

最常见的预处理是C语言和C++语言。ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase),通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。

作用:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。

比如在hello.c中:

图2.1 hello.c的头文件

这三行代码的#include命令告诉预处理器读取系统头文件stdio.h、unistd.h、stdlib.h的内容,并把它直接插入到程序文本中,结果就得到了另一个C程序。

2.2在Ubuntu下预处理的命令

cpp hello.c  > hello.i

图2.2 预处理

2.3 Hello的预处理结果解析

查看hello.i文件,有3000多行:

图2.3 hello.i文件内容

可以看到在文件的最后是我们的源代码,在此之前的3000行是对外部文件的引用,包含着各种函数以及重命名、结构体。

2.4 本章小结

本章介绍了预处理阶段的相关概念、定义、应用以及方法,通过具体的hello实例说明预处理过程中对头文件stdio.h的解析、对头文件stdlib.h的解析、对头文件unistd.h的解析,表现了预处理对于C程序的作用。预处理过程中进行了头文件引用,define宏替换以及注释的删除。

第3章 编译

3.1 编译的概念与作用

编译(compilation , compile):1、利用编译程序从源语言编写的源程序产生目标程序的过程。2、用编译程序产生目标程序的动作。编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。

作用:

1.优化程序性能

2.理解链接时出现的错误

3.避免安全漏洞

3.2 在Ubuntu下编译的命令

gcc –S hello.c –o hello.s

图3.1 编译

3.3 Hello的编译结果解析

查看hello.s文件,共80行:

图3.2 hello.s文件

3.3.1 数据

(1)常量

图3.3 常量的存储位置

源代码中的两个字符串常量保存在.LC0和.LC1中,都属于.rodata段。

(2)变量

argv、argc和i为局部变量,无全局变量。

图3.4 argc和argv的存储位置

argc要求与3比较,在汇编程序中存放在寄存器%edi中,比较过程中放在栈中,帧指针%rbp-20的位置。argv地址放在了寄存器%rsi中,使用时放在栈中,帧指针%rbp-32的过程。

图3.5 i的位置和作用

i是循环变量,初值为0每次循环加1,和8比较,共迭代8次。

3.3.2 赋值

程序共有两次赋值:

图3.6 赋值源代码

i = 0 和 i++(i = i + 1)

分别在.L2和.L4中:

图3.7 赋值汇编代码

3.3.3 算数操作

程序仅有一次算数操作,即i++:

图3.8 算数操作源码

其汇编代码在.L4中:

图3.9 算数操作汇编代码

3.3.4 关系操作

程序中有两次关系操作,分别是argc和4比较以及i和8比较:

图3.10 关系操作源码

其汇编代码在.LFB6和.L3中:

图3.11 关系操作汇编代码

3.3.5 数组操作

程序中的printf函数和sleep函数中的atoi函数进行了数组操作:

图3.12 数组操作源码

其汇编代码在.L4中:

图3.13 数组操作汇编代码

3.3.6 控制转移

程序中有两次控制转移,分别是if处和for循环处:

图3.14 控制转移源码

其汇编代码在.LFB6和.L4中:

图3.15 控制转移汇编代码

3.3.7 函数操作

程序使用了printf、exit、sleep、atoi和getchar这五个函数:

图3.16 函数操作源码

其汇编代码分别在.LFB6、.L4和.L3中:

图3.17 函数操作汇编代码

3.4 本章小结

本节介绍了编译器通过编译将.i文件转换为汇编语言的.s文件的过程。同时解析了变量,相关运算,以及各类c语言的基本语句的汇编表示以及他们在汇编代码中的存储位置,从而更容易理解高级语言的底层表示方法。

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将hello.s文件翻译成二进制机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存到目标文件hello.o中。hello.o是一个二进制文件,它包含着程序中函数main的指令编码。如果我们用文本编辑器打开hello.o文件,将看到一堆乱码。

作用:汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示。

4.2 在Ubuntu下汇编的命令

gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

图4.1 汇编

      

4.3 可重定位目标elf格式

首先使用readelf命令查看hello.o的ELF格式:

readelf -a hello.o > hello1elf.txt

图4.2 hello1elf.txt文件

4.3.1 ELF

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图4.3 ELF头

hello.o的ELF以一个16进制序列:

    7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

作为ELF头的开头。这个序列描述了生成该文件的系统的字的大小为8字节和字节顺序为小端序。

ELF头的大小为64字节,目标文件的类型为REL(可重定位文件)、机器类型为Advanced Micro Devices X86-64即AMD X86-64、节头部表的文件偏移为0,以及节头部表中条目的大小,其数量为13。

4.3.2 节头

节头部表描述了14个节的相关信息:

图4.4 节头

.text:已编译程序的机器代码

.rodata:只读数据

.data:已初始化的全局和静态C变量

.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量

.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息

.rel.text:一个.text节中位置的列表

.rel.data:被模块引用或定义的所有全局变量的重定位信息

.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量

.line:原始C源程序中的行号和.text节中机器指令之间的映射

.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字

.eh_frame:处理异常

.rela.eh_frame:.eh_frame的重定位信息

4.3.3 符号表

一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。符号表由汇编器构造,使用编译器输出到汇编语言.s文件中的符号。

图4.5 符号表

该符号表一共提供18个符号。其中:

main是一个位于.text节(Ndx=1)偏移量(value)为0,大小为125个字节的全局符号,类型为变量。

puts、exit、printf、sleep、getchar为NOTYPE未知类型,未定义(UND)符号。hello.c为文件,ABS表示不该被重定位的符号。

4.3.4 重定位条目

重定位条目常见共2种:

R_X86_64_32:重定位绝对引用。重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

R_X86_64_PC32:重定位PC相对引用。重定位时使用一个32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。

图4.6 重定位条目

注意:计算结果为无符号整型。

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

首先采用objdump进行反汇编:

objdump -s -d hello.o > hello.o.txt

图4.7 反汇编代码

机器语言由几个分开的段构成,而反汇编代码由分工明确的各个段组成。

数据上,反汇编代码中的立即数用十六进制表示,而机器语言用十进制表示,如图所示:

图4.8 反汇编代码立即数

 

图4.9 机器语言立即数

此外还可以看出,反汇编代码的数据传送指令没有区分寄存器大小,而机器语言则按照寄存器大小严格区分。

跳转指令方面,反汇编代码采用了地址跳转,而机器语言直接声明跳转目标的段存储位置:

图4.10 反汇编代码跳转

图4.11 机器语言跳转

关于重定位条目,反汇编代码采用重定向的方式跳转,并且留下一些地址以供返回时重定向,而机器语言仍采用直接声明的方式跳转。

图4.12 反汇编代码调用函数

图4.13 机器语言调用函数

4.5 本章小结

本章通过汇编和反汇编,深入了解了程序的汇编过程,并且通过查看汇编和反汇编的源码以及ELF条目,比对反汇编和汇编代码,了解了程序反汇编后代码和数据的存放位置、函数的调用方式、跳转方式、指令执行内容等等。

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。

作用:把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件。使得分离编译成为可能。

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的格式

首先使用readelf命令查看hello的ELF格式:

readelf -a hello > hello2elf.txt

5.3.1 ELF

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图5.2 ELF头

hello.o的ELF以一个16进制序列:

    7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

作为ELF头的开头。这个序列描述了生成该文件的系统的字的大小为8字节和字节顺序为小端序。

ELF头的大小为64字节,目标文件的类型为REL(可重定位文件)、机器类型为Advanced Micro Devices X86-64即AMD X86-64、节头部表的文件偏移为0,以及节头部表中条目的大小,其数量为25。

5.3.2 节头

如图5.3所示,节头共有27个节,对比之前未链接时多了13个节。

图5.3 节头

多出来的部分为:

interp:动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由ELF文件中的 .interp段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于/lib/ld-linux.so.2。(通常是软链接)

dynamic:该段中保存了动态链接器所需要的基本信息,是一个结构数组,可以看做动态链接下 ELF文件的“文件头”。存储了动态链接会用到的各个表的位置等信息。

dynsym:该段与 “.symtab”段类似,但只保存了与动态链接相关的符号,很多时候,ELF文件同时拥有 .symtab 与 .synsym段,其中 .symtab 将包含 .synsym 中的符号。该符号表中记录了动态链接符号在动态符号字符串表中的偏移,与.symtab中记录对应。

dynstr:该段是 .dynsym 段的辅助段,.dynstr 与 .dynsym 的关系,类比与 .symtab 与 .strtab的关系 hash段:在动态链接下,需要在程序运行时查找符号,为了加快符号查找过程,增加了辅助的符号哈希表,功能与 .dynstr 类似

rel.dyn:对数据引用的修正,其所修正的位置位于 “.got”以及数据段(类似重定位段 “rel.data”)

rel.plt:对函数引用的修正,其所修正的位置位于 “.got.plt”。

5.3.3 符号表

一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。符号表由汇编器构造,使用编译器输出到汇编语言.s文件中的符号。

图5.4 符号表

5.3.4 程序头

程序头表是一个结构数组。每种结构都描述了系统准备程序执行所需的段或其他信息。目标文件段包含一个或多个节,如段内容中所述。

程序头仅对可执行文件和共享目标文件有意义。文件使用 ELF 头的 e_phentsize 和 e_phnum 成员来指定各自的程序头大小。

图5.5 程序头

5.3.5 动态节

图5.6 动态节

5.3.6 重定位条目

图5.7 重定位条目

5.3.7 版本信息

图5.8 版本信息

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。  

命令:edb –run hello

图5.9 edb

观察其数据存储:

图5.10 data dump

可以看出虚拟内存地址是从0x400000开始的。

观察图5.3,可知.interp段的地址为0x4002e0,大小为1c,偏移量为0x2e0,因此在edb中查看0x4002e0地址的内容,可以得到链接相关信息。

图5.11 .interp段

在节头中寻找.text段,其地址为0x4010f0,偏移量为0x10f0。在edb中查看,里面存放的是已编译程序的机器代码,和之前查看的hello.s文件一致。

图5.12 .text段

查看.rodata段,在节头中显示其地址为0x402000,偏移量0x2000。在edb中查看,里面存放的是只读数据,比如printf语句中的格式串。

图5.13 .rodata段

5.5 链接的重定位过程分析

命令行输入:

objdump -d -r hello > hello2.txt

图5.14 链接重定位

hello.o的反汇编代码中,内存地址从0开始,而hello的反汇编代码中,虚拟内存地址从0x400000开始。

hello.o的反汇编代码开头为.text段,hello的反汇编代码以各种函数开头。

两个代码文件中call引用的目标地址也有所不同。

重定位地址计算公式为:

refaddr = ADDR(s) + r.offset

*refptr = (unsigned) (ADDR(r.symbol) + r.addend - reffaddr)

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

图5.15 edb打开hello

程序名

程序地址

ld-2.30.so!_dl_start

0x00007f8e3839b260

ld-2.30.so! dl_init

0x00007f8e383aa340

hello!_start

0x004010f0

ld-2.30.so!_libc_start_main

0x004010a0

libc-2.30.so! cxa_atexit

0x00007f8e38220580

hello!_libc_csu_init

0x004011c0

hello!_init

0x00401000

libc-2.30.so!_setjmp

0x00007f8e38220550

libc-2.30.so!_sigsetjmp

0x00007f8e38220540

libc-2.30.so!__sigjmp_save

0x00007a8e38220510

hello_main

0x00401125

hello!puts@plt

0x00401030

hello!exit@plt

0x00401070

hello!printf@plt

0x00401040

hello!sleep@plt

0x00401080

hello!getchar@plt

0x00401050

ld-2.30.so!_dl_runtime_resolve_avx

0x00007f1172b951f0

libc-2.30.so!exit

0x00007f80023e5530

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

动态链接项目中,查看dl_init前后项目变化。对于动态共享链接库中PIC函数,编译器加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略,将过程地址的绑定推迟到第一次调用该过程。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。

在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令:

图5.16 调用前

图5.17 调用后

在dl_init调用之后, 0x404010处的两个8字节的数据分别发生改变。

和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问时,GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结

本章进行了对hello.o的链接,将其和静态库链接在一起,生成了可执行文件hello,并通过不同方式对比了链接前后的汇编代码,深入了解了链接机制,以及重定位条目里的各项信息。

第6章 hello进程管理

6.1 进程的概念与作用

进程是计算机科学中最深刻、最成功的概念之一。

进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。

作用:通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。处理器就好像是无间断地一条接一条地执行我们程序中的指令。

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

Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件,同时它又是一种程序设计语言。

Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。Bash还能从文件中读取命令,这样的文件称为脚本。和其他Unix shell 一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。包括关键字、语法在内的基本特性全部是从sh借鉴过来的。其他特性,例如历史命令,是从csh和ksh借鉴而来。总的来说,Bash虽然是一个满足POSIX规范的shell,但有很多扩展。

功能:作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。

处理流程:

1.读取终端输入的命令

2.将输入字符串分割获得所有的参数

3.如果是内置命令,立即解释这个命令

4.如果是可执行目标文件,会在一个新的子进程的上下文中加载并运行这个文件

5.接收键盘输入信号并进行处理

6.3 Hello的fork进程创建过程

在命令行中输入./hello,这不是一个内置的shell命令,因此shell会认为hello是一个可执行文件,通过调用某个驻留在存储器中被称为加载器的操作系统代码来运行它。

父进程调用fork函数创建子进程,其地址空间与shell父进程完全相同,包括只读代码段、读写数据段、堆及用户栈等。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。

图6.1 fork

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。

父进程调用fork创建子进程后,调用execve函数在当前进程(新创建的子进程)中加载并运行hello程序,将.text,.data,.bss节等内容加载到当前进程的虚拟地址空间。

图6.2 进程的虚拟内存

6.5 Hello的进程执行

多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。

操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重启一个被抢占的进程所需得状态。

在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个决策称为调度。

hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。所以其实hello在sleep时就是这样的切换。

程序在进行一些操作时会发生内核与用户状态的不断转换。这是为了保持在适当的时候有足够的权限和不容易出现安全问题。

简单看hello sleep进程调度的过程:当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行1)保存以前进程的上下文2)恢复新恢复进程被保存的上下文,3)将控制传递给这个新恢复的进程,来完成上下文切换。

图6.3 上下文切换

如上图所示,hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时(2secs)发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

当hello调用getchar函数的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

首先运行:./hello 1190201420 马立凡 秒数

6.6.1 不停乱按

如果乱按过程中没有回车,则只是把输入屏幕的字符串缓存起来。如果输入最后是回车,使用getchar函数读回车,并把回车前的字符串当作shell输入的命令。

图6.4 不停乱按

6.6.2 Ctrl-Z

输入Ctrl-Z会发送一个SIGTSTP信号给前台进程组的每个进程,结束前台作业。

图6.5 Ctrl-Z

6.6.3 Ctrl-C

输入Ctrl-C会让内核发送一个SIGINT信号给前台进程组的每个进程,终止前台进程。

图6.6 Ctrl-C

6.6.4 Ctrl-Z后可以运行ps jobs pstree fg kill等命令

图6.7 ps

图6.8 jobs

图6.9 pstree

fg命令使一个后台作业变为前台作业,因此hello程序又开始执行。

图6.10 fg

6.7本章小结

本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁。介绍了shell的基本操作以及各种内核信号和命令,之后通过实例和画图描述在shell中使用fork新建子进程、使用execve加载并运行进程、hello进程的上下文切换。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:

在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。

物理地址:

在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。

虚拟地址:

CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。

线性地址:

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。

索引号,这里可以直接理解成数组下标,它对应的“数组”就是段描述符表,段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。

Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。

GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],

看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

把Base + offset,就是要转换的线性地址了。

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

计算机使用页表来完成虚拟地址到物理地址的转换。

1.处理器生成一个虚拟地址,并把它传送给MMU

2.MMU生成PTE地址,并从高速缓存中请求得到它

3.高速缓存向MMU返回PTE

4.MMU构造物理地址,并把它传送给高速缓存

5.高速缓存返回锁清秋的数据字给处理器

图7.1 页式管理

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。

因为所有的地址翻译都是在芯片上的MMU中进行的,因此非常快。

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

图7.2 TLB的组成

图7.3 多级页表

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

对于一个虚拟地址请求,首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就先在结合多级页表,得到物理地址,去cache中找,到了L1里面以后,寻找物理地址又要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3。上一级作为下一级的缓存,层层递进寻找地址。

图7.4 三级Cache

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

正如在第8章中学到的,execve函数在当前进程中加载并运行的hello程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

删除已存在的用户区域。删除当前进程(shell)虚拟地址的用户部分中的已存在的区域结构。

映射私有区域。为hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。

映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

设置程序计数器(PC) 。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

图7.5 加载器是如何映射用户地址空间的区域的

      

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

(1)处理器将虚拟地址发送给MMU

(2)(3)MMU利用虚拟地址对应的虚拟页号生成页表项(PTE)地址,并从页表中找到对应的PTE

(4)PTE中的有效位为0,MMU触发缺页异常

(5)缺页处理程序选择物理内存中的牺牲页(若页面被修改,则换出到磁盘)

(6)缺页处理程序调入新的页面到内存,并更新PTE

(7)缺页处理程序返回到原来进程,再次执行导致缺页的指令

图7.5 缺页异常及其处理

      

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。简单来说,动态分配器就是我们平时在C语言上用的malloc和free,realloc,通过分配堆上的内存给程序,我们通过向堆申请一块连续的内存,然后将堆中连续的内存按malloc所需要的块来分配,不够了,就继续向堆申请新的内存,也就是扩展堆,这里设定,堆顶指针向上伸展(堆的大小变大)。

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

分配器有两种基本风格:显式分配器和隐式分配器。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。

1.显式分配器:要求应用显式地释放任何已分配的块。例如,C程序提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++的new和delete操作符与C中的malloc和free相当。

2.隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

图7.6 堆

7.10本章小结

本章介绍了内存的相关知识,包含物理内存和虚拟内存以及他们之间的映射关系、转化方法,了解了段式管理和页式管理,fork和execve函数的内存映射,以及缺页故障和缺页中断管理机制,最后还介绍了动态内存的相关知识。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O接口统一操作:

设备可以通过Unix I/O接口被映射为文件,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

打开文件。 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

Linux shell创建的每个进程开始时都有三个打开的文件: 标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。

改变当前的文件位置。 对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

读写文件。 一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

关闭文件。 当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O接口函数:

①. 进程是通过调用open函数来打开一个存在的文件或者创建一个新文件的,函数声明如下:

int open(char *filename, int flags, mode_t mode);

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。

②. 进程通过调用close函数关闭一个打开的文件。函数声明如下:

int close(int fd);

成功返回0错误返回EOF

③. 应用程序是通过分别调用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的当前文件位置。

④. 通过调用lseek函数,应用程序能够显式地修改当前文件的位置。函数声明如下:

off_t lseek(int handle, off_t offset, int fromwhere);

8.3 printf的实现分析

首先查看printf函数源码:

图8.1 printf

printf函数的作用是接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。它用到了两个外部函数,一个是vsprintf,还有一个是write。

vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

write函数将buf中的i个元素写到终端。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

首先查看getchar函数的源码:

图8.2 getchar

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数的实现原理。

结论

hello所经历的过程:

hello一开始是一个源程序(文本),是用C语言编写的hello.c文件。

预处理:对#开头的指令进行解析,通过预处理器(cpp)解释预处理指令,生成hello.i文件。

编译:编译器(ccl)将解释完预处理指令的hello.i文件进行编译,得到汇编程序(文本)hello.s文件。

汇编:汇编器(as)将汇编程序hello.s中的汇编语言转换成机器语言,生成重定位信息,将这些代码和信息生成为一个可重定位目标程序(二进制)hello.o。

链接:由于hello程序调用了printf函数,其存在于一个名为printf.o的单独的预编译好了的目标文件中,所以链接器(ld)将hello.o和printf.o链接,处理合并为hello文件,这就是我们需要的可执行目标文件。

创建进程:在shell命令行输入./hello,shell解释命令并调用fork函数创建一个子进程。

加载程序:加载器调用execve函数,在当前进程(新创建的子进程)的上下文中运行hello程序。

内存管理:运行hello时,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache等计算机中的各个组成部件共同运转和配合,完成对内存地址的解析、请求、返回、访问。

异常处理:如果产生缺页异常,则缺页处理程序选择合适的牺牲页替换,并重新加载相应命令。

结束:当hello运行完毕,向父进程shell发送SIGCHLD信号,提示进程已终止,然后父进程回收hello,内核删除为这个进程创建的所有数据结构。

感悟:

学习了计算机系统,对在之前学习中的很多问题都仿佛看透了本质,明白了各种问题出现的原因以及解决方法。本次大作业中,一个小小的hello程序的实现,实际上牵动了计算机系统中的方方面面,看似复杂,实则巧妙,让我不得不感叹一代代前人的伟大,是他们的智慧和不懈奋斗,才造就了今天的计算机产业,造就了如今的美好时代。

附件

文件名

作用

hello

可执行目标文件

hello.c

源程序

hello.i

预处理完的程序

hello.o

可重定位目标程序

hello.o.txt

hello.o的反汇编文件

hello.s

汇编程序

hello1elf.txt

ELF格式的hello.o

hello2.txt

hello的反汇编文件

hello2elf.txt

ELF格式的hello

参考文献

为完成本次大作业你翻阅的书籍与网站等

[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.

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值