如何在没有 Glibc 的情况下在 C 中使用内联程序集获取参数值



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

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

这里有一个程序的最小示例(对于 x86 和 x86_64),它读取argcargv,在 stdout 上打印argv中的所有值(用换行符分隔)并使用argc作为状态代码退出;它可以使用通常的gcc -nostdlib(和-static进行编译,以确保不涉及ld.so;并不是说它在这里有任何伤害)。

#ifdef __x86_64__
asm(
".global _startn"
"_start:n"
"   xorl %ebp,%ebpn"       // mark outermost stack frame
"   movq 0(%rsp),%rdin"    // get argc
"   lea 8(%rsp),%rsin"     // the arguments are pushed just below, so argv = %rbp + 8
"   call bare_mainn"       // call our bare_main
"   movq %rax,%rdin"       // take the main return code and use it as first argument for...
"   movl $60,%eaxn"        // ... the exit syscall
"   syscalln"
"   int3n");               // just in case
asm(
"bare_write:n"             // write syscall wrapper; the calling convention is pretty much ok as is
"   movq $1,%raxn"         // 1 = write syscall on x86_64
"   syscalln"
"   retn");
#endif
#ifdef __i386__
asm(
".global _startn"
"_start:n"
"   xorl %ebp,%ebpn"       // mark outermost stack frame
"   movl 0(%esp),%edin"    // argc is on the top of the stack
"   lea 4(%esp),%esin"     // as above, but with 4-byte pointers
"   sub $8,%espn"          // the start starts 16-byte aligned, we have to push 2*4 bytes; "waste" 8 bytes
"   pushl %esin"           // to keep it aligned after pushing our arguments
"   pushl %edin"
"   call bare_mainn"       // call our bare_main
"   add $8,%espn"          // fix the stack after call (actually useless here)
"   movl %eax,%ebxn"       // take the main return code and use it as first argument for...
"   movl $1,%eaxn"         // ... the exit syscall
"   int $0x80n"
"   int3n");               // just in case
asm(
"bare_write:n"             // write syscall wrapper; convert the user-mode calling convention to the syscall convention
"   pushl %ebxn"           // ebx is callee-preserved
"   movl 8(%esp),%ebxn"    // just move stuff from the stack to the correct registers
"   movl 12(%esp),%ecxn"
"   movl 16(%esp),%edxn"
"   mov $4,%eaxn"          // 4 = write syscall on i386
"   int $0x80n"
"   popl %ebxn"            // restore ebx
"   retn");                // 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 文件中的注释中提取。

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

您正在"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移八,产生正常编译函数所期望的对齐情况。

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

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

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

(与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),或者使用内联 asm 运行xor %edi,%edi/mov $60, %eax/syscall(sys_exit(0) 在 Linux 上),Glibc 的_exit()也可以工作,因此您甚至不必静态链接 libc。(gcc -O3 -nostdlib)


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

  • argc是正常函数的返回地址(由 RSP 指向)的位置。 GNU C 有一个内置的函数,用于访问当前函数的返回地址(或用于在堆栈中向上移动)。
  • argv[0]是第 7 个整数/指针 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] = %sn", argc, argv[argc-1]);
printf("%fn", 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的地址会低于它复制 arg 的rbp,而不是它的原始位置。 但显然它有效。

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

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

这很少能保证有效,但它实际上确实适用于当前 x86-64 Linux 上的当前 gcc,并且过去已经工作了多年。 如果它坏了,你可以保留两块。通过省略 CRT 启动代码并仅依靠动态链接器来运行 glibc 初始化函数,IDK 会破坏哪些 C 功能。 此外,获取 arg 的地址并访问其上方的指针是 UB,因此您可能会损坏代码生成。 在这种情况下,GCC7.3 恰好可以执行您所期望的操作。

肯定会打破的东西

  • atexit()清理,例如刷新 stdio 缓冲区。
  • 动态链接库中静态对象的静态析构函数。 (在进入_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,这就是为什么我在文档中查看并找到一个用于 32 位的属性,当 ABI 从仅要求 4 字节堆栈对齐过渡到当前要求 16 位对齐时,ESP在 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 的堆栈重新对齐代码非常笨拙,因此我们希望避免这种情况。 (并不是说你真的会用它来编译一个你关心效率的重要程序,请只使用这个愚蠢的计算机技巧作为学习实验。

但无论如何,请参阅带有和不带-mpreferred-stack-boundary=3的 Godbolt 编译器资源管理器上的代码。

最新更新