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

2024-10-10 08:38:00
admin
原创
74
摘要:问题描述:如何在没有 Glibc 的情况下使用 C 中的内联汇编获取参数值?我需要此代码用于Linux建筑x86_64和i386。如果您了解MAC OS X或Windows,也请提交并指导。void exit(int code) { //This function not important! ...

问题描述:

如何在没有 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。所以这个代码是错误的!请帮忙。


解决方案 1:

如注释中所述,argcargv是在堆栈上提供的,因此您不能使用常规 C 函数来获取它们,即使使用内联汇编也是如此,因为编译器将触及堆栈指针来分配局部变量、设置堆栈框架等;因此,必须用汇编语言编写,就像在 glibc ( x86 ; x86_64_start )中一样。可以编写一个小存根来抓取内容并根据常规调用约定将其转发到您的“真实”C 入口点。

这里有一个程序的最小示例(适用于 x86 和 x86_64),它读取argc并打印stdoutargv上的所有值(以换行符分隔)并使用状态码退出;它可以用通常的方法进行编译(并确保不涉及;这不会造成任何伤害)。argv`argcgcc -nostdlib-static`ld.so

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

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

asm(
        "bare_write:
"             // write syscall wrapper; convert the user-mode calling convention to the syscall convention
        "   pushl %ebx
"           // ebx is callee-preserved
        "   movl 8(%esp),%ebx
"    // just move stuff from the stack to the correct registers
        "   movl 12(%esp),%ecx
"
        "   movl 16(%esp),%edx
"
        "   mov $4,%eax
"          // 4 = write syscall on i386
        "   int $0x80
"
        "   popl %ebx
"            // restore ebx
        "   ret
");                // 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, "
", 1);
    }
    return argc;
}

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

解决方案 2:

此答案仅适用于 x86-64、64 位 Linux ABI。提到的所有其他操作系统和 ABI 大致相似,但在细节上却有很大差异,因此您需要_start为每个操作系统和 ABI 编写一次自定义代码。

您要查找的是“ x86-64 psABI ”中的初始进程状态规范,或者,完整标题为“System V 应用程序二进制接口,AMD64 架构处理器补充(带有 LP64 和 ILP32 编程模型)”。我将在此处重现图 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

(完全未经测试。)

(如果您想知道为什么没有调整来保持堆栈指针对齐,这是因为在正常的过程调用中,8(%rsp)是 16 字节对齐的,但是在_start调用时,%rsp它本身是 16 字节对齐的。每条call指令%rsp向下移动 8 位,产生正常编译函数所期望的对齐情况。)

更彻底的操作_start会做更多的事情,比如清除所有其他寄存器、安排比默认值更大的堆栈指针对齐(如果需要)、调用 C 库自己的初始化函数、设置environ、初始化线程本地存储使用的状态、使用辅助向量进行一些建设性的操作等。

您还应该知道,如果存在动态链接器(可执行文件中的部分),它会在执行之前PT_INTERP接收控制。Glibc不能与 glibc 本身以外的任何 C 库一起使用;如果您正在编写自己的 C 库,并且想要支持动态链接,您还需要编写自己的。(是的,这很不幸;理想情况下,动态链接器将是一个单独的开发项目,并且将指定其完整的接口。) _start`ld.so`ld.so

解决方案 3:

作为一种快速而粗略的破解方法,您可以制作一个可执行文件,使用已编译的 C 函数作为 ELF 入口点。只需确保使用exit_exit而不是返回即可。

(使用 链接gcc -nostartfiles省略 CRT 但仍然链接其他库,并_start()在 C 中写入。注意 ABI 违规,如堆栈对齐,例如使用-mincoming-stack-boundary=2__attribte__on _start,如在没有 libc 的情况下编译时一样)

如果它是动态链接的,您仍然可以在 Linux 上使用 glibc 函数(因为动态链接器运行 glibc 的 init 函数)。并非所有系统都是这样,例如在 cygwin 上,如果您(或 CRT 启动代码)没有按正确的顺序调用 libc init 函数,您肯定无法调用 libc 函数。我不确定这是否保证在 Linux 上有效,所以除了在您自己的系统上进行实验外,不要依赖它。

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

_exit()即使 glibc 未初始化,Glibc 也能工作( gcc -O3 -static),或者使用内联 asm 来运行// xor %edi,%edi(Linux 上为 sys_exit(0)),这样您甚至不必静态链接 libc。(mov $60, %eaxsyscall`gcc -O3 -nostdlib`


通过更加肮脏的黑客攻击和 UB,您可以通过了解您正在编译的 x86-64 System V ABI 来访问 argc 和 argv(有关 ABI 文档的引用,请参阅@zwol 的回答),以及进程启动状态与函数调用约定的不同之处:

  • argc是普通函数的返回地址(由 RSP 指向)。GNU C 有一个内置函数,用于访问当前函数的返回地址(或用于遍历堆栈)。

  • argv[0]是第 7 个整数/指针参数应该在的位置(第一个堆栈参数,位于返回地址上方)。它恰好/似乎可以取其地址并将其用作数组!

// 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
", argc, argv[argc-1]);

    printf("%f
", 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复制参数的位置下方,而不是其原始位置。但显然它可以运行。

gcc -nostartfiles链接 glibc 但不链接CRT 启动文件。

gcc -nostdlib省略了库和 CRT 启动文件。

这些代码很少能保证能正常工作,但实际上它确实可以在当前 x86-64 Linux 上与当前的 gcc 配合使用,并且在过去多年中一直有效。如果它出现故障,您可以保留这两个部分。 我不知道省略 CRT 启动代码并仅依靠动态链接器来运行 glibc init 函数会破坏哪些 C 特性。此外,获取参数的地址并访问其上方的指针是 UB,因此您可能会得到损坏的代码生成。在这种情况下,gcc7.3 恰好可以完成您所期望的事情。

必定会损坏的东西

  • atexit()清理,例如刷新 stdio 缓冲区。

  • 动态链接库中静态对象的静态析构函数。(进入时_start,RDX 是一个函数指针,因此您应该向 atexit 注册。在动态链接的可执行文件中,动态链接器在您的之前运行_start,并在跳转到您的之前设置 RDX _start。在 Linux 下,静态链接的可执行文件的 RDX=0。)


gcc -mincoming-stack-boundary=3(即 2^3 = 8 字节)是让 gcc 重新对齐堆栈的另一种方法,因为-mpreferred-stack-boundary=4默认值 2^4 = 16 仍然有效。但是,这使得 gcc 假设所有函数(而不仅仅是)的 RSP 都未对齐,_start这就是为什么我在文档中查找ESP并找到一个用于 32 位的属性,当 ABI 从仅要求 4 字节堆栈对齐过渡到32 位模式下的当前 16 字节对齐要求时。

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用