百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

Linux如何启动一个进程

sinye56 2025-01-12 18:19 5 浏览 0 评论

在本文中,您将了解当一个进程调用execve()时,Linux内核内部发生了什么,内核如何准备栈,并且控制权如何传递给用户空间进程进行执行。

概述

  1. 内核接收用户空间程序的SYS_execve()调用。
  2. 内核将可执行文件(特定部分)读入特定的内存位置。
  3. 内核准备栈、堆、信号等。
  4. 内核将执行权传递给用户空间程序。

检查二进制文件

我们从一个简单的Linux C程序开始:

int main(int argc, char *argv[0]) {
        return 0;
}

使用gcc -static -o none none.c编译它,并找出一些细节:

$ readelf -h none
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4014f0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          760112 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

第一条指令从0x4014f0的“入口点”开始。这些指令是由编译器(gcc、go等)生成的。它们根据编译器的不同而有所不同。

让我们将二进制文件加载到gdb中,并对0x4014f0处的指令进行反汇编。这些指令执行一些清理工作,但最终会调用main()函数(或Go语言的等效函数)。

让我们在入口点(0x4014f0)设置一个断点,并使用两个命令行选项(firstarg和secondarg)运行该应用程序:

gdb ./none
pwndbg> disass 0x4014f0
pwndbg> br *0x4014f0
pwndbg> r firstarg secondarg
 ? 0x4014f0 <_start>       xor    ebp, ebp
   0x4014f2 <_start+2>     mov    r9, rdx
   0x4014f5 <_start+5>     pop    rsi
[...]
──────────────────────[ STACK ]──────────────────────
00:0000│ rsp 0x7ffca4229540 ?— 0x3
01:0008│     0x7ffca4229548 —? 0x7ffca422a4b3 ?— '/sec/root/none'
02:0010│     0x7ffca4229550 —? 0x7ffca422a4c2 ?— 'firstarg'
03:0018│     0x7ffca4229558 —? 0x7ffca422a4cb ?— 'secondarg'
04:0020│     0x7ffca4229560 ?— 0x0
05:0028│     0x7ffca4229568 —? 0x7ffca422a4d5 ?— 'BASH_ENV=/etc/shellrc'
[...]

(如果您使用的是没有pwngdb的gdb,则可能需要使用x/64a $rsp来列出堆栈的前64个条目。)

堆栈指针rsp位于0x7ffd4f48bd10。让我们使用grep -F '[stack]' /proc/$(pidof none)/maps找出堆栈的结束位置:

7ffd4f46c000-7ffd4f48d000 rw-p 00000000 00:00 0         [stack]

内核已将堆栈内存分配从0x7ffd4f46c000到0x7ffd4f48d000,总共132 KB。它将动态增长到8MB(ulimit -s kilobytes)。我们的程序(目前为止;参见rsp)只使用从rsp地址(0x7ffd4f48bd10)向下到堆栈的相同结束位置(0x7ffd4f48d000)的堆栈,共计4,848字节(echo $((0x7ffd4f48d000 - 0x7ffd4f48bd10)) == 4848)。

这是执行的“诞生”:内核将控制权交给了我们的程序。我们的程序即将执行它的第一条指令——迈出第一步(可以这么说)。

堆栈上现在包含了程序从内核获取的所有运行信息。它包含了参数列表、环境变量和许多其他有趣的信息。

让我们dump堆栈:

pwndbg> dump binary memory stack.dat $rsp 0x7ffd4f48d000

然后将其加载到hd <stack.dat或者xxd <stack.dat,并进行一次小小的预览...

 03 00 00 00 00 00 00 00  b3 a4 22 a4 fc 7f 00 00  |..........".....|
 c2 a4 22 a4 fc 7f 00 00  cb a4 22 a4 fc 7f 00 00  |..".......".....|
 00 00 00 00 00 00 00 00  d5 a4 22 a4 fc 7f 00 00  |..........".....|
 eb a4 22 a4 fc 7f 00 00  11 a5 22 a4 fc 7f 00 00  |..".......".....|
 25 a5 22 a4 fc 7f 00 00  30 a5 22 a4 fc 7f 00 00  |%.".....0.".....|
[...]
 b4 5c 18 e0 ed f9 fb 0d  30 78 38 36 5f 36 34 00  |.\......0x86_64.|
 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
 00 00 00 2f 73 65 63 2f  72 6f 6f 74 2f 6e 6f 6e  |.../sec/root/non|
 65 00 66 69 72 73 74 61  72 67 00 73 65 63 6f 6e  |e.firstarg.secon|
 64 61 72 67 00 42 41 53  48 5f 45 4e 56 3d 2f 65  |darg.BASH_ENV=/e|
 74 63 2f 73 68 65 6c 6c  72 63 00 43 48 45 41 54  |tc/shellrc.CHEAT|
 5f 43 4f 4e 46 49 47 5f  50 41 54 48 3d 2f 65 74  |_CONFIG_PATH=/et|
[...]
 55 4d 4e 53 3d 31 31 38  00 2f 73 65 63 2f 72 6f  |UMNS=118./sec/ro|
 6f 74 2f 6e 6f 6e 65 00  00 00 00 00 00 00 00 00  |ot/none.........|

很多指针。很多字符串。很多未知的内容。

让我们跟踪从execve()到程序的入口点的调用。

execve()通过系统调用调用内核,然后调用do_execve()函数:

最终,这将进入do_execveat_common()函数。bprm结构被创建,并将关于程序的各种信息分配给它(参见binfmts.h)。

对于我们来说,程序的文件名、环境变量和选项(argv)从内核内存复制到进程的堆栈中。堆栈向较低的地址增长:堆栈上放置的第一项(bprm->filename)因此位于堆栈的最大地址(底部),在它的上方(较小的地址)是envp,然后是argv。

我们继续跟踪到bprm_execve()函数,在调用exec_binprm()之前完成一些检查。然后进入search_binary_handler()函数,内核检查二进制文件是否为ELF格式、是否为shebang(#!)或是否是通过binfmt-misc模块注册的其他类型。内核然后调用适当的函数来加载二进制文件。

在我们的情况下,它是一个ELF二进制文件,因此调用load_elf_binary()函数。内核创建内存,然后将二进制文件的节映射到内存中。它调用begin_new_exec()函数为新进程设置所有的凭据和权限。

然后,内核检查ELF二进制文件是否应该由解释器(ld.so)加载:

或者,对于像我们的静态二进制文件这样的情况,直接加载而无需解释器:

最后,调用create_elf_tables()函数。这是我们感兴趣的堆栈魔术发生的地方。

首先,函数arch_align_stack()向堆栈中添加随机数量的零(例如堆栈随机化),以使(某些)缓冲区溢出攻击的可靠性稍微降低。然后,它将堆栈对齐到16字节(例如,将堆栈指针设置为下一个较低的地址,该地址与16字节对齐,使用& ~0xf)。

然后,内核将"x86_64\0"放入堆栈中,然后在其上方添加16字节的随机数据(libc将其用作其伪随机数生成器的种子):

然后,内核创建ELF辅助表:这是一个包含(id,value)对的集合,描述正在运行的程序以及它所运行的环境的有用信息,从内核传递到用户空间。

列表以零标识符和零值(例如16字节的0x00)结束。列表中大约有20个条目(320字节)。

表以ARCH_DLINFO(它展开为AT_SYSINFO_EHDR + AT_MINSIGSTKSZ)开始。

这些“标识符”在auxvec.h中定义:

在gdb中,我们程序的堆栈上的ELF辅助表如下所示(注意:上面的“标识符”值是十进制的,但gdb显示为十六进制):

[... above is argc ...]
[... above is argvp ...]
[... above here is envp ...]
0x7ffca42296a8: 0x21    0x7ffca4351000    <-- AT_SYSINFO_EDHR
0x7ffca42296b8: 0x33    0xd30             <-- AT_MINSIGSTKSZ
0x7ffca42296c8: 0x10    0x178bfbff        <-- AT_HWCAP
0x7ffca42296d8: 0x6     0x1000            <-- AT_PAGESZ
0x7ffca42296e8: 0x11    0x64
0x7ffca42296f8: 0x3     0x400040
0x7ffca4229708: 0x4     0x38
0x7ffca4229718: 0x5     0xa
0x7ffca4229728: 0x7     0x0
0x7ffca4229738: 0x8     0x0
0x7ffca4229748: 0x9     0x4014f0         <-- Our entry point
0x7ffca4229758: 0xb     0x0
0x7ffca4229768: 0xc     0x0
0x7ffca4229778: 0xd     0x0
0x7ffca4229788: 0xe     0x0
0x7ffca4229798: 0x17    0x0
0x7ffca42297a8: 0x19    0x7ffca42297f9   <-- Ptr to Random
0x7ffca42297b8: 0x1a    0x2
0x7ffca42297c8: 0x1f    0x7ffca422afe9   <-- Ptr to filename
0x7ffca42297d8: 0xf     0x7ffca4229809   <-- Ptr to x86_64
0x7ffca42297e8: 0x0     0x0                  <-- NULL + NULL
[... ELF table stops here ...]
0x7ffca42297f8: 0xe8e8de3a49831f00      0xdfbf9ede0185cb4 <-- RND16
0x7ffca4229808: 0x34365f363878af        0x0  <-- "x86_64" + '\0'
[... below is empty space from stack randomization ...]
[... below are argv strings ...]
[... below are env strings ...]
[... last is the filename (/root/none) ...]

然后,内核分配堆栈内存来存储elf-aux表、argv和env指针以及argc值(+1),并将堆栈顶部对齐到16字节。(它尚未将elf-aux表复制到堆栈上。这将在稍后进行):

...然后将argc、argv指针和env指针放入堆栈中:

...然后将elf-aux表(即上述的elf_info)复制到堆栈上(对齐后,在env指针的下方)。

(聪明的读者可能会注意到,'RND16'并不以对齐的地址开始 - 0x7ffca42297f9:这是因为在调用STACK_ROUND()函数将elf-info表和env/argv指针放入堆栈之前,RND16已经被放入了堆栈中。)

现在回到load_elf_binary()函数中,内核设置寄存器,清除一些内容,最后 (!) 调用START_THREAD()函数来启动程序。

事后想法:有人指出https://lwn.net/Articles/631631/。他们的ASCII艺术比我的好。它展示了在执行之前堆栈的布局(但是他们把它画反了;从顶部开始以最大地址,最底部是最小地址):

相关推荐

程序员:JDK的安装与配置(完整版)_jdk的安装方法

对于Java程序员来说,jdk是必不陌生的一个词。但怎么安装配置jdk,对新手来说确实头疼的一件事情。我这里以jdk10为例,详细的说明讲解了jdk的安装和配置,如果有不明白的小伙伴可以评论区留言哦下...

Linux中安装jdk并配置环境变量_linux jdk安装教程及环境变量配置

一、通过连接工具登录到Linux(我这里使用的Centos7.6版本)服务器连接工具有很多我就不一一介绍了今天使用比较常用的XShell工具登录成功如下:二、上传jdk安装包到Linux服务器jdk...

麒麟系统安装JAVA JDK教程_麒麟系统配置jdk

检查检查系统是否自带java在麒麟系统桌面空白处,右键“在终端打开”,打开shell对话框输入:java–version查看是否自带java及版本如图所示,系统自带OpenJDK,要先卸载自带JDK...

学习笔记-Linux JDK - 安装&amp;配置

前提条件#检查是否存在JDKrpm-qa|grepjava#删除现存JDKyum-yremovejava*安装OracleJDK不分系统#进入安装文件目...

Linux新手入门系列:Linux下jdk安装配置

本系列文章是把作者刚接触和学习Linux时候的实操记录分享出来,内容主要包括Linux入门的一些理论概念知识、Web程序、mysql数据库的简单安装部署,希望能够帮到一些初学者,少走一些弯路。注意:L...

测试员必备:Linux下安装JDK 1.8你必须知道的那些事

1.简介在Oracle收购Sun后,Java的一系列产品就被整合到Oracle官网中,打开官网乍眼一看也不知道去哪里下载,还得一个一个的摸索尝试,而且网上大多数都是一些Oracle收购Sun前,或者就...

Linux 下安装JDK17_linux 安装jdk1.8 yum

一、安装环境操作系统:JDK版本:17二、安装步骤第一步:下载安装包下载Linux环境下的jdk1.8,请去官网(https://www.oracle.com/java/technologies/do...

在Ubuntu系统中安装JDK 17并配置环境变量教程

在Ubuntu系统上安装JDK17并配置环境变量是Java开发环境搭建的重要步骤。JDK17是Oracle提供的长期支持版本,广泛用于开发Java应用程序。以下是详细的步骤,帮助你在Ubuntu系...

如何在 Linux 上安装 Java_linux安装java的步骤

在桌面上拥抱Java应用程序,然后在所有桌面上运行它们。--SethKenlon(作者)无论你运行的是哪种操作系统,通常都有几种安装应用程序的方法。有时你可能会在应用程序商店中找到一个应用程序...

Windows和Linux环境下的JDK安装教程

JavaDevelopmentKit(简称JDK),是Java开发的核心工具包,提供了Java应用程序的编译、运行和开发所需的各类工具和类库。它包括了JRE(JavaRuntimeEnviro...

linux安装jdk_linux安装jdk软连接

JDK是啥就不用多介绍了哈,外行的人也不会进来看我的博文。依然记得读大学那会,第一次实验课就是在机房安装jdk,编写HelloWorld程序。时光飞逝啊,一下过了十多年了,挣了不少钱,买了跑车,娶了富...

linux安装jdk,全局配置,不同用户不同jdk

jdk1.8安装包链接:https://pan.baidu.com/s/14qBrh6ZpLK04QS8ogCepwg提取码:09zs上传文件解压tar-zxvfjdk-8u152-linux-...

运维大神教你在linux下安装jdk8_linux安装jdk1.7

1.到官网下载适合自己机器的版本。楼主下载的是jdk-8u66-linux-i586.tar.gzhttp://www.oracle.com/technetwork/java/javase/downl...

window和linux安装JDK1.8_linux 安装jdk1.8.tar

Windows安装JDK1.8的步骤:步骤1:下载JDK打开浏览器,找到JDK下载页面https://d.injdk.cn/download/oraclejdk/8在页面中找到并点击“下载...

最全的linux下安装JavaJDK的教程(图文详解)不会安装你来打我?

默认已经有了linux服务器,且有root账号首先检查一下是否已经安装过java的jdk任意位置输入命令:whichjava像我这个已经安装过了,就会提示在哪个位置,你的肯定是找不到。一般我们在...

取消回复欢迎 发表评论: