C P*_*nce 2 c assembly x86-64 i386
如何使用没有Glibc的C中的内联汇编来获取参数值?
我需要这个代码用于Linuxarchecture 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);
}
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
但它仍然返回0.所以这段代码错了!请帮忙.
正如注释中所指定的那样,argc并且argv在堆栈上提供,因此您无法使用常规C函数来获取它们,即使使用内联汇编,因为编译器将触摸堆栈指针以分配局部变量,设置堆栈框架和co .因此,_start必须用汇编语言编写,因为它是在glibc(x86 ; x86_64)中完成的.根据常规调用约定,可以编写一个小存根来抓取内容并将其转发到"真正的"C入口点.
这里的程序的小例子(均为86和x86_64)读取argc和argv,打印在所有的值argv在stdout(由换行分隔),并用退出argc作为状态码; 它可以用通常编译gcc -nostdlib(并-static确保ld.so不涉及;不是它在这里有任何伤害).
#ifdef __x86_64__
asm(
".global _start\n"
"_start:\n"
" xorl %ebp,%ebp\n" // mark outermost stack frame
" movq 0(%rsp),%rdi\n" // get argc
" lea 8(%rsp),%rsi\n" // the arguments are pushed just below, so argv = %rbp + 8
" call bare_main\n" // call our bare_main
" movq %rax,%rdi\n" // take the main return code and use it as first argument for...
" movl $60,%eax\n" // ... the exit syscall
" syscall\n"
" int3\n"); // just in case
asm(
"bare_write:\n" // write syscall wrapper; the calling convention is pretty much ok as is
" movq $1,%rax\n" // 1 = write syscall on x86_64
" syscall\n"
" ret\n");
#endif
#ifdef __i386__
asm(
".global _start\n"
"_start:\n"
" xorl %ebp,%ebp\n" // mark outermost stack frame
" movl 0(%esp),%edi\n" // argc is on the top of the stack
" lea 4(%esp),%esi\n" // as above, but with 4-byte pointers
" sub $8,%esp\n" // the start starts 16-byte aligned, we have to push 2*4 bytes; "waste" 8 bytes
" pushl %esi\n" // to keep it aligned after pushing our arguments
" pushl %edi\n"
" call bare_main\n" // call our bare_main
" add $8,%esp\n" // fix the stack after call (actually useless here)
" movl %eax,%ebx\n" // take the main return code and use it as first argument for...
" movl $1,%eax\n" // ... the exit syscall
" int $0x80\n"
" int3\n"); // just in case
asm(
"bare_write:\n" // write syscall wrapper; convert the user-mode calling convention to the syscall convention
" pushl %ebx\n" // ebx is callee-preserved
" movl 8(%esp),%ebx\n" // just move stuff from the stack to the correct registers
" movl 12(%esp),%ecx\n"
" movl 16(%esp),%edx\n"
" mov $4,%eax\n" // 4 = write syscall on i386
" int $0x80\n"
" popl %ebx\n" // restore ebx
" ret\n"); // 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;
}
Run Code Online (Sandbox Code Playgroud)
请注意,这里忽略了几个细微之处 - 特别是atexit位.有关机器特定启动状态的所有文档都是从上面链接的两个glibc文件中的注释中提取的.
这个答案仅适用于x86-64,64位Linux ABI.所提到的所有其他操作系统和ABI将大致相似,但在精细细节方面有足够的不同,您需要_start为每个操作系统编写一次自定义.
您正在寻找" x86-64 psABI "中初始过程状态的规范,或者给它完整标题,"System V Application Binary Interface,AMD64 Architecture Processor Supplement(with LP64 and ILP32 Programming Models)".我将重现图3.9,"初始进程堆栈",这里:
Run Code Online (Sandbox Code Playgroud)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
Run Code Online (Sandbox Code Playgroud)
(完全未经测试.)
(如果你想知道为什么没有调整来维持堆栈指针对齐,这是因为在正常的过程调用时,8(%rsp)是16字节对齐,但是当_start调用时,%rsp它本身是16字节对齐的.每条call指令%rsp向下移动8 ,产生正常编译函数所期望的对齐情况.)
更彻底的_start做更多的事情,比如清除所有其他寄存器,安排比默认情况下更大的堆栈指针对齐,调用C库自己的初始化函数,设置environ,初始化线程本地存储使用的状态,用辅助矢量等做一些有建设性的事情
您还应该知道,如果存在动态链接器(PT_INTERP可执行文件中的部分),它会在之前 接收控制_start.ld.so除了glibc本身之外,Glibc 不能用于任何C库; 如果您正在编写自己的C库,并且希望支持动态链接,则还需要编写自己的C库ld.so.(是的,这是不幸的;理想情况下,动态链接器将是一个单独的开发项目,并且将指定其完整的接口.)
As a quick and dirty hack, you can make an executable with a compiled C function as the ELF entry point. Just make sure you use exit or _exit instead of returning.
(Link with gcc -nostartfiles to omit CRT but still link other libraries, and write a _start() in C. Beware of ABI violations like stack alignment, e.g. use -mincoming-stack-boundary=2 or an __attribte__ on _start, as in Compiling without libc)
If it's dynamically linked, you can still use glibc functions on Linux (because the dynamic linker runs glibc's init functions). Not all systems are like this, e.g. on cygwin you definitely can't call libc functions if you (or the CRT start code) hasn't called the libc init functions in the correct order. I'm not sure it's even guaranteed that this works on Linux, so don't depend on it except for experimentation on your own system.
I have used a C _start(void){ ... } + calling _exit() for making a static executable to microbenchmark some compiler-generated code with less startup overhead for perf stat ./a.out.
Glibc's _exit() works even if glibc wasn't initialized (gcc -O3 -static), or use inline asm to run xor %edi,%edi / mov $60, %eax / syscall (sys_exit(0) on Linux) so you don't have to even statically link libc. (gcc -O3 -nostdlib)
With even more dirty hacking and UB, you can access argc and argv by knowing the x86-64 System V ABI that you're compiling for (see @zwol's answer for a quote from ABI doc), and how the process startup state differers from the function calling convention:
argc is where the return address would be for a normal function (pointed to by RSP). GNU C has a builtin for accessing the return address of the current function (or for walking up the stack.)argv[0] is where the 7th integer/pointer arg should be (the first stack arg, just above the return address). It happens to / seems to work to take its address and use that as an array!// 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\n", argc, argv[argc-1]);
printf("%f\n", 1.234); // segfaults if RSP is misaligned
exit(0);
//_exit(0); // without flushing stdio buffers!
}
Run Code Online (Sandbox Code Playgroud)
# 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
Run Code Online (Sandbox Code Playgroud)
This works with or without optimization, with gcc7.3. I was worried that without optimization, the address of argv0 would be below rbp where it copies the arg, rather than its original location. But apparently it works.
gcc -nostartfiles links glibc but not the CRT start files.
gcc -nostdlib omits both libraries and CRT startup files.
Very little of this is guaranteed to work, but it does in practice work with current gcc on current x86-64 Linux, and has worked in the past for years. If it breaks, you get to keep both pieces. IDK what C features are broken by omitting the CRT startup code and just relying on the dynamic linker to run glibc init functions. Also, taking the address of an arg and accessing pointers above it is UB, so you could maybe get broken code-gen. gcc7.3 happens to do what you'd expect in this case.
Things that definitely break
atexit() cleanup, e.g. flushing stdio buffers._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,这就是为什么我查看文档并发现当 ABI 从仅需要 4 字节堆栈对齐转换为 32 位时,有一个用于 32 位的属性当前ESP32 位模式下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);
}
Run Code Online (Sandbox Code Playgroud)
使用-mincoming-stack-boundary=3,我们可以在不需要的地方获得堆栈重新对齐代码。gcc 的堆栈重新对齐代码非常笨重,因此我们希望避免这种情况。(并不是说你真的会用它来编译一个你关心效率的重要程序,请仅使用这个愚蠢的计算机技巧作为学习实验。)
但无论如何,请参阅 Godbolt 编译器资源管理器上带和不带-mpreferred-stack-boundary=3.
| 归档时间: |
|
| 查看次数: |
624 次 |
| 最近记录: |