如何在没有Glibc或CRT启动文件的情况下,在C中使用内联汇编获取_start中的参数值并调用main()?

h9a6wy2h  于 2023-10-16  发布在  其他
关注(0)|答案(3)|浏览(96)

如何在没有Glibc的C中使用内联汇编获取参数值?
我需要这个代码为Linux架构x86_64i386。如果你知道MAC OS XWindows,也提交并请指导。

void exit(int code)
{
    //This function not important!
    //...
}
void _start()
{
    //How Get arguments value using inline assembly
    //in C without Glibc?
    //argc
    //argv
    exit(0);
}

新更新

https://gist.github.com/apsun/deccca33244471c1849d29cc6bb5c78e

#define ReadRdi(To) asm("movq %%rdi,%0" : "=r"(To));
#define ReadRsi(To) asm("movq %%rsi,%0" : "=r"(To));
long argcL;
long argvL;
ReadRdi(argcL);
ReadRsi(argvL);
int argc = (int) argcL;
//char **argv = (char **) argvL;
exit(argc);

但它仍然返回0。所以这个代码是错误的!请帮帮忙。

cu6pst1q

cu6pst1q1#

正如注解中所指出的,argcargv是在堆栈上提供的,所以你不能使用常规的C函数来获取它们,即使是内联汇编,因为编译器将触摸堆栈指针来分配局部变量,设置堆栈框架。因此,_start必须用汇编语言编写,就像在glibc(x86; x86_64)。可以编写一个小存根,以便根据常规调用约定获取内容并将其转发到“真实的”C入口点。
这里是一个程序的最小例子(x86和x86_64),它读取argcargv,在标准输出(由换行符分隔)上打印argv中的所有值,并使用argc作为状态码退出;它可以用通常的gcc -nostdlib编译(和-static以确保不涉及ld.so;并不是说它在这里有任何伤害)。

#ifdef __x86_64__
asm(
        ".global _start\n"
        "_start:\n"
        "   xorl %ebp,%ebp\n"       // mark outermost stack frame
        "   movq 0(%rsp),%rdi\n"    // get argc
        "   lea 8(%rsp),%rsi\n"     // the arguments are pushed just below, so argv = %rbp + 8
        "   call bare_main\n"       // call our bare_main
        "   movq %rax,%rdi\n"       // take the main return code and use it as first argument for...
        "   movl $60,%eax\n"        // ... the exit syscall
        "   syscall\n"
        "   int3\n");               // just in case

asm(
        "bare_write:\n"             // write syscall wrapper; the calling convention is pretty much ok as is
        "   movq $1,%rax\n"         // 1 = write syscall on x86_64
        "   syscall\n"
        "   ret\n");
#endif
#ifdef __i386__
asm(
        ".global _start\n"
        "_start:\n"
        "   xorl %ebp,%ebp\n"       // mark outermost stack frame
        "   movl 0(%esp),%edi\n"    // argc is on the top of the stack
        "   lea 4(%esp),%esi\n"     // as above, but with 4-byte pointers
        "   sub $8,%esp\n"          // the start starts 16-byte aligned, we have to push 2*4 bytes; "waste" 8 bytes
        "   pushl %esi\n"           // to keep it aligned after pushing our arguments
        "   pushl %edi\n"
        "   call bare_main\n"       // call our bare_main
        "   add $8,%esp\n"          // fix the stack after call (actually useless here)
        "   movl %eax,%ebx\n"       // take the main return code and use it as first argument for...
        "   movl $1,%eax\n"         // ... the exit syscall
        "   int $0x80\n"
        "   int3\n");               // just in case

asm(
        "bare_write:\n"             // write syscall wrapper; convert the user-mode calling convention to the syscall convention
        "   pushl %ebx\n"           // ebx is callee-preserved
        "   movl 8(%esp),%ebx\n"    // just move stuff from the stack to the correct registers
        "   movl 12(%esp),%ecx\n"
        "   movl 16(%esp),%edx\n"
        "   mov $4,%eax\n"          // 4 = write syscall on i386
        "   int $0x80\n"
        "   popl %ebx\n"            // restore ebx
        "   ret\n");                // notice: the return value is already ok in %eax
#endif

int bare_write(int fd, const void *buf, unsigned count);

unsigned my_strlen(const char *ch) {
    const char *ptr;
    for(ptr = ch; *ptr; ++ptr);
    return ptr-ch;
}

int bare_main(int argc, char *argv[]) {
    for(int i = 0; i < argc; ++i) {
        int len = my_strlen(argv[i]);
        bare_write(1, argv[i], len);
        bare_write(1, "\n", 1);
    }
    return argc;
}

请注意,这里忽略了几个细微之处-特别是atexit位。所有关于特定于机器的启动状态的文档都是从上面链接的两个glibc文件的注解中提取的。

pbpqsu0x

pbpqsu0x2#

此答案仅适用于x86-64、64位Linux ABI。提到的所有其他OS和ABI都大致相似,但在细节上有很大的不同,您需要为每个OS和ABI编写一次自定义_start
您正在“x86-64 psABI“中查找 * 初始进程状态 * 的规范,或者给予它的完整标题“System V Application Binary Interface,AMD 64 Architecture Processor Supplement(With LP 64 and ILP 32 Programming Models)"。我将重现图3.9,“初始流程堆栈”,在这里:

Purpose                            Start Address                  Length
------------------------------------------------------------------------
Information block, including                                      varies
argument strings, environment
strings, auxiliary information
...
------------------------------------------------------------------------
Null auxiliary vector entry                                  1 eightbyte
Auxiliary vector entries...                            2 eightbytes each
0                                                              eightbyte
Environment pointers...                                 1 eightbyte each
0                                  8+8*argc+%rsp               eightbyte
Argument pointers...               8+%rsp                argc eightbytes
Argument count                     %rsp                        eightbyte

它接着说,初始寄存器是未指定的,除了%rsp,这当然是堆栈指针,和%rdx,它可能包含“一个函数指针,以atexit注册”。
因此,您要查找的所有信息都已经存在于内存中,但它没有按照正常的调用约定进行布局,这意味着您必须用汇编语言编写_start_start负责根据上述内容设置调用main的所有内容。最小的_start看起来像这样:

_start:
        xorl   %ebp, %ebp       #  mark the deepest stack frame

  # Current Linux doesn't pass an atexit function,
  # so you could leave out this part of what the ABI doc says you should do
  # You can't just keep the function pointer in a call-preserved register
  # and call it manually, even if you know the program won't call exit
  # directly, because atexit functions must be called in reverse order
  # of registration; this one, if it exists, is meant to be called last.
        testq  %rdx, %rdx       #  is there "a function pointer to
        je     skip_atexit      #  register with atexit"?

        movq   %rdx, %rdi       #  if so, do it
        call   atexit

skip_atexit:
        movq   (%rsp), %rdi           #  load argc
        leaq   8(%rsp), %rsi          #  calc argv (pointer to the array on the stack)
        leaq   8(%rsp,%rdi,8), %rdx   #  calc envp (starts after the NULL terminator for argv[])
        call   main

        movl   %eax, %edi   # pass return value of main to exit
        call   exit

        hlt                 # should never get here

(完全未经测试)
(In如果你想知道为什么没有调整来保持堆栈指针对齐,这是因为在正常的过程调用时,8(%rsp)是16字节对齐的,但是当_start被调用时,%rsp本身是16字节对齐的。每个call指令将%rsp向下移动8个位置,从而产生正常编译函数所期望的对齐情况。)
更彻底的_start将做更多的事情,例如清除所有其他寄存器,如果需要,安排比默认值更大的堆栈指针对齐,调用C库自己的初始化函数,设置environ,初始化线程本地存储所使用的状态,对辅助向量进行建设性的操作,等等。
您还应该知道,如果有一个动态链接器(可执行文件中的PT_INTERP部分),它会在 * _start之前 * 接收控制。Glibc的ld.so不能与glibc本身以外的任何C库一起使用;如果您正在编写自己C库,并希望支持动态链接,则还需要编写自己的ld.so。(是的,这是不幸的;在理想情况下,动态链接器应该是一个独立的开发项目,并指定其完整的接口。

eeq64g8w

eeq64g8w3#

作为一个快速和肮脏的黑客,你可以 * 使一个可执行文件与编译的C函数作为ELF入口点。只要确保使用exit_exit而不是返回即可。

(Link使用gcc -nostartfiles省略CRT,但仍然链接其他库,并在C中编写_start()。小心ABI违规,如堆栈对齐,例如在_start上使用-mincoming-stack-boundary=2__attribte__,如在不使用libc的情况下编译)
如果它是动态链接的,您仍然可以在Linux上使用glibc函数(因为动态链接器运行glibc的init函数)。不是所有的系统都是这样的,例如。在cygwin上,如果你(或CRT启动代码)没有按照正确的顺序调用libc init函数,你肯定不能调用libc函数。我甚至不能保证这在Linux上有效,所以除了在您自己的系统上进行实验之外,不要依赖它。

我使用了一个C _start(void){ ... }+调用_exit()来创建一个静态可执行文件,以微基准测试一些编译器生成的代码,同时减少perf stat ./a.out的启动开销

即使glibc没有初始化(gcc -O3 -static),Glibc的_exit()也可以工作,或者使用内联asm运行xor %edi,%edi/mov $60, %eax/syscall(Linux上的sys_exit(0)),这样您甚至不必静态链接libc。(gcc -O3 -nostdlib

使用更脏的hacking和UB,你可以通过了解你正在编译的x86-64 System V ABI来访问argc和argv(参见@zwol对ABI文档的回答),以及进程启动状态如何从函数调用约定中调用:
*argc是正常函数的返回地址(由RSP指向)。当前函数的GNU C has a builtin for accessing the return address(或用于遍历堆栈)。
*argv[0]是第七个整数/指针arg应该在的地方(第一个堆栈arg,就在返回地址的上方)。它碰巧/似乎可以将其地址用作数组!

// Works only for the x86-64 SystemV ABI; only tested on Linux.
// DO NOT USE THIS EXCEPT FOR EXPERIMENTS ON YOUR OWN COMPUTER.

#include <stdio.h>
#include <stdlib.h>

// tell gcc *this* function is called with a misaligned RSP
__attribute__((force_align_arg_pointer))
void _start(int dummy1, int dummy2, int dummy3, int dummy4, int dummy5, int dummy6, // register args
        char *argv0) {

    int argc = (int)(long)__builtin_return_address(0);  // load (%rsp), casts to silence gcc warnings.
    char **argv = &argv0;

    printf("argc = %d, argv[argc-1] = %s\n", argc, argv[argc-1]);

    printf("%f\n", 1.234);  // segfaults if RSP is misaligned
    exit(0);
    //_exit(0);  // without flushing stdio buffers!
}
# with a version without the FP printf
peter@volta:~/src/SO$ gcc -nostartfiles _start.c -o bare_start 
peter@volta:~/src/SO$ ./bare_start 
argc = 1, argv[argc-1] = ./bare_start
peter@volta:~/src/SO$ ./bare_start abc def hij
argc = 4, argv[argc-1] = hij
peter@volta:~/src/SO$ file bare_start
bare_start: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=af27c8416b31bb74628ef9eec51a8fc84e49550c, not stripped
 # I could have used  -fno-pie -no-pie to make a non-PIE executable

这在gcc7.3中可以使用,无论有没有优化。我担心如果没有优化,argv0的地址将低于rbp,在那里它复制了arg,而不是它的原始位置。但很明显这很有效。
gcc -nostartfiles链接glibc,但 * 不 * CRT启动文件。
gcc -nostdlib省略了库和CRT启动文件。

这是保证 * 工作的很少,但它在实践中与当前x86-64 Linux上的当前gcc一起工作,并且在过去几年中一直工作。**IDK通过省略CRT启动代码并仅依赖动态链接器来运行glibc init函数而破坏了哪些C功能。此外,获取arg的地址并访问其上方的指针是UB,因此您可能会得到损坏的code-gen。gcc7.3恰好做了你在这种情况下所期望的事情。
肯定会破碎的东西

  • atexit()清理,例如刷新stdio缓冲区。
  • static destructors用于动态链接库中的静态对象。(在进入_start时,RDX是一个函数指针,因此您应该向atexit注册。在动态链接的可执行文件中,动态链接器在_start之前运行,并在跳转到_start之前设置RDX。静态链接的可执行文件在Linux下的RDX=0。)

gcc -mincoming-stack-boundary=3(即2^3 = 8字节)是另一种让gcc重新对齐堆栈的方法,因为-mpreferred-stack-boundary=4默认值2^4 = 16仍然存在。但是这使得gcc假设所有 * 函数的RSP都是欠对齐的,而不仅仅是_start,这就是为什么当ABI从只需要4字节堆栈对齐转换到32位模式下ESP的16字节对齐时,我looked in the docs发现了一个用于32位的属性。
64位模式的SysV ABI要求一直是16字节对齐,但gcc选项允许您编写不遵循ABI的代码。

// test call to a function the compiler can't inline
// to see if gcc emits extra code to re-align the stack

// like it would if we'd used -mincoming-stack-boundary=3 to assume *all* functions
// have only 8-byte (2^3) aligned RSP on entry, with the default -mpreferred-stack-boundary=4
void foo() {
    int i = 0;
    atoi(NULL);
}

-mincoming-stack-boundary=3中,我们在不需要的地方得到了堆栈重对齐代码。gcc的堆栈重对齐代码非常笨重,所以我们希望避免这种情况。(并不是说你真的会用它来编译一个你关心效率的重要程序,请把这个愚蠢的计算机技巧当作一个学习实验。
但无论如何,请查看Godbolt编译器资源管理器上的代码,有-mpreferred-stack-boundary=3和没有-mpreferred-stack-boundary=3

相关问题