核心, linux,

Linux可执行文件的真正功能

在Linux中执行文件时会发生什么?文件是什么意思 可执行文件?我们只能执行编译的二进制文件吗?那shell脚本呢?如果可以执行Shell脚本,还可以执行什么操作?在本文中,我们将尝试回答这些问题。

 

什么 involves executing a file

 

从基础开始,让’尝试了解当我们在终端中键入以下内容时会发生什么

$ /usr/bin/sleep 30

我们在用户空间中的程序与内核交互的方式是通过 系统调用。实际上,我们需要与内核进行交互以执行几乎所有有趣的事情,例如打印输出,读取输入,读取文件等等。

负责执行文件的syscall是 执行ve() 系统调用。当我们编码时,我们通常通过 执行 标准库中存在的函数系列,或者更常见的是通过更高级别的抽象,例如 popen() 或者 系统().

重要的是要注意,当我们通过以下方式执行文件时 执行ve() ,不会生成任何新过程。相反,我们的调用过程将变为新的可执行文件的执行实例。 PID赢了’更改,但进程的机器代码,数据,堆和堆栈将 已取代 在内核空间中。

这与我们从终端启动可执行文件的方式不同。当我们键入 睡30 在终端,我们得到一个 孩子 的过程 重击,并且后者不会消失。

$ sleep 30 &
$ ps -HF
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
nacho    21628 13409  1  4123  4312   3 17:59 pts/1    00:00:00 bash
nacho    21916 21628  0  1489   712   2 18:00 pts/1    00:00:00   sleep 30

在这里,另一个系统调用正在起作用, 叉() 系统调用。 重击 将首先在另一个子进程中创建自身的副本,然后该子进程将调用 执行ve() 为了把自己变成 睡觉。这条路 重击 doesn’t消失了,在什么时候将在那里接管控制权 睡觉 30秒后死亡。

我们可以跳过分叉步骤 执行 重击 bultin

$ echo $$          # this is the PID of bash
23714
$ exec sleep 30

在另一个终端中,我们可以看到睡眠取代了PID

$ ps -e | grep sleep
23714 pts/7 00:00:00 sleep

果然,30秒后 睡觉 退出时没有bash会话,因此终端窗口将关闭。

内核方面

 

到目前为止,我们已经看到用户空间通过syscall进行交互,让’看看另一边会发生什么。

这些系统调用的实现位于内核中。一般而言, 执行ve() syscall请求内核在磁盘上执行某个文件,并且内核需要将该文件加载到内存中,CPU可以在其中访问该文件。

这是系统调用的入口点,位于 fs / exec.c

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);
}

从这里开始,内核首先执行所有必需的准备工作,以便开始执行二进制文件,例如设置 虚拟内存 for the process

static int __bprm_mm_init(struct linux_binprm *bprm)
{
	...
	vma->vm_end = STACK_TOP_MAX;
	vma->vm_start = vma->vm_end - PAGE_SIZE;
	vma->vm_flags = VM_SOFTDIRTY | VM_STACK_FLAGS | VM_STACK_INCOMPLETE_SETUP;
	vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
	INIT_LIST_HEAD(&vma->anon_vma_chain);

	err = insert_vm_struct(mm, vma);
	...
}

,并设置文件名,命令行参数和继承的环境。是的,环境变量也是Linux内核的一等公民。

/*
 * sys_execve() executes a new program.
 */
static int do_execveat_common(int fd, struct filename *filename,
			      struct user_arg_ptr argv,
			      struct user_arg_ptr envp,
			      int flags)
{ 
       ...
       retval = copy_strings_kernel(1, &bprm->filename, bprm);
	if (retval < 0)
		goto out;

	bprm->exec = bprm->p;
	retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out;

	retval = copy_strings(bprm->argc, argv, bprm);
        ...
}

最后,文件“will be executed”.

精灵 binaries

 

二进制不仅是一大堆机器代码。现代二进制文件使用二进制ELF格式,例如 可执行和可链接格式。简而言之,这是一种将具有某些属性的不同代码和数据段打包的方法,例如对 。文本 代码部分,以便将它们映射到虚拟内存中以供执行。另外,还有机器独立的 标头 提供有关可执行文件的基本信息,例如静态或动态链接的结构以及体系结构等。

负责解析ELF格式的内核代码存在于 fs / binfmt_elf.c。在这里,ELF标头 被阅读 and analyzed

/**
 * load_elf_phdrs() - load ELF program headers
 * @elf_ex:   ELF header of the binary whose program headers should be loaded
 * @elf_file: the opened ELF binary file
 *
 * Loads ELF program headers from the binary file elf_file, which has the ELF
 * header pointed to by elf_ex, into a newly allocated array. The caller is
 * responsible for freeing the allocated data. Returns an ERR_PTR upon failure.
 */
static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex,
				       struct file *elf_file)
{
  ...
}

,以及PT_LOAD部分 被加载 into virtual memory

static int load_elf_binary(struct linux_binprm *bprm)
{
        ...

        /* Now we do a little grungy work by mmapping the ELF image into
	   the correct location in memory. */
	for(i = 0, elf_ppnt = elf_phdata;
	    i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
		int elf_prot = 0, elf_flags, elf_fixed = MAP_FIXED_NOREPLACE;
		unsigned long k, vaddr;
		unsigned long total_size = 0;

		if (elf_ppnt->p_type != PT_LOAD)
			continue;
         ...
}

对于动态链接的程序,这只是稍微复杂一点。内核通过PT_INTERP标头识别动态链接的程序。

$ readelf -l /usr/bin/sleep

Elf file type is DYN (Shared object file)
Entry point 0x1710
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000001f8 0x00000000000001f8  R E    0x8
  INTERP         0x0000000000000238 0x0000000000000238 0x0000000000000238
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000006a38 0x0000000000006a38  R E    0x200000
  LOAD           0x0000000000006bb0 0x0000000000206bb0 0x0000000000206bb0
                 0x00000000000004d0 0x0000000000000690  RW     0x200000
  DYNAMIC        0x0000000000006c78 0x0000000000206c78 0x0000000000206c78
                 0x00000000000001b0 0x00000000000001b0  RW     0x8
  NOTE           0x0000000000000254 0x0000000000000254 0x0000000000000254
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_EH_FRAME   0x0000000000005bf0 0x0000000000005bf0 0x0000000000005bf0
                 0x000000000000025c 0x000000000000025c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000006bb0 0x0000000000206bb0 0x0000000000206bb0
                 0x0000000000000450 0x0000000000000450  R      0x1

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

该标头在编译时使用运行时链接程序的路径进行硬编码。 ld-linux-x86-64.so 需要使用它来运行它。运行时链接程序将在文件系统中找到 。所以 二进制文件需要执行的库,并将它们加载到内存中。

$ ldd /usr/bin/sleep
        linux-vdso.so.1 (0x00007ffc9dbfe000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f71d401d000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f71d45e1000)

在这种简单情况下,只需动态链接标准C库,因为vDSO是放置在其中的特殊虚拟库,可有效执行只读syscall。

内核将识别PT_INTERP标头,还将加载运行时链接程序(也称为ELF解释器) 搜搜 and execute it.

static int load_elf_binary(struct linux_binprm *bprm)
{
        ...
        if (elf_interpreter) {
		unsigned long interp_map_addr = 0;

		elf_entry = load_elf_interp(&loc->interp_elf_ex,
					    interpreter,
					    &interp_map_addr,
                                            load_bias, interp_elf_phdata);
         ...
}

然后 搜搜 将找到并加载 。所以 动态库,如果未定义的符号仍未解决,则失败,最后将执行跳转到原始二进制代码的开头(AT_ENTRY)。

我们认为内核是检查二进制文件并对其进行处理的内核,但实际上,我们不是在执行二进制文件,而是在执行ELF解释器 搜搜 反而。从技术上讲,这是我们的二进制文件’机器代码,它仍在执行,但我们面临一个概念,即 口译员 这是实际可执行的,而实际上是“interpreting” our file.

归根结底,我们正在这样做

$ /lib64/ld-linux-x86-64.so.2 /bin/sleep 30

什么 about scripts?

 

我一直在努力避免这个词 二元 因为你可以“execute”非二进制文件。这些文件在技术上不会自行执行,因为它们不会’t必须包含机器代码,但是 口译员 as we just saw.

现在,让’看一下可执行脚本的运行方式。我曾经想像bash进程(或文件管理器)会检查文件的前几个字节,以及是否找到了 社bang, 例如 #!/ bin / python2,然后调用相应的解释器。事实证明这不是它的工作方式。 shebang检测实际上发生在Linux内核本身中。

这意味着执行不仅仅是ELF:Linux支持很多 二元 格式, 只是ELF就是其中之一。在内核内部,每种二进制格式都由一个 处理程序 知道如何处理所述文件。标准内核附带了一些处理程序,但是可以通过可加载模块添加其他一些处理程序。

每当要通过执行文件 执行ve(),将读取其前128个字节并将其传递给每个处理程序。这发生在 fs / exec.c

/*
 * cycle the list of binary formats handler, until one recognizes the image
 */
int search_binary_handler(struct linux_binprm *bprm)
{
        ...
        list_for_each_entry(fmt, &formats, lh) {
            ...
            retval = fmt->load_binary(bprm);     // OYB: the first 128B are in bprm->buf[128]
            ...
        }
        ...
}

然后,每个处理程序可以接受或忽略它,通常取决于 魔法 在二进制文件的第一个字节中。这样,适当的处理程序将负责该二进制文件的执行,或将这样做的机会传递给另一个处理程序。

在ELF格式的情况下,魔术是0x7F‘ELF’在字段e_ident中

#define EI_NIDENT 16
typedef struct {
    unsigned char e_ident[EI_NIDENT]; /* 0x7F 'ELF' four byte ELF magic number */
    uint16_t      e_type;
    uint16_t      e_machine;
    uint32_t      e_version;
    ElfN_Addr     e_entry;
    ElfN_Off      e_phoff;
    ElfN_Off      e_shoff;
    uint32_t      e_flags;
    uint16_t      e_ehsize;
    uint16_t      e_phentsize;
    uint16_t      e_phnum;
    uint16_t      e_shentsize;
    uint16_t      e_shnum;
    uint16_t      e_shstrndx;
} ElfN_Ehdr;

这由ELF处理程序在处检查 binfmt_elf.c 为了接受二进制。

static int load_elf_binary(struct linux_binprm *bprm)
{
    ...
    loc->elf_ex = *((struct elfhdr *)bprm->buf);

    if (memcmp(elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
        goto out;
    ...
}

那么脚本会发生什么呢?好吧,事实证明,有一个处理程序 在内核中,可以在以下位置找到 binfmt_script.c。

全部 二进制格式处理程序 提供与 执行ve(),例如,这是ELF格式的

static struct linux_binfmt elf_format = {
	.module         = THIS_MODULE,
	.load_binary    = load_elf_binary,
	.load_shlib     = load_elf_library,
	.core_dump      = elf_core_dump,
	.min_coredump   = ELF_EXEC_PAGESIZE,
};

这里的主要钩子是 load_binary() 对于每个处理程序,该函数将有所不同。

对于脚本处理程序,其 load_binary() 钩在 binfmt_script.c,并且开始是这样。

static int load_script(struct linux_binprm *bprm)
{
	const char *i_arg, *i_name;
	char *cp;
	struct file *file;
	int retval;

	if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
        return -ENOEXEC;
        ...
}

所以这是 核心 实际上,他会解析脚本的第一行,然后以脚本路径作为参数将执行传递给解释器。只要文件以shebang#!开头,它将被解释为脚本,可以是python,awk,sed,perl,bash,ash,sh,zsh或任何其他类似的脚本。

再一次,我们实际上并没有执行脚本,但是我们正在做类似的事情

$ /bin/bash ./sleep30.sh

我们开始看到这个名字 二进制格式 有点误导,因为我们可以执行非二进制文件。还有其他用于特殊格式或旧二进制格式的处理程序,例如 flat format,或旧的 格式,尤其是功能非常强大且用途广泛的处理程序, binfmt_misc handler.

使用bitfmt_misc执行任何操作

 

现在我们知道什么是二进制处理程序,并且我们可以理解 binfmt_misc。这是一种灵活的格式处理程序,它使我们可以指定针对特定文件类型运行的userland解释程序。它没有’不仅可以查看文件开头的硬编码魔术,而且还支持使用掩码通过扩展名检测二进制文件,并提供 / proc 与系统管理员的界面。请记住,所有这些都是在内核空间中发生的。该处理程序的加载程序是 load_misc_binary()fs / binfmt_misc.c.

如果 / proc 接口尚未为我们安装,我们可以使用

# mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc

让’s have a look at it

$ ls -l /proc/sys/fs/binfmt_misc
total 0
-rw-r--r-- 1 root root 0 May 16 10:38 CLR
-rw-r--r-- 1 root root 0 May 16 11:15 python2.7
-rw-r--r-- 1 root root 0 May 16 11:15 python3.5
--w------- 1 root root 0 May 16 11:15 register
-rw-r--r-- 1 root root 0 May 16 11:15 status

我们可以看到我们已经用一些python和其他条目填充了它。

$ cat /proc/sys/fs/binfmt_misc/python3.5 
enabled
interpreter /usr/bin/python3.5
flags: 
offset 0
magic 160d0d0a

我们可以删除,启用或禁用这些条目。

  • echo 1Â启用输入
  • echo 0禁用输入
  • 回声-1Â删除条目

真正很酷的是,我们可以通过以下方式轻松添加自定义条目 binfmt_misc。为了添加条目,您将格式字符串回显到 登记。可以找到有关如何配置所有这些标志,掩码和魔术值的详细信息 这里.

例如,让’为JPG图片格式创建处理程序,以供 h 图像查看器。在这种情况下,我们通过扩展名进行匹配(因此 E)

# echo ':fehjpg:E::jpg::/usr/bin/feh:' > /proc/sys/fs/binfmt_misc/register

该处理程序现已注册。我们可以检查一下

cat /proc/sys/fs/binfmt_misc/fehjpg
enabled
interpreter /usr/bin/feh
flags:
extension .jpg

现在让’拍张照片,制作可执行文件, .

$ chmod +x ncpotato.jpg
$ ./ncpotato.jpg

让’现在可以根据魔术数字检测创建可执行的TODO列表(M)。

# echo ':TODOlist:M::#~[TODO]::/usr/bin/vi:' > /proc/sys/fs/binfmt_misc/register
$ cat /proc/sys/fs/binfmt_misc/TODOlist
enabled
interpreter /usr/bin/vi
flags:
offset 0
magic 237e5b544f444f5d

PDF以文字开头 %PDF,如 规格.

$ head test.pdf
%PDF-1.4
%íì¦"
% Created by calibre 2.57.1 [http://calibre-ebook.com]

4 0 obj
<< /Type /XObject /ColorSpace /DeviceRGB /BitsPerComponent 8 /Subtype /Image /Filter [/DCTDecode] /Length 206115 /Height 2000 /DL 206115 /Width 1525 >>
stream
JFIFddC

所以我们只是

# echo ':PDF:M::%PDF::/usr/bin/evince:' > /proc/sys/fs/binfmt_misc/register
$ chmod +x test.pdf
$ ./test.pdf

另一个示例,Libreoffice文件的扩展名

# echo ':ODT:E::odt::/usr/bin/soffice:' > /proc/sys/fs/binfmt_misc/register
chmod +x test.odt
./test.odt

我们有所有新条目 处理

# ls -l /proc/sys/fs/binfmt_misc
total 0
-rw-r--r-- 1 root root 0 May 22 20:54 ODT
-rw-r--r-- 1 root root 0 May 23 14:14 PDF
-rw-r--r-- 1 root root 0 May 19 14:07 TODOlist
-rw-r--r-- 1 root root 0 May 16 14:09 fehjpg
-rw-r--r-- 1 root root 0 May 18 09:40 python2.7
-rw-r--r-- 1 root root 0 May 18 09:40 python3.5
--w------- 1 root root 0 May 23 14:14 register
-rw-r--r-- 1 root root 0 May 18 09:40 status

通过这种技术,我们可以透明地运行 爪哇 应用程序(基于 0xCAFEBABE  魔法)

# binfmt_misc support for Java applications:
# echo ':Java:M::\xca\xfe\xba\xbe::/usr/local/bin/javawrapper:' > /proc/sys/fs/binfmt_misc/register

# binfmt_misc support for executable Jar files:
# echo ':ExecutableJAR:E::jar::/usr/local/bin/jarwrapper:' > /proc/sys/fs/binfmt_misc/register

# binfmt_misc support for Java Applets:
# echo ':Applet:E::html::/opt/java/bin/appletviewer:' > /proc/sys/fs/binfmt_misc/register

这需要使用包装器,您可以从 拱形维基.

这也适用于 单核细胞增多症,甚至DOS!为了透明地运行良好的旧文明,请安装 档案盒, 配置 binfmt_misc

# echo ':DOSEXE:M::MZ::/usr/bin/dosbox:' > /proc/sys/fs/binfmt_misc/register

,现在我们可以

$ ./CIV.EXE

 

一些适用于Windows仿真的二进制文件

# echo ':DOSWin:M::MZ::/usr/local/bin/wine:' > /proc/sys/fs/binfmt_misc/register
$ ./winemine.exe

问题是所有DOS,Windows和Mono二进制文件共享相同的 MZ 魔术,因此为了将它们组合起来,我们需要使用特殊的包装器,该包装器能够检测文件中更深的差异,例如 启动文件.

如果要永久保留这些设置,则可以在启动时通过以下方式设置此配置: /etc/binfmt.d。例如,如果我们想在启动时设置PDF处理程序,只需将新文件添加到该文件夹​​中

# echo ':PDF:M::%PDF::/usr/bin/evince:' > /etc/binfmt.d/pdf.conf

我们可以看到这种方法非常灵活和强大。它的一个问题是拥有它有点不合常规“regular files”除了脚本作为可执行文件。这样做的好处是,它确实是一个系统范围的设置,因此,一旦在内核中进行设置,它便可以在您的所有shell,文件管理器和其他任何用户中使用。 执行ve() system call.

我们将看到一些可以做的有用的事情 binfmt_misc 在里面 以下帖子.

参考

 

本文旨在对 binfmt_misc 以及执行过程中的一些实际示例。如果需要详细信息,请考虑阅读以下参考。

程序如何运行

程序如何运行:ELF二进制文件

系统调用剖析,第2部分

Linux内核中的系统调用

作者: nachoparker

谦虚地分享我认为有用的东西 [ 的github 码头工人hub ]

15 Comments

  1. 如果进入递归定义会怎样?例如,您安装了由另一个处理程序处理的处理程序?如果您配置一个双处理程序,一个调用另一个,该怎么办?

  2. 太好了,非常感谢。
    因此,内核使用前几个字节来确定要执行哪种类型的文件(脚本或二进制文件),对吗?

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *