如何在没有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。所以这个代码是错误的!请帮帮忙。
3条答案
按热度按时间cu6pst1q1#
正如注解中所指出的,
argc
和argv
是在堆栈上提供的,所以你不能使用常规的C函数来获取它们,即使是内联汇编,因为编译器将触摸堆栈指针来分配局部变量,设置堆栈框架。因此,_start
必须用汇编语言编写,就像在glibc(x86; x86_64)。可以编写一个小存根,以便根据常规调用约定获取内容并将其转发到“真实的”C入口点。这里是一个程序的最小例子(x86和x86_64),它读取
argc
和argv
,在标准输出(由换行符分隔)上打印argv
中的所有值,并使用argc
作为状态码退出;它可以用通常的gcc -nostdlib
编译(和-static
以确保不涉及ld.so
;并不是说它在这里有任何伤害)。请注意,这里忽略了几个细微之处-特别是
atexit
位。所有关于特定于机器的启动状态的文档都是从上面链接的两个glibc文件的注解中提取的。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,“初始流程堆栈”,在这里:
它接着说,初始寄存器是未指定的,除了
%rsp
,这当然是堆栈指针,和%rdx
,它可能包含“一个函数指针,以atexit注册”。因此,您要查找的所有信息都已经存在于内存中,但它没有按照正常的调用约定进行布局,这意味着您必须用汇编语言编写
_start
。_start
负责根据上述内容设置调用main
的所有内容。最小的_start
看起来像这样:(完全未经测试)
(In如果你想知道为什么没有调整来保持堆栈指针对齐,这是因为在正常的过程调用时,
8(%rsp)
是16字节对齐的,但是当_start
被调用时,%rsp
本身是16字节对齐的。每个call
指令将%rsp
向下移动8个位置,从而产生正常编译函数所期望的对齐情况。)更彻底的
_start
将做更多的事情,例如清除所有其他寄存器,如果需要,安排比默认值更大的堆栈指针对齐,调用C库自己的初始化函数,设置environ
,初始化线程本地存储所使用的状态,对辅助向量进行建设性的操作,等等。您还应该知道,如果有一个动态链接器(可执行文件中的
PT_INTERP
部分),它会在 *_start
之前 * 接收控制。Glibc的ld.so
不能与glibc本身以外的任何C库一起使用;如果您正在编写自己C库,并希望支持动态链接,则还需要编写自己的ld.so
。(是的,这是不幸的;在理想情况下,动态链接器应该是一个独立的开发项目,并指定其完整的接口。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,就在返回地址的上方)。它碰巧/似乎可以将其地址用作数组!这在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缓冲区。_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的代码。
在
-mincoming-stack-boundary=3
中,我们在不需要的地方得到了堆栈重对齐代码。gcc的堆栈重对齐代码非常笨重,所以我们希望避免这种情况。(并不是说你真的会用它来编译一个你关心效率的重要程序,请把这个愚蠢的计算机技巧当作一个学习实验。但无论如何,请查看Godbolt编译器资源管理器上的代码,有
-mpreferred-stack-boundary=3
和没有-mpreferred-stack-boundary=3
。