[英]Number of executed Instructions different for Hello World program Nasm Assembly and C
我有一个简单的调试器(使用 ptrace : http : //pastebin.com/D0um3bUi )来计算为给定的输入可执行程序执行的指令数。 它使用 ptrace 单步执行模式来计数指令。
为此,当程序 1) 的可执行文件(来自 gcc main.c 的 a.out)作为输入提供给我的测试调试器时,它会在执行指令时打印大约 100k。 当我使用-static
选项时,它给出了 10681 条指令。
现在在 2) 我创建一个汇编程序并使用 NASM 进行编译和链接,然后当这个可执行文件作为测试调试器输入时,它显示 8 条指令作为计数,这是恰当的。
由于在运行时将程序与系统库链接,程序 1) 中执行的指令数量很高? 使用 -static 并将计数减少 1/10。 我如何确保指令计数只是程序 1) 中的主要函数的指令计数以及程序 2) 向调试器报告的方式?
1)
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
return 0;
}
我使用 gcc 创建可执行文件。
2)
; 64-bit "Hello World!" in Linux NASM
global _start ; global entry point export for ld
section .text
_start:
; sys_write(stdout, message, length)
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, message ; message address
mov rdx, length ; message string length
syscall
; sys_exit(return_code)
mov rax, 60 ; sys_exit
mov rdi, 0 ; return 0 (success)
syscall
section .data
message: db 'Hello, world!',0x0A ; message and newline
length: equ $-message ; NASM definition pseudo-
我构建:
nasm -f elf64 -o main.o -s main.asm
ld -o main main.o
由于在运行时将程序与系统库链接,程序 1) 中执行的指令数量很高?
是的,动态链接加上 CRT(C 运行时)启动文件。
使用
-static
并将计数减少 1/10。
所以只剩下 CRT 启动文件,它在调用main
之前和之后做一些事情。
如何确保指令计数只是程序 1) 中的主要函数的指令计数
测量一个空的main
,然后从未来的测量中减去该数字。
除非您的指令计数器更智能,并查看可执行文件中它所跟踪的进程的符号,否则它将无法分辨哪些代码来自何处。
这就是程序 2) 向调试器报告的方式。
这是因为在该程序中没有其他的代码。 并不是你以某种方式帮助调试器忽略了一些指令,而是你制作了一个没有任何指令的程序,你没有自己放在那里。
如果您想查看运行 gcc 输出时实际发生的情况,请使用gdb a.out
、 b _start
、 r
和单步执行。 一旦你深入调用树,你就是概率。 想要使用fin
来完成当前函数的执行,因为您不想单步执行 100 万条指令,甚至 10k。
相关: 如何确定在 C 程序中执行的 x86 机器指令的数量? 显示perf stat
将在执行mov eax, 231
/ syscall
链接到静态可执行文件的 NASM 程序中计算总共 3 条用户空间指令。
彼得给出了一个很好的答案,我将跟进一个值得畏惧的回应,可能会获得一些反对票。 当直接与LD或间接与GCC 链接时, ELF可执行文件的默认入口点是标签_start
。
您的NASM代码使用全局标签_start
因此当您的程序运行时,程序中的第一个代码将是_start
的指令。 使用GCC 时,程序的典型入口点是函数main
。 对您隐藏的是您的C程序也有一个_start
标签,但它是由C运行时启动对象提供的。
现在的问题是 - 有没有办法绕过C启动文件,从而避免启动代码? 从技术上讲是的,但这是一个危险的领域,可能会产生未定义的行为。 如果您喜欢冒险,您实际上可以告诉GCC使用-e
命令行选项更改程序的入口点。 而不是_start
我们可以让我们的入口点main
绕过C启动代码。 由于我们绕过了C启动代码,因此我们还可以不用在C运行时启动代码中使用-nostartfiles
选项进行链接。
你可以使用这个命令行来编译你的C程序:
gcc test.c -e main -nostartfiles
不幸的是,在C代码中必须修复一些问题。 通常,当使用C运行时启动对象时,在环境初始化后,会对main
进行CALL 调用。 通常main
执行一个RET指令,该指令返回到C运行时代码。 此时, C运行时会优雅地退出您的程序。 当使用-nostartfiles
选项时, RET没有任何地方可以返回,因此它可能会出现段错误。 为了解决这个问题,我们可以调用C库_exit
函数来退出我们的程序。
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
_exit(0); /* We exit application here, never reaching the return */
return 0;
}
除非您省略帧指针,否则GCC会发出一些额外的指令来设置堆栈帧并将其拆除,但开销很小。
上述过程似乎不适用于标准 glibc C库的静态构建( GCC 中的-static
选项)。 这在此Stackoverflow 答案中进行了讨论。 动态版本之所以有效,是因为共享对象可以注册一个函数,该函数被动态加载器调用以执行初始化。 静态构建时,这通常由C运行时完成,但我们跳过了初始化。 因此,像printf
这样的GLIBC函数可能会失败。 有一些符合标准的替代C库,可以在没有C运行时初始化的情况下运行。 一种这样的产品是MUSL 。
在 64 位 Ubuntu 上,这些命令应该构建和安装 64 位版本的MUSL :
git clone git://git.musl-libc.org/musl
cd musl
./configure --prefix=/usr/local/musl/x86-64
make
sudo make install
然后,您可以使用GCC的MUSL包装器来处理MUSL的C库,而不是大多数 Linux 发行版上的默认GLIBC库。 参数就像GCC,所以你应该能够做到:
/usr/local/musl/x86-64/bin/musl-gcc -e main -static -nostartfiles test.c
当运行由./a.out
生成的./a.out 时,它可能会出现段错误。 MUSL在使用大多数C库函数之前不需要初始化,因此即使使用-static
GCC选项它也应该可以工作。
您比较的问题之一是您直接在NASM 中调用SYS_WRITE系统调用,在C 中您使用的是printf
。 用户 EOF 正确评论说,您可能希望通过调用C 中的write
函数而不是printf
来进行更公平的比较。 write
开销要少得多。 您可以将代码修改为:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
char *str = "Hello, world\n";
write (STDOUT_FILENO, str, 13);
_exit(0);
return 0;
}
这将比NASM的直接SYS_WRITE系统调用有更多的开销,但远低于printf
生成的开销。
我将发出警告,除了一些软件开发的边缘案例外,在代码审查中可能不会很好地采用此类代码和技巧。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.