[크래프톤 정글 ] hello.c의 실행 과정
hello.c
를 실행할 때 호출되는 시스템 호출(system call)과 어셈블리 파일의 내부 구조에 대해 자세히 알아보자.
전처리 및 컴파일
1. 소스 코드 작성 및 컴파일
- C 소스 코드 작성:
hello.c
파일에 "Hello, World!"를 출력하는 코드가 작성된다. - 컴파일: 소스 코드는 컴파일러에 의해 어셈블리 코드로 변환된다. 이 과정에서 다음과 같은 어셈블리 코드가 생성된다:
이 어셈블리 코드는 고수준의leaq .LC0(%rip), %rdi ; 문자열 "Hello, World!"의 주소를 레지스터 %rdi에 저장 call printf ; printf 함수 호출
printf
호출이 저수준의 명령어로 변환된 결과다.
2. 라이브러리 함수 호출
- 라이브러리 함수 준비: 컴파일러는 소스 코드에서
printf
와 같은 표준 라이브러리 함수에 대한 호출을 포함한다. 이때,printf
함수는 실제로 내부적으로 여러 작업을 수행하여 출력할 데이터를 준비하고, 시스템 콜을 호출하기 전에 필요한 매개변수를 설정한다. - 레지스터 설정:
printf
함수는 출력할 문자열의 주소를%rdi
와 같은 특정 레지스터에 저장하고, 준비된 데이터를 기반으로 시스템 콜을 호출할 준비를 한다.
3. 시스템 콜 인터럽트 발생
- 시스템 콜 호출 준비: 라이브러리 함수(
printf
)가 데이터를 준비한 후, 시스템 콜을 호출하기 위해 인터럽트가 발생한다. - 인터럽트 발생: x86-64 아키텍처에서는 시스템 콜을 호출할 때
syscall
명령어가 사용되며, 이를 통해 커널 모드로 전환된다. 이때write
시스템 콜이 호출되며, 파일 디스크립터, 버퍼, 버퍼 크기 등의 정보가 레지스터에 설정된다.
실행
1. 시스템 콜 (System Calls)
hello.c
와 같은 간단한 프로그램에서 실제로 호출되는 시스템 콜은 크게 두 가지다:
write
:printf
함수가 문자열을 터미널에 출력할 때, 내부적으로write
시스템 호출이 사용된다.exit
: 프로그램이 종료될 때exit
시스템 호출을 통해 운영체제에 프로그램이 종료되었음을 알린다.
1.1. write
시스템 콜
printf
함수는 실제로 write
시스템 콜을 호출하여 표준 출력(파일 디스크립터 1)을 사용해 터미널에 문자열을 출력한다. write
시스템 콜은 다음과 같이 동작한다:
- 파일 디스크립터: 표준 출력(
stdout
)의 파일 디스크립터는 1이다. - 버퍼: 출력할 데이터(문자열).
- 버퍼 크기: 출력할 데이터의 길이.
시스템 콜의 형태는 다음과 같다:
ssize_t write(int fd, const void *buf, size_t count);
fd
가 표준 출력(1)이고, buf
에 출력할 문자열, count
에 출력할 길이가 들어간다.
1.2. exit
시스템 콜
프로그램이 정상적으로 종료되면 exit
시스템 콜이 호출된다. 이는 운영체제에 프로그램이 종료되었음을 알리며, 종료 코드를 반환한다.
시스템 콜의 형태는 다음과 같다:
void exit(int status);
status
에 반환할 종료 코드를 전달하며, 보통 성공적으로 종료되었음을 알리는 0이 반환된다.
2. 어셈블리 파일의 내부
C 코드를 컴파일하면, 각 고수준 명령어는 저수준 어셈블리 명령어로 변환된다. 여기서는 hello.c
의 어셈블리 코드로 변환된 예시를 보여주겠다. 이를 위해 GCC를 사용하여 어셈블리 파일을 생성할 수 있다.
gcc -S hello.c -o hello.s
이 명령을 실행하면 hello.s
라는 어셈블리 파일이 생성된다. 이 파일에는 hello.c
가 어셈블리 코드로 변환된 내용이 포함된다. 일반적으로 어셈블리 코드는 크게 다음과 같은 구조로 나뉜다:
2.1 어셈블리 파일의 주요 부분
.file "hello.c"
.section .rodata
.LC0:
.string "Hello, World!"
위 코드는 프로그램에서 사용된 문자열 "Hello, World!"를 메모리에 저장하는 부분이다. printf
에서 출력할 문자열이 .rodata
(읽기 전용 데이터) 섹션에 저장된다.
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
이 부분은 main
함수의 시작이다. 스택 프레임을 설정하기 위해 %rbp
레지스터에 현재 스택 포인터 %rsp
를 저장하고, 기존의 %rbp
값을 스택에 저장한다.
leaq .LC0(%rip), %rdi
call puts
leaq
명령어는 문자열 "Hello, World!"의 메모리 주소를 계산하여 %rdi
레지스터에 저장한다. 이때 %rip
는 현재 명령어 위치 레지스터를 나타내며, 메모리 주소를 기반으로 오프셋을 계산한다. 이후 puts
함수가 호출되어 문자열을 출력한다.
movl $0, %eax
popq %rbp
ret
이 부분은 함수 종료를 처리한다. $0
을 %eax
에 저장하는 것은 main
함수가 0을 반환한다는 의미이다. 스택 프레임을 복구한 후 ret
명령어로 함수가 종료된다.
2.2 printf
내부 어셈블리 동작
printf
함수는 C 라이브러리에 정의된 함수로, 내부적으로 write
시스템 콜을 호출하여 문자열을 출력한다. printf
는 가변 인자 함수이므로 전달된 인자의 개수에 따라 다르게 동작하지만, 일반적으로 다음과 같은 흐름으로 동작한다:
- 문자열 준비: 출력할 문자열을 메모리에 준비한다.
- 시스템 콜 준비:
write
시스템 콜을 호출할 준비를 한다. 파일 디스크립터(표준 출력), 버퍼(문자열), 버퍼 크기(문자열 길이)를 설정한다. - 시스템 콜 호출: CPU가 시스템 콜 인터럽트를 발생시켜 커널 모드로 전환되고,
write
시스템 콜이 실행된다.
movq $1, %rdi ; 파일 디스크립터 1 (표준 출력)
movq $주소, %rsi ; 출력할 문자열 주소
movq $문자열_길이, %rdx ; 출력할 문자열의 길이
syscall ; 시스템 콜 실행
위와 같은 어셈블리 코드를 통해 시스템 콜을 실행하여 실제로 문자열을 출력하게 된다.
전체 실행 플로우 요약
- 소스 코드 컴파일:
hello.c
는 어셈블리 코드로 변환되고, 기계어로 컴파일된다.
- 메모리 로드 및 실행:
printf
함수가 호출되면서write
시스템 콜을 사용해 문자열을 출력한다.- 프로그램 종료 시
exit
시스템 콜을 사용해 프로그램이 정상 종료된다.
- 어셈블리 내부:
main
함수는 스택 프레임을 설정하고, 문자열을 출력한 후 정상 종료된다.