【问题标题】:What does call _start in x86?什么在 x86 中调用 _start?
【发布时间】:2024-01-22 21:54:01
【问题描述】:

有一个 c 运行时库,根据 https://en.wikipedia.org/wiki/Crt0 在文件 ctr0.o 中调用以在调用 main 之前初始化变量。我在这里复制了它:

.text
    .globl _start
str : .asciz "abcd\n"
_start:
    xor %ebp, %ebp #basePointer == 0
    mov (%rsp), %edi #argc from stack
    lea 8(%rsp), %rsi #pointer to argv
    lea 16(%rsp,%rdi,8), %rdx #pointer to envp
    xor %eax, %eax
    call main
    mov %eax, %edi
    xor %eax, %eax
    call _exit

main:
    lea str(%rip), %rdi
    call puts

我对实施有一些疑问:

  1. 调用_start 之前堆栈中的内容应该是链接器的唯一条目吗?我之所以问是因为存在诸如mov (%rsp), %edi #argc from stack 之类的表达式,其中_start 正在从堆栈中获取值,但_start 不应该有任何argc(只有main 有)也没有argv 和@987654332 @。所有这些参数都是main 函数的一部分,而不是_start 入口点。那么_start之前的堆栈是什么?

  2. 这应该被设计为提供来自.data.bss 段的变量的初始化,但我在这里看不到它们的这种初始化。它可能与堆栈有关,但我不知道如何。在变量被初始化之前(应该在ctr0.o,这里),保持initial值和链接器为它们保留空间(也来自那个链接)。 gcc 在内存类型的哪个部分为那些未初始化的变量保留空间?

  3. 最后,如何编译这个程序集,没有stdlib,但需要它的一些功能(puts_exit)才能工作?我试过cc -nostdlib foo.s 但是

    /usr/bin/ld: /tmp/ccSKxoPY.o: in function `_start':
    (.text+0x21): undefined reference to `_exit'
    /usr/bin/ld: /tmp/ccSKxoPY.o: in function `main':
    (.text+0x2d): undefined reference to `puts'
    collect2: error: ld returned 1 exit status
    

(不能使用stdlib,否则会有2个_start入口点声明)。

【问题讨论】:

  • 1) 参见例如this article 或 ELF 规范。 TL;DR:操作系统将参数、环境变量和其他东西放在堆栈上 2) .data 只是从文件中映射,.bss 在调用入口点时由加载器分配并归零 3)不要那样做,即使在某些情况下它可能会起作用
  • 不链接libc就不能调用libc函数。 (当然,除非您定义自己的版本。)没有魔法,它们只是共享或静态库中某些指令前面的标签。 (当他们使用syscall 指令调用内核代码时,魔法就会发生。)
  • @PeterCordes 您能否给出完整的答案或详细说明您的评论?我为什么要syscall,我的代码在哪里,它会添加什么,堆栈和syscall 之间的关系是什么?您只是在没有上下文的情况下提出了一些想法,但我想要上下文(完整答案)
  • 关于问题3,如果你解释你想做什么,我们可以给出更好的答案。
  • 也明白 crt0 是一个特定的术语和实现,而不是通用的语言。此外,编译器碰巧使用的调用约定并不一定要反映操作系统在如何传递命令行方面的设计选择。引导带必须符合操作系统,而不是相反。操作系统设计选择他们希望如何传递命令行等。

标签: assembly x86-64 ld glibc


【解决方案1】:

首先,当使用相同的 CPU(例如 x86-64 CPU)时,对于不同的操作系统,您需要不同的crt0.S 文件。

对于未使用操作系统启动的程序(例如操作系统本身),您需要不同的crt0.S

调用_start 之前堆栈中的内容应该是链接器的唯一条目吗?

这取决于操作系统。 Linux 会将argc、参数 (argv[n]) 和环境 (environ[n]) 复制到堆栈的某个位置。

您问题中的文件适用于将argc 置于rsp+0,后跟参数和环境的操作系统。

但是,我记得有一个(32 位)操作系统将argc 放在esp+0x80 而不是esp+0,所以这也是可能的......

据我所知,Windows 不会将任何东西放在堆栈上(至少不是正式的)。对应的crt0.S代码必须调用DLL文件中的函数来获取命令行参数。

对于在 CPU(微控制器)启动后立即启动的设备固件,crt0.S 代码甚至必须首先将堆栈指针设置为有效值。在这种情况下,内存(包括堆栈)通常是完全未初始化的。

不用说,在这种情况下,堆栈不包含任何有用的值。

这应该被设计为提供来自.data的变量的初始化...

对于由操作系统启动的软件,操作系统将初始化.data 部分。这意味着crt0.S 代码不必这样做。

对于微控制器程序(设备固件),crt0.S 代码必须执行此操作。

因为您的文件显然是为操作系统准备的,所以它不会初始化 .data 部分。

最后,如何在没有 stdlib 的情况下编译这个程序集...

如果您想使用问题中的crt0.S 文件,您肯定需要_exit() 函数。

如果你想在代码中使用函数puts(),你还需要这个函数。

如果您不使用标准库,则必须自己编写这些函数:

    ...
main:
    lea str(%rip), %rdi
    call puts
    ret

_exit:
    ...

puts:
    ...

具体实现取决于您使用的操作系统。

puts() 实现起来会有点棘手; write() 会更容易。

注意:

也请不要忘记main()函数末尾的ret; (或者你可以jmpputs() 而不是calling...)

【讨论】:

    【解决方案2】:
    1. 调用_start 之前堆栈中的内容应该是链接器的唯一条目吗?

    这是由系统的 ABI 定义的。我假设您使用的是使用 System V ABI 的 Linux。在这种情况下,堆栈包含argcargv 指针(由 null 终止)、envp 指针(由 null 终止)、辅助向量(由 null 终止),最后指向的值到前面的指针。

    _start 不应该有任何argc(只有main 有)也不应该有argvenvp。所有这些参数都是main 函数的一部分,而不是_start 入口点。

    这是不对的。如果_start 没有得到这些,那么main 还能从哪里得到它们?

    1. 这应该旨在为来自.data.bss 段的变量提供初始化,但我在这里看不到它们的这种初始化。

    内核在将进程映射到内存时会处理这些问题。唯一需要代码来初始化它们的情况就像在 C++ 中一样,如果你有一个变量初始化为不是编译时常量的东西。

    gcc 在内存类型的哪个部分为那些未初始化的变量保留空间?

    这正是.bss 的用途。

    1. 最后,如何编译这个程序集,没有 stdlib,但需要它的一些功能(puts_exit)才能工作?

    如果你想使用 libc 函数,那么你需要使用 libc。正确的方法是根据系统调用自己实现这些功能。对于_exit,这很简单:

    _exit:
            movl    $60, %eax
            syscall
    

    对于puts,它会稍微复杂一些,因为你必须自己做strlen(提示:repnz scasb),在循环中处理调用write 系统调用,并编写一个尾随换行符,但它仍然应该是完全可行的。

    只是为了好玩,您可以尝试使用 -nostartfiles 而不是 -nostdlib 然后调用 libc 函数,但这可能会非常糟糕。自己编写函数绝对是更好的方法。

    【讨论】: