程序的运行(6/11)

程序的运行分为两种:一种基于操作系统环境,另一种是在无操作系统的环境下执行裸机程序。在 Linux 环境下,可执行文件是 ELF 格式(除了基本的代码段、数据段、还有文件头、符号表等用来辅助程序运行的信息),在裸机环境下执行的程序一般是 BIN/HEX 格式,它们是纯指令文件。

虽然两种程序运行环境不同,文件格式也有所差异,但原理是相通的:都要将指令加载到内存中的指定位置,而这个指定位置和执行文件链接时的链接地址有关。

操作系统环境下的程序运行

一个装有操作系统的计算机系统,当执行一个应用程序时,首先会运行一个叫做加载器的程序,加载器会根据软件的安装路径信息,将可执行文件从 ROM 中加载到内存,然后进行一些与初始化、动态库重定位相关的操作,最后才跳转到程序的入口运行。在 Linux 命令行模式下运行一个应用程序,类似 sh、bash 这样的 Shell 终端程序充当加载器的角色:把程序加载到内存,封装成进程,参与操作系统的调度和运行。

一个可执行文件可由不同的 section 组成,分为代码段、数据段、BSS 段等。加载器在加载程序运行时,会将这些代码段、数据段分别加载到内存的不同位置。可执行文件的文件头提供了文件类型、运行平台、程序的入口地址等基本信息,加载器在加载程序之前会首先根据文件头的信息做一些判断,如果发现程序的运行平台和当前的环境不符,则会报错。

除此之外,可执行文件中还有一个叫做段头表的段,段头表中记录的是如何将可执行文件加载到内存的相关信息,包括可执行文件中要加载到内存中的段、入口地址等信息。在一个可执行文件中,加载器要加载程序到内存,要依赖段头表提供的信息,因此段头表是必需的。

jiaming@jiaming-pc:~/Documents/CSDN_Project$ readelf -l a.out 

Elf file type is EXEC (Executable file)
Entry point 0x1030c
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x000718 0x00010718 0x00010718 0x00008 0x00008 R   0x4
  PHDR           0x000034 0x00010034 0x00010034 0x00120 0x00120 R   0x4
  INTERP         0x000154 0x00010154 0x00010154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.3]
  LOAD           0x000000 0x00010000 0x00010000 0x00724 0x00724 R E 0x10000
  LOAD           0x000f10 0x00020f10 0x00020f10 0x00118 0x0011c RW  0x10000
  DYNAMIC        0x000f18 0x00020f18 0x00020f18 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x00010168 0x00010168 0x00044 0x00044 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000f10 0x00020f10 0x00020f10 0x000f0 0x000f0 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     .ARM.exidx 
   01     
   02     .interp 
   03     .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .ARM.exidx .eh_frame 
   04     .init_array .fini_array .dynamic .got .data .bss 
   05     .dynamic 
   06     .note.gnu.build-id .note.ABI-tag 
   07     
   08     .init_array .fini_array .dynamic 

在 Linux 环境下运行的程序一般都会封装成进程,参与操作系统的统一调度和运行。在 Shell 环境下运行一个程序,Shell 终端程序一般会先 fork 一个子进程,创建一个独立的虚拟进程地址空间,接着调用 execve 函数将要运行的程序加载到进程空间:通过可执行文件的文件头,找到程序的入口地址,建立进程虚拟地址空间与可执行文件的映射关系,将 PC 指针设置为可执行文件的入口地址,即可启动运行。一段 C 程序、编译生成的可执行文件、可执行文件运行时的进程之间的对应关系如图:

在这里插入图片描述
不同的编译器有不同的链接起始地址。在 Linux 环境下,GCC 链接时一般以 0x08040000 为起始地址开始存放代码段,而 ARM GCC 交叉编译器一般以 0x10000 为链接起始地址。紧挨着代码段,从一个 4KB 边界对齐的地址处开始存放数据段。紧挨着数据段,就是 BSS 段。BSS 段后面的第一个 4KB 地址对齐处,就是我们在程序中使用 malloc() / free() 申请的堆空间。

对于每一个运行的进程,Linux 内核都会使用一个 task_struct 结构体来表示,多个结构体通过指针构成链表。操作系统基于该链表就可以对这些进程进行管理、调度和运行。不同进程的代码段和数据段分别存储在物理内存不同的物理页上,进程间彼此独立,通过上下文切换,轮流占用 CPU 去执行自己的指令。当 Linux 环境下有多个进程并发运行时,C 源程序、可执行文件、进程和物理内存之间的对应关系如图:

在这里插入图片描述

裸机环境下的程序运行

在一个裸机平台下,系统上电后,没有程序运行的环境,需要借助第三方工具将程序加载到内存,然后才能正常运行。

很多集成开发环境如 ADS1.2、Keil、RVDS 等 IDE,不仅提供了程序编辑、编译的功能,同时支持程序运行、调试、烧写。以 ADS1.2 集成开发环境为例,可以通过 JTAG 接口和开发板通信,将在 PC 上编译好的 BIN/HEX 格式的 ARM 可执行文件下载到开发板的内存中运行。可以根据开发板的实际 RAM 物理地址,在编译程序时通过 ADS1.2 集成开发环境提供的 Debug Setting 设置选项来设置。

在这里插入图片描述

在一个嵌入式 Linux 系统中,Linux 内核镜像的运行其实就是裸机环境下的程序运行。Linux 内核镜像一般会借助 U-boot 这个加载工具将其从 Flash 存储分区加载到内存中运行,u-boot 在 Linux 启动过程中扮演了加载器的角色。

程序入口 main() 函数分析

加载器将指令加载到内存后,接着就开始运行程序了,main() 函数是通常理解意义上所有程序的入口函数,但默认的程序入口是 _start 符号,而不是 main,后者只是一个约定。

在 main 函数运行之前,已经做了许多初始化操作:它们主要完成运行 main 函数之前的一些初始化工作,如初始化堆栈指针、初始化 data 段内容,初始化的全局变量中,int 类型的全部初始化为 0,布尔型的变量初始化为 False,指针类型初始化为 NULL。完成初始化环境之后,这部分代码还会将用户传入的参数传递给 main,最后才跳入 main 函数运行。

这部分初始化代码是在程序编译阶段,由编译器自动添加到可执行文件中的。这部分代码属于 C 运行库(C Running Time,CRT)中的代码,编译器厂商在开发编译器时,除了实现 C 语言标准中规定的 printf、fopen、fread 等标准函数,还会实现这部分初始化代码,完成进入 main 函数之前的一系列初始化操作。

  • C 语言运行的基本堆栈环境、进程环境。
  • 动态库的加载、释放、初始化、清理等工作。
  • 向 main 函数传参 argc、argv,调用 main 函数执行。
  • 在 main 函数退出后,调用 exit 函数,结束进程的运行。

在 ARM 交叉编译器安装路径下的 lib 目录下,会看到 crt1.o 目标文件,这个文件由汇编初始化代码编译生成,是 CRT 的一部分。在链接过程中,链接器会将 crt1.o 这个目标文件和项目中的目标组装在一起,生成最终的可执行文件。

BSS 段

对于未初始化额全局变量和静态局部变量,编译器将其放置在 BSS 段中。BSS 段是不占用可执行文件存储空间,设置 BSS 段的目的是减少可执行文件的体积,节省磁盘空间。

虽然 BSS 段在可执行文件中不占用存储空间,但是当程序加载到内存运行时,加载器会在内存中给 BSS 段开辟一段存储空间。在段表中会记录 BSS 段的大小,在符号表中会记录每个变量的地址和大小。加载器会根据这些信息,在数据段的后面分配指定大小的内存空间并清零,根据符号表中各个变量的地址在这片内存中给各个未初始化的全局变量、静态变量分配存储空间。

猜你喜欢

转载自blog.csdn.net/weixin_39541632/article/details/132228915
今日推荐