CS程序人生

大作业

题     目  程序人生-Hello’s P2P 

专       业        人工智能        

学     号       2022120050      

班     级         22E0361        

学       生                

指 导 教 师          郑贵滨       

计算机科学与技术学院

2023年5月

摘  要

当我们开始学习信息系统时,做过漫游计算机系统的实验。通过课堂上的学习我们了解到我们需要先将源程序 (.c) 进行预处理、编译、汇编、链接从而处理成二进制可执行目标程序,这样计算机才能运行该程序。在程序运动的同时,计算机系统中的处理器等硬件系统需要协助完成运行。本次大作业的内容是展现一个简单的程序,如何在计算机系统内逐步完成运行。

关键词:计算机系统;汇编;链接;数据预处理……                        

目  录

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

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

1.3 中间结果... - 4 -

1.4 本章小结... - 4 -

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

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

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

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

2.4 本章小结... - 8 -

第3章 编译... - 9 -

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

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

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

3.4 本章小结... - 11 -

第4章 汇编... - 12 -

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

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

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

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

4.5 本章小结... - 15 -

第5章 链接... - 16 -

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

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

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

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

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

5.6 hello的动态链接分析... - 20 -

5.7 本章小结... - 21 -

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

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

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

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

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

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

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

6.7本章小结... - 26 -

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

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

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

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

7.4 缺页故障与缺页中断处理... - 27 -

7.5本章小结... - 28 -

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

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

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

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

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

8.5本章小结... - 30 -

结论... - 31 -

附件... - 32 -

参考文献... - 33 -

第1章 概述

(0.5分)

1.1 Hello简介

P2P:当我们编写一个hello.c C程序时,它将被存储为程序的文本。当这个程序运行时,它由编译器驱动程序启动,读取hello.c文件,然后对其进行预处理以获得另一个C语言程序hello.i文件;然后编译器编译Hello.i:它成为一个汇编语言程序,即Hello.s。然后将汇编器交给汇编器进行组装:获得一系列机器语言指令,并将这些机器语言指令打包在“可变目标程序”中并存储在hello.o(二进制)文件中。最后,链接器链接:结果是可执行对象文件:hello。然后,计算机可以运行问候语文件。后来,在计算机 Bash (shell) 中,操作系统会为问候创建一个分支,这样在计算机系统中,问候语就有自己独特的进程(Process),问候语可以在其中运行。

020:程序不在内存空间中启动,即以 0 开头。操作系统为 hello 分叉子进程,然后在 execve 中运行 hello 程序,操作系统为其打开一个虚拟内存块,并将该程序加载到虚拟内存映射到的物理内存中。当程序完成运行时,操作系统会声明该程序,并且程序的可用内存空间也会恢复,再次变为 0。

1.2 环境与工具

CPU:AMD Ryzen7 5800H,16GB内存。

虚拟机:Ubuntu 20.04.4 LTS,VMWare Workstation 16

文本编辑器gedit,反汇编工具edb,反汇编objdump,编译环境gcc等。

1.3 中间结果

原始代码hello.c → 预处理hello.i → 汇编代码hello.s →

目标文件hello.o → objdump结果ans.txt → 执行文件hello →

objdump结果ans0.txt

1.4 本章小结

       本章介绍了hello中P2P,050的过程,使用的环境与工具,以及程序生成的过程中的中间结果。

第2章 预处理

(0.5分)

2.1 预处理的概念与作用

预处理器根据以字符#开头的命令,修改原始的C程序。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

预处理阶段根据放置在文件中的预处理指令更改源文件的内容。例如,#include 是一个预处理指令,用于将头文件的内容添加到.cpp文件中。这种预处理机制提高了源文件的灵活性,可以适应不同的计算机和操作系统;而且,通过预处理指令,可以使用已经封装好的库函数,大大提高了编程效率。

2.2在Ubuntu下预处理的命令

在终端内输入命令gcc -E hello.c,得到结果。

2.3 Hello的预处理结果解析

如下图所示,hello.i文件内容。

2.4 本章小结

本章对hello.c进行了预处理操作,将汇编文件存在hello.i文件中,通过分析得知,在计算机系统运行的过程中,对代码进行了大量的操作,最终得到结果

第3章 编译

2分)

3.1 编译的概念与作用

当源程序C进行预处理时,编译器将C语法原始格式的代码转换为面向CPU的机器指令,转换后的结果保存为汇编语言文本。此过程在 hello.c 中可执行文件转换时最为重要,因为它直接决定了程序的机器级表示形式。 

3.2 在Ubuntu下编译的命令

在终端内输入命令/usr/lib/gcc/x86_64-linux-gnu/9/cc1 hello.i -o hello.s,得到结果如下。

3.3 Hello的编译结果解析

      

3.3.1 数据传递

此步骤中将数据存入寄存器,通过movl,movq指令对不同长度的字符串进行数据传递。

3.3.2 数据局部变量赋值

此步骤通过movl指令,为寄存器内赋值为1。

3.3.3数据的加法

此步骤通过addq指令,实现对寄存器内数值加8的过程i++。

3.3.4 条件判断与比较

此步骤通过cmpl和jle指令,比较寄存器内数值大小。

3.3.5 循环指令

此步骤通过jmp指令,完成跳转,如果运行到此段,则跳转到L3。

3.4 本章小结

在本章中,我们使用示例hello.i ->hello.s来直接观察计算机编译结果,了解语言的整个运行过程,了解了各种指令。

第4章 汇编

2分)

4.1 汇编的概念与作用

当编译器完成编译时,它实际上知道哪些机器指令应该与hello.c中的语句C匹配,但编译结果仍然是文本的形式。此时,汇编程序必须将汇编语言直接转换为处理器可以直接执行的机器的代码形式。

4.2 在Ubuntu下汇编的命令

在终端内输入命令as hello.s -o hello.o,得到结果如下。

4.3 可重定位目标elf格式

在终端内输入命令readelf -h hello.o,得到结果如下。

从ELF Header我们可以看到hello.oELF格式的一些基本信息。比如main函数的指令编码、操作系统的版本,节头文件的起始地址等等。

在终端内输入命令readelf -h hello.o,得到结果如下。

4.4 Hello.o的结果解析

在终端内输入命令objdump -d -r hello.o,得到结果如下。

我们会发现每行代码末尾的指令本质上是相同的,但每条指令前面都会有一串十六进制编码。从hello.o和hello.s文件的内容分析来看,hello.s是由汇编语言组成的,相比计算机可以识别的机器级指令,汇编代码仍然是一种抽象语言,反汇编得到的代码不仅是汇编代码,也是机器语言的代码。机器语言代码是计算机可以识别和执行的纯二进制代码。

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

4.5 本章小结

可以看出,汇编这一步骤使得hello程序真正开始从文本状态转化为二进制状态,在本章节中,我们分析了hello.o与hello.s之前的相同与不同之处,更加清楚汇编语言与机器语言的关系。

第5章 链接

1分)

5.1 链接的概念与作用

链接是将不同的代码和数据片段收集并组合成单个文件的过程,该文件可以加载到内存中并执行。

链接器在软件开发中起着关键作用,因为它们使分离编译成为可能。我们可以将其分解为更小、管理更好的模块,而不是将大型应用程序组织成单个巨大的源文件,这些模块可以独立修改和编译。当我们更改其中一个模块时,我们只需重新编译它并重新连接应用程序,而无需重新编译其他文件。

5.2 在Ubuntu下链接的命令

在终端内输入命令:ld-ohello.o-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/usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.ohello.o-lc/usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello得到结果如下。

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

在终端内输入命令readelf -h hello,得到结果如下。

在终端内再次输入命令readelf -h hello,得到结果如下。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息。

5.5 链接的重定位过程分析

在终端内输入命令objdump -d -r hello,得到结果如下。

链接器首先会将所有模块的节都组织起来,为可执行文件的虚拟地址空间定型,再根据这个虚拟地址空间将那些存在hello.o里的.rel.text和.rel.data节的重定位条目指向的位置的操作数都设置为正确的地址。

5.6 hello的动态链接分析

PLT 是一个数组,其中每个条目都是一个 16 字节的代码。PLT[0]是跳转到动态链接器的特殊输入。可执行程序调用的每个库函数都有自己的 PLT 条目。每个条目负责调用一个特定的函数。

GOT是一个数组,其中每个条目都是一个8字节的地址。当与 PLT 结合使用时,GOT[O]和 GOT[1]包含动态链接在解析函数地址时使用的信息。GOT [2] 是1d-linux.so 模块中动态链接器的入口点。剩余的每个条目对应于一个被调用的函数,该函数的地址必须在运行时解析。每个输入都有一个相应的 PLT 输入。

通过hello的ELF文件,可以看到GOT和PLT节的起始地址:

5.7 本章小结

通过上面的操作,我们了解了hello.o、静态库、动态链接库是如何通过链接机制组合起来的,了解了一个程序从加载到退出程序的全过程。该程序背后的问候语实际上比看起来要复杂得多,这来源于链接操作,使得在这些库的帮助下编写我们的程序变得非常容易,以便它们在操作系统提供的平台上正确运行。

第6章 hello进程管理

1分)

6.1 进程的概念与作用

进程是正在执行的程序的实例,系统的每个程序都在进程的上下文中运行。上下文由运行程序的某种状态组成,包括代码、数据、堆栈、寄存器、程序占用的资源存储在内存中等。进程的机制使得似乎每个程序都垄断了处理器和内存,但实际上,可以有许多进程同时在逻辑上同时运行,这是现代多任务操作系统的基础。

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

shell 是一个交互式应用程序级程序,可以代表用户运行其他程序。shell是信号处理的代表,负责创建每个进程,加载和运行程序,前端和后台控制,调用工作,发送和管理信号等。shell 持续读取用户输入的命令行,然后对其进行分析和处理,将命令行视为新作业,创建有关工作的子进程并在前台或后台运行它。

6.3 Hello的fork进程创建过程

父进程通过fork函数创建一个新的运行的子进程;子进程中,fork返回0,父进程中,返回子进程的PID;

新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程虚拟地址空间相同的但是独立的一份副本,子进程获得与父进程任何打开文件描述符相同的副本,最大区别是子进程有不同于父进程的PID;

当我们运行hello程序时,在shell中输入./hello,此时OS就会fork创建一个子进程来运行这一程序。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。与fork一次调用返回两次不同,execve调用一次,从不返回(除了出现错误)。

简单来说,execve函数就是用来执行一个可执行目标文件。

6.5 Hello的进程执行

shell 收到 ./hello 命令行后,对其进行分析,发现是上传并运行可执行文件的命令,于是会先创建一个对应的作业 ./hello,然后创建一个 child-by-fork process(),这个子进程和父进程几乎和父进程一模一样, 它们具有相同的代码段、数据段、堆、共享库和堆栈段,但它们的 PID 和 fork 返回值不同,因此可以区分它们。然后,父进程将使用 setpgid()将新创建的子进程放在新的进程组中,以便该进程组对应于 ./hello 作业,并且 shell 可以通过向进程组中的所有进程发出信号来管理任务。

6.5 hello的异常与信号处理

正常运行:

停乱按(除了后续的特定键入组合):

Ctrl-Z:

Ctrl-C:

Ctrl-z后运行ps:

Ctrl-z后运行jobs:

Ctrl-z后运行pstree:

Ctrl-z后运行fg:

Ctrl-z后运行kill:

6.7本章小结

本章我们逐步分析了如何通过shell执行我们得到的可执行目标文件。将Program转化成一个Process。

第7章 hello的存储管理

2分)

7.1 hello的存储器地址空间

逻辑地址是程序直接使用的地址,它表示为“段:偏移地址”的形式,由一个段选择子(一般存在段寄存器中)再加上段内的偏移地址构成。

线性地址(或者叫虚拟地址)是虚拟内存空间内的地址,它对应着虚拟内存空间内的代码或数据,表示为一个64位整数。

物理地址是真正的内存地址,CPU可以直接将物理地址传送到与内存相连的地址信号线上,对实际存在内存中的数据进行访问。物理地址决定了数据在内存中真正存储在何处。

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

对于一个以“段:偏移地址”形式给出的逻辑地址,CPU将会通过其中的16位段选择子定位到GDT/LDT中的段描述符,通过这个段描述符得到段的基址,与段内偏移地址相加得到的64位整数就是线性地址。这就是CPU的段式管理机制,其中,段的划分,也就是GDT和LDT都是由操作系统内核控制的。

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

虚拟地址空间会被分为若干页,即分页机制。CPU对于一个线性地址会取它的高若干位,通过它们去存储在内存中的页表里查询对应的页表条目,得到这个线性地址对应的物理页起始地址,然后与线性地址的低位(页中的偏移)相加就是物理地址。

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

当CPU执行某条指令的内存访问时,如果页表中的PTE表明这个地址对应的页不在物理内存中,那么就会引发缺页故障,这使得跳转入内核态,执行操作系统提供的缺页中断处理程序,然后缺页中断处理程序能够将存在磁盘上的页使用一定的替换策略加载到物理内存,并更新页表。

7.5本章小结

本章主要介绍了hello程序的存储管理,辨析了逻辑地址、线性地址、虚拟地址和物理地址的关系;又分析了逐步地将地址翻译为最终物理地址的翻译;更深层次的理解了页表、Cache、内存映射的概念,对fork、execve有了新的理解视角;又介绍了动态内存管理的基本方法和策略。

第8章 hello的IO管理

2分)

8.1 Linux的IO设备管理方法

在 Linux 中,所有 IO 设备(网络、磁盘、终端等)都建模为文件,所有输入和输出都作为对相应文件的读写来执行。这种优雅的设备到文件映射模式允许 Linux 内核获得称为 Unix I/O 的简单、低级应用程序接口,以便所有输入和输出都可以以统一和一致的方式执行。

8.2简述Unix IO接口及其函数

UNIX IO 接口允许 4 种基本操作:

(1)打开文件,应用程序要求内核打开相应的文件,宣布要访问IO设备,内核返回该文件的描述符来标识该文件。shell 创建的每个进程都以 3 个打开的文件开头:标准输入 (stdin)、标准输出 (stdout) 和标准错误 (stderr)。

(2) 要更改当前文件位置,应用程序通过执行搜索操作将当前文件位置显式设置为 k。

(3)读写文件,读取操作必须从当前位置k开始,将文件中的n个字节复制到内存中,然后增加k到k+n,当k超过文件长度时,应用程序可以被EOF检测到。写入操作将 n 个字节从内存复制到文件,从 k 文件的当前位置开始,然后更新 k。

(4)关闭文件,当应用程序完成对文件的访问,通知内核关闭文件,内核释放打开文件时创建的数据结构和内存资源。

8.3 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;

}

首先,printf 打开一个输出缓冲区,然后使用 vsprintf 在输出缓冲区中生成输出字符串。然后通过写入将此字符串输出到屏幕。写入内核跳过系统调用陷阱,内核的显示驱动生成像素数据,通过这些字符串及其字体进行显示,并将它们传输到屏幕上相应区域的显示VRAM。显示芯片以刷新率逐行读取VRAM,并通过信号线将每个点(RGB组件)传输到液晶显示器。

8.4 getchar的实现分析

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;

      }

      return(--n>=0)?(unsigned char)*bb++:EOF;

}

首先,getchar 打开一个静态输入缓冲区,如果输入缓冲区为空,则调用 read 以读取输入缓冲区中的字符串。读取内核会跳过系统调用陷阱,这会使调用者等待。按下键盘时,键盘暂停处理程序执行,将键盘端口读取的扫描代码转换的字符输入输入缓冲区,直到调用方在按 Enter 后停止等待。 因此,getchar 所做的就是继续从输入缓冲区中获取一个字符,如果没有,则等待输入。

8.5本章小结

通过上面的分析,我们从基本的输入输出机制中揭示了hello如何在屏幕上打印信息以及它如何支持键盘输入,而它们背后的机制是软件通过底层IO端口与外部硬件的交互或中断。

结论

(1)在计算机的文本编辑器上用C语言编写hello.c的源文件;

(2)hello.c在预处理之后,将头文件的内容插入到程序文本中,得到hello.i;

(3)编译器对hello.i进行编译,从而得到汇编文件hello.s;

(4)经过汇编器汇编,得到与汇编语言一一对应的机器语言指令,在汇编之后,得到了可重定位目标文件hello.o,一个二进制文件;

(5)使用链接器对hello.o中调用函数的指令进行重定位,将调用的系统函数如printf.o等链接到hello.o,得到可执行目标文件hello;

(6)在shell-Bash中输入运行hello的命令行./hello,OS就为hello创建一个子进程,hello就在这个独一无二的进程当中运行;

(7)程序的运行一定伴随着对存储、地址的操作;首先,在hello中的地址为虚拟地址,要讲虚拟地址翻译映射到物理地址,才能对该地址进行操作;

(8)hello程序要想正常运行,需要用户输入用户的学号、姓名和间隔时间,所以我们还要了解计算机是如何管理I/O设备的;

(9)最后由shell父进程回收终止的hello进程。

计算机系统的设计必须合理地协调系统的各个部分,不仅要保证每个模块的正常运行,还要尽可能提高每个模块的执行速度,以及相互配合的效率。计算机系统是软硬件的单元,我们对计算机系统设计和实现的理解不能抛弃硬件,也不能忽视软件的水平。

附件

原始代码hello.c →

预处理hello.i →

汇编代码hello.s →

目标文件hello.o →

objdump结果ans.txt →

执行文件hello →

objdump结果ans0.txt

参考文献

[1] Randal E,Brynant, David R. O’Hallaron. 深入理解计算机系统(原书第三版). 北京:机械工业出版社,2016.

[2] https://blog.csdn.net/daide2012/article/details/73065204?spm=1001.2014.3001.5506

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值