Hello World 程序 Nasm 程序集和 C 的执行指令数不同
Number of executed Instructions different for Hello World program Nasm Assembly and C
我有一个简单的调试器(使用 ptrace:http://pastebin.com/D0um3bUi)来计算给定输入可执行程序执行的指令数。它使用ptrace单步执行模式来计算指令。
为此,当程序 1) 的可执行文件(a.out 来自 gcc main.c)作为我的测试调试器的输入时,它会在执行指令时打印大约 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
The number of instructions executed in program 1) is high because of linking the program with system library's at runtime?
是的,动态链接加上 CRT(C 运行时间)启动文件。
used -static
and which reduces the count by a factor of 1/10.
所以只剩下 CRT 启动文件,它在调用 main
之前和之后做一些事情。
How can I ensure that the instruction count is only that of the main function in Program 1)`
测量一个空的 main
,然后从以后的测量中减去该数字。
除非您的指令计数器更智能,并查看可执行文件中的符号以了解其正在跟踪的进程,否则它将无法判断哪些代码来自何处。
and which is how Program 2) is reporting for the debugger.
那是因为该程序中没有其他代码。并不是你以某种方式帮助调试器忽略了一些指令,而是你编写了一个没有任何指令的程序,这不是你自己放的。
如果你想看看当你 运行 gcc 输出时 实际上 发生了什么,gdb a.out
, b _start
, r
,和单步。一旦你深入调用树,你就很可能。将要使用 fin
来完成当前函数的执行,因为您不想单步执行 100 万条指令,甚至 10k。
相关:How do I determine the number of x86 machine instructions executed in a C program? 显示 perf stat
将在 NASM 程序中计算 3 个用户-space 指令mov eax, 231
/ syscall
,链接成静态可执行文件。
Peter 给出了一个非常好的答案,我将跟进一个令人畏缩的回应,并且可能会获得一些反对票。当直接链接 LD 或间接链接 GCC 时,ELF 可执行文件的默认入口点是标签_start
.
您的 NASM 代码使用全局标签 _start
所以当您的程序是 运行 时,您程序中的第一个代码将是_start
的说明。使用 GCC 时,程序的典型入口点是函数 main
。对你隐藏的是你的 C 程序也有一个 _start
标签,但它是由 C 运行时间启动对象。
现在的问题是 - 有没有办法绕过 C 启动文件,从而避免启动代码?从技术上讲是的,但这是一个危险的领域,可能会产生未定义的行为。如果您喜欢冒险,您实际上可以告诉 GCC 使用 -e
命令行选项更改程序的入口点。我们可以让入口点 main
绕过 C 启动代码,而不是 _start
。由于我们绕过了 C 启动代码,我们也可以免除 C 运行time 启动代码中的链接 -nostartfiles
选项。
您可以使用此命令行来编译您的 C 程序:
gcc test.c -e main -nostartfiles
不幸的是,C 代码中有一些问题需要修复。通常在使用 C 运行 时间启动对象时,在环境初始化后,会对 main
进行 CALL。通常 main
执行 RET 指令,return 返回 C 运行 时间码。那时 C 运行 时间优雅地退出你的程序。当使用 -nostartfiles
选项时,RET 没有任何地方可以 return,因此它可能会出现段错误。为了解决这个问题,我们可以调用 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[=113= 的静态构建(GCC 中的-static
选项) ] 图书馆。 Whosebug answer. The dynamic version works because a shared object can register a function that gets called by the dynamic loader to perform initialization. When building statically this is generally done by the C runtime, but we've skipped that initialization. Because of that GLIBC functions like printf
can fail. There are replacement C libraries that are standards compliant that can operate without C runtime initialization. One such product is MUSL 对此进行了讨论。
安装 MUSL 作为 GLIBC 的替代品
在 Ubuntu 64 位上,这些命令应该构建并安装 MUSL:
的 64 位版本
git clone git://git.musl-libc.org/musl
cd musl
./configure --prefix=/usr/local/musl/x86-64
make
sudo make install
然后您可以使用 MUSL 包装器 GCC 来处理 MUSL' s C 库,而不是大多数 Linux 发行版上的默认 GLIBC 库。参数就像 GCC 所以你应该能够做到:
/usr/local/musl/x86-64/bin/musl-gcc -e main -static -nostartfiles test.c
当 运行ning ./a.out
使用 GLIBC 生成时,它可能会出现段错误。 MUSL 在使用大多数 C 库函数之前不需要初始化,因此它甚至可以与 -static
GCC 选项。
更公平的比较
您的比较的一个问题是您直接在 NASM[=113= 中调用 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
会产生什么。
我要发出警告,除了一些软件开发的边缘案例外,代码审查可能不会很好地采用此类代码和技巧。
我有一个简单的调试器(使用 ptrace:http://pastebin.com/D0um3bUi)来计算给定输入可执行程序执行的指令数。它使用ptrace单步执行模式来计算指令。
为此,当程序 1) 的可执行文件(a.out 来自 gcc main.c)作为我的测试调试器的输入时,它会在执行指令时打印大约 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
The number of instructions executed in program 1) is high because of linking the program with system library's at runtime?
是的,动态链接加上 CRT(C 运行时间)启动文件。
used
-static
and which reduces the count by a factor of 1/10.
所以只剩下 CRT 启动文件,它在调用 main
之前和之后做一些事情。
How can I ensure that the instruction count is only that of the main function in Program 1)`
测量一个空的 main
,然后从以后的测量中减去该数字。
除非您的指令计数器更智能,并查看可执行文件中的符号以了解其正在跟踪的进程,否则它将无法判断哪些代码来自何处。
and which is how Program 2) is reporting for the debugger.
那是因为该程序中没有其他代码。并不是你以某种方式帮助调试器忽略了一些指令,而是你编写了一个没有任何指令的程序,这不是你自己放的。
如果你想看看当你 运行 gcc 输出时 实际上 发生了什么,gdb a.out
, b _start
, r
,和单步。一旦你深入调用树,你就很可能。将要使用 fin
来完成当前函数的执行,因为您不想单步执行 100 万条指令,甚至 10k。
相关:How do I determine the number of x86 machine instructions executed in a C program? 显示 perf stat
将在 NASM 程序中计算 3 个用户-space 指令mov eax, 231
/ syscall
,链接成静态可执行文件。
Peter 给出了一个非常好的答案,我将跟进一个令人畏缩的回应,并且可能会获得一些反对票。当直接链接 LD 或间接链接 GCC 时,ELF 可执行文件的默认入口点是标签_start
.
您的 NASM 代码使用全局标签 _start
所以当您的程序是 运行 时,您程序中的第一个代码将是_start
的说明。使用 GCC 时,程序的典型入口点是函数 main
。对你隐藏的是你的 C 程序也有一个 _start
标签,但它是由 C 运行时间启动对象。
现在的问题是 - 有没有办法绕过 C 启动文件,从而避免启动代码?从技术上讲是的,但这是一个危险的领域,可能会产生未定义的行为。如果您喜欢冒险,您实际上可以告诉 GCC 使用 -e
命令行选项更改程序的入口点。我们可以让入口点 main
绕过 C 启动代码,而不是 _start
。由于我们绕过了 C 启动代码,我们也可以免除 C 运行time 启动代码中的链接 -nostartfiles
选项。
您可以使用此命令行来编译您的 C 程序:
gcc test.c -e main -nostartfiles
不幸的是,C 代码中有一些问题需要修复。通常在使用 C 运行 时间启动对象时,在环境初始化后,会对 main
进行 CALL。通常 main
执行 RET 指令,return 返回 C 运行 时间码。那时 C 运行 时间优雅地退出你的程序。当使用 -nostartfiles
选项时,RET 没有任何地方可以 return,因此它可能会出现段错误。为了解决这个问题,我们可以调用 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[=113= 的静态构建(GCC 中的-static
选项) ] 图书馆。 Whosebug answer. The dynamic version works because a shared object can register a function that gets called by the dynamic loader to perform initialization. When building statically this is generally done by the C runtime, but we've skipped that initialization. Because of that GLIBC functions like printf
can fail. There are replacement C libraries that are standards compliant that can operate without C runtime initialization. One such product is MUSL 对此进行了讨论。
安装 MUSL 作为 GLIBC 的替代品
在 Ubuntu 64 位上,这些命令应该构建并安装 MUSL:
的 64 位版本git clone git://git.musl-libc.org/musl
cd musl
./configure --prefix=/usr/local/musl/x86-64
make
sudo make install
然后您可以使用 MUSL 包装器 GCC 来处理 MUSL' s C 库,而不是大多数 Linux 发行版上的默认 GLIBC 库。参数就像 GCC 所以你应该能够做到:
/usr/local/musl/x86-64/bin/musl-gcc -e main -static -nostartfiles test.c
当 运行ning ./a.out
使用 GLIBC 生成时,它可能会出现段错误。 MUSL 在使用大多数 C 库函数之前不需要初始化,因此它甚至可以与 -static
GCC 选项。
更公平的比较
您的比较的一个问题是您直接在 NASM[=113= 中调用 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
会产生什么。
我要发出警告,除了一些软件开发的边缘案例外,代码审查可能不会很好地采用此类代码和技巧。