5
votes

I have a simple debugger (using ptrace : http://pastebin.com/D0um3bUi) to count the number of instructions executed for a given input executable program. It uses ptrace single step execution mode to count instructions.

For that when the program 1)'s executable (a.out from gcc main.c) is given as input to my test debuggger it prints around 100k as instructions executed. When I use -static option it gives 10681 instructions.

Now in 2) I create an assembly program and use NASM for compiling and linking and then when this executable is given as test debuggers input it is showing 8 instructions as the count and which is apt.

The number of instructions executed in program 1) is high because of linking the program with system library's at runtime ? used -static and which reduces the count by a factor of 1/10. How can I ensure that the instruction count is only that of the main function in Program 1) and which is how Program 2) is reporting for the debugger?

1)

#include <stdio.h>

int main()
{
    printf("Hello, world!\n");
    return 0;
}    

I use gcc to create the executable.

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-                             

I build with:

nasm -f elf64 -o main.o -s main.asm  
ld -o main main.o
2
Why don't you do a fair comparison, call write() instead of printf() in your C-program. Also, you can actually see some of the code that runs before main() if you disassemble the executable (e.g. objdump -d [executable]).EOF
Yes.Sure,that's a finer point.crackerplace
Not sure why would someone want to close this.crackerplace
Why are there so many instructions (thousands) for such a simple 5 lines hello world program?Matt

2 Answers

6
votes

The number of instructions executed in program 1) is high because of linking the program with system library's at runtime?

Yep, dynamic linking plus CRT (C runtime) startup files.

used -static and which reduces the count by a factor of 1/10.

So that just left the CRT start files, which do stuff before calling main, and after.

How can I ensure that the instruction count is only that of the main function in Program 1)`

Measure an empty main, then subtract that number from future measurements.

Unless your instruction-counters is smarter, and looks at symbols in the executable for the process it's tracing, it won't be able to tell which code came from where.

and which is how Program 2) is reporting for the debugger.

That's because there is no other code in that program. It's not that you somehow helped the debugger ignore some instructions, it's that you made a program without any instructions you didn't put there yourself.

If you want to see what actually happens when you run the gcc output, gdb a.out, b _start, r, and single-step. Once you get deep in the call tree, you're prob. going to want to use fin to finish execution of the current function, since you don't want to single-step through literally 1 million instructions, or even 10k.


related: How do I determine the number of x86 machine instructions executed in a C program? shows perf stat will count 3 user-space instructions total in a NASM program that does mov eax, 231 / syscall, linked into a static executable.

6
votes

Peter gave a very good answer, and I'm going to followup with a response that is cringe worthy and might garner some down votes. When linking directly with LD or indirectly with GCC, the default entry point for ELF executables is the label _start.

Your NASM code uses a global label _start so when your program is run the first code in your program will be the instructions of _start. When using GCC your program's typical entry point is the function main. What is hidden from you is that your C program also has a _start label but it is supplied by the C runtime startup objects.

The question now is - is there a way to bypass the C startup files so that the startup code can be avoided? Technically yes, but this is perilous territory that could yield undefined behaviour. If you are adventurous you can actually tell GCC to change the entry point of your program with the -e command line option. Rather than _start we could make our entry point main bypassing the C startup code. Since we are bypassing the C startup code we can also dispense with linking in the C runtime startup code with the -nostartfiles option.

You could use this command line to compile your C program:

gcc test.c -e main -nostartfiles

Unfortunately, there is a bit of a gotchya that has to be fixed in the C code. Normally when using the C runtime startup objects, after the environment is initialized a CALL is made to main. Normally main does a RET instruction which returns back to the C runtime code. At that point the C runtime gracefully exits your program. RET doesn't have anywhere to return when the -nostartfiles option is used, so it will likely segfault. To get around that we can call the C library _exit function to exit our program.

#include <stdio.h>

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

    return 0;
}   

Unless you omit frame pointers there are a few extra instructions emitted by GCC to setup the stack frame and tear it down, but the overhead is minimal.

Special Note

The process above doesn't seem to work for static builds (-static option in GCC) with standard glibc C library. This is discussed in this Stackoverflow 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.

Installing MUSL as an alternative to GLIBC

On Ubuntu 64-bit these commands should build and install the 64-bit version of MUSL:

git clone git://git.musl-libc.org/musl
cd musl
./configure --prefix=/usr/local/musl/x86-64
make
sudo make install

You can then use the MUSL wrapper for GCC to work with the MUSL's C library instead of the default GLIBC library on most Linux distributions. Parameters are just like GCC so you should be able to do:

/usr/local/musl/x86-64/bin/musl-gcc -e main -static -nostartfiles test.c

When running ./a.out generated with GLIBC it would likely segfault. MUSL doesn't need initialization prior to using most of the C library functions, so it should work even with the -static GCC option.


A fairer comparison

One of the issues with your comparison is that you call the SYS_WRITE system call directly in NASM, in C you are using printf. User EOF correctly commented that you might want to make it a fairer comparison by calling the write function in C instead of printf. write has far less overhead to it. You could amend your code to be:

#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;
}

This will have more overhead than NASM's direct SYS_WRITE syscall, but far less than what printf would generate.


I'm going to issue the caveat that such code and trickery would likely not be taken well in a code review except for some fringe cases of software development.