为什么 GCC 根据文件创建共享对象而不是可执行二进制文件?

Why does GCC create a shared object instead of an executable binary according to file?

我正在建设一个图书馆。当我 运行 以下任一项时,我的所有对象都会编译并 link 相继: ar rcs lib/libryftts.a $^

gcc -shared $^ -o lib/libryftts.so

在我的 Makefile 中。我也能够成功地将它们安装到 /usr/local/lib 当我用 nm 测试文件时,所有的功能都在那里。 我的问题是,当我 运行 gcc testing/test.c -lryftts -o test && file ./testgcc testing/test.c lib/libryftts.a -o test && file ./test 它说:

test: ELF 64-bit LSB shared object 而不是我期望的 test: ELF 64-bit LSB executable 。我做错了什么?

What am I doing wrong?

没有。

听起来您的 GCC 默认配置为构建 -pie 个二进制文件。这些二进制文件确实 共享库(类型 ET_DYN),除了它们 运行 就像普通的可执行文件一样。

所以你应该 运行 你的二进制文件,并且(如果它有效)不用担心它。

或者您可以 link 带有 gcc -no-pie ... 的二进制文件,这应该会生成 ET_EXEC 类型的非 PIE 可执行文件,file 将说 ELF 64-bit LSB executable.

file 5.36说的很清楚

file 5.36 实际上会清楚地显示可执行文件是否为 PIE,如所示:https://unix.stackexchange.com/questions/89211/how-to-test-whether-a-linux-binary-was-compiled-as-position-independent-code/435038#435038

例如,PIE 可执行文件显示为:

main.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, not stripped

和一个 non-PIE 一个:

main.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

该功能是在 5.33 中引入的,但它只是做了一个简单的 chmod +x 检查。在此之前,它只是为 PIE 打印 shared object

在 5.34 中,它原本打算开始检查更专业的 DF_1_PIE ELF 元数据,但由于提交 9109a696f3289ba00eaa222fd432755ec4287e28 时的实现中存在错误,它实际上破坏了一些东西并将 GCC PIE 可执行文件显示为shared objects.

该错误已在提交 03084b161cf888b5286dbbcd964c31ccad4f64d9 时在 5.36 中修复。

该错误特别存在于 Ubuntu 18.10 中,其中 file 5.34.

由于巧合ld -pie链接汇编代码时没有出现

源代码分解显示在本答案的“file 5.36 源代码分析”部分。

Linux内核5.0根据ET_DYN

判断是否可以使用ASLR

file "confusion" 的根本原因是 PIE executables 和共享库都是位置无关的,可以放置在随机的内存位置。

fs/binfmt_elf.c内核只接受这两种类型的ELF文件:

/* First of all, some simple consistency checks */
if (interp_elf_ex->e_type != ET_EXEC &&
        interp_elf_ex->e_type != ET_DYN)
        goto out;

然后,只有 ET_DYN 才会将 load_bias 设置为非零值。然后 load_bias 决定了 ELF 偏移量:How is the address of the text section of a PIE executable determined in Linux?

/*
 * If we are loading ET_EXEC or we have already performed
 * the ET_DYN load_addr calculations, proceed normally.
 */
if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
        elf_flags |= elf_fixed;
} else if (loc->elf_ex.e_type == ET_DYN) {
        /*
         * This logic is run once for the first LOAD Program
         * Header for ET_DYN binaries to calculate the
         * randomization (load_bias) for all the LOAD
         * Program Headers, and to calculate the entire
         * size of the ELF mapping (total_size). (Note that
         * load_addr_set is set to true later once the
         * initial mapping is performed.)
         *
         * There are effectively two types of ET_DYN
         * binaries: programs (i.e. PIE: ET_DYN with INTERP)
         * and loaders (ET_DYN without INTERP, since they
         * _are_ the ELF interpreter). The loaders must
         * be loaded away from programs since the program
         * may otherwise collide with the loader (especially
         * for ET_EXEC which does not have a randomized
         * position). For example to handle invocations of
         * "./ld.so someprog" to test out a new version of
         * the loader, the subsequent program that the
         * loader loads must avoid the loader itself, so
         * they cannot share the same load range. Sufficient
         * room for the brk must be allocated with the
         * loader as well, since brk must be available with
         * the loader.
         *
         * Therefore, programs are loaded offset from
         * ELF_ET_DYN_BASE and loaders are loaded into the
         * independently randomized mmap region (0 load_bias
         * without MAP_FIXED).
         */
        if (elf_interpreter) {
                load_bias = ELF_ET_DYN_BASE;
                if (current->flags & PF_RANDOMIZE)
                        load_bias += arch_mmap_rnd();
                elf_flags |= elf_fixed;
        } else
                load_bias = 0;

我通过实验证实了这一点:What is the -fPIE option for position-independent executables in gcc and ld?

file 5.36行为细分

从源头研究了 file 的工作原理。我们将得出结论:

  • 如果Elf32_Ehdr.e_type == ET_EXEC
    • 打印executable
  • 否则如果Elf32_Ehdr.e_type == ET_DYN
    • if DT_FLAGS_1 动态部分条目存在
      • 如果 DF_1_PIE 设置在 DT_FLAGS_1 中:
        • 打印pie executable
      • 其他
        • 打印shared object
    • 其他
      • 如果文件可以被用户、组或其他人执行
        • 打印pie executable
      • 其他
        • 打印shared object

这里有一些实验证实:

Executable generation        ELF type  DT_FLAGS_1  DF_1_PIE  chdmod +x      file 5.36
---------------------------  --------  ----------  --------  -------------- --------------
gcc -fpie -pie               ET_DYN    y           y         y              pie executable
gcc -fno-pie -no-pie         ET_EXEC   n           n         y              executable
gcc -shared                  ET_DYN    n           n         y              pie executable
gcc -shared                  ET_DYN    n           n         n              shared object
ld                           ET_EXEC   n           n         y              executable
ld -pie --dynamic-linker     ET_DYN    y           y         y              pie executable
ld -pie --no-dynamic-linker  ET_DYN    y           y         y              pie executable

在 Ubuntu 18.10、GCC 8.2.0、Binutils 2.31.1 中测试。

每种类型实验的完整测试示例描述于:

ELF typeDF_1_PIE分别由:

决定
readelf --file-header main.out | grep Type
readelf --dynamic     main.out | grep FLAGS_1

file 5.36源码分析

要分析的关键文件是magic/Magdir/elf.

这种神奇的格式只根据固定位置的字节值来确定文件类型。

格式本身记录在:

man 5 magic

所以此时您需要准备好以下文件:

在文件末尾,我们看到:

0       string          7ELF         ELF
!:strength *2
>4      byte            0               invalid class
>4      byte            1               32-bit
>4      byte            2               64-bit
>5      byte            0               invalid byte order
>5      byte            1               LSB
>>0     use             elf-le
>5      byte            2               MSB
>>0     use             \^elf-le

7ELF 是每个 ELF 文件开头的 4 个魔法字节。 70x7F 的八进制。

然后通过与标准中的 Elf32_Ehdr 结构进行比较,我们看到字节 4(第 5 个字节,魔术标识符之后的第一个字节)确定了 ELF class:

e_ident[EI_CLASSELFCLASS]

它的一些可能值是:

ELFCLASS32 1
ELFCLASS64 2

然后在 file 源中,我们有:

1 32-bit
2 64-bit

32-bit64-bitfile输出到标准输出的字符串!

所以现在我们在那个文件中搜索 shared object,我们被引导到:

0       name            elf-le
>16     leshort         0               no file type,
!:mime  application/octet-stream
>16     leshort         1               relocatable,
!:mime  application/x-object
>16     leshort         2               executable,
!:mime  application/x-executable
>16     leshort         3               ${x?pie executable:shared object},

所以这个 elf-le 是包含在代码前面部分的某种标识符。

字节16正好是ELF类型:

Elf32_Ehdr.e_type

它的一些值是:

ET_EXEC 2
ET_DYN  3

因此,ET_EXEC 总是打印为 executable

ET_DYN 然而有两种可能性取决于 ${x:

  • pie executable
  • shared object

${x 问:文件是否可以被用户、组或其他人执行?如果是,显示 pie executable,否则显示 shared object.

这个扩展是在src/softmagic.c中的varexpand函数中完成的:

static int
varexpand(struct magic_set *ms, char *buf, size_t len, const char *str)
{
    [...]
            case 'x':
                    if (ms->mode & 0111) {
                            ptr = t;
                            l = et - t;
                    } else {
                            ptr = e;
                            l = ee - e;
                    }
                    break;

然而,还有一个技巧!在 src/readelf.c 函数 dodynamic 中,如果存在动态部分 (PT_DYNAMIC) 的 DT_FLAGS_1 标志条目,则 st->mode 中的权限将被存在覆盖或缺少 DF_1_PIE 标志:

case DT_FLAGS_1:
        if (xdh_val & DF_1_PIE)
                ms->mode |= 0111;
        else
                ms->mode &= ~0111;
        break;

5.34的bug是初始代码写成:

    if (xdh_val == DF_1_PIE)

这意味着如果设置了另一个标志,w由于 DF_1_NOW,ich GCC 默认执行,可执行文件显示为 shared object

ELF 标准中没有描述 DT_FLAGS_1 flags 条目,因此它必须是 Binutils 扩展。

该标志在 Linux 内核 5.0 或 glibc 2.27 中没有用处,因此我似乎只是提供信息来指示文件是否为 PIE。