繁体   English   中英

Hello World 程序 Nasm Assembly 和 C 的执行指令数不同

[英]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.outb _startr和单步执行。 一旦你深入调用树,你就是概率。 想要使用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

安装 MUSL 作为 GLIBC 的替代方案

在 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

然后,您可以使用GCCMUSL包装器来处理MUSLC库,而不是大多数 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM