如何在没有 Glibc 的情况下在 C 中使用内联程序集获取参数值?
我需要此代码来Linux
原型x86_64
和i386
。 如果您了解MAC OS X
或Windows
,也提交并请指导。
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。 所以这个代码是错误的! 请帮忙。
如注释中所述,堆栈上提供了argc
和argv
,因此即使使用内联汇编,您也不能使用常规 C 函数来获取它们,因为编译器将触摸堆栈指针来分配局部变量,设置堆栈帧等; 因此,_start
必须用汇编编写, 就像在glibc(x86;x86_64)中所做的那样。可以编写一个小存根来抓取内容并根据常规调用约定将其转发到您的"真实"C 入口点。
这里有一个程序的最小示例(对于 x86 和 x86_64),它读取argc
和argv
,在 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 编译器资源管理器上的代码。