【问题标题】:Build static ELF without libc using unistd.h from Linux headers使用 Linux 头文件中的 unistd.h 构建没有 libc 的静态 ELF
【发布时间】:2013-01-02 16:32:31
【问题描述】:

我有兴趣使用 Linux 头文件提供的 unistd.h 构建一个没有 (g)libc 的静态 ELF 程序。

我已经阅读了这些文章/问题,这些文章/问题大致了解了我正在尝试做的事情,但并不完全是: http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html

Compiling without libc

https://blogs.oracle.com/ksplice/entry/hello_from_a_libc_free

我有只依赖于 unistd.h 的基本代码,我的理解是这些函数中的每一个都是由内核提供的,应该不需要 libc。以下是我采取的似乎最有希望的道路:

    $ gcc -I /usr/include/asm/ -nostdlib grabbytes.c -o grabbytesstatic
    /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400144
    /tmp/ccn1mSkn.o: In function `main':
    grabbytes.c:(.text+0x38): undefined reference to `open'
    grabbytes.c:(.text+0x64): undefined reference to `lseek'
    grabbytes.c:(.text+0x8f): undefined reference to `lseek'
    grabbytes.c:(.text+0xaa): undefined reference to `read'
    grabbytes.c:(.text+0xc5): undefined reference to `write'
    grabbytes.c:(.text+0xe0): undefined reference to `read'
    collect2: error: ld returned 1 exit status

在此之前,我必须根据内核头文件中的值手动定义 SEEK_END 和 SEEK_SET。否则会出错,说那些没有定义,这是有道理的。

我想我需要链接到一个未剥离的 vmlinux 以提供要使用的符号。然而,我通读了这些符号,虽然有很多 llseek,但它们并不是 llseek 逐字记录的。

所以我的问题可以分为几个方向:

如何指定一个 ELF 文件来使用其中的符号?而且我猜测是否/如何可能,符号将不匹配。如果这是正确的,是否有一个现有的头文件将重新定义 llseek 和 default_llseek 或内核中的任何内容?

有没有更好的方法在没有 libc 的情况下用 C 编写 Posix 代码?

我的目标是使用(可能仅)unistd.h 编写或移植相当标准的 C 代码,并在没有 libc 的情况下调用它。如果没有一些 unistd 函数,我可能没问题,并且不确定哪些函数“纯粹”作为内核调用存在。我喜欢组装,但这不是我的目标。希望尽可能严格地保持 C(如果必须的话,我可以使用一些外部程序集文件),以便在某些时候允许使用 libc-less 静态系统。

感谢您的阅读!

【问题讨论】:

  • 我最初以为您想从用户空间使用这个静态二进制文件(在这种情况下,答案是如果您想使用系统调用,则需要系统调用包装器,无论是来自 libc 还是自己编写)。但是后来你提到链接到(未剥离的)内核,所以我猜你希望直接在裸机上运行这段代码(即,而不是内核)。请澄清您在这一点上的问题。
  • 感谢您的回复!我的意思是使用内核作为符号表引用进行链接,并在 Linux 主机的用户空间中运行它。我将搜索现有的系统调用包装器,看看是否有一个与我正在尝试做的类似。
  • 好的,如果你打算在用户空间运行它,那么你不能链接到内核(如果成功,这会将这些系统调用的内核实现拉到你的代码中,即不是你想要的:你想调用内核)。您必须自己实现open()read(),方法是调用内核ABI 指定的正确操作,这通常涉及设置寄存器然后执行某种CPU 陷阱指令。问题是这方面的细节是特定于架构的(ARM 与 x86 等),并且由于诸如 vsyscall 之类的东西而变得复杂。
  • 我看不出这样做的意义。使用 libc - 如果您不使用复杂的函数,则不会有太多开销 - 使用 -static,您将获得一个仅包含所需函数的二进制文件。不使用libc的目的到底是什么?请注意,如果没有某种系统调用包装,您不能从用户模式调用内核 - 因为您确实需要适当的调用方法才能从用户模式转换到内核模式 - 这不能在纯 C 中完成,需要写入适合处理器的汇编器[如果内核发生变化,可能会发生变化]。
  • 我猜理想的场景是内联汇编头文件。我找到了这个question,它产生的结果几乎是我想要的,但我无法让 argc/argv 与 void _start() 一起工作。 @MatsPetersson:glibc 有很多开销,即使只使用 unistd.h,它也会产生 800KB 或更大的文件。据我所知,我的代码中的所有内容都只是系统调用,所以我不明白为什么我不能简单地让 gcc 生成代码,直接通过 Linux 头文件调用这些代码。

标签: linux gcc posix elf libc


【解决方案1】:

如果您希望用 C 编写 POSIX 代码,那么放弃 libc 不会有帮助。尽管您可以在汇编程序中实现 syscall 函数,并从内核头文件中复制结构和定义,但您实际上是在编写自己的 libc,这几乎肯定不符合 POSIX 标准。有了所有伟大的 libc 实现,几乎没有理由开始实现自己的。

dietlibcmusl libc 都是节俭的 libc 实现,可以产生非常小的二进制文件。只要编写了一个库以避免意外引入大量依赖项,那么只有您使用的函数才会真正链接到您的程序中。

这是一个简单的hello world程序:

#include<unistd.h>

int main(){
    char str[] = "Hello, World!\n";
    write(1, str, sizeof str - 1);
    return 0;
}

用下面的 musl 编译它会产生一个小于 3K 的二进制文件

$ musl-gcc -Os -static hello.c
$ strip a.out 
$ wc -c a.out
2800 a.out

dietlibc 生成的二进制文件更小,不到 1.5K:

$ diet -Os gcc hello.c
$ strip a.out 
$ wc -c a.out
1360 a.out

【讨论】:

    【解决方案2】:

    这远非理想,但一点点 (x86_64) 汇编程序让我的大小低于 5KB(但其中大部分是“代码以外的东西”——实际代码低于 1KB [准确地说是 771 字节) ],但文件大小要大得多,我认为是因为代码大小四舍五入到 4KB,然后添加了一些页眉/页脚/额外的东西]

    这是我所做的: gcc -g -static -nostdlib -o glibc start.s glibc.c -Os -lc

    glibc.c 包含:

    #include <unistd.h>
    
    int main()
    {
        const char str[] = "Hello, World!\n";
        write(1, str, sizeof(str));
    
        _exit(0);
    }
    

    start.s 包含:

        .globl _start
    _start: 
        xor %ebp, %ebp
        mov %rdx, %r9
        mov %rsp, %rdx
        and $~16, %rsp
        push    $0
        push    %rsp
    
        call    main
    
        hlt
    
    
        .globl _exit
    _exit:
        //  We known %RDI already has the exit code... 
        mov $0x3c, %eax
        syscall
        hlt
    

    这不是要表明占用大量空间的不是 glibc 的系统调用部分,而是“准备工作” - 请注意,如果您要调用例如 printf,甚至可能(v)sprintf 或 exit() 或任何其他“标准库”函数,您处于“没人知道会发生什么”的境地。

    编辑:更新“start.s”以将 argc/argv 放在正确的位置:

    _start: 
        xor %ebp, %ebp
        mov %rdx, %r9
        pop     %rdi
        mov %rsp, %rsi
        and $~16, %rsp
        push    %rax
        push    %rsp
    
        // %rdi = argc, %rsi=argv
        call    main
    

    请注意,我已经更改了哪个寄存器包含什么东西,以便它与 main 匹配 - 我在前面的代码中让它们的顺序略有错误。

    【讨论】:

    • 谢谢!您的解决方案与one 非常接近。我可以确认这对我有用,但是 argc/argv 传递不起作用。您是否知道我可以通过在 start.s 程序集部分获得 argc/argv 支持的好资源?我不熟悉 argc/argv 的具体工作原理。
    • 我不知道,但我相当确定 argc/argv 是从内核在寄存器中传递的。我会挖掘的。
    • 我在一个新的“_start”函数中进行了编辑。不要问我你是怎么做“环境”的,我不确定这是否容易。
    • 非常感谢您的帮助,马茨!这完美无缺。有趣的是,如果在不存在的文件上调用 open() 会出现段错误(动态版本不会),但这是另一天的任务。
    • glibc 很可能正在使用一些未初始化的变量,或者在这种情况下使用一些类似的变量。就像我想说的那样,这不是我真正建议做的事情......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-10
    • 1970-01-01
    • 1970-01-01
    • 2019-03-31
    • 1970-01-01
    • 2014-01-17
    • 1970-01-01
    相关资源
    最近更新 更多