C - 在没有标准库的情况下打印参数
C - print args without stdlibs
我刚刚编写了一个 C 程序,它在不使用标准库或 main()
函数的情况下打印其命令行参数。我的动机只是出于好奇,想了解如何使用内联汇编。我正在使用 Ubuntu 17.10 x86_64 与 4.13.0-39-generic 内核和 GCC 7.2.0.
下面是我的代码,我已尽我所能对其进行评论。系统需要 print
、print_1
、my_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 [=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
我刚刚编写了一个 C 程序,它在不使用标准库或 main()
函数的情况下打印其命令行参数。我的动机只是出于好奇,想了解如何使用内联汇编。我正在使用 Ubuntu 17.10 x86_64 与 4.13.0-39-generic 内核和 GCC 7.2.0.
下面是我的代码,我已尽我所能对其进行评论。系统需要 print
、print_1
、my_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 [=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