哈工大计算机系统期末大作业:程序人生

计算机系统

 

大作业

题     目  程序人生-Hellos P2P  

专       业  未来技术人工智能模块      

学     号   2022113063            

班    级   wl028                   

学       生   杨亦乔               

指 导 教 师   郑贵滨                  

计算机科学与技术学院

20245

摘  要

本文深入剖析了Linux环境下C语言程序的编译、链接与执行流程,以hello.c程序为例,全面解析了代码从源文件到可执行文件的转变过程。文章首先介绍了预处理、编译、汇编等编译阶段,每个环节都对程序的结构和功能产生重要影响。随后,探讨了链接阶段,详细阐述了目标文件合并、地址空间重定位等关键操作。

在程序执行阶段,文章着重分析了操作系统在进程创建、调度和资源分配中的作用,特别是用户态与核心态的切换以及系统调用的处理。同时,对操作系统的内存管理机制进行了深入探讨,包括内存映射、动态存储分配以及缺页故障的处理策略。

此外,文章还探讨了Linux系统在IO设备管理方面的策略,如何通过Unix IO接口实现程序与外部设备的交互。通过这些分析,本文不仅加深了对C语言程序编译执行过程的理解,也对操作系统的内存和IO设备管理有了更深入的认识。

关键词:计算机系统、编译过程、C语言程序、内存管理、IO设备管理

;                            

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 HELLO进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 HELLO的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 HELLO的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

在开始Hello 简介之前,我想先谈谈Hello 的重要意义:"Hello, World!"程序,虽然简单,却承载着深远的意义,它不仅是编程乃至计算机教育的起点,让初学者通过这个经典的例子理解计算机的基本概念,而且成为了编程语言特性展示的窗口,让程序员迅速把握新语言的基本结构。它还是计算机社区的一个传统,新成员通过编写这个程序来庆祝他们加入计算机世界。此外,"Hello, World!"作为一个跨语言的桥梁,促进了不同背景程序员之间的交流,同时也是技术演示和教学中不可或缺的基础示例。它不仅是编程文化的符号,代表着开放、分享和创新,也是技术普及和教育平权的象征,鼓励更多人参与到技术学习和创新中来。"Hello, World!"程序还激励和启发着程序员不断探索、学习和成长,见证了计算机技术的发展,成为了全球程序员共同的语言和连接点。

Hello程序的P2P过程:

Hello程序的生命周期(P2P过程)涉及从源代码到进程的转换,分为两个主要阶段:编译和执行。在编译阶段,源代码通过编译器(如gcc)转换成可执行文件(program)。执行阶段则由shell触发,通过调用execve函数,将程序加载到内存并创建一个进程(process)来运行。

编译过程的四个阶段:

1. 预处理阶段:预处理器(cpp)处理以#开头的预处理指令,对原始C程序进行必要的文本替换和条件编译,生成一个修改后的C程序,通常以.i为扩展名。

2. 编译阶段:编译器(如gcc中的ccl)将预处理后的C程序(hello.i)翻译成汇编语言程序(hello.s)。

3. 汇编阶段:汇编器(as)将汇编语言程序(hello.s)转换成机器语言指令,并打包成可重定位目标文件格式(hello.o)。

4. 链接阶段:链接器(ld)合并hello.o文件中的目标代码与其他必要的代码片段,生成最终的可执行文件(hello),该文件随后可被加载到内存中并由系统执行。

执行过程的020生命周期:

当shell接收到运行hello程序的命令时,它通过execve函数加载程序,映射到虚拟内存,并将程序载入对应的物理内存。程序初始化时,通过mmap系统调用来申请内存空间。随后,程序进入main函数执行其核心逻辑,CPU为该进程分配时间片,控制程序的执行流程。程序执行完毕后,由父进程shell进行资源回收,内核随后删除程序的相关信息并释放占用的资源。

流程图简述:

流程图展示了从shell命令的输入到hello程序的执行,再到程序终止和资源回收的完整生命周期。它清晰地描绘了程序从源代码到进程的转换,以及执行过程中的关键步骤和系统调用。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:X64 CPU;3.2GHz;16G RAM

软件环境:Windows11 64位;MacOS12.4;Ubuntu 20.04.4

开发和调试工具:gcc;objdump;edb;clion;vim等

1.2.1 硬件环境

  型号名称: MacBook Pro

  型号标识符: Mac14,7

  芯片: Apple M2

  核总数: 8(4性能和4能效)

  内存: 16 GB

  系统固件版本: 7459.121.3

  操作系统加载程序版本: 7459.121.3

  序列号(系统): HM4L652RPX

  硬件UUID:93F36213-A827-5D9B-9E93-8815D6D97743

  预置UDID: 00008112-000848482E31401E

  激活锁状态: 已启用

设备名称 kalinka

处理器 12th Gen Intel(R) Core(TM) i5-12400F   2.50 GHz

机带 RAM 16.0 GB (15.8 GB 可用)

设备 ID 05931DD7-7FEB-4D66-83D6-5A6A0CEA31DE

产品 ID 00330-80000-00000-AA682

系统类型 64 位操作系统, 基于 x64 的处理器

笔和触控 没有可用于此显示器的笔或触控输入

1.3 中间结果

hello.i:预处理得到的C程序

hello.s:编译得到的汇编语言程序

hello.o:汇编得到的可重定位目标程序文件

hello_fromo.s:hello.o反汇编得到的汇编语言程序

hello:链接得到的可执行目标文件

hello_fromout.s:hello反汇编得到的汇编语言程序

1.4 本章小结

本章介绍了hello的P2P、020过程,完成此大作业使用的开发环境和工具,以及由hello.c生成的中间文件,包括hello.i,hello.s,hello.o,hello_o.s,hello和hello_out.s。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

预处理是编译过程的第一阶段,它发生在实际编译之前。在C语言和其他一些编程语言中,预处理器(通常由编译器的一部分或一个独立的工具来实现)负责处理源代码文件中的预处理指令。这些指令以井号(#)开始,指示预处理器进行特定的操作。以下是预处理阶段的一些关键功能:宏替换:预处理器可以定义宏(#define),并在源代码中替换宏名称为其对应的值或代码。例如,`#define PI 3.14159` 会将源代码中的所有 `PI` 替换为 `3.14159`;文件包含:预处理器可以包含其他文件的内容,通过 `#include "filename.h"` 或 `<filename.h>` 指令。这允许代码重用和模块化;条件编译:预处理器支持条件编译,允许根据不同的条件包含或排除代码段。例如,`#ifdef`, `#ifndef`, `#endif`, `#else` 等指令可以根据宏是否定义来包含或排除代码;除此以外还可能执行错误检查,行控制,注释处理,预定义宏等任务

预处理完成后,源代码文件会被转换成一个新的文本文件,通常以 `.i` 或 `.ii` 作为扩展名,这个文件随后会被编译器进一步编译成目标代码。预处理是编译过程中不可或缺的一步,它使得程序设计更加灵活和强大。

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

可以看出,在预处理阶段,hello.c中的头文件#include被完整的解释在main函数上方,同时代码中注释的内容被删除。

2.4 本章小结

本章简要介绍了预处理过程并通过对hello.c进行预处理,具体而详细地介绍了预处理的概念和作用,体现了其引入头文件、并删除注释的功能。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译是将高级编程语言编写的源代码转换成计算机硬件能够执行的低级机器代码的过程;它涉及多个步骤,旨在优化代码、解决依赖关系以及生成机器可理解的指令;编译器在编译过程中会检查源代码中的语法错误、类型错误和其他潜在问题,确保生成的代码是正确的;同时,编译器会对代码进行优化,以提高程序的执行效率,包括消除冗余计算、优化循环结构、改进内存访问模式等;编译器还处理代码中的依赖关系,确保所有必要的库和模块在编译时被正确链接;最终,编译的结果是生成一个可执行文件,它包含了程序的所有机器指令和资源,可以直接在操作系统上运行;编译器允许开发者为不同的平台和架构编写代码,通过编译过程生成适用于特定平台的可执行文件;在编译过程中还会处理程序的内存分配请求,确保程序运行时能够有效地管理内存资源;编译器可以生成包含性能分析信息的代码,帮助开发者了解程序的性能瓶颈并进行优化;编译过程遵循特定的标准和规范,确保生成的程序能够在预期的环境中稳定运行;通过编译,可以隐藏源代码的具体实现细节,只暴露可执行文件,从而提高程序的安全性。        

简单来说编译过程中,编译器(ccl)将C语言编码的hello.i翻译成汇编的hello.s。

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

完整的汇编代码如下:
    .file    "hello.c"
    .text
    .section    .rodata
    .align 8
.LC0:
    .string    "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \346\211\213\346\234\272\345\217\267 \347\247\222\346\225\260\357\274\201"
.LC1:
    .string    "Hello %s %s %s\n"
    .text
    .globl    main
    .type    main, @function
main:
.LFB6:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    cmpl    $5, -20(%rbp)
    je    .L2
    leaq    .LC0(%rip), %rax
    movq    %rax, %rdi
    call    puts@PLT
    movl    $1, %edi
    call    exit@PLT
.L2:
    movl    $0, -4(%rbp)
    jmp    .L3
.L4:
    movq    -32(%rbp), %rax
    addq    $24, %rax
    movq    (%rax), %rcx
    movq    -32(%rbp), %rax
    addq    $16, %rax
    movq    (%rax), %rdx
    movq    -32(%rbp), %rax
    addq    $8, %rax
    movq    (%rax), %rax
    movq    %rax, %rsi
    leaq    .LC1(%rip), %rax
    movq    %rax, %rdi
    movl    $0, %eax
    call    printf@PLT
    movq    -32(%rbp), %rax
    addq    $32, %rax
    movq    (%rax), %rax
    movq    %rax, %rdi
    call    atoi@PLT
    movl    %eax, %edi
    call    sleep@PLT
    addl    $1, -4(%rbp)
.L3:
    cmpl    $9, -4(%rbp)
    jle    .L4
    call    getchar@PLT
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE6:
    .size    main, .-main
    .ident    "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long    1f - 0f
    .long    4f - 1f
    .long    5
0:
    .string    "GNU"
1:
    .align 8
    .long    0xc0000002
    .long    3f - 2f
2:
    .long    0x3
3:
    .align 8
4:

依据以上的汇编代码,分别举例一下操作:

数据类型:

1. 常量

   - 代码中的`.LC0`和`.LC1`是常量字符串,它们存储在程序的只读数据段(`.rodata`)中。

   - `.LC0: .string "Hello, World!"` 定义了一个字符串常量。

2. 变量。

   - 局部变量:通过栈帧访问,如`movl %edi, -20(%rbp)`将函数参数移动到栈上的局部变量位置。

3. 表达式

   - 表达式在编译时被展开,如`cmpl $5, -20(%rbp)`比较局部变量与常量5。

4. 类型

   - 类型信息在汇编中通常体现为操作数的大小和对齐,如`movq`操作64位的值。

5. 宏

   - 宏在预处理阶段展开,如`endbr64`可能是一个宏,用于防止循环展开优化。

 赋值和初始化

1. 赋值操作:

   - `movl $0, -4(%rbp)`将立即数0赋值给局部变量。

2. 赋初值/不赋初值:

   - 局部变量通常在使用前初始化,如上例中的`movl $0, -4(%rbp)`。

类型转换

1. 隐式类型转换:

   - 在`call atoi@PLT`调用中,字符串隐式转换为整数。

算术操作

1. 基本算术操作:

   - `addl $1, -4(%rbp)`将局部变量的值增加1。

2. 递增/递减操作:

   - 通过增加或减少变量的值实现,如`addl $1, -4(%rbp)`。

逻辑/位操作没有出现

关系操作:

   - `cmpl $9, -4(%rbp)`和`jle .L4`展示了如何使用比较操作和条件跳转实现关系操作。

数组/指针/结构操作

1. 指针操作:

   - `movq -32(%rbp), %rax`加载指向第一个参数的指针。

2. 结构操作:

   - 通过偏移量访问结构体成员,如`addq $24, %rax`和`movq (%rax), %rcx`。

控制转移

1. if/else:

   - `je .L2`实现if条件跳转。

2. for/while/do-while:

   - `jmp .L3`和`jle .L4`实现循环控制。

函数操作

1. 参数传递:

   - `movl %edi, -20(%rbp)`和`movq %rsi, -32(%rbp)`展示了如何将函数参数传递到栈上。

2. 函数调用:

   - `call printf@PLT`展示了函数调用。

3. 局部变量:

   - 如前所述,局部变量通过栈帧访问。

4. 函数返回:

   - `movl $0, %eax`设置返回值,`ret`指令返回。

3.4 本章小结


在本章中,我们深入探讨了编译过程的核心概念及其重要性。通过细致分析hello.c的源代码如何被转换成hello.s汇编代码,我们揭示了C语言与底层汇编语言之间的直接联系。我们不仅观察了函数的创建和结束,还详细探讨了常量和变量如何在汇编层面被存储和处理。

我们进一步分析了参数的加载机制以及函数调用的过程,这些都是程序能够执行更复杂操作的基础。同时,我们对程序流程控制结构,如if条件判断和for循环,如何在汇编层面实现进行了深入的讨论。这些控制结构对于程序的逻辑流程至关重要。

除此之外,我们还关注了返回值的设置方式,这是函数执行结果传递的关键环节。通过这些分析,我们不仅加深了对编译过程的理解,还对程序如何从高级语言转化为机器可执行指令有了直观的认识。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

汇编过程或汇编链接涉及将汇编语言编写的代码转换成计算机可以直接执行的机器指令;汇编器是一个程序,它读取用汇编语言编写的源文件,并将其转换成机器码,它将每条汇编指令映射到对应的机器指令,这个过程涉及到指令集架构(ISA)的规则,确保每条汇编指令有一个唯一的二进制表示;同时,汇编器解析代码中的符号,如标签和常量,将它们替换为内存地址或立即数,并为代码和数据分配内存地址,确保指令和数据在内存中正确定位。

汇编过程生成的机器码是计算机硬件能够理解和执行的二进制代码,它是针对特定平台和处理器架构的,这意味着汇编代码需要为每种目标架构重新汇编;现代汇编器也可能执行一些优化,比如指令选择和调度,以提高生成代码的效率;在有外部符号引用的情况下,汇编器会确保这些符号在最终的可执行文件或库中得到正确的解析;此外,汇编器可以生成调试信息,这些信息对于后续的程序调试和性能分析至关重要;在生成最终的可执行文件或库文件时,汇编器会处理代码和数据的重定位,确保它们在内存中的地址正确;汇编过程是构建软件过程中的一个环节,通常在编译之后,链接之前进行;汇编器确保汇编代码符合目标处理器的指令集架构,从而生成兼容的机器码;对于性能要求极高的代码段,汇编过程可以生成高度优化的机器码,以达到最佳性能。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

,在终端输入readelf -h hello.o得到的的ELF头如下图所示。第1-4个表示文件类型,第5个02说明为64位目标,第6个01说明为小端法,第7个01指ELF版本,第8个00说明未指定运行操作系统,第9个00指ABI版本,余下为补零位。图中还能直观地获取一些其他信息。

节头表如下图所示。每节的名称、类型、地址、偏移等信息都包含在图中。

使用readelf -r hello.o命令查看重定位节如下:可以看出,例如puts,printf,getchar等不在hello.c中定义但是仍然调用的函数出现,以便下一阶段链接器将hello.o与stdio标准库中的文件文件组合。

4.4 Hello.o的结果解析

使用objdump -d -r hello.o > hello_o.s得到hello.o的反汇编文件hello_fromo.s,它与hello.s相比较,代码最终完成的逻辑相同,可能有少量的细微步骤或目的的达成方式有些许不同。

但是整体上还有以下区别:

总的来说hello_o.s 是编译后、链接前的对象文件的反汇编,而 hello.s 是链接后、执行前的最终汇编代码。

当使用 `objdump` 工具对目标文件 `hello.o` 进行反汇编操作时,得到的 `hello_fromo.s` 文件与源汇编文件 `hello.s` 在几个关键方面存在差异:

1. 立即数的16进制表示:

   `hello_fromo.s` 文件中的立即数将以16进制形式展示,而不是10进制。16进制是汇编语言中表示数字的标准方式,因为它更简洁,且易于与二进制进行转换。例如,一个立即数可能显示为 `0x1A3` 而不是 `419`。

2. 伪指令和声明的缺失:

   `hello_fromo.s` 文件中不会包含伪指令或额外的声明,这些通常用于为汇编器提供指令,如定义常量、分配内存空间或指定节(sections)。在源汇编文件 `hello.s` 中,程序员可能会显式地使用这些伪指令来组织代码和数据。

3. 直接地址的使用:

   在 `hello_fromo.s` 中,跳转和调用指令将使用直接的内存地址,而不是节标记或函数名。这意味着,例如,一个跳转指令可能显示为 `jmp 0x123456` 而不是 `jmp .Lsome_label` 或 `jmp some_function`。这种直接地址引用在链接过程中会被解析为正确的目标地址。

4. 操作数大小的省略:

   反汇编文件 `hello_fromo.s` 中的指令可能不会明确指出操作数占用的字节大小。在汇编语言中,操作数的大小通常由指令的编码方式决定,而在反汇编的输出中,这个信息可能被认为是显而易见的,因此被省略。例如,一个移动指令可能简单地写作 `mov val, reg` 而不是 `movl val, reg` 或 `movq val, reg`,其中 `l` 或 `q` 分别指明了32位或64位操作的大小。

这些差异是可以理解的,其实际上是反映了 `objdump` 工具的输出特点,它侧重于展示目标文件中的实际机器指令,而不是提供汇编编程时可能需要的额外上下文信息。这种格式的输出更适合于分析和理解编译后的程序行为,而不是作为编写或编辑汇编代码的参考。

4.5 本章小结

本章介绍了汇编的概念、作用。通过gcc完成hello.s生成hello.o为实例,并分析了汇编生成ELF格式文件的过程,除此以外还将hello.o返回汇编成为hello_fromo.s,并比较了两者的相同与不同之处 


5章 链接

5.1 链接的概念与作用

链接是软件编译过程中的一个关键步骤,它发生在编译器生成汇编代码并被汇编器转换成机器码之后;链接器是一个工具,用于将一个或多个编译后生成的目标文件(.o 文件)以及库文件(库中也包含目标文件)组合起来,创建一个单一的可执行文件或库文件;链接器负责解析代码中的符号引用,将它们与定义这些符号的代码段连接起来,这包括变量、函数、常量等;链接器还为所有代码和数据分配最终的内存地址,确保程序中的每个部分在执行时都能正确访问;链接器还会进行重定位,调整代码和数据中的地址引用,确保它们与最终的内存布局一致;

链接器还有许多重要的作用:链接器将分散在多个目标文件中的代码和数据整合到一个可执行文件中,使得程序可以作为一个整体运行;程序通常会调用标准库或其他外部库中的函数,链接器负责将这些库中的代码整合到最终的可执行文件中;链接器解决程序中的符号引用问题,确保每个符号(变量、函数等)都能正确地与其定义绑定;链接器可以对程序的内存布局进行优化,比如通过合并相同内容的代码段或数据段来减小最终文件的大小;通过链接过程中的优化,可以提高程序的加载速度和运行效率;链接器还支持动态链接,允许程序在运行时加载和解析库代码,这有助于共享代码和减少内存占用;链接器可以生成包含调试信息的可执行文件,这对于后续的程序调试和性能分析非常重要;链接器在生成最终可执行文件时,可以应用各种安全措施,如地址空间布局随机化(ASLR)等;链接器确保最终生成的可执行文件与操作系统和硬件平台兼容;链接是软件构建过程中的最终步骤之一,它确保了程序的各个组成部分能够协同工作,生成一个可以被操作系统加载和执行的完整程序;通过链接,开发者能够将分散的代码片段和库函数组合成一个完整的应用程序,为用户提供最终的软件产品。

5.2 在Ubuntu下链接的命令

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

在命令行输入:readelf hello –segmenst查看hello的段信息。可以看到段有PHDR,INTERP,LOAD,DYNAMIC,NOTE,GUN_PROPERTY,GUN_STACK,GUN_RELRO,并且在filesize、memsize等处并给出了它们的物理与虚拟地址、大小、偏移、节映射等信息。

hello的段信息:

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,使用edb加载hello查看本进程的虚拟地址空间各段信息如下图所示,从0x0000000000401000开始,到0x0000000000402000结束。根据5.3中的elf表中可以查找到.init等的起始地址,仿照如此便可以在edb中找到对应。

5.5 链接的重定位过程分析

使用objdump -d -r hello > hello_fromout.s得到hello的反汇编文件hello_fromout.s,与hello.o的反汇编文件hello_o.s作对比,如下图所示:

 

通过对比 `hello_fromout.s` 和 `hello_fromo.s`,我们可以更清楚地理解链接过程以及链接器在其中所扮演的角色:

1. 重定位节和符号定义:

   `hello_fromout.s` 展示了链接器如何将所有相同类型的节(sections)合并为统一的新聚合节,并为这些聚合节以及输入模块中定义的每个节和符号分配了运行时内存地址。这意味着程序中的每条指令和全局变量现在都有了一个唯一的运行时内存地址。相比之下,`hello_fromo.s` 中的地址是相对的或尚未分配,因为链接过程尚未发生。因此,`hello_fromout.s` 中的内容会更加丰富,因为它包含了完整的程序结构和确切的内存地址信息。

2. 重定位节中的符号引用:

   在 `hello_o.s` 中,代码节和数据节中的符号引用可能包含重定位指令,如 `R_X86_64_PC32` 或 `R_X86_64_PLT32`,这些指令指示链接器在链接过程中需要对地址进行调整。在 `hello_out.s` 中,这些引用已经被链接器修改,指向了正确的运行时地址。这意味着所有的 `lea`(加载有效地址)和 `call`(调用)指令现在都包含了具体的、可以直接跳转或调用的内存地址。   

3. 库函数的整合:

   `hello_fromout.s` 展示了链接器如何处理库函数的整合。所有对库函数的调用,如 `printf` 或 `malloc`,在 `hello_fromo.s` 中可能只是作为符号引用存在,而在 `hello_out.s` 中,这些调用已经被解析为具体的库代码。`hello_fromout.s` 中的代码和数据布局代表了程序在内存中的最终形态。链接器不仅处理了重定位,还可能对代码和数据进行了优化,比如合并相同代码段或常量池,以及调整数据结构以提高内存访问效率。

4. 程序的自包含性:

   `hello_fromout.s` 中的程序是自包含的,意味着它包含了所有必要的代码和数据,可以独立运行。而 `hello_fromo.s` 可能依赖于其他目标文件或库文件中的代码和数据。

总的来说,通过对比 `hello_fromout.s` 和 `hello_fromo.s`,我们不仅可以看到链接器在重定位和符号解析方面的工作,还可以理解链接器如何整合代码、数据和库函数,以及它如何优化程序的内存布局和性能。这些步骤是将编译后的目标文件转换成一个完整、可执行程序的关键环节。

5.6 hello的执行流程

在Hello执行的过程中hello调用与跳转的各个子程序名如下表所示:

子程序名

hello!_start

libc-2.31.so!__libc_start_main

hello!_init

hello!main

hello!printf@plt

hello!atoi@plt

hello!sleep@plt

hello!getchar@plt

hello!exit@plt

hello!__libc_csu_init

hello!_fini

5.7 Hello的动态链接分析

   

在elf中可以看到.got的地址

使用edb调试,dl_init(即初始化)前该区域内容如下图所示:

dl_init(即初始化)后该区域内容如下图所示:

在初始化后,.got地址处的内容发生了改变,全局偏移量表存储函数的绝对目标地址,过程链接表利用这些地址进行跳转。在加载时,动态链接器会修正全局偏移量表,将每个条目的地址变为指向实际目标函数的绝对地址,实现动态链接,这是因为动态链接器通过过程链接表和全局偏移量表协作。

5.8 本章小结

本章介绍了链接的概念和作用,以hello为例,分析了可执行文件的虚拟地址空间,并比较了hello_fromout.s和hello_fromo.s,由此分析了重定位的过程与动态链接的过程。

(第5章1分)


6章 hello进程管理

6.1 进程的概念与作用

进程是计算机中程序执行的动态实例,拥有独立的内存地址空间;它由操作系统分配和管理,每个进程都有其独特的进程控制块(PCB),记录着进程的属性和状态;进程实现了代码的隔离执行,确保了程序间相互独立;并发性允许多个进程在宏观上同时运行,提高了资源的利用率。

进程的作用在于实现资源的合理分配和管理,每个进程独立使用CPU时间、内存和I/O设备;调度方面,操作系统通过调度算法决定进程的运行顺序,实现多任务处理;隔离性提供了稳定性和安全性,每个进程的失败不会影响其他进程;进程间的通信机制允许数据交换和协作,支持复杂的应用程序运行。

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

Shell是操作系统用户界面的一部分,它提供了一个交互式的命令行环境;用户可以通过Shell执行各种系统命令和程序,Shell解释用户输入的命令并将其转换为系统调用;Shell处理流程开始于读取用户输入的命令,接着进行语法分析,确定命令的类型和参数;对于内置命令,Shell会直接执行而无需创建新进程;对于外部命令,Shell通过`fork`系统调用创建一个新的子进程,然后在子进程中使用`execve`加载并执行指定的程序;如果命令需要在后台运行,Shell会立即返回,继续监听新的用户输入;对于前台运行的命令,Shell会通过`waitpid`或其他等待调用,挂起运行直到相关程序完成执行;一旦程序终止,Shell负责回收其使用的资源,并继续执行用户的后续命令。)

6.3 Hello的fork进程创建过程

fork首先进行系统调用:在hello程序中,当需要创建一个新进程时,会用fork系统调用。fork调用执行后,它会复制当前进程(父进程)的地址空间,包括代码段、数据段、堆、栈等,创建一个新的子进程。fork调用在父进程中返回新创建子进程的PID(进程ID),而在子进程中返回0。这样,父进程和子进程就可以通过返回值来区分自己的身份。子进程是一个独立的实体,拥有自己的进程ID,并且有自己的执行栈。从fork返回之后,子进程和父进程将独立地执行后续的代码。虽然子进程复制了父进程的资源,但是子进程会设置自己的一些特定属性,如更改工作目录、打开文件等,以便执行特定的任务。

6.4 Hello的execve过程

execve函数可以在当前进程的上下文中加载并运行一个程序。在运行hello时shell的子进程使用execve加载hello,并调用启动代码设置栈,将控制权传递给hello的main函数。

6.5 Hello的进程执行

1. 初始阶段:

   - 用户在终端输入`./hello`命令,Shell通过`execve`系统调用加载`hello`程序到其地址空间,替换当前Shell进程的内容,准备执行`hello`程序。

2. `fork`系统调用:

   - `hello`程序执行到需要创建子进程的部分,调用`fork`。操作系统创建一个新的子进程,复制父进程的地址空间,包括代码段、数据段、堆、栈等。

   - `fork`调用在父进程中返回子进程的PID,在子进程中返回0。

3. 子进程的独立执行:

   - 子进程开始独立执行,它从`fork`调用之后的代码继续运行。如果`hello`程序设计为在子进程中执行特定的任务,此时子进程将进行这些任务。

4. 进程上下文信息:

   - 每个进程都有自己的进程控制块(PCB),包含了进程状态、程序计数器、寄存器集合、调度信息等。操作系统利用这些信息进行进程管理和调度。

5. 进程时间片和调度:

   - 在多任务操作系统中,多个进程可以并发执行。操作系统的调度器按照时间片轮转或其他调度算法,分配CPU时间给各个进程。

   - 当一个进程的时间片用完,调度器会将其置于就绪状态,并选择另一个进程运行。

6. 用户态与核心态的转换:

   - 在执行系统调用(如`fork`、`execve`或其他I/O操作)时,进程从用户态切换到核心态,此时操作系统接管CPU,执行系统调用请求的操作。

   - 系统调用完成后,操作系统将进程从核心态切换回用户态,进程继续执行用户空间的代码。

7. 进程的等待和结束:

   - 如果`hello`程序中的子进程需要在父进程中等待其结束,父进程可以使用`waitpid`系统调用等待子进程结束,并获取其退出状态。

   - 子进程完成任务后,会通过`exit`系统调用结束自己,释放资源,并将退出状态报告给父进程。

8. 资源回收和进程终止:

   - 父进程通过`waitpid`回收子进程的资源,操作系统将子进程使用的内存、打开的文件等资源标记为可用。

   - `hello`程序执行完成后,也会通过`exit`调用结束自己,操作系统回收其所有资源,并将控制权返回给Shell。

6.6 hello的异常与信号处理

hello在执行过程中可能出现的异常有中断、陷阱和系统调用、故障、终止;

运行时乱按并按回车,可以看到对程序运行并无影响

在运行时按control  + z可以停止程序运行

分别按ps、jobs观察运行结果如上图

输入pstree可以得到上图

最后可以使用kill 杀掉进程,终止京城,可以看到在kill前后,kill前hello存在,kill后hello 不再存在。

6.7本章小结

本章介绍了进程的概念和作用,Shell-bash的作用,以及fork进程创建过程、execve过程,描述了进程的执行和hello的异常和信号处理。

(第6章1分)


7章 hello的存储管理

7.1 hello的存储器地址空间

1. 逻辑地址(也称程序地址或相对地址):

   - 逻辑地址是程序在编译时生成的地址,它们是相对于程序的起始点计算的偏移量。

   - 在执行"Hello"程序时,逻辑地址用于指令和变量的引用,例如,一个变量可能在代码中以相对于某个固定点的偏移量被引用。

2. 线性地址(也称虚拟地址):

   - 线性地址是程序在执行时使用的地址,它们构成了一个平坦的地址空间。

   - 当"Hello"程序运行时,CPU将逻辑地址转换为线性地址,这个过程称为地址转换。

   - 线性地址空间由操作系统管理,可能包括程序代码、数据、堆、栈等多个段。

3. 虚拟地址:

   - 虚拟地址是程序在运行时看到的地址,它对程序员是透明的,由操作系统和内存管理单元(MMU)管理。

   - 在现代操作系统中,虚拟地址通常与线性地址是同一个概念,它们都指的是程序能够访问的地址空间。

4. 物理地址:

   - 物理地址是实际内存中的实际位置,即RAM中的地址。

   - 当CPU通过虚拟地址访问内存时,MMU将虚拟地址转换为物理地址,这一过程称为地址映射或内存寻址。

在执行"Hello"程序的过程中,当程序引用一个变量或需要执行一条指令时,CPU会使用逻辑地址。操作系统和MMU共同工作,将这些逻辑地址转换为虚拟地址,然后通过页表等机制将虚拟地址映射到物理地址,最终实现对实际内存的访问。这种分层的地址转换机制使得程序能够以一种抽象和简化的方式访问内存,同时操作系统可以有效地管理内存资源,实现内存保护、共享和虚拟化等高级功能。

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

Intel体系结构中,逻辑地址到线性地址的转换过程涉及到段式管理,这是一种内存管理技术,用于提供更大的地址空间并允许程序分段。以下是段式管理的概念和转换过程:

- 段式内存管理:它允许操作系统将内存分割成多个段(segment),每个段可以独立地管理。段寄存器:CPU中有一些特殊的寄存器,如CS(代码段寄存器)、DS(数据段寄存器)、ES、FS、GS和SS(堆栈段寄存器),它们存储着当前段的基地址。

1. 逻辑地址的使用:

   - 在编写程序时,程序员使用的是逻辑地址,也就是相对于段基址的偏移量。

2. 段寄存器的选择:

   - 根据访问的数据类型(代码、数据、堆栈等),CPU会自动选择相应的段寄存器。

3. 基地址的加权:

    逻辑地址中的偏移量与段寄存器中的基地址相加,得到完整的物理地址。

4. 段界限检查:

   - 在地址转换过程中,操作系统还会检查生成的地址是否超出了段的界限。如果超出界限,将引发一个段错误(segmentation fault)。

5. 实际的物理地址获取:

   - 如果地址在界限内,它将被送到内存管理单元(MMU),MMU会将这个地址转换为实际的物理地址。

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

页式管理过程中,CPU首先将虚拟地址发送给内存管理单元MMU,由MMU逐级查找页表PTE,将其转换为物理地址。CPU再根据物理地址逐级访存,获得所需数据。

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

TLB(Translation Lookaside Buffer)和四级页表机制是Intel处理器中实现虚拟地址(VA)到物理地址(PA)转换的关键技术。以下是对这两个概念的重新组织和介绍:

TLB(Translation Lookaside Buffer):

- TLB是一个高速缓存,用于存储最近或频繁访问的虚拟地址到物理地址的映射关系。

- 当处理器需要访问内存时,它首先检查TLB,看是否已经有了所需的VA到PA的映射。如果TLB命中,即找到了映射关系,处理器就可以直接使用这个映射,显著减少访问延迟。

- 如果TLB未命中,即没有找到映射关系,处理器将转向页表进行查找。

四级页表:

- 四级页表是Intel处理器中用于存储虚拟地址到物理地址映射的另一种机制,它允许处理器支持非常大的地址空间。

- 在四级页表中,虚拟地址被分为多个部分,每部分通过一系列索引和表项来定位物理页框号。

- 首先,虚拟地址的一部分用作页目录的索引,页目录中的每个条目指向一个页表。

- 接着,虚拟地址的另一部分用作所选页表中的索引,页表中的每个条目指向一个页。

- 依此类推,经过四次这样的索引和查找,最终得到物理页框号。

- 将物理页框号与虚拟地址中的最后一部分(页内偏移)结合,就得到了完整的物理地址。

 TLB与四级页表的协同工作:

- 当TLB未命中且需要从页表中解析地址时,处理器会加载必要的页表项到TLB中,为未来的访问提供快速映射。

- 这种机制确保了即使在TLB未命中的情况下,处理器也能通过页表查找到正确的物理地址,并将新发现的映射关系缓存到TLB中,以便后续快速访问。

- 结合TLB的快速访问和四级页表的详细映射,Intel处理器能够高效地处理虚拟地址到物理地址的转换,从而提高整体的内存访问性能。

通过这种分层的页表结构和TLB的快速缓存,Intel处理器不仅能够管理庞大的地址空间,还能够保持高效的内存访问速度,满足现代应用程序对性能的需求。

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

在现代计算机体系结构中,三级Cache(L1、L2、L3)构成了一个高效的内存访问层级。L1 Cache作为最接近CPU核心的缓存,拥有最快的访问速度,当CPU请求数据时,首先在L1 Cache中进行搜索。如果L1 Cache命中,数据几乎可以立即被返回,避免了对主存的访问,从而显著提升了访问效率。

当L1 Cache未命中时,请求会转向L2 Cache。L2 Cache的容量较L1更大,尽管访问速度略慢,但仍然远快于主存。如果数据在L2 Cache中被找到,访问速度虽然略低于L1 Cache,但仍然非常迅速。

若L2 Cache也未命中,请求最终会到达L3 Cache。L3 Cache是系统中最大的缓存,其设计目的是进一步减少对主存的访问频率。L3 Cache的命中可以进一步提升CPU性能,因为它的数据传输速度接近于主存。

当三级Cache均未命中时,CPU将不得不访问物理内存,也就是主存。这一过程相对较慢,因为内存位于CPU外部,需要通过总线进行数据交换。在等待数据返回期间,系统可能会暂停其他操作。

一旦数据从主存返回,L3 Cache会首先接收并缓存数据,随后L2和L1 Cache也会根据需要进行更新,这样做可以减少未来对主存的访问次数,从而降低延迟,提高系统的整体性能。当数据被频繁访问时,高缓存命中率可以带来更显著的性能提升。

7.6 hello进程fork时的内存映射

在Linux操作系统中,当hello进程执行fork系统调用时,会触发一系列内存映射操作,以创建一个新的子进程。首先,内核会为子进程创建必要的数据结构,如进程控制块(PCB)和进程描述符,并分配一个唯一的进程标识符(PID)。

接着,内核会复制父进程的内存管理结构,包括内存描述符(mm_struct)、区域结构(vm_area_struct)和页表。这些结构定义了进程的虚拟内存布局和内存映射关系。

为了实现写时复制(Copy-On-Write, COW)机制,内核将父子进程中的所有页面标记为只读,并将区域结构标记为私有的写时复制。这意味着,当任一进程尝试写入这些页面时,内核将捕获写操作,并为该进程创建一个新的物理内存页副本,同时将该页的访问权限更改为可写。

在fork操作完成后,子进程的虚拟内存布局与父进程在fork调用时的布局完全相同。这表示子进程继承了父进程的所有内存映射,包括代码、数据、堆栈以及共享库的映射。

如果父子进程中的任何一个在fork之后尝试写操作,写时复制机制将确保只有实际被修改的页面才会被复制,从而避免了不必要的内存复制,提高了内存使用效率。

7.7 hello进程execve时的内存映射

当hello进程调用execve函数以执行一个新程序时,会发生一系列内存映射的变更。首先,当前进程的执行上下文将被终止,包括清除栈、关闭文件描述符等,这会释放大部分内存映射。

随后,execve会加载新的程序到内存中。内核为新程序创建一个新的、独立的内存映射,这通常包括代码段(text)、数据段(data)、堆(heap)和栈(stack)。如果新程序需要加载共享库,内核会为每个进程创建一个私有映射,以确保线程安全并允许写时复制。

在新程序开始执行前,内核会重置进程的内存映射,确保新程序的代码、数据和动态分配的内存区域与新加载的程序文件相对应。这可能涉及到加载新的页表和重新初始化区域结构。

由于execve替换了整个进程的执行环境,之前进程的内存映射通常会被清除,以避免安全和资源问题。最后,内核会根据新程序的入口点初始化新进程的状态,包括设置堆栈指针、程序计数器等。

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

缺页故障是操作系统中的一种常见异常,当程序访问的内存页不在物理内存(RAM)中,而是在磁盘上的交换区或文件系统中时,就会发生。处理缺页故障的流程如下:

中断响应:CPU检测到缺页故障时,会中断当前执行流程,并跳转到缺页中断处理程序。

查找页表:处理程序检查页表,确认发生故障的页是否确实不在物理内存中。

加载页面:确认缺页后,操作系统从磁盘读取相应页面到物理内存,这是最耗时的步骤。

更新页表:页面加载后,操作系统更新页表,标记该页为有效,并记录物理地址。

重新执行指令:更新页表后,重新执行导致缺页故障的指令。

内存管理:如果物理内存已满,操作系统可能需要执行页面置换算法,如LRU或FIFO,以腾出空间。

优化内存使用:在整个过程中,操作系统会努力优化内存使用,确保系统性能。

7.9动态存储分配管理

动态存储分配是程序运行时请求内存的一种方式。以下是动态内存管理的基本方法与策略:

内存分配:使用malloc等函数从堆中分配内存,返回指向分配内存的指针。若分配失败,返回NULL。

内存释放:使用free函数释放malloc分配的内存,使内存块可被重新分配。

内存管理:操作系统或运行时库负责管理堆内存,跟踪内存分配与空闲状态。

内存分配策略包括:

首次适应(First-fit):找到第一个足够大的空闲块。

最佳适应(Best-fit):找到最小的足够大的空闲块。

最坏适应(Worst-fit):分配最大的空闲块。

伙伴系统(Buddy System):分割内存为对称块,通过合并减少碎片。

内存回收策略包括:

立即回收:释放内存时立即合并空闲块。

延迟回收:保留释放的内存块,直到需要时合并。

内存碎片管理涉及:

内部碎片:分配的内存块内部未使用空间。

外部碎片:空闲内存被分割成小块,难以满足大内存请求。

内存分配器优化技术包括:

池分配(Pool Allocation):预先分配固定大小内存块,快速分配回收。

垃圾回收(Garbage Collection):自动跟踪不再使用的内存,无需程序员显式调用free。

良好的动态内存管理策略对提高程序性能和稳定性至关重要,需要仔细考虑内存分配和释放的时机,避免内存泄漏和悬挂指针等问题。

7.10本章小结

本章以文字性叙述介绍为主,主要介绍了hello的储存管理技术,包括地址空间的转换、存储器数据的访问,程序加载时的内存映射,缺页处理与动态内存分配等相关内容。(第7章 2分)


8章 hello的IO管理

8.1 Linux的IO设备管理方法

在Linux系统中,I/O设备管理采用了一种统一的文件抽象模型。这意味着,无论是网络接口、磁盘驱动器还是终端设备,它们都被视作文件对象。通过这种方式,所有的输入输出操作都被简化为对文件的读写操作。Linux内核通过Unix I/O接口,为应用程序提供了一个简洁而底层的访问接口,使得对各种设备的访问都遵循统一的模式。

8.2 简述Unix IO接口及其函数

Unix I/O接口的核心在于提供了一组标准的系统调用,使得应用程序能够以统一的方式执行输入输出操作。以下是Unix I/O接口的几个关键功能:

打开文件:应用程序通过调用内核来打开一个文件,从而获得对I/O设备的访问权限。每个Linux进程在启动时都会打开三个标准文件:标准输入(文件描述符0)、标准输出(文件描述符1)和标准错误(文件描述符2)。

改变文件位置:内核为每个打开的文件维护一个当前文件位置指针,初始值为0,应用程序可以修改这个位置指针来读取或写入文件的不同部分。

读写文件:读取操作将文件中的字符复制到内存中,而写入操作则相反。当到达文件末尾时,读取操作会触发EOF(文件结束)条件。

关闭文件:完成文件访问后,应用程序通知内核关闭文件,释放相关资源。

Unix I/O接口的关键函数包括:

open:打开一个文件或创建一个新文件,并返回一个文件描述符。

close:关闭一个已打开的文件描述符。

read:从指定的文件描述符读取数据到内存中。

write:将数据从内存写入到指定的文件描述符。

8.3 printf的实现分析

vsprintf函数的具体实现如下图所示,基于下图我们对其实现进行具体的分析。。

`printf`函数的实现涉及从格式化字符串生成显示信息,到调用底层系统函数write,再到操作系统内核的陷阱处理机制。以下是详细过程:

1. 格式化字符串处理:`printf`首先调用`vsprintf`函数,该函数解析格式化字符串`fmt`,并结合可变参数`args`生成最终的格式化输出字符串。`vsprintf`内部使用一个循环来处理格式化指令,例如`%x`用于输出十六进制数,`%s`用于输出字符串等。

2. 系统调用write:生成的字符串存储在缓冲区`buf`中,`printf`随后调用系统调用`write`来输出这个缓冲区的内容。`write`函数的原型为:

   ```c

   ssize_t write(int fd, const void *buf, size_t n);

   ```

   其中`fd`是文件描述符,`buf`是要写入的数据缓冲区,`n`是缓冲区的大小。

3. 陷阱和系统调用处理:在Linux系统中,`write`函数通过一个陷阱(例如`int 0x80`或`syscall`指令)触发系统调用。这个指令导致控制权转移到操作系统内核,内核中的系统调用表会根据`eax`寄存器的值(在x86架构中,`eax`通常用于传递系统调用号)找到对应的系统调用函数。

4. 字符显示驱动子程序:内核中的write系统调用处理函数将数据写入到指定的文件描述符,如果是标准输出(文件描述符1),内核会调用字符显示驱动子程序。该子程序负责将ASCII码转换为字模库中的字模数据。

5. 显示VRAM更新:字模数据随后被映射到显示VRAM(Video RAM),VRAM存储了每个像素点的RGB颜色信息。显示芯片按照设定的刷新频率逐行读取VRAM中的数据。

6. 信号线传输:显示芯片通过信号线将RGB分量传输给液晶显示器,从而在屏幕上渲染出最终的字符显示。

整个过程从用户空间的`printf`调用开始,通过系统调用接口进入内核,最终由硬件驱动程序将数据呈现在用户的屏幕上,展示了从软件到硬件的完整路径。har的实现分析

8.4 getc

(### 8.4 getchar的实现分析

`getchar`函数的实现与异步异常处理紧密相关,特别是键盘中断的处理。以下是详细的实现过程:

1. 键盘中断处理子程序:当用户按下键盘上的一个键时,键盘硬件生成一个扫描码,这个扫描码随后触发一个中断请求。操作系统中的键盘中断处理子程序被调用,负责处理这个中断。

2. 扫描码转换:键盘中断处理子程序接收到扫描码后,将其转换成对应的ASCII码。这一转换依赖于键盘布局和操作系统的键盘驱动程序。

3. 键盘缓冲区:转换得到的ASCII码随后被保存到系统的键盘缓冲区中。键盘缓冲区是一个临时存储区域,用于暂存用户输入的字符,直到它们被读取

4. 返回读取的字符:`read`函数从键盘缓冲区中读取一个字符,并将其返回给`getchar`。如果缓冲区中没有字符,`read`会等待,直到有字符可用。当用户按下回车键时,表示输入结束,`read`会返回一个特殊的EOF(文件结束)标记。

5. `getchar`返回:`getchar`函数最终返回`read`读取的字符。如果读取成功,它返回读取的ASCII码;如果遇到EOF,它返回一个特定的值(通常是-1),表示没有更多的输入。

整个过程展示了从用户按键到`getchar`函数返回字符的完整流程,涵盖了硬件中断、操作系统中断处理、系统调用以及用户空间的I/O操作。

8.5本章小结

本章深入探讨了Linux系统中的I/O设备管理方法,详细解释了Unix I/O接口及其提供的系统调用函数。通过分析printf和getchar函数的实现,我们可以看到标准I/O库是如何与底层系统调用交互,以提供格式化输出和字符输入功能的。此外,还讨论了异步异常处理,特别是键盘中断的处理机制,以及它如何影响字符输入。这些内容为理解Linux系统中的I/O操作提供了坚实的基础。

(第8章1分)

结论

预处理(Preprocessing)是旅程的起点,`hello.c`中的源代码被送入预处理器的怀抱。预处理器是个细心的工匠,它移除注释,扩展宏定义,包含头文件,为后续的编译打下坚实的基础。;接下来,编译器这位大师登场。它将人类可读的C语言代码转化为机器能理解的汇编语言。这个过程就像是将诗歌翻译成一种只有机器能懂的秘密语言;随后,汇编器这位翻译家将汇编语言进一步转化为机器码。这是将抽象的概念具体化为计算机硬件能直接执行的指令的过程;最后,链接器这位智者将编译生成的目标文件与其他库文件结合起来,形成一个完整的可执行文件。它就像是将散落的珍珠串成一串美丽的项链。

当`hello`程序被执行时,操作系统这位伟大的指挥家将可执行文件加载到内存中。它就像是将剧本搬上舞台,准备开始一场精彩的演出;CPU这位主角开始按照剧本(指令)行动。它控制着程序的每一个步骤,从打印"Hello, World!"到等待用户的回车,每一个动作都精确无误。;在执行过程中,如果遇到信号这样的意外情况,操作系统会进行异常处理,就像是在演出中应对突发状况,确保一切能够顺利进行。;当`hello`程序完成它的使命,输出了那句经典的问候之后,它优雅地退出舞台。操作系统这位指挥家再次出现,将进程的资源回收,内存释放,确保系统的整洁有序。

在整个过程中,`hello.c`不仅仅是代码,它是一段有情感、有生命力的旅程。从预处理到链接,我们看到了一个想法如何被赋予形态;从加载到终止,我们见证了一个进程的完整生命周期。这不仅是技术的展示,更是计算机科学魔法的体现,每一次运行都是对这段奇妙旅程的一次新的探索和体验。(结论0分,缺失 -1分,根据内容酌情加分)


附件

hello.i:预处理得到的C程序

hello.s:编译得到的汇编语言程序

hello.o:汇编得到的可重定位目标程序文件

hello_fromo.s:hello.o反汇编得到的汇编语言程序

hello:链接得到的可执行目标文件

hello_fromout.s:hello反汇编得到的汇编语言程序

(附件0分,缺失 -1分)


参考文献

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

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

(参考文献0分,缺失 -1分)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值