cs

[크래프톤 정글 ] hello.c의 실행 과정

하루이2222 2024. 9. 30. 18:37

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와 같은 간단한 프로그램에서 실제로 호출되는 시스템 콜은 크게 두 가지다:

  1. write: printf 함수가 문자열을 터미널에 출력할 때, 내부적으로 write 시스템 호출이 사용된다.
  2. 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는 가변 인자 함수이므로 전달된 인자의 개수에 따라 다르게 동작하지만, 일반적으로 다음과 같은 흐름으로 동작한다:

  1. 문자열 준비: 출력할 문자열을 메모리에 준비한다.
  2. 시스템 콜 준비: write 시스템 콜을 호출할 준비를 한다. 파일 디스크립터(표준 출력), 버퍼(문자열), 버퍼 크기(문자열 길이)를 설정한다.
  3. 시스템 콜 호출: CPU가 시스템 콜 인터럽트를 발생시켜 커널 모드로 전환되고, write 시스템 콜이 실행된다.
    movq    $1, %rdi            ; 파일 디스크립터 1 (표준 출력)
    movq    $주소, %rsi          ; 출력할 문자열 주소
    movq    $문자열_길이, %rdx   ; 출력할 문자열의 길이
    syscall                     ; 시스템 콜 실행

위와 같은 어셈블리 코드를 통해 시스템 콜을 실행하여 실제로 문자열을 출력하게 된다.


전체 실행 플로우 요약

  1. 소스 코드 컴파일:
    • hello.c는 어셈블리 코드로 변환되고, 기계어로 컴파일된다.
  2. 메모리 로드 및 실행:
    • printf 함수가 호출되면서 write 시스템 콜을 사용해 문자열을 출력한다.
    • 프로그램 종료 시 exit 시스템 콜을 사용해 프로그램이 정상 종료된다.
  3. 어셈블리 내부:
    • main 함수는 스택 프레임을 설정하고, 문자열을 출력한 후 정상 종료된다.