1

I'm working on reverse engineering a classic game (early 2000s) and am fortunate enough to have debug symbols for a different platform other than PC for reference.

I've noticed that the same functions on the PC version (compiled using visual studio) often have several other functions inside the actual function, breaking functionality into smaller chunks. But on the other platform (compiled using GCC), it's just one large function, are these compiler generated? Or maybe GCC optimized them out?

2 Answers 2

3

That really depends. There are two primary explanations to this phenomenon, which point to opposite answers.

First, a compiler optimization known as "inlining" inserts the body of the called function into the calling function, thereby eliminating the call instruction, but making the calling function bigger.

Secondly, games are usually written in C++. Different compilers have different implementations for the C++ standard library (and standard template library). That is to say, both binaries will have completely different implementations for C++ library functions, such as std::vector<GameObject>::push_back. They can differ in every aspect, such as how many functions they call internally, the control flow structure of the function, and so on.

Either one of, or both of, those phenomena could be the cause of what you're seeing.

2

The generated machine code is going to vary by compiler, and even within the same compiler depending on compiler optimization settings. Sometimes optimizations will "inline" functions, removing what was a function call in the code. Sometimes external libraries will be compiled "statically" i.e. their functions get incorporated directly into the module vs being call externally (i.e. via a DLL in Windows) I find a very good way to explore some of this behavior is with online tool https://godbolt.org/

For most accurate results related to your game you would need to work out which specific compiler was used i.e. Microsoft Visual C++ 6.0 and you can specify compiler options to output assembly to see what kind of code it can generate.

Taking a very simplistic example:

The following code:

#include <iostream>

int main()
{
    printf("Hello, World!");
    return 0;
}

We can see the output with x86 msvc v19 latest:

$SG34000 DB     'Hello, World!', 00H
std::_Fake_allocator const std::_Fake_alloc ORG $+1         ; std::_Fake_alloc
unsigned __int64 `__local_stdio_printf_options'::`2'::_OptionsStorage DQ 01H DUP (?) ; `__local_stdio_printf_options'::`2'::_OptionsStorage

_main   PROC
        push    ebp
        mov     ebp, esp
        push    OFFSET $SG34000
        call    _printf
        add     esp, 4
        xor     eax, eax
        pop     ebp
        ret     0
_main   ENDP

But with optimize for speed:

unsigned __int64 `__local_stdio_printf_options'::`2'::_OptionsStorage DQ 01H DUP (?) ; `__local_stdio_printf_options'::`2'::_OptionsStorage
`string' DB 'Hello, World!', 00H ; `string'

_main   PROC                                      ; COMDAT
        push    OFFSET `string'
        call    _printf
        add     esp, 4
        xor     eax, eax
        ret     0
_main   ENDP

But if optimized for size:

    unsigned __int64 `__local_stdio_printf_options'::`2'::_OptionsStorage DQ 01H DUP (?) ; `__local_stdio_printf_options'::`2'::_OptionsStorage
`string' DB 'Hello, World!', 00H ; `string'

_main   PROC                                      ; COMDAT
        push    OFFSET `string'
        call    _printf
        pop     ecx
        xor     eax, eax
        ret     0
_main   ENDP

Then look at GCC default output:

    .LC0:
        .string "Hello, World!"
main:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        mov     eax, 0
        pop     rbp
        ret
__static_initialization_and_destruction_0(int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        cmp     DWORD PTR [rbp-4], 1
        jne     .L5
        cmp     DWORD PTR [rbp-8], 65535
        jne     .L5
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        call    __cxa_atexit
.L5:
        nop
        leave
        ret
_GLOBAL__sub_I_main:
        push    rbp
        mov     rbp, rsp
        mov     esi, 65535
        mov     edi, 1
        call    __static_initialization_and_destruction_0(int, int)
        pop     rbp
        ret

With optimization -O3 gcc produces:

  .LC0:
            .string "Hello, World!"
    main:
            sub     rsp, 8
            mov     edi, OFFSET FLAT:.LC0
            xor     eax, eax
            call    printf
            xor     eax, eax
            add     rsp, 8
            ret
    _GLOBAL__sub_I_main:
            sub     rsp, 8
            mov     edi, OFFSET FLAT:_ZStL8__ioinit
            call    std::ios_base::Init::Init() [complete object constructor]
            mov     edx, OFFSET FLAT:__dso_handle
            mov     esi, OFFSET FLAT:_ZStL8__ioinit
            mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
            add     rsp, 8
            jmp     __cxa_atexit

Meanwhile clang produces:

    __cxx_global_var_init:                  # @__cxx_global_var_init
        push    rbp
        mov     rbp, rsp
        movabs  rdi, offset std::__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        movabs  rdi, offset std::ios_base::Init::~Init() [complete object destructor]
        movabs  rsi, offset std::__ioinit
        movabs  rdx, offset __dso_handle
        call    __cxa_atexit
        pop     rbp
        ret
main:                                   # @main
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     dword ptr [rbp - 4], 0
        movabs  rdi, offset .L.str
        mov     al, 0
        call    printf
        xor     eax, eax
        add     rsp, 16
        pop     rbp
        ret
_GLOBAL__sub_I_example.cpp:             # @_GLOBAL__sub_I_example.cpp
        push    rbp
        mov     rbp, rsp
        call    __cxx_global_var_init
        pop     rbp
        ret
.L.str:
        .asciz  "Hello, World!"

Clang with optimizations:

main:                                   # @main
        push    rax
        mov     edi, offset .L.str
        xor     eax, eax
        call    printf
        xor     eax, eax
        pop     rcx
        ret
_GLOBAL__sub_I_example.cpp:             # @_GLOBAL__sub_I_example.cpp
        push    rax
        mov     edi, offset std::__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edi, offset std::ios_base::Init::~Init() [complete object destructor]
        mov     esi, offset std::__ioinit
        mov     edx, offset __dso_handle
        pop     rax
        jmp     __cxa_atexit                    # TAILCALL
.L.str:
        .asciz  "Hello, World!"

If we have a simple function called:

    #include <iostream>

int addone(int n)
{
    return n+1;
}

int main()
{
    printf("%i",addone(1));
    return 0;
}

Without optimization msvc produces:

$SG34003 DB     '%i', 00H
std::_Fake_allocator const std::_Fake_alloc ORG $+1         ; std::_Fake_alloc
unsigned __int64 `__local_stdio_printf_options'::`2'::_OptionsStorage DQ 01H DUP (?) ; `__local_stdio_printf_options'::`2'::_OptionsStorage

_n$ = 8                                       ; size = 4
int addone(int) PROC                                    ; addone
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _n$[ebp]
        add     eax, 1
        pop     ebp
        ret     0
int addone(int) ENDP                                    ; addone

_main   PROC
        push    ebp
        mov     ebp, esp
        push    1
        call    int addone(int)               ; addone
        add     esp, 4
        push    eax
        push    OFFSET $SG34003
        call    _printf
        add     esp, 8
        xor     eax, eax
        pop     ebp
        ret     0
_main   ENDP

but with optimizations the function "addone" is no longer called, and just the direct value "2" is used:

unsigned __int64 `__local_stdio_printf_options'::`2'::_OptionsStorage DQ 01H DUP (?) ; `__local_stdio_printf_options'::`2'::_OptionsStorage
`string' DB '%i', 00H                    ; `string'

_n$ = 8                                       ; size = 4
int addone(int) PROC                                    ; addone, COMDAT
        mov     eax, DWORD PTR _n$[esp-4]
        inc     eax
        ret     0
int addone(int) ENDP                                    ; addone

_main   PROC                                      ; COMDAT
        push    2
        push    OFFSET `string'
        call    _printf
        add     esp, 8
        xor     eax, eax
        ret     0
_main   ENDP
1
  • Keep in mind, however, that between now and that game there are ~20 years of compiler evolution. Still a valid and valuable explanation.
    – 0xC0000022L
    Jan 11, 2022 at 8:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.