从 Shell 加载到执行 “Hello World“ 的完整流程

从 Shell 加载到执行 “Hello World” 的完整流程

引言

本文将详细探讨从 Shell 输入命令到执行一个简单的 “Hello World” 程序的整个过程,帮助开发者深入了解底层机制。

Hello World 源码

首先,我们来看一下这个简单的 C 程序:

// hello.c
#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

这段代码非常直观:它定义了一个 main() 函数,该函数调用 printf 输出字符串 "Hello, World!\n" 并返回 0 表示成功退出。

编译与反汇编

使用 GCC 编译器将此代码编译为 AArch64 架构的目标文件:

aarch64-linux-gnu-gcc -o hello hello.c

为了分析其内部工作原理,我们可以使用 objdump 反汇编生成的二进制文件:

aarch64-linux-gnu-objdump -d hello > hello_disasm.txt
用户交互与 Shell 解析

当用户在 Shell 中键入 ./hello 并按下回车时,Shell 解析命令行参数,并准备启动指定的应用程序。这标志着程序执行的第一步。

创建新进程
  1. Fork 函数:Shell 使用 fork() 函数,调用clone()系统调用来创建一个新的子进程。此时,子进程是父进程(通常是 Shell)的一个副本,包括内存空间、文件描述符等。
  2. Exec 系列函数:在子进程中,Shell 调用 execve() 或其他 exec 系列函数之一来加载并执行指定的 ELF 文件。这一步会替换当前进程的用户空间镜像为新程序的镜像。
加载器解析 ELF 文件

接下来,操作系统的加载器读取 ELF 文件头和程序头表,了解程序的入口点、段信息等,并将程序的各个段(如代码段、数据段)映射到内存中适当的位置。如果程序依赖于共享库(如 glibc),加载器还会解析动态链接信息,并加载必要的共享库到内存中。

初始化运行时环境

在这个阶段,初始化栈和其他寄存器,设置堆管理器(malloc/free),初始化线程系统(如果程序是多线程的),以及完成动态链接器的初始化,确保所有符号都能正确解析。

_start 入口点

控制权传递给程序的入口点 _start。这是由编译器提供的启动代码的一部分,负责设置初始的栈帧和其他必要的环境变量,并准备参数来调用 _libc_start_main

0000000000401000 <_start>:
  401000:       a9be7bfd        stp     x29, x30, [sp,#-16]!
  401004:       910003fd        mov     x29, sp
  ...
  401020:       90000013        bl      4011b0 <__libc_start_main@plt>
  401024:       d65f03c0        ret

最后,通过 bl 指令跳转到 _libc_start_main

__libc_start_main 初始化

libc_start_main 是 glibc 内部函数,负责进一步初始化运行时环境,包括调用__libc_csu_init初始化全局构造函数和静态构造函数,即执行所有带有__attribute((constructor))属性的函数以及C++中的静态对象构造函数。
最终调用 main()。

main() 函数执行

控制权交给用户定义的 main() 函数,在这个例子中,它调用了 puts 来打印 "Hello, World!\n",然后返回 0 表示成功退出。

0000000000401158 <main>:
  401158:       a9be7bfd        stp     x29, x30, [sp,#-16]!
  40115c:       910003fd        mov     x29, sp
  ...
  401168:       90000010        bl      4010e0 <puts@plt>
  ...
  401170:       52800000        mov     w0, #0x0                    // #0
  401174:       a8c17bfd        ldp     x29, x30, [sp],#16
  401178:       d65f03c0        ret
程序结束

main() 执行完毕后,任何全局或静态对象的析构函数会被调用,以清理资源。最后,调用 _exit() 系统调用来终止进程,返回 main() 的返回值作为整个程序的退出状态码。

总结

从用户在 Shell 中输入命令开始,经过一系列复杂的初始化和加载过程,最终到达 main() 函数的执行,每个阶段都有其特定的任务,以确保程序能够在正确的环境中开始执行。这一过程中涉及多个关键步骤,包括创建新进程、加载和解析 ELF 文件、初始化运行时环境以及调用 _libc_start_main 和用户定义的 main() 函数。


参考资料


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值