使用Linux头文件中的unistd.h构建不带libc的静态ELF

12

我想构建一个静态的ELF程序,而不使用(g)libc,而是使用Linux头文件提供的unistd.h。

我已经阅读了这些文章/问题,大致了解了我正在尝试做什么,但还不够清楚:

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或与内核中实际情况完全相同的内容?

是否有更好的方法在C中编写没有libc的Posix代码?

我的目标是编写或移植相当标准的C代码,只使用(也许仅限于)unistd.h,并在没有libc的情况下调用它。我可能不需要一些unistd函数,而且不确定哪些函数“纯粹”作为内核调用存在。我喜欢汇编语言,但这不是我的目标。希望尽可能严格地保持C(如果必须,我可以使用一些外部汇编文件),以便在某个时候实现一个没有libc的静态系统。

谢谢您的阅读!


我最初认为你想要从用户空间使用这个静态二进制文件(如果是这样,那么答案是:如果你想使用系统调用,则需要系统调用包装器,可以从libc中获取,或者编写自己的包装器)。但是后来你提到了链接到(未剥离)内核,所以我猜你希望直接在裸机上运行此代码(而不是在内核中)。请澄清一下你对这一点的问题。 - Celada
谢谢回复!我的意思是使用内核作为符号表引用进行链接,并在Linux主机的用户空间中运行它。我将搜索现有的系统调用包装器,看看是否有类似于我尝试做的事情。 - sega01
1
我不明白为什么要这样做。使用libc - 如果您没有使用复杂的函数,则几乎没有开销 - 使用-static,您将获得仅包含所需功能的二进制文件。不使用libc的目的是什么?请注意,如果您需要从用户模式调用内核,则无法进行某种系统调用包装 - 因为您确实需要适当的调用方法才能从用户模式转换到内核模式 - 这不能在纯C中完成,需要使用适当处理器的汇编语言编写[并且如果内核更改,则可能会发生变化]。 - Mats Petersson
我猜理想的情况是内联汇编头文件。我找到了这个问题,它产生了一个几乎符合我的要求的结果,但我无法让argc/argv与void _start()一起工作。@MatsPetersson:glibc有很多开销,即使只使用unistd.h也会导致800KB或更大的文件。据我所知,我的代码中的所有内容都只是系统调用,所以我不明白为什么不能简单地让gcc通过Linux头文件直接调用它们生成代码。 - sega01
如果你从链接glibc中得到了800kb,那么可能会拖入一些不必要的东西,或者是你在做某些错误的事情。 - Mats Petersson
显示剩余2条评论
2个回答

6
如果你想用C语言编写POSIX代码,放弃libc是没有帮助的。虽然你可以在汇编中实现syscall函数,并从内核头文件中复制结构和定义,但你实际上会编写自己的libc,几乎肯定不符合POSIX标准。由于有许多出色的libc实现,几乎没有理由开始实现自己的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

4
这远非理想,但是一点点(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

请注意,我已更改了哪个寄存器包含什么内容,以使其与主要的匹配 - 在先前的代码中,它们的顺序稍有不同。

谢谢!你的解决方案与此方案非常接近。我可以确认这在我的环境中有效,但是 argc/argv 传递不起作用。你知道一个好的资源,供我查看如何在 start.s 汇编部分支持 argc/argv 吗?我对 argc/argv 的工作原理不太熟悉。 - sega01
我已经编辑了一个新的"_start"函数。不要问我如何使用"environ",因为我不确定这是否那么容易。 - Mats Petersson
非常感谢您的帮助,Mats!那个完美地运行了。有趣的是,如果在不存在的文件上调用open()函数(动态版本不会),它会导致段错误,但这是另一天的任务。 - sega01
我做了,但我也阅读了你使用的手语来证明5K文件大小的合理性。dietlibc文件经过正确初始化和未剥离后只有2.7K。我发现看到glibc膨胀实际上是从启动开始的部分很有趣。我认为你从一开始就应该更明确地表述,而且应该使用“-nostartfiles”而不是“-nostdlib -lc”。 - Dave
envp数组在argv数组之后开始。一旦你已经将argc和argv加载到rdi和rsi中,“lea 8(%rsi,%rdi,8),%rdx”应该将envp加载到rdx中。[示例](https://github.com/eloj/nolibc-example) - eloj
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接