首页 > 解决方案 > C - 在没有标准库的情况下打印 args

问题描述

我刚刚编写了一个 C 程序,它在不使用标准库或main()函数的情况下打印其命令行参数。我的动机只是好奇和了解如何使用内联汇编。我正在使用带有 4.13.0-39-generic 内核和 GCC 7.2.0 的 Ubuntu 17.10 x86_64。

下面是我的代码,我尽可能多地对其进行评论。系统需要函数printprint_1my_exit_start()来运行可执行文件。实际上,没有_start()链接器会发出警告并且程序会出现段错误。

功能print不同print_1。第一个将字符串打印到控制台,在内部测量字符串的长度。第二个函数需要作为参数传递的字符串长度。该my_exit()函数只是退出程序,返回所需的值,在我的例子中是字符串长度或命令行参数的数量。

print_1需要字符串长度作为参数,因此使用while()循环计算字符并将长度存储在strLength. 在这种情况下,一切都很好。

当我使用该函数时会发生奇怪的事情,该print函数在内部测量字符串长度。简单地说,看起来这个函数以某种方式将字符串指针更改为指向应该是下一个指针的环境变量,而不是函数打印的第一个参数"CLUTTER_IM_MODULE=xim",这是我的第一个环境变量。我的解决方法是分配*a*b下一行。

我在计数过程中找不到任何解释,但看起来它正在改变我的字符串指针。

unsigned long long print(char * str){
unsigned long long ret;
__asm__(
        "pushq %%rbx \n\t"
        "pushq %%rcx \n\t"                      //RBX and RCX to the stack for further restoration
        "movq %1, %%rdi \n\t"                   //pointer to string (char * str) into RDI for SCASB instruction
        "movq %%rdi, %%rbx \n\t"                //saving RDI in RBX for final substraction
        "xor %%al, %%al \n\t"                   //zeroing AL for SCASB comparing
        "movq $0xffffffff, %%rcx \n\t"          //max string length for REPNE instruction
        "repne scasb \n\t"                      //counting "loop"       see details: https://www.felixcloutier.com/x86/index.html   for REPNE and SCASB instructions
        "sub %%rbx, %%rdi \n\t"                 //final substraction
        "movq %%rdi, %%rdx \n\t"                //string length for write syscall
        "movq %%rdi, %0 \n\t"                   //string length into ret to return from print
        "popq %%rcx \n\t"
        "popq %%rbx \n\t"                       //RBX and RCX restoration

        "movq $1, %%rax \n\t"                   //write - 1 for syscall
        "movq $1, %%rdi \n\t"                   //destination pointer for string operations $1 - stdout
        "movq %1, %%rsi \n\t"                   //source string pointer
        "syscall \n\t"
        : "=g"(ret)
        : "g"(str)
        );
return ret; }

void print_1(char * str, int l){
int ret = 0;

__asm__("movq $1, %%rax \n\t"                   //write - 1 for syscall
        "movq $1, %%rdi \n\t"                   //destination pointer for string operations
        "movq %1, %%rsi \n\t"                   //source pointer for string operations
        "movl %2, %%edx \n\t"                   //string length
        "syscall"
        : "=g"(ret)
        : "g"(str), "g" (l));}


void my_exit(unsigned long long ex){
int ret = 0;
__asm__("movq $60, %%rax\n\t"               //syscall 60 - exit
        "movq %1, %%rdi\n\t"                //return value
        "syscall\n\t"
        "ret"
        : "=g"(ret)
        : "g"(ex)
);}

void _start(){

register int ac __asm__("%rsi");                        // in absence of main() argc seems to be placed in rsi register
//int acp = ac;
unsigned long long strLength;
if(ac > 1){
    register unsigned long long * arg __asm__("%rsp");  //argv array
    char * a = (void*)*(arg + 7);                       //pointer to argv[1]
    char * b = a;                                       //work around for print function
    /*version with print_1 and while() loop for counting
        unsigned long long strLength = 0;
        while(*(a + strLength)) strLength++;
        print_1(a, strLength);
        print_1("\n", 1);
    */
    strLength = print(b);
    print("\n");
}
//my_exit(acp);         //echo $?   prints argc
my_exit(strLength);     //echo $?   prints string length}

标签: clinuxgccassemblyinline-assembly

解决方案


char * a = (void*)*(arg + 7);完全是“碰巧起作用”的事情,如果它完全起作用的话。除非您正在编写__attribute__((naked))使用内联汇编的函数,否则完全取决于编译器如何布置堆栈内存。看来您正在获得,尽管对于这种不受支持的本地寄存器 asm 的使用不能保证这一点。(仅当用作内联 asm 语句的操作数时才能保证使用请求的寄存器。)rsp

如果您在禁用优化的情况下进行编译,gcc 将为本地人保留堆栈槽,因此char * b = a;gcc 会通过更多函数入口来调整 RSP,这就是为什么您的 hack 碰巧更改了 gcc 的代码生成以匹配硬编码+7(乘以 8 个字节)偏移您放入源码。

在进入 时_start,堆栈内容是:argc(%rsp)argv[]从 开始8(%rsp)。在 argv[] 的终止 NULL 指针上方,envp[]数组也在堆栈内存中。所以这就是为什么CLUTTER_IM_MODULE=xim当你的硬编码偏移量得到错误的堆栈槽时你会得到。

// in absence of main() argc seems to be placed in rsi register

这可能是动态链接器(之前在您的进程中运行)遗留下来的_start。如果您使用 编译gcc -static -nostdlib -fno-pie,您_start将是直接从内核到达的真正进程入口点,所有寄存器 = 0(RSP 除外)。请注意,ABI 表示未定义;Linux 选择将它们归零以避免信息泄漏。

可以void _start(){}在 GNU C 中编写一个在启用不启用优化的情况下可靠地工作的,并且出于正确的原因而工作,没有内联 asm(但仍然依赖于 x86-64 SysV ABI 的调用约定和进程入口堆栈布局)。不需要对碰巧在 gcc 的代码生成中发生的偏移进行硬编码。 如何在没有 Glibc 的情况下使用 C 中的内联汇编获取参数值?. 它使用像int argc = (int)__builtin_return_address(0);因为_start不是函数这样的东西:堆栈上的第一件事是 argc 而不是返回地址。它不漂亮也不推荐,但考虑到调用约定,您可以让 gcc 生成知道事物在哪里的代码。


您的代码破坏者在不告诉编译器的情况下注册。 这段代码的一切都很糟糕,没有理由期望其中任何一个都能始终如一地工作。如果确实如此,那是偶然的,并且可能会因不同的周围代码或编译器选项而中断。如果要编写整个函数,请在独立的 asm 中(或在全局范围内的内联 asm 中)并声明一个 C 原型,以便编译器可以调用它。

查看 gcc 的 asm 输出以了解它围绕您的代码生成的内容。(例如,将您的代码放在http://godbolt.org/上)。您可能会使用您在 asm 中破坏的寄存器看到它。(除非您在禁用优化的情况下进行编译,在这种情况下,它不会在 C 语句之间的寄存器中保留任何内容以支持一致的调试。只有破坏 RSP 或 RBP 会导致问题;其他内联 asm clobber 错误将不会被发现。)但是破坏红色区域仍然是一个问题。

另请参阅https://stackoverflow.com/tags/inline-assembly/info以获取指南和教程的链接。


使用 inline asm 的正确方法(如果有正确的方法)通常是让编译器尽可能多地做。因此,要进行 write 系统调用,您将使用输入/输出约束来做所有事情,并且 asm 模板中的唯一指令将是"syscall",就像这个很好的示例my_write函数:如何通过 sysenter 在内联汇编中调用系统调用? (实际答案有 32 位int $0x80和 x86-64 syscall,但不是使用 32 位的内联 asm 版本,sysenter因为这不是保证稳定的 ABI)。

另请参阅“asm”、“__asm”和“__asm__”有什么区别?再举一个例子。

https://gcc.gnu.org/wiki/DontUseInlineAsm有很多你不应该使用它的原因(比如击败常量传播和其他优化)。

请注意,内联 asm 语句的指针输入约束并不意味着指向的内存也是输入或输出。使用"memory"clobber,或查看at&t asm inline c++ 问题以获得虚拟操作数解决方法。


推荐阅读