我刚刚编写了一个C程序,该程序在不使用标准库或Program.cs
函数的情况下打印其命令行参数。我的动机只是出于好奇心,并了解如何进行内联汇编。我正在将Ubuntu 17.10 x86_64和4.13.0-39通用内核以及GCC 7.2.0一起使用。
下面是我的代码,据我所知,我已尝试对其进行注释。系统需要功能main()
,print
,print_1
和my_exit
来运行可执行文件。实际上,在没有_start()
的情况下,链接器将发出警告,并且程序将出现段错误。
功能_start()
和print
不同。第一个将字符串输出到控制台,在内部测量字符串的长度。第二个函数需要作为参数传递的字符串长度。 print_1
函数仅退出程序,返回所需的值,在我的情况下是字符串长度或命令行参数的数量。
my_exit()
需要将字符串长度作为参数,以便使用print_1
循环对字符进行计数,并将长度存储在while()
中。在这种情况下,一切正常。
当我使用strLength
函数时会发生奇怪的事情,该函数在内部测量字符串的长度。简而言之,该函数看起来以某种方式将字符串指针更改为指向环境变量,而该环境变量应该是下一个指针,并且函数将打印第一个环境变量print
而不是第一个参数。我的解决方法是在下一行中将"CLUTTER_IM_MODULE=xim"
分配给*a
。
我在计数过程中找不到任何解释,但似乎正在改变我的字符串指针。
*b
答案 0 :(得分:2)
char * a = (void*)*(arg + 7);
完全是一种“可以工作的东西”,如果它可以工作的话。除非您编写的__attribute__((naked))
函数仅 使用内联asm,否则完全由编译器决定如何布置堆栈内存。看来您正在使用rsp
,尽管不能保证这种不支持的register-asm local用法。 (只有在用作内联asm语句的操作数时,才保证使用请求的寄存器。)
如果您在禁用优化的情况下进行编译,则gcc会为本地人保留堆栈插槽,因此char * b = a;
使gcc通过函数输入上的更多内容来调整RSP ,因此这就是您的黑客碰巧更改gcc代码的原因-gen以匹配您在源代码中放置的硬编码+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而不是返回地址。它不是很漂亮,也不建议使用,但是考虑到调用约定,您可以使用gcc生成知道事物在哪里的代码。
您的代码伪造者在注册时没有告知编译器。与该代码有关的一切都是令人讨厌的,没有理由期望任何代码都能始终如一地工作。如果这样做,这是偶然的,并且可能会因周围的不同代码或编译器选项而中断。如果要编写整个函数,请在独立的asm(或在全局范围内的内联asm)中进行操作,并声明一个C原型,以便编译器可以调用它。
查看gcc的asm输出,以查看它在代码中周围生成了什么。 (例如,将您的代码放在http://godbolt.org/上)。您可能会使用破坏了asm的寄存器来看到它。 (除非您在禁用优化的情况下进行编译,否则在C语句之间的寄存器中将不保留任何内容以支持一致的调试。只有破坏RSP或RBP会导致问题;其他内联asm破坏bug不会被发现。)但是破坏红色区域仍然是个问题。
另请参阅https://stackoverflow.com/tags/inline-assembly/info,以获取指南和教程的链接。
使用内联汇编的正确方法(如果有正确的方法)通常是让编译器尽可能多地进行操作。因此,要进行写系统调用,您需要在输入/输出约束下进行所有操作,而asm模板中的唯一指令就是"syscall"
,就像这个不错的示例my_write
函数:{{3} }(实际答案有32位int $0x80
和x86-64 syscall
,但不是使用32位sysenter
的嵌入式asm版本,因为这不是保证稳定的ABI)。< / p>
另请参见How to invoke a system call via sysenter in inline assembly?。
What is the difference between 'asm', '__asm' and '__asm__'?出于很多原因,您不应该使用它(例如击败常数传播和其他优化方法)。
请注意,内联asm语句的指针输入约束不会不暗示指向的内存也是输入或输出。使用"memory"
垃圾桶,或使用https://gcc.gnu.org/wiki/DontUseInlineAsm了解虚拟操作数的解决方法。
答案 1 :(得分:1)
非常感谢您在回答和评论中提出的每条建议,这确实很有帮助。 Peter Cordes,谢谢您的链接https://stackoverflow.com/a/50261819。 我使用此代码作为基础,并按照您的建议在全局范围内编写内联汇编。 经过几天的浏览并阅读了一些文档,终于有了执行我一直在寻找的代码(检查没有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 $60,%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 $1,%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