我刚刚编写了一个C程序,它不使用标准库或main()函数即可打印其命令行参数.我的动机只是出于好奇心,并了解如何进行内联汇编.我正在将Ubuntu 17.10 x86_64与4.13.0-39通用内核和GCC 7.2.0一起使用.
以下是我尝试尽我所能理解的代码.系统需要使用函数print,print_1,my_exit和_start()来运行可执行文件.实际上,如果没有_start(),则链接器将发出警告,并且程序将出现段错误.
函数print和print_1不同.第一个将字符串输出到控制台,在内部测量字符串的长度.第二个函数需要作为参数传递的字符串长度. my_exit()函数只是退出程序,返回所需的值,在我的情况下,该值是字符串长度或命令行参数的数量.
print_1需要将字符串长度作为参数,以便使用while()循环对字符进行计数,并将长度存储在strLength中.在这种情况下,一切正常.
当我使用打印功能时会发生奇怪的事情,该功能在内部测量字符串的长度.简单地说,该函数看起来以某种方式将字符串指针更改为指向环境变量,而该环境变量应该是下一个指针,而不是第一个参数,函数将打印“ 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}
解决方法:
char * a =(void *)*(arg 7);如果有的话,那完全是“上班的机会”.除非编写仅使用内联asm的__attribute __((naked))函数,否则完全由编译器决定其对堆栈内存的布局方式.似乎您正在获得rsp,尽管不能保证这种不受支持的register-asm local用法. (仅当用作内联asm语句的操作数时,才保证使用请求的寄存器.)
如果在禁用优化的情况下进行编译,则gcc将为本地人保留堆栈插槽,因此char * b = a;使gcc通过更多的函数输入来调整RSP,因此这就是为什么您的黑客碰巧更改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中编写一个无效的_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而不是返回地址.它不是很漂亮,也不推荐使用,但是考虑到调用约定,您可以使用gcc生成知道事物在哪里的代码.
您的代码伪造者在注册时没有告诉编译器.关于此代码的所有内容都是令人讨厌的,没有理由期望任何代码都能始终如一地工作.如果这样做,这是偶然的,并且可能会因周围的不同代码或编译器选项而中断.如果要编写整个函数,请在独立的asm(或在全局范围内的内联asm)中进行操作,并声明一个C原型,以便编译器可以调用它.
查看gcc的asm输出,看看它在代码中生成了什么. (例如,将代码放在http://godbolt.org/上).您可能会使用破坏了asm的寄存器来看到它. (除非您在禁用优化的情况下进行编译,否则在C语句之间的寄存器中将不保留任何内容以支持一致的调试.仅破坏RSP或RBP会引起问题;其他内联asm破坏虫将无法检测到.)但是破坏红色区仍然是一个问题.
有关指南和教程的链接,另请参见https://*.com/tags/inline-assembly/info.
使用内联汇编的正确方法(如果有正确的方法)通常是让编译器尽可能多地执行.因此,要进行写系统调用,您需要使用输入/输出约束来完成所有操作,而asm模板中的唯一指令就是“ syscall”,例如以下示例my_write函数:How to invoke a system call via sysenter in inline assembly?(实际答案具有32位int $0x80和x86-64系统调用,但不是使用32位sysenter的嵌入式asm版本,因为这不是保证稳定的ABI).
另请参见What is the difference between ‘asm’, ‘__asm’ and ‘__asm__’?.
由于许多原因(例如击败常数传播和其他优化),您不应该使用https://gcc.gnu.org/wiki/DontUseInlineAsm.
注意,内联asm语句的指针输入约束并不意味着指向的内存也是输入或输出.请使用“内存”清除器,或参见at&t asm inline c++ problem以了解虚拟操作数的解决方法.