声明:本实验纯粹出于兴趣,高度参考该视频 ,作者非OS专业研究者,文章内容仅供参考,不保证完全准确。如有错误,欢迎指正交流。
概述 程序加载与执行是操作系统最核心的功能之一。当用户在shell中输入命令时,操作系统需要完成一系列复杂操作:读取可执行文件、解析文件格式、分配内存空间、建立虚拟地址映射、设置执行环境,最终将控制权转移至用户程序。这一过程涉及文件系统、内存管理、进程调度等多个子系统的协同工作,是理解操作系统运行机制的重要切入点。
本实验通过动态调试方法,深度剖析Linux内核执行ELF格式可执行文件的完整流程。实验基于自编译的Linux内核,使用GDB跟踪从execve系统调用到用户程序首条指令执行的全过程,观察关键数据结构变化,分析汇编代码执行逻辑,揭示操作系统程序加载的底层机制。
实验目的 在Linux系统中,执行./demo即可运行一个ELF可执行文件,但这一简单操作的背后,操作系统底层究竟发生了什么?本实验旨在回答以下问题:
文件定位与加载 :操作系统如何根据路径找到可执行文件并将其加载到内存?
格式识别与解析 :内核如何识别ELF文件格式并解析其结构?
执行环境初始化 :系统如何设置寄存器状态和内存布局,为程序执行做准备?
控制权转移 :内核如何从系统调用返回并跳转到用户程序入口?
通过GDB动态调试内核执行流程,本实验将深入剖析从execve系统调用到用户程序首条指令执行的完整过程,揭示Linux程序加载机制的底层原理。
实验设计 技术路线 本实验采用动态内核调试方法,通过在关键执行路径设置断点,跟踪程序执行流程,观察数据结构变化。实验核心思路是编写一个最小化的测试程序,利用GDB从用户态execve系统调用入手,逐步深入内核,通过源码分析、汇编代码追踪和寄存器状态检查,完整还原OS对ELF文件的处理流程。
为便于调试分析,实验采用自编译的定制化Linux内核,关闭地址空间布局随机化(KASLR)等安全特性,启用完整的内核调试支持。使用QEMU虚拟机提供隔离的实验环境,通过GDB远程调试协议实现对内核的精确控制。
系统调用机制分析 execve系统调用是程序执行的入口点,其C标准库接口定义如下:
1 int execve (const char *pathname, char *const argv[], char *const envp[]) ;
其中,pathname指定要执行的可执行文件路径,argv是传递给新程序的参数数组,envp是环境变量数组。当用户程序调用这个函数时,控制流程会通过系统调用机制转入内核态,内核负责完成实际的程序加载和执行工作。
当用户程序调用该函数时,控制流通过系统调用机制切换至内核态,由内核完成实际的程序加载工作。
使用strace可观察execve的执行细节,以whoami为例:
1 2 3 4 % strace -e execve whoami execve("/usr/bin/whoami" , ["whoami" ], 0x7ffe8dc557d0 /* 92 vars */) = 0 username +++ exited with 0 +++
输出显示了完整的系统调用参数:可执行文件路径、参数数组以及包含92个条目的环境变量数组。
实验环境说明 QEMU QEMU是一个功能强大的开源虚拟化和硬件仿真平台,支持多种CPU架构。本实验利用QEMU的GDB调试功能,通过-s参数在1234端口开启GDB远程调试服务器,实现对虚拟机内核的实时调试。
GDB GDB(GNU Debugger)是Linux平台强大的调试工具。通过远程调试协议连接到QEMU,可实现对内核代码的精确控制。配合内核编译时生成的符号表(vmlinux),GDB能够将汇编指令映射到源代码,实现源码级内核调试,极大提升调试效率。
init与initramfs Linux启动过程中,内核完成硬件初始化后,需要启动第一个用户态进程——init进程(PID=1)。init进程负责用户空间初始化,包括挂载文件系统、启动系统服务等。
initramfs(initial RAM filesystem)是Linux启动早期阶段的临时根文件系统。本实验构建了仅包含bash(作为init进程)和测试程序的极简initramfs,排除其他干扰因素,便于专注于程序加载机制的分析。
实验流程 编译 Linux 内核 为便于调试分析,需要编译定制化的Linux内核,关闭影响调试的安全特性(如KASLR)和无关功能,启用完整的调试支持。实验使用Linux 6.18-rc5版本,从源代码构建。
禁用功能 :
Randomize the address of the kernel image (KASLR)
Virtualization
Enable loadable module support
Networking support
启用功能 :
Debug information > Rely on the toolchain’s implicit default DWARF version
Provide GDB scripts for kernel debugging
KGDB: kernel debugger
配置完成后执行编译:
1 2 make -j16 make scripts_gdb
编译生成两个关键文件:
vmlinux:包含完整调试符号的未压缩内核镜像,供GDB加载符号表使用
bzImage:压缩的可启动内核镜像,供QEMU加载运行
编译 bash 本实验使用静态编译的bash 5.3作为init进程,静态链接可避免动态库依赖,便于分析ELF加载过程。
1 2 ./configure --enable-static-link make -j16
验证编译结果:
1 2 3 4 % file bash bash: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, with debug_info, not stripped % ldd bash not a dynamic executable
输出确认bash已静态链接,无动态库依赖。
编写测试程序 为便于后续调试,编写一个调用系统调用的最小化测试程序。该程序使用标准库的write函数向标准输出写入字符串。
write函数手册说明摘录:
NAME write - write to a file descriptor
SYNOPSIS #include <unistd.h> ssize_t write(int fd, const void buf[.count], size_t count);
DESCRIPTION write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.
源文件demo.c(退出状态码设为0xcc以便反汇编识别):
1 2 3 4 5 6 7 #include <unistd.h> void main () { write(1 , "hello world!\n" , 13 ); _exit(0xcc ); }
编译并测试:
1 gcc -static demo.c -o demo
验证:
1 2 3 4 5 6 7 8 9 10 % ./demo hello world! % ldd demo not a dynamic executable % file demo demo: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, not stripped % strace -e execve ./demo execve("./demo" , ["./demo" ], 0x7ffdfd177ad0 /* 92 vars */) = 0 hello world! +++ exited with 204 +++
程序成功执行,退出码204(0xcc)符合预期。
构建 initramfs 使用cpio工具创建包含bash和测试程序的initramfs镜像:
1 echo "init\ndemo" | cpio -o -H newc > init.cpio
此命令将init(bash)和demo打包为newc格式的cpio归档,作为初始RAM文件系统。
启动 QEMU 使用以下命令启动虚拟机:
1 2 3 qemu-system-x86_64 -enable-kvm -cpu host \ -kernel ./linux-6.18-rc5/arch/x86/boot/bzImage \ -initrd init.cpio -s
参数说明:
-enable-kvm -cpu host:启用KVM加速,使用宿主机CPU特性
-kernel:指定内核镜像
-initrd:指定initramfs镜像
-s:在1234端口开启GDB调试服务器
系统成功启动后进入bash命令行,执行./demo可验证测试程序正常运行:
建立调试连接 vmlinux文件包含完整的符号表和调试信息,是内核级调试的基础。与压缩的bzImage不同,vmlinux保留了所有函数名、变量名、源码行号等信息,使GDB能够实现源码级调试。
启动GDB并连接到QEMU:
1 2 3 4 5 gdb vmlinux (gdb) target remote :1234 Remote debugging using :1234 0xffffffff81d244ef in pv_native_safe_halt () at arch /x86/kernel/paravirt.c:82 82 }
连接成功后,可见内核当前正在执行pv_native_safe_halt函数,该函数用于CPU空闲时的暂停执行(HLT指令)。
调试与分析 系统调用入口点分析 execve系统调用的实现 通过分析Linux内核源代码fs/exec.c,可以找到execve系统调用的定义:
1 2 3 4 5 6 7 SYSCALL_DEFINE3(execve, const char __user *, filename, const char __user *const __user *, argv, const char __user *const __user *, envp) { return do_execve(getname(filename), argv, envp); }
SYSCALL_DEFINE3并非函数,而是一个宏,表示这是一个接受3个参数的系统调用。该宏会自动生成必要的包装代码,处理用户态到内核态的参数传递、权限检查、错误处理等。宏展开后生成实际的系统调用函数__x64_sys_execve及相关的参数验证和类型转换代码。
Linux内核定义了一系列SYSCALL_DEFINE宏以支持不同参数数量的系统调用:
1 2 3 #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__) #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__) #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
开始调试 在do_execve处设置断点,在QEMU中执行测试程序,观察断点命中情况:
1 2 3 4 5 6 7 8 (gdb) b do_execve Breakpoint 1 at 0xffffffff81502706 : do_execve. (2 locations) (gdb) c Continuing. Breakpoint 1.1 , do_execve (filename=0xffff888003373000 , __argv=0x1f5d0fd0 , __envp=0x1f5cc010 ) at fs/exec.c:1934 1934 return do_execveat_common(AT_FDCWD, filename, argv, envp, 0 );
单步跟踪进入do_execveat_common函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 (gdb) sdo_execveat_common (fd=fd@entry=-100 , filename=0xffff888003373000 , flags=0 , envp=..., argv=...) at ./include/linux/err.h:70 70 return IS_ERR_VALUE ((unsigned long )ptr) ; (gdb) ndo_execveat_common (fd=fd@entry=-100 , filename=0xffff888003373000 , flags=0 , envp=..., argv=...) at fs/exec.c:1801 1801 if ((current->flags & PF_NPROC_EXCEEDED) && (gdb) do_execveat_common (fd=fd@entry=-100 , filename=0xffff888003373000 , flags=0 , envp=..., argv=...) at ./arch/x86/include/asm /current.h:23 23 return this_cpu_read_const(const_current_task);(gdb) 1811 bprm = alloc_bprm(fd, filename, flags);
可发现该函数进行了一系列检查,包括进程数量限制验证,并为程序分配linux_binprm结构体。该结构体存储可执行文件的相关信息,在GDB中查看其内容:
1 2 3 4 5 6 7 8 9 10 (gdb) p *bprm$1 = {vma = 0xffff888003d960c0, vma_pages = 0, argmin = 0, mm = 0xffff888003050b80, p = 140737488351224, have_execfd = 0, execfd_creds = 0, secureexec = 0, point_of_no_return = 0, comm_from_dentry = 0, is_check = 0, executable = 0x0, interpreter = 0x0, file = 0xffff88800332e180, cred = 0x0, unsafe = 0, per_clear = 0, argc = 1, envc = 0, filename = 0xffff888003373020 "/demo" , interp = 0xffff888003373020 "/demo" , fdpath = 0x0, interp_flags = 0, execfd = 0, exec = 0, rlim_stack = { rlim_cur = 8388608, rlim_max = 18446744073709551615}, buf = '\000' <repeats 255 times >}
接下来的代码进行bprm参数准备工作,包括设置栈空间限制、复制环境变量和参数数组等。完成准备后,进入bprm_execve函数开始实际的程序加载流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 (gdb) n1823 if (retval < 0 ) (gdb) 1825 bprm->envc = retval; (gdb) 1827 retval = bprm_stack_limits(bprm); (gdb) 1828 if (retval < 0 ) (gdb) 1831 retval = copy_string_kernel(bprm->filename, bprm); (gdb) 1832 if (retval < 0 ) (gdb) 1834 bprm->exec = bprm->p; (gdb) 1836 retval = copy_strings(bprm->envc, envp, bprm); (gdb) 1837 if (retval < 0 ) (gdb) 1840 retval = copy_strings(bprm->argc, argv, bprm); (gdb) 1841 if (retval < 0 ) (gdb) 1850 if (bprm->argc == 0 ) { (gdb) 1860 retval = bprm_execve(bprm); (gdb) s
进入bprm_execve函数后,完成安全凭证准备、执行环境检查和调度优化后,在1754行调用exec_binprm开始实际的二进制文件加载,下面继续进入exec_binprm方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 (gdb) advance bprm_execvebprm_execve (bprm=0xffff888003d80000 ) at fs/exec.c:1731 1731 { (gdb) s1734 retval = prepare_bprm_creds(bprm); (gdb) list 1729 1730 static int bprm_execve (struct linux_binprm *bprm) 1731 {1732 int retval;1733 1734 retval = prepare_bprm_creds(bprm);1735 if (retval)1736 return retval;1737 1738 1743 check_unsafe_exec(bprm);1744 current->in_execve = 1 ;1745 sched_mm_cid_before_execve(current);1746 1747 sched_exec();1748 (gdb) 1749 1750 retval = security_bprm_creds_for_exec(bprm);1751 if (retval || bprm->is_check)1752 goto out;1753 1754 retval = exec_binprm(bprm);1755 if (retval < 0 )1756 goto out;
在exec_binprm函数中,关键是1702行的search_binary_handler函数,用于确定文件的处理方式。深入该函数分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 (gdb) advance 1754 exec_binprm (bprm=0xffff888003d93000 ) at fs/exec.c:1691 1691 old_pid = current->pid; (gdb) list 1686 {1687 pid_t old_pid, old_vpid;1688 int ret, depth;1689 1690 1691 old_pid = current->pid;1692 rcu_read_lock();1693 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));1694 rcu_read_unlock();1695 (gdb) 1696 1697 for (depth = 0 ;; depth++) {1698 struct file *exec ;1699 if (depth > 5 )1700 return -ELOOP;1701 1702 ret = search_binary_handler(bprm);
在search_binary_handler函数中,核心逻辑是list_for_each_entry遍历,该函数遍历所有已注册的二进制格式处理器(如ELF、脚本等),尝试使用每个处理器加载可执行文件,直到成功或所有处理器均失败。在1670行设置断点观察遍历过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 (gdb) advance search_binary_handlersearch_binary_handler (bprm=0xffff888003d93000 ) at fs/exec.c:1656 1656 retval = prepare_binprm(bprm); (gdb) list 1651 static int search_binary_handler (struct linux_binprm *bprm) 1652 {1653 struct linux_binfmt *fmt ;1654 int retval;1655 1656 retval = prepare_binprm(bprm);1657 if (retval < 0 )1658 return retval;1659 1660 retval = security_bprm_check(bprm); (gdb) 1661 if (retval)1662 return retval;1663 1664 read_lock(&binfmt_lock);1665 list_for_each_entry(fmt, &formats, lh) {1666 if (!try_module_get(fmt->module))1667 continue ;1668 read_unlock(&binfmt_lock);1669 1670 retval = fmt->load_binary(bprm); (gdb) 1671 1672 read_lock(&binfmt_lock);1673 put_binfmt(fmt);1674 if (bprm->point_of_no_return || (retval != -ENOEXEC)) {1675 read_unlock(&binfmt_lock);1676 return retval;1677 }1678 }1679 read_unlock(&binfmt_lock);1680 (gdb) 1681 return -ENOEXEC;1682 }
通过以下输出可观察到遍历过程依次尝试了misc_format、script_format和elf_format三种格式,当匹配到elf_format时步入进一步查看具体逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 (gdb) b 1670 Breakpoint 2 at 0xffffffff8150025f : file fs/exec.c, line 1670. (gdb) c Continuing. Breakpoint 2 , search_binary_handler (bprm=0xffff888003d93000 ) at fs/exec.c:1670 1670 retval = fmt->load_binary(bprm); (gdb) p fmt $1 = (struct linux_binfmt *) 0xffffffff82579da0 <misc_format> (gdb) c Continuing. Breakpoint 2 , search_binary_handler (bprm=0xffff888003d93000 ) at fs/exec.c:1670 1670 retval = fmt->load_binary(bprm); (gdb) p fmt $2 = (struct linux_binfmt *) 0xffffffff82579e40 <script_format> (gdb) c Continuing. Breakpoint 2 , search_binary_handler (bprm=0xffff888003d93000 ) at fs/exec.c:1670 1670 retval = fmt->load_binary(bprm); (gdb) p fmt $3 = (struct linux_binfmt *) 0xffffffff82579ea0 <elf_format>
ELF文件加载器 当fmt为elf_format时,步入load_binary,可以发现先后进入了GEN-for-each-reg.h和retpoline.S,这是内核的间接跳转保护机制,用于防御Spectre v2等CPU侧信道攻击。
load_binary最终调用了load_elf_binary函数加载可执行文件。在857行可见使用memcmp检测文件魔数是否匹配ELFMAG:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 Breakpoint 2 , search_binary_handler (bprm=0xffff888003d93000 ) at fs/exec.c:1670 1670 retval = fmt->load_binary(bprm); (gdb) s __x86_indirect_thunk_array () at ./arch/x86/include/asm /GEN-for -each-reg.h:6 6 GEN(rax) (gdb) ssrso_alias_return_thunk () at arch/x86/lib/retpoline.S:220 220 call srso_alias_safe_ret (gdb) ssrso_alias_safe_ret () at arch/x86/lib/retpoline.S:210 210 lea 8(%_ASM_SP) , %_ASM_SP (gdb) nsrso_alias_safe_ret () at arch/x86/lib/retpoline.S:213 213 ret (gdb) nload_elf_binary (bprm=0xffff888003d93000 ) at fs/binfmt_elf.c:833 833 { (gdb) list 828 829 return ret == -ENOENT ? 0 : ret;830 }831 832 static int load_elf_binary (struct linux_binprm *bprm) 833 { ...857 if (memcmp (elf_ex->e_ident, ELFMAG, SELFMAG) != 0 ) (gdb) 858 goto out;859 860 if (elf_ex->e_type != ET_EXEC && elf_ex->e_type != ET_DYN)861 goto out;862 if (!elf_check_arch(elf_ex))863 goto out;864 if (elf_check_fdpic(elf_ex))865 goto out;
elf_ex->e_ident字段是ELF文件的标识信息,为16字节数组,位于ELF头部开始位置。该字段在load_elf_binary函数开始时通过读取文件头获得。内核先读取文件前几百字节到缓冲区,再将其解释为ELF头部结构。
通过GDB查看e_ident内容:
1 2 (gdb) p elf_ex.e_ident $7 = "\177ELF\002\001\001\003\000\000\000\000\000\000\000"
ELF验证流程包括:魔数检查(\x7fELF)、文件类型检查(ET_EXEC/ET_DYN)、架构兼容性验证(elf_check_arch)和mmap支持检查,代码过长此处略去。经过数百行ELF解析逻辑后,在1379行可见最终的线程准备逻辑。START_THREAD是调用start_thread的宏,此处实际设置用户态寄存器,并非启动线程:
1 2 3 4 1377 1378 finalize_exec(bprm);1379 START_THREAD(elf_ex, regs, elf_entry, bprm->p);1380 retval = 0 ;
用户态执行环境初始化 寄存器状态设置 ELF文件解析完成后,内核需要设置CPU寄存器状态,为用户程序执行做准备。这一过程通过start_thread函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 (gdb) advance 1379 load_elf_binary (bprm=<optimized out>) at fs/binfmt_elf.c:1379 1379 START_THREAD(elf_ex, regs, elf_entry, bprm->p); (gdb) sstart_thread (regs=regs@entry=0xffffc90000173f58 , new_ip=new_ip@entry=4204480 , new_sp=140725702259520 ) at arch/x86/kernel/process_64.c:584 584 { (gdb) s585 start_thread_common(regs, new_ip, new_sp, (gdb) s start_thread_common (regs=regs@entry=0xffffc90000173f58 , new_ip=new_ip@entry=4204480 , new_sp=140725702259520 , _cs=_cs@entry=51 , _ds=_ds@entry=0 , _ss=43 ) at arch/x86/kernel/process_64.c:535 535 WARN_ON_ONCE(regs != current_pt_regs()); (gdb) list 530 static void 531 start_thread_common(struct pt_regs *regs, unsigned long new_ip,532 unsigned long new_sp,533 u16 _cs, u16 _ss, u16 _ds)534 {535 WARN_ON_ONCE(regs != current_pt_regs());536 537 if (static_cpu_has(X86_BUG_NULL_SEG)) {538 539 loadsegment(fs, __USER_DS);540 load_gs_index(__USER_DS);541 }542 543 reset_thread_features();544 545 loadsegment(fs, 0 );546 loadsegment(es, _ds);547 loadsegment(ds, _ds);548 load_gs_index(0 );549 550 regs->ip = new_ip;551 regs->sp = new_sp;552 regs->csx = _cs;553 regs->ssx = _ss;
在550-553行,代码设置了四个关键寄存器:
ip:指令指针寄存器(RIP)
sp:栈指针寄存器(RSP)
csx:代码段选择器(CS)
ssx:栈段选择器(SS)
通过跳转到551行,可以打印IP寄存器的值,该值指向程序入口地址:
1 2 3 4 5 6 7 8 9 (gdb) advance 551 start_thread_common (regs=regs@entry=0xffffc90000173f58 , new_ip=new_ip@entry=4204480 , new_sp=140725702259520 , _cs=_cs@entry=51 , _ds=_ds@entry=0 , _ss=43 ) at arch/x86/kernel/process_64.c:551 551 regs->sp = new_sp; (gdb) p regs.ip $8 = 4204480 (gdb) p/x regs.ip $9 = 0x4027c0
使用GDB独立检查demo程序的入口地址,发现与IP寄存器值一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 % gdb demo GNU gdb (Gentoo 16.3 vanilla) 16.3(gdb) info file Symbols from "/home/username/Project/LINUX/demo". Local exec file: `/home/username/Project/LINUX/demo', file type elf64-x86-64. Entry point: 0x4027c0 0x0000000000400270 - 0x0000000000400468 is .rela.plt 0x0000000000401000 - 0x000000000040101b is .init 0x0000000000401020 - 0x0000000000401170 is .plt 0x0000000000401180 - 0x000000000046c4f1 is .text 0x000000000046c4f4 - 0x000000000046c501 is .fini 0x000000000046d000 - 0x0000000000488b44 is .rodata
至此,已完整分析了从execve系统调用到ELF文件识别、加载,最终设置用户态寄存器的全过程。内核已完成程序加载任务,并将IP寄存器指向程序入口,接下来将返回用户态开始执行用户程序。下面使用finish命令逐层退出调用栈,追踪系统调用返回路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 (gdb) finish Run till exit from #0 start_thread_common (regs=regs@entry=0xffffc90000173f58 , new_ip=new_ip@entry=4204480 , new_sp=140725702259520 , _cs=_cs@entry=51 , _ds=_ds@entry=0 , _ss=43 ) at arch/x86/kernel/process_64.c:551 load_elf_binary (bprm=<optimized out>) at fs/binfmt_elf.c:1382 1382 return retval; (gdb) Run till exit from #0 load_elf_binary (bprm=<optimized out>) at fs/binfmt_elf.c:1382 0xffffffff8150026b in search_binary_handler (bprm=0xffff888003d93000 ) at fs/exec.c:1672 1672 read_lock (&binfmt_lock) ; Value returned is $10 = 0 (gdb) Run till exit from #0 0xffffffff8150026b in search_binary_handler ( bprm=0xffff888003d93000 ) at fs/exec.c:1672 exec_binprm (bprm=0xffff888003d93000 ) at fs/exec.c:1703 1703 if (ret < 0 ) (gdb) Run till exit from #0 exec_binprm (bprm=0xffff888003d93000 ) at fs/exec.c:1703 bprm_execve (bprm=0xffff888003d93000 ) at fs/exec.c:1754 1754 retval = exec_binprm(bprm); (gdb) Run till exit from #0 bprm_execve (bprm=0xffff888003d93000 ) at fs/exec.c:1754 bprm_execve (bprm=0xffff888003d93000 ) at fs/exec.c:1781 1781 return retval; (gdb) Run till exit from #0 bprm_execve (bprm=0xffff888003d93000 ) at fs/exec.c:1781 0xffffffff8150173c in do_execveat_common (fd=fd@entry=-100 , filename=0xffff888003373000 , flags=0 , envp=..., argv=...) at fs/exec.c:1860 1860 retval = bprm_execve(bprm); Value returned is $11 = 0 (gdb) Run till exit from #0 0xffffffff8150173c in do_execveat_common ( fd=fd@entry=-100 , filename=0xffff888003373000 , flags=0 , envp=..., argv=...) at fs/exec.c:1860 __x64_sys_execve (regs=<optimized out>) at fs/exec.c:2005 2005 SYSCALL_DEFINE3(execve, Value returned is $12 = 0 (gdb) list 2000 return ;2001 2002 __mm_flags_set_mask_dumpable(mm, value);2003 }2004 2005 SYSCALL_DEFINE3(execve,2006 const char __user *, filename,2007 const char __user *const __user *, argv,2008 const char __user *const __user *, envp)2009 { (gdb) 2010 return do_execve(getname(filename), argv, envp);2011 }
在退回SYSCALL_DEFINE3后使用next命令继续观察命令跳转:
1 2 3 4 5 6 7 8 9 10 11 12 13 (gdb) Run till exit from #0 0xffffffff8150173c in do_execveat_common ( fd=fd@entry=-100 , filename=0xffff888003373000 , flags=0 , envp=..., argv=...) at fs/exec.c:1860 __x64_sys_execve (regs=<optimized out>) at fs/exec.c:2005 2005 SYSCALL_DEFINE3 (execve, Value returned is $25 = 0 (gdb) Run till exit from #0 __x64_sys_execve (regs=<optimized out>) at fs/exec.c:2005 0xffffffff81d202e4 in do_syscall_x64 (regs=0xffffc90000173f58 , nr=<optimized out>) at arch/x86/entry/syscall_64.c:63 63 regs->ax = x64_sys_call(regs, unr);Value returned is $26 = 0
返回SYSCALL_DEFINE3后继续追踪,在100行可见系统调用返回用户态的逻辑。汇编函数common_interrupt_return的659行有iretq指令,该指令用于从中断处理程序返回用户态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 (gdb) ndo_syscall_64 (regs=0xffffc90000173f58 , nr=<optimized out>) at arch/x86/entry/syscall_64.c:99 99 instrumentation_end () ; (gdb) 100 syscall_exit_to_user_mode(regs); (gdb) 113 if (unlikely(regs->cx != regs->ip || regs->r11 != regs->flags)) (gdb) srso_alias_return_thunk () at arch/x86/lib/retpoline.S:220 220 call srso_alias_safe_ret (gdb) ssrso_alias_safe_ret () at arch/x86/lib/retpoline.S:210 210 lea 8(%_ASM_SP) , %_ASM_SP (gdb) nsrso_alias_safe_ret () at arch/x86/lib/retpoline.S:213 213 ret (gdb) entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:130 130 ALTERNATIVE "testb %al, %al; jz swapgs_restore_regs_and_return_to_usermode", \ Value returned is $5 = false (gdb) common_interrupt_return () at arch/x86/entry/entry_64.S:561 561 IBRS_EXIT (gdb) 570 POP_REGS (gdb) common_interrupt_return () at arch/x86/entry/entry_64.S:571 571 add $8, %rsp /* orig_ax */ (gdb) common_interrupt_return () at arch/x86/entry/entry_64.S:575 575 swapgs (gdb) 578 testb $3, 8(%rsp) (gdb) 579 jnz .Lnative_iret (gdb) 647 testb $4, (SS-RIP)(%rsp) (gdb) 648 jnz native_irq_return_ldt (gdb) 659 iretq
按需分页机制 虚拟内存的延迟加载 系统调用返回用户态后,CPU跳转到程序入口地址执行。然而,此时会遇到无法读取指令的问题:
1 2 3 4 5 659 iretq (gdb) 0x00000000004027c0 in ?? () (gdb) x/i $rip => 0x4027c0 : Cannot access memory at address 0x4027c0
指令指针寄存器虽指向正确的程序入口地址0x4027c0,却无法访问该地址内容。这是因为现代操作系统采用了按需分页 (Demand Paging)技术。
在程序加载过程中,内核仅建立了虚拟地址到文件的映射关系,并未将程序代码实际加载到物理内存。当CPU首次访问某虚拟地址时,若对应物理页面不存在,会触发缺页异常 (Page Fault)。
缺页中断处理机制 操作系统通过缺页中断在检测到缺页时自动从磁盘加载相关数据。在页错误处理函数设置断点,可观察该机制的运作:
1 2 3 4 5 (gdb) b asm_exc_page_fault Breakpoint 4 at 0xffffffff81001280 : file ./arch/x86/include/asm /idtentry.h, line 618. (gdb) ni Breakpoint 4 , asm_exc_page_fault () at ./arch/x86/include/asm /idtentry.h, line 618.
CPU尝试执行位于0x4027c0的指令时,由于该地址对应的物理页面未分配,硬件自动触发页错误中断。内核页错误处理程序执行以下步骤:
权限验证 :检查进程是否有权访问该虚拟地址
物理页面分配 :从物理内存中分配空闲页面
内容加载 :从磁盘读取对应数据到物理页面
页表映射 :在页表中建立虚拟地址到物理地址的映射
重新执行 :返回用户态,重新执行引发页错误的指令
在OS加载对应地址的数据后,即可正常访问:
1 2 3 0x00000000004027c0 in ?? () (gdb) x/i $rip => 0x4027c0 : endbr64
C库入口封装 经过缺页处理后,程序开始正式执行。需要注意,最先运行的并非用户编写的代码,而是C标准库封装的入口函数_start。该函数进行栈初始化、参数解析和环境配置等准备工作,完成后才调用用户的main函数。以下省略了_start函数的大部分汇编代码,重点关注与测试程序相关的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 0x00000000004027c0 in ?? () (gdb) x/i $rip => 0x4027c0 : endbr64 ... (gdb) 0x4028f5 : endbr64 (gdb) 0x4028f9 : push %rbp (gdb) 0x4028fa : mov %rsp,%rbp (gdb) 0x4028fd : mov $0xd ,%edx (gdb) 0x402902 : lea 0x6a707 (%rip),%rax # 0x46d010 (gdb) 0x402909 : mov %rax,%rsi (gdb) 0x40290c : mov $0x1 ,%edi (gdb) 0x402911 : call 0x410970 (gdb) 0x402916 : mov $0xcc ,%edi (gdb) 0x40291b : call 0x410930
在0x402916处可见将退出码0xcc赋值给寄存器的指令,0x402902处加载字符串地址。
查看地址0x46d010时,再次遇到缺页错误。需继续执行至0x402916,此时代码已实际访问字符串,字符串已加载至内存,可正常打印0x46d010的值,正是测试程序中的hello world!\n:
1 2 3 4 5 6 7 8 9 (gdb) x/s 0x46d010 0x46d010 : <error: Cannot access memory at address 0x46d010 > (gdb) Breakpoint 5 , 0x0000000000402916 in ?? () (gdb) x/i $rip => 0x402916 : mov $0xcc ,%edi (gdb) x/s 0x46d010 0x46d010 : "hello world!\n"
使用nm查看符号表验证,可以发现在上述代码地址0x402911调用的即为libc库的write方法,地址0x40291b调用的为_exit方法,符合预期:
1 2 3 4 5 6 7 -> % nm demo | grep 410970 0000000000410970 T __libc_write 0000000000410970 W __write 0000000000410970 W write -> % nm demo | grep 410930 0000000000410930 T _exit 0000000000410930 W _Exit
使用continue继续执行,程序成功运行并输出预期结果。在QEMU控制台可见程序输出”hello world!”字符串,随后正常退出。
系统调用流程总结 通过本实验的深入调试分析,完整跟踪了Linux系统执行ELF可执行文件的全过程。该流程从用户程序调用execve系统调用开始,经历系统调用处理、文件格式识别、内存映射建立、执行环境初始化等关键阶段,最终将控制权转移给用户程序执行。
程序执行的完整调用链路如下图所示:
文本格式图表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 用户态程序调用 execve() ↓ 标准C库包装函数 ↓ 系统调用接口 (syscall指令) ↓ 内核系统调用分发器 ↓ __x64_sys_execve() ↓ do_execve(getname(filename), argv, envp) ↓ do_execveat_common() ├── 参数验证和权限检查 ├── 分配 linux_binprm 结构 ├── 准备参数和环境变量 └── bprm_execve() ↓ exec_binprm() ↓ search_binary_handler() ├── 尝试 misc_format (失败) ├── 尝试 script_format (失败) └── 尝试 elf_format (成功) ↓ load_elf_binary() ├── ELF头部验证 │ ├── 魔数检查 (\x7fELF) │ ├── 文件类型检查 (ET_EXEC/ET_DYN) │ ├── 架构兼容性检查 │ └── 版本兼容性检查 ├── 程序头表解析 ├── 内存映射建立 ├── 虚拟地址空间分配 └── start_thread() ↓ start_thread_common() ├── 段寄存器初始化 ├── 线程特性重置 └── 关键寄存器设置 ├── RIP = 程序入口地址 ├── RSP = 用户栈地址 ├── CS = 用户代码段 └── SS = 用户数据段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 %%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#f5f5f5','primaryTextColor':'#000','primaryBorderColor':'#666','lineColor':'#666','secondaryColor':'#e8e8e8','tertiaryColor':'#fff'}}}%% flowchart TD A[用户态程序调用 execve] -->|系统调用| B[标准C库包装函数] B -->|syscall指令| C[内核系统调用分发器] C --> D[__x64_sys_execve] D --> E[do_execve] E --> F[do_execveat_common] F --> G[参数验证和权限检查] F --> H[分配 linux_binprm 结构] F --> I[准备参数和环境变量] I --> J[bprm_execve] J --> K[exec_binprm] K --> L[search_binary_handler] L --> M[尝试 misc_format] M -->|失败| N[尝试 script_format] N -->|失败| O[尝试 elf_format] O -->|成功| P[load_elf_binary] P --> Q[ELF头部验证] Q --> Q1[魔数检查 \x7fELF] Q --> Q2[文件类型检查 ET_EXEC/ET_DYN] Q --> Q3[架构兼容性检查] Q --> Q4[版本兼容性检查] P --> R[程序头表解析] P --> S[内存映射建立] P --> T[虚拟地址空间分配] Q1 --> U[start_thread] Q2 --> U Q3 --> U Q4 --> U R --> U S --> U T --> U U --> V[start_thread_common] V --> W[段寄存器初始化] V --> X[线程特性重置] V --> Y[关键寄存器设置] Y --> Y1[RIP = 程序入口地址] Y --> Y2[RSP = 用户栈地址] Y --> Y3[CS = 用户代码段] Y --> Y4[SS = 用户数据段]