C - 在没有标准库的情况下打印参数

C - print args without stdlibs

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

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

函数 printprint_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 [=10=]xffffffff, %%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 , %%rax \n\t"                   //write - 1 for syscall
        "movq , %%rdi \n\t"                   //destination pointer for string operations  - 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 , %%rax \n\t"                   //write - 1 for syscall
        "movq , %%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 , %%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}

char * a = (void*)*(arg + 7); 完全是 "happens to work" 的东西,如果它能工作的话。除非您正在编写 使用内联 asm 的 __attribute__((naked)) 函数,否则完全取决于编译器如何布局堆栈内存。看起来您得到的是 rsp,尽管不能保证不受支持地使用本地寄存器汇编。 (仅当用作内联 asm 语句的操作数时才能保证使用请求的寄存器。)

如果您在禁用优化的情况下进行编译,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 选择将它们清零以避免信息泄露。

可以 在 GNU C 中编写一个 void _start(){},它可以在不启用优化的情况下可靠地与 一起工作,并且出于正确的原因而工作,没有内联 asm(但仍然依赖于 x86-64 SysV ABI 的调用约定和进程入口堆栈布局)。不需要硬编码在 gcc 的代码生成中发生的偏移量。 How Get arguments value using inline assembly in C without Glibc?。它使用 int argc = (int)__builtin_return_address(0); 之类的东西,因为 _start 不是函数:堆栈上的第一件事是 argc 而不是 return 地址。它不漂亮也不推荐,但考虑到调用约定,您可以让 gcc 生成知道东西在哪里的代码。


您的代码在未告知编译器的情况下破坏了寄存器。 这段代码的一切都令人讨厌,没有理由期望其中的任何部分能够始终如一地工作。如果确实如此,那是偶然的,并且可能会破坏不同的周围代码或编译器选项。如果您想编写整个函数,请在独立的 asm 中(或在全局范围内的内联 asm 中)并声明一个 C 原型以便编译器可以调用它。

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

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


使用inline asm的正确方法(如果有正确的方法)通常是让编译器尽可能多地做。所以要进行 write 系统调用,你会做所有带有输入/输出约束的事情,asm 模板中的唯一指令是 "syscall",就像这个很好的例子 my_write 函数:How to invoke a system call via sysenter in inline assembly?(实际答案有 32 位 int [=31=]x80 和 x86-64 syscall,但不是使用 32 位 sysenter 的内联 asm 版本,因为那不是保证稳定的 ABI)。

另见 What is the difference between 'asm', '__asm' and '__asm__'? 另一个例子。

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

请注意,内联 asm 语句的指针输入约束确实 而不是 暗示指向的内存也是输入或输出。使用 "memory" 破坏器,或查看 at&t asm inline c++ problem 虚拟操作数解决方法。

非常感谢您在回答和评论中提出的每一个建议,这真的很有帮助。 Peter Cordes , thanks for this link 。 我使用此代码作为基础,并按照您的建议在全局范围内编写内联 asm。 在看了几天并阅读了一些文档之后,终于有了我一直在寻找的代码(检查没有 stdlib 的命令行参数和环境变量)。

欢迎任何改进和建议。

编译时使用:gcc -Wall -o getArgs getArgs.c -nostdlib -nostartfiles -fno-ident -static -s

运行 : ./getArgs -args 大家好 -envs

*** Environment variable ***
/bin/bash

*** Command line arguments ***
-args
Hello
everybody
-envs

调用约定: 用户级应用程序用作传递序列的整数寄存器 * %rdi、%rsi、%rdx、%rcx、%r8 和 %r9。所以在函数调用中我们应该有ex。打印(%rdi,%rsi,%rdx); * 内核接口使用 %rdi、%rsi、%rdx、%r10、%r8 和 %r9。

asm(
    ".global _start\n\t"
    "_start:\n\t"
    "   xorl %ebp,%ebp\n\t"                     // Clear the frame pointer. As ABI suggests
    "   movq 0(%rsp),%rdi\n\t"                  // argc
    "   lea 8(%rsp),%rsi\n\t"                   // argv = %rsp + 8
    "   lea   8(%rsp,%rdi,8), %rdx\n\t"         // pointer to environment variables     (8*(argc+1))(%rsp)  envp[0]
    "   call __main\n\t"                        // call main function
    "   movq %rax,%rdi\n\t"                     // main return code as an argument for exit syscall
    "   movl ,%eax\n\t"                      // 60 = exit
    "   syscall\n\t");
asm(
    "print:\n\t"                    // thanks to the calling convention when we call our print we get:  int fd (%rdi), const void *buf (%rsi), unsigned count (%rdx)
    "   movq ,%rax\n\t"         // 1 = write syscall on x86_64
    "   syscall\n\t"
    "   ret\n\t"
);



int print(int fd, const void *buf, unsigned count); //do not forget to declare function from inline assembly


unsigned strLen(const char *ch) {
    const char *ptr;
    for(ptr = ch; *ptr; ++ptr);             //ptr points to same place as ch, then looping until *ptr is not 0. If so, after substraction we get string length.
    return ptr-ch;      }                   //"When you substract two pointers, as long as they point into the same array, the result is the number of elements separating them"

char strCmp(const char * a, const char * b){
     char t = 0;
     int aLength = strLen(a);
     int bLength = strLen(b);
    if(aLength == bLength){
        for(int j = 0; j < aLength; j++){
            if(a[j] == b[j])
                t++;
        }
        if(t == aLength)
            return 1;
        else
            return 0;
    }else{
        return 0;
}}   //strCmp - comparing 2 strings up to the length of first string, returns 1 if equal and 0 if not

char * getEnv(char * env, char **envp){
     char * val;
     int valL = strLen(env);
     int k = 1;
//environment variables is null terminated array of strings, last array element is 0
while(*(envp + k)){
    char t = 0;
    for(val = *(envp + k); *val != 0x3d; ++val);        //counting up to 3d (=) //ascii hex of "=" is 0x3d
    int envpL = val - *(envp + k);                      //counting length of envp
    if(valL == envpL){
        for(int j = 0; j < valL; j++){
            if(*(*(envp + k) + j) == *(env + j)){
                t++;
            }
        }
        if(t == valL){
            return ++val;
        }
    }
    k++;
}

return "";}     //getEnv - looping through environment variables "envp" looking for "env", using strLen()


int __main(int argc, char **argv, char **envp) {
         char arg1 = 0, arg2 = 0; int length;                       //arg1, arg2 - flags for argv checking
          //arrays to compare with command line arguments
         char envs[6] = {0x2d, 0x65, 0x6e, 0x76, 0x73, 0x00};           //ascii hex of "-envs"      
         char args[6] = {0x2d, 0x61, 0x72, 0x67, 0x73, 0x00};   //ascii hex of "-args"      
         //first of all we check for control arguments
        for(int i = 1; i < argc; i++) {

              if(strCmp(*(argv + i), envs))
                        arg1 = 1;
              if(strCmp(*(argv + i), args))
                        arg2 = 1;
         }

       if(arg1){
           char * b = getEnv("SHELL", envp);          //we are looking for "SHELL"
           print(1, "*** Environment variable ***\n", 30);
           print(1, b, strLen(b));
           print(1, "\n", 1);
       }
       if(arg2){
           print(1, "\n", 1);
           print(1, "*** Command line arguments ***\n", 31);
           for(int i = 1; i < argc; i++) {
               length = strLen(*(argv + i));
               print(1, *(argv + i), length);
               print(1, "\n", 1);
            }
        }

       return argc; }//number of arguments