摘 要
本研究聚焦于解释C语言程序如何从源代码转换为可执行文件。在Linux系统下,以简单的C语言文件hello.c为对象,全面深入地探讨了该程序的完整生命周期。从最初的原始程序出发,系统性地研究了编译、链接、加载、运行、终止和回收等关键阶段,以揭示hello.c文件整个的生命过程。此文不仅理论上探讨了这些工具的原理和方法,还实际演示了它们的操作和结果,阐述了计算机系统的工作原理和体系结构,意在助读者深入理解和掌握C语言程序的编译和执行过程,赋予冰冷的指令以浪漫的色彩。
关键词:计算机系统;C语言;程序生命周期;底层原理
目 录
第1章 概述
1.1 Hello简介
P2P:即From Program to Process。指从hello.c(Program)变为运行时进程(Process)。要让hello.c这个C语言程序运行起来,需要先把它变成可执行文件,这个变化过程有四个阶段:预处理,编译,汇编,链接,完成后就得到了可执行文件,然后就可以在shell中执行它,shell会给它分配进程空间。
020:即From Zero-0 to Zero-0。指最初内存并无hello文件的相关内容,shell用execve函数启动hello程序,把虚拟内存对应到物理内存,并从程序入口开始加载和运行,进入main函数执行目标代码,程序结束后,shell父进程回收hello进程,内核删除hello文件相关的数据结构。
1.2 环境与工具
硬件环境:
处理器:12th Gen Intel(R) Core(TM)i5-12500H 2.50 GHz
机带RAM:16.0GB
系统类型:64位操作系统,基于x64的处理器
软件环境:Windows11 64位,VMware,Ubuntu 20.04 LTS
开发与调试工具:Visual Studio 2021 64位;vim objump edb gcc readelf等工具
1.3 中间结果
hello.i 预处理后得到的文本文件
hello.s 编译后得到的汇编语言文件
hello.o 汇编后得到的可重定位目标文件
hello.elf 用readelf读取hello.o得到的ELF格式信息
hello.asm 反汇编hello.o得到的反汇编文件
hello1.elf 由hello可执行文件生成的.elf文件
hello1.asm 反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章首先介绍了hello的P2P,020流程,包括流程的设计思路和实现方法;然后,详细说明了本实验所需的硬件配置、软件平台、开发工具以及本实验生成的各个中间结果文件的名称和功能。
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念
预处理步骤是指预处理器在程序运行前,对源文件进行简单加工的过程。预处理过程主要进行代码文本的替换工作,用于处理以#开头的指令,还会删除程序中的注释和多余的空白字符。预处理指令可以简单理解为#开头的正确指令,它们会被转换为实际代码中的内容(替换)。
2.1.2预处理的作用
预处理过程中并不直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换,主要有以下作用:
头文件包含:将所包含头文件的指令替代。
宏定义:将宏定义替换为实际代码中的内容。
条件编译:根据条件判断是否编译某段代码。
其他:如注释删除等。
简单来说,预处理是一个文本插入与替换的过程预处理器。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
在Linux下打开hello.i文件,我们对比了源程序和预处理后的程序。结果显示,除了预处理指令被扩展成了3000行之外,源程序的其他部分都保持不变,说明.c文件的确是被修改过了。
在代码中, main 函数之前的大段代码源自于头文件 <stdio.h>, <unistd.h>, <stdlib.h> 的展开。下面以 stdio.h 的展开为例进行说明:
在预处理过程中,#include 指令的作用是将指定的头文件内容包含到源文件中。<stdio.h> 是标准输入输出库的头文件,它包含了用于读写文件、标准输入输出的函数原型和宏定义等内容。
当预处理器遇到 #include<stdio.h> 时,它会在系统的头文件路径下查找 stdio.h 文件(一般位于 /usr/include 目录下),然后将 stdio.h 文件中的内容复制到源文件中。stdio.h 文件中可能还有其他的 #include 指令,比如 #include<stddef.h> 或 #include<features.h> 等,这些头文件也会被递归地展开并包含到源文件中。
预处理器不会对头文件中的内容进行任何计算或转换,只是简单地进行复制和替换。
预处理器的作用:预处理器在编译过程中首先处理 #include 指令,它通过在系统的头文件路径中查找指定的头文件并将其内容包含到源文件中来展开这些指令。
头文件的作用:头文件通常包含函数原型、宏定义和其他必要的声明,这些声明允许在多个源文件中共享代码和定义。
递归展开:当 stdio.h 包含其他头文件时,比如 stddef.h 或 features.h,这些头文件也会被递归地展开,确保所有必要的定义和声明都包含在最终的源文件中。
简单替换:预处理器只是简单地复制和替换头文件内容,不进行任何形式的计算或逻辑操作。
通过这种方式,所有相关的头文件内容都被包含到源文件中,从而使编译器在后续的编译阶段可以正确解析和编译代码。
2.4 本章小结
本章讲述了在linux环境中,如何用命令对C语言程序进行预处理,以及预处理的含义和作用。然后用一个简单的hello程序演示了从hello.c到hello.i的过程,并用具体的代码分析了预处理后的结果。通过分析,我们可以发现预处理后的文件hello.i包含了标准输入输出库stdio.h的内容,以及一些宏和常量的定义,还有一些行号信息和条件编译指令。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
计算机程序编译的概念是指将用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式程序的翻译过程。
3.1.2编译的作用
计算机程序编译的作用是使高级语言源程序变为汇编语言,提高编程效率和可移植性。计算机程序编译的基本流程包括词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成等阶段。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1汇编初始部分
在main函数前有一部分字段展示了节名称:
.file 声明出源文件
.text 表示代码节
.section .rodata 表示只读数据段
.align 声明对指令或者数据的存放地址进行对齐的方式
.string 声明一个字符串
.globl 声明全局变量
.type 声明一个符号的类型