이 글에서는 GCC(GNU Compiler Collection)를 사용하여 C 또는 C++ 소스 코드에서 어셈블리 언어 출력을 생성하는 방법을 알아봅니다.
GCC의 -S 옵션이란?
GCC는 소스 코드를 컴파일하는 과정에서 발생하는 모든 중간 산출물을 확인할 수 있는 강력한 기능을 제공합니다. 그중 -S 옵션을 사용하면 어셈블러에 전달되기 직전, 즉 컴파일이 완료된 시점의 어셈블리 코드를 확인할 수 있습니다.
명령어 사용 문법은 다음과 같습니다.
gcc –S program.cpp
이 명령을 실행하면 program.cpp 파일이 오브젝트 파일까지 빌드되지 않고, 어셈블리 코드가 담긴 program.s 파일만 생성됩니다.
예제 코드
간단한 프로그램으로 실제 출력 결과를 살펴보겠습니다. 이 프로그램은 두 개의 숫자를 변수 x와 y에 저장한 뒤, 두 수의 합을 다른 변수에 대입하고 결과를 화면에 출력합니다.
#include <iostream>
using namespace std;
main() {
int x, y, sum;
x = 50;
y = 60;
sum = x + y;
cout << "Sum is: " << sum << endl;
}어셈블리 출력 결과
위 코드를 gcc -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
출력 결과 해설
생성된 어셈블리 코드에서 주목할 만한 핵심 부분은 다음과 같습니다.
- 변수 초기화:
movl $50, -12(%rbp)와movl $60, -8(%rbp)는 각각 x와 y에 50과 60을 저장하는 구문입니다. - 덧셈 연산:
addl %edx, %eax가 두 값을 더해 sum 변수에 저장하는 역할을 수행합니다. - 문자열 상수:
.LC0섹션에는 "Sum is: " 문자열이 rodata(읽기 전용 데이터) 영역에 저장되어 있습니다. - 함수 호출:
call명령들을 통해 cout 출력 및 endl 처리를 위한 내부 함수들이 호출됩니다.
C++ 코드이기 때문에 이름 맹글링(name mangling)이 적용된 함수명(_ZNSolsEi@PLT 등)도 함께 보입니다. 참고로 AT&T 문법 대신 인텔 문법으로 출력하고 싶다면 -masm=intel 옵션을 추가로 사용하면 됩니다.