作为一项统一任务,我为 write 系统调用编写了一个非常简单的包装器。这是针对 i386 的。代码编译为:
gcc -ffreestanding -fno-stack-protector -nostdlib -nostdinc -static -m32 -Wall -g -O2
我想知道为什么注释掉的代码只打印出 4 个字符,无论 num_bytes 是多少。
int my_write(int fd, void *buf, unsigned num_bytes){
int ret;
asm volatile (
"mov $4, %%eax;"
"mov %1, %%ebx;"
"mov %2, %%ecx;"
"mov %3, %%edx;"
"int $0x80;"
: "=r"(ret)
: "r"(fd), "r"(buf), "r"(num_bytes)
: "eax", "ebx", "ecx", "edx", "memory"
);
/* Does not work
asm volatile (
"mov $4, %%eax;"
"mov %1, %%ebx;"
"mov %2, %%ecx;"
"mov %3, %%edx;"
"int $0x80;"
"mov %%eax, %0;"
: "=a"(ret)
: "r"(fd), "r"(buf), "r"(num_bytes)
: "ebx", "ecx", "edx", "memory"
);
*/
return ret;
}
我知道我可以使用:
asm volatile (
"int $0x80;"
: "=a"(ret)
: "a"(4), "b"(fd), "c"(buf), "d"(num_bytes)
: "memory"
);
这也很好用。 但是我想知道上述两种方法之间有什么不同。 使用
=a
或 =r
应该没有关系,因为 i386 的调用约定规定 syscall 的返回值在 eax
中。
我尝试编写静态缓冲区并使用固定的 num_bytes,但问题仍然存在。 我还尝试使用另一个优化标志进行编译
-Og
。使用除 -O2
之外的任何内容都会导致代码在启动后立即以状态 1 退出,即使版本正常工作。但这可能是由代码中的某些其他函数引起的,因为使用 my_write
和 -O2
编译的 -Og
部分的 objdump 没有显示差异。但是我没有在任何其他功能中发现错误,所以我希望有人有一个想法。
感谢您的任何意见。
编辑:请原谅与格式的斗争
在我看来,您将寄存器的clobber列表视为保留寄存器的列表,您想为自己使用它,并认为编译器会远离它们。
但事实并非如此。 clobber list 的作用是通知编译器这些寄存器在执行此
asm
期间可能已更改值,并且之后的值将被视为未定义。
但是,通过在代码中按名称使用寄存器 和 使用
r
约束,您可能会面临编译器为您在代码中使用的约束选择相同寄存器的风险。这很可能会失败。
为了调试此类问题,您应该反汇编生成的代码并将其与您的意图进行比较。例如,您注释的代码:
asm volatile (
"mov $4, %%eax;"
"mov %1, %%ebx;"
"mov %2, %%ecx;"
"mov %3, %%edx;"
"int $0x80;"
"mov %%eax, %0;"
: "=a"(ret)
: "r"(fd), "r"(buf), "r"(num_bytes)
: "ebx", "ecx", "edx", "memory"
);
在我的机器中拆解为:
mov 0x10(%esp),%eax // load fd
mov 0x14(%esp),%esi // load buf
mov 0x18(%esp),%edi // load num_bytes
mov $0x4,%eax // ooops!
mov %eax,%ebx
mov %esi,%ecx
mov %edi,%edx
int $0x80
mov %eax,%eax // store ret
如您所见,为
%1
选择的寄存器是 eax
,但在使用之前您要使用 $4
覆盖它。
未注释的代码的工作只是偶然,因为
eax
寄存器没有被选择做任何事情。
您有两种选择来解决这个问题。最简单的方法是使用特定的寄存器来进行约束,正如您在问题末尾所示的那样。这样就不会发生冲突。
另一个解决方案是要格外小心,不要覆盖任何可能仍然需要的寄存器。像这样的东西:
asm volatile (
"push %1;"
"push %2;"
"push %3;"
"pop %%edx;"
"pop %%ecx;"
"pop %%ebx;"
"mov $4, %%eax;"
"int $0x80;"
: "=a"(ret)
: "r"(fd), "r"(buf), "r"(num_bytes)
: "ebx", "ecx", "edx", "memory"
);
编译为:
mov 0x10(%esp),%eax
mov 0x14(%esp),%esi
mov 0x18(%esp),%edi
push %eax
push %esi
push %edi
pop %edx
pop %ecx
pop %ebx
mov $0x4,%eax
int $0x80
这看起来有点愚蠢,但它应该有效。
顺便说一句,如果将
"=a"(ret)
写为输出约束,则 mov %%eax, %0
是多余的。这就是为什么你的代码最后会得到一个额外的 mov %eax, %eax
。