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

cra*_*ace 5 linux assembly gcc x86-64 nasm

我有一个简单的调试器(使用ptrace:http://pastebin.com/D0um3bUi)来计算给定输入可执行程序执行的指令数.它使用ptrace单步执行模式来计算指令.

为此,当程序1)的可执行文件(来自gcc main.c的a.out)作为输入提供给我的测试调试器时,它会在执行指令时打印大约100k.当我使用-static选项时,它会给出10681条指令.

现在在2)我创建一个汇编程序并使用NASM进行编译和链接,然后当这个可执行文件作为测试调试器输入时,它显示8个指令作为计数,哪个是apt.

程序1)中执行的指令数量很高,因为在运行时将程序与系统库链接起来了?使用-static并将计数减少1/10.如何确保指令计数仅是程序1)中主要功能的指令,以及程序2)为调试器报告的方式?

1)

#include <stdio.h>

int main()
{
    printf("Hello, world!\n");
    return 0;
}    
Run Code Online (Sandbox Code Playgroud)

我使用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-                             
Run Code Online (Sandbox Code Playgroud)

nasm -f elf64 -o main.o -s main.asm
ld -o main main.o

Mic*_*tch 6

彼得给出了一个非常好的答案,我将跟进一个非常有价值的反应并且可能会获得一些选票.当直接与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
Run Code Online (Sandbox Code Playgroud)

不幸的是,有一些必须在C代码中修复的gotchya .通常情况下使用时Ç运行时启动对象,环境初始化之后CALL就是制作main.通常main会返回返回C运行时代码的RET指令.此时,C运行时会正常退出程序.使用该选项时,RET无处可返回,因此可能会出现段错误.为了解决这个问题,我们可以调用C库函数来退出程序. -nostartfiles_exit

#include <stdio.h>

int main()
{
    printf("Hello, world!\n");
    _exit(0);  /* We exit application here, never reaching the return */

    return 0;
}   
Run Code Online (Sandbox Code Playgroud)

除非省略帧指针,否则GCC会发出一些额外的指令来设置堆栈帧并将其拆除,但开销很小.

特别说明

上面的过程似乎不适用于标准glibc C库的静态构建(GCC中的-static选项).这在Stackoverflow的答案中讨论过.动态版本有效,因为共享对象可以注册由动态加载程序调用以执行初始化的函数.静态构建时,这通常由C运行时完成,但我们已经跳过了初始化.因为GLIBC的功能就像失败一样.有一些符合标准的替换C库可以在没有C运行时初始化的情况下运行.其中一种产品是MUSL.printf

安装MUSL作为GLIBC的替代品

在Ubuntu 64位上,这些命令应该构建并安装64位版本的MUSL:

git clone git://git.musl-libc.org/musl
cd musl
./configure --prefix=/usr/local/musl/x86-64
make
sudo make install
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用MUSL包装器GCC与合作MUSLç库,而不是默认GLIBC在大多数Linux发行库.参数就像GCC一样,所以你应该能够做到:

/usr/local/musl/x86-64/bin/musl-gcc -e main -static -nostartfiles test.c
Run Code Online (Sandbox Code Playgroud)

./a.out使用GLIBC生成运行时,它可能会发生段错误.在使用大多数C库函数之前,MUSL不需要初始化,因此即使使用GCC选项也应该可以工作.-static


更公平的比较

一个与你的比较问题是,你调用SYS_WRITE直接系统调用在NASM,在ç你正在使用printf.用户EOF正确评论说您可能希望通过调用C中write函数而不是使用它来使其更公平.它的开销要少得多.您可以修改您的代码:printfwrite

#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;
}
Run Code Online (Sandbox Code Playgroud)

这将比NASM的直接SYS_WRITE系统调用具有更多的开销,但远远低于printf生成的开销.


我将发出警告,除了软件开发的一些边缘情况之外,代码审查中可能不会很好地利用这些代码和技巧.


Pet*_*des 5

程序1)中执行的指令数量很高,因为在运行时将程序与系统库链接起来了?

是的,动态链接加上CRT(C运行时)启动文件.

使用-static并减少了1/10的计数.

因此,只需离开CRT启动文件,这些文件在调用之前执行,然后执行main.

如何确保指令计数仅为程序1)中主函数的指令

测量一个空main,然后从未来的测量值中减去该数字.

除非您的指令计数器更智能,并查看可执行文件中的符号以查找它正在跟踪的进程,否则它将无法分辨哪些代码来自哪里.

以及程序2)如何报告调试器.

这是因为有在该程序中没有其他的代码.并不是因为你以某种方式帮助调试器忽略了一些指令,而是你创建了一个没有任何指令的程序,你没有自己放在那里.

如果你想看看有什么实际当你运行GCC的输出情况,gdb a.out,b _start,r,和单步执行.一旦深入了解调用树,你就可能了.想要用来fin完成当前函数的执行,因为你不想单步执行100万条指令,甚至10k.