이 글에서는 GCC(GNU Compiler Collection)를 사용하여 C 또는 C++ 소스 코드에서 어셈블리 코드를 생성하고 확인하는 방법을 알아봅니다.
GCC는 소스 코드를 컴파일하는 과정에서 발생하는 모든 중간 결과물을 확인할 수 있는 강력한 기능을 제공합니다. 전처리기 출력, 어셈블리 코드, 오브젝트 파일 등 각 단계별 산출물을 손쉽게 검토할 수 있는데, 그중 어셈블러 출력을 얻으려면 '-S' 옵션을 사용하면 됩니다.
이 옵션을 사용하면 컴파일이 완료된 후, 어셈블러로 전달되기 직전 단계의 어셈블리 코드를 화면에 출력해 줍니다. 명령어 구문은 다음과 같습니다.
gcc -S program.cpp
위 명령을 실행하면 실행 파일을 생성하지 않고, 대신 program.s라는 이름의 어셈블리 파일이 생성됩니다. 출력 파일명을 직접 지정하고 싶다면 '-o' 옵션을 함께 사용할 수 있습니다(예: gcc -S program.cpp -o output.s).
예제 프로그램
실제 출력 결과가 어떤 형태로 나타나는지 확인해 보겠습니다. 여기서는 두 개의 숫자를 변수 x와 y에 저장한 뒤, 그 합을 다른 변수에 저장하고 결과를 출력하는 간단한 C++ 프로그램을 사용합니다.
#include <iostream>
using namespace std;
main() {
int x, y, sum;
x = 50;
y = 60;
sum = x + y;
cout << "Sum is: " << sum << endl;
}어셈블리 출력 결과
위 프로그램을 g++ -S test_cpp.cpp 명령으로 컴파일하면 다음과 같은 어셈블리 코드가 생성됩니다.
.file "test_cpp.cpp"
.text
.section .rodata
.type _ZStL19piecewise_construct, @object
.size _ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:
.zero 1
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.LC0:
.string "Sum is: "
.text
.globl main
.type main, @function
main:
.LFB1493:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $50, -12(%rbp)
movl $60, -8(%rbp)
movl -12(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
leaq .LC0(%rip), %rsi
leaq _ZSt4cout(%rip), %rdi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
movq %rax, %rdx
movl -4(%rbp), %eax
movl %eax, %esi
movq %rdx, %rdi
call _ZNSolsEi@PLT
movq %rax, %rdx
movq
_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip),
%rax
movq %rax, %rsi
movq %rdx, %rdi
call _ZNSolsEPFRSoS_E@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1493:
.size main, .-main
.type _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB1982:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
cmpl $1, -4(%rbp)
jne .L5
cmpl $65535, -8(%rbp)
jne .L5
leaq _ZStL8__ioinit(%rip), %rdi
call _ZNSt8ios_base4InitC1Ev@PLT
leaq __dso_handle(%rip), %rdx
leaq _ZStL8__ioinit(%rip), %rsi
movq _ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rax
movq %rax, %rdi
call __cxa_atexit@PLT
.L5:
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1982:
.size _Z41__static_initialization_and_destruction_0ii,
.-_Z41__static_initialization_and_destruction_0ii
.type _GLOBAL__sub_I_main, @function
_GLOBAL__sub_I_main:
.LFB1983:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $65535, %esi
movl $1, %edi
call _Z41__static_initialization_and_destruction_0ii
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1983:
.size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I_main
.hidden __dso_handle
.ident "GCC: (Ubuntu 7.3.0-16ubuntu3) 7.3.0"
.section .note.GNU-stack,"",@progbits출력 결과 해설
생성된 어셈블리 코드에서 주목할 만한 부분은 다음과 같습니다.
1. 변수 할당: movl $50, -12(%rbp)와 movl $60, -8(%rbp) 구문은 값 50과 60을 스택에 위치한 변수 x와 y에 각각 저장하는 부분입니다.
2. 덧셈 연산: addl %edx, %eax 명령이 실제 덧셈을 수행하며, 그 결과가 -4(%rbp), 즉 sum 변수에 저장됩니다.
3. 함수 호출: C++의 cout 출력 연산자는 내부적으로 _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc와 같은 맹글링(mangling)된 함수 이름으로 변환되어 호출됩니다. 이는 C++의 이름 맹글링(name mangling) 특성 때문입니다.
4. CFI 지시문: .cfi_startproc, .cfi_endproc 등의 지시문은 디버깅과 스택 되감기(stack unwinding)를 위한 호출 프레임 정보(Call Frame Information)를 나타냅니다.
이처럼 GCC의 '-S' 옵션을 활용하면 고급 언어로 작성한 코드가 실제 어떤 기계 수준의 명령으로 변환되는지 확인할 수 있으며, 이는 성능 최적화나 저수준 디버깅, 컴파일러 동작 학습에 매우 유용하게 활용됩니다.