如何在没有 Glibc 或 CRT 启动文件的情况下,使用 C 中的内联汇编获取 _start 中的参数值并调用 main()?
- 2024-10-10 08:38:00
- admin 原创
- 73
问题描述:
如何在没有 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。所以这个代码是错误的!请帮忙。
解决方案 1:
如注释中所述,argc
和argv
是在堆栈上提供的,因此您不能使用常规 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, %eax
)syscall
`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
。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件