GCC는 C를 어떻게 기계어로 짜깁기할까? - CS:APP 3.2 요약과 인사이트

2025. 4. 7. 15:53·CS:APP

사람이 작성한 고급 언어(C)의 소스코드를 GCC C 컴파일러는 어셈블리 코드 로 만들고, 어셈블러와 링커를 통해 기계어 코드를 생성한다.

고급 언어의 높은 수준의 추상화로 인하여, 밑단에서 일어나는 동작들을 알 수 없다.

그래서 개발자들은 어셈블리어를 이해하고 활용해야하는데, 그 이유는 다음과 같다

  • 최적화 성능 확인 및 튜닝 : 작성한 코드의 컴파일러를 통해 어셈블리 코드를 확인하고, 컴파일러의 성능 및 소스코드의 효율성 분석 및 소스코드의 변경을 통한 성능 극대화
  • 보안 취약점 분석 : 런타임 제어 정보 저장방식 약점을 활용한 버퍼 오버플로우 같은 공격에 대한 보호

3.2 프로그램의 인코딩

linux> gcc -Og -o p p1.c p2.c

코드 설명

  • -Og : 최적화 수준 지정 (학습목적 상 더 높은 최적화 적용시, 코드 구조의 변경 우려)
    • -O0, -O1, -O2, -O3, -Og:
      • -O0: 최적화를 전혀 하지 않음. 디버깅에 유리함.
      • -O1: 기본적인 최적화 적용, 간단한 인라이닝, 상수 전파, 불필요한 코드 제거 등 수행함.
      • -O2: -O1의 최적화에 추가로 루프 언롤링, 더욱 공격적인 인라이닝 등 대부분의 최적화 기법들을 적용함.
      • -O3: -O2의 최적화에 더해, 벡터화와 같은 고급 최적화를 수행함.
      • -Og: 디버깅에 적합한 최적화 옵션으로, 디버깅 정보를 잘 유지하면서 어느 정도 최적화를 적용함.
  • -o p : p1.c, p2.c를 컴파일 하여 생성될 실행파일의 명칭을 p로 지정

실행 결과



3.2.1 기계수준 코드

컴파일러는 추상화된 언어로 표현된 프로그램을, 프로세서가 실행하는 매우 기초적인 명령어(인스트럭션)들로 변환

명령어는 ISA(Instruction set architecture)에 의해 정의

  • ISA : 프로세서 상태, 명령어 형식, 명령어 실행에 따른 프로세서 상태변화 등에 대한 정의 (명세서)
    • 기본적인 명령어 필드 형태

  • CPU 제조사마다 설계가 다름

x86-64 명령어 포맷(가변 길이 명령어 형식)

- Prefix (옵션): 세그먼트, 락(LOCK), operand-size, REX 등 다양한 목적의 프리픽스가 붙을 수 있음
- Opcode: 연산 종류 지정
- ModR/M / SIB: 레지스터·메모리 오퍼랜드 지정 방식, 주소 계산 시 Base, Index, Scale 정보 포함
- Displacement: 메모리 주소 계산 시 사용되는 오프셋
- Immediate: 즉시값(상수) 지정
- → 복잡하고 유연한 인코딩 구조로, x86 호환성과 다양한 기능(세그먼트, 64비트 확장 등)을 지원하기 위해 계속 확장됨

ARM 명령어 포맷(고정 길이 명령어 형식)

  • 32비트(또는 16비트 Thumb 모드 등) 고정 길이 명령어.
  • Cond: 조건 코드(조건부 실행)
  • Immdediate Operand : 명령어에 상수값을 직접 입력
  • Opcode: 연산 종류
  • Set condition code(S 비트) : 플래그로, 이 명령어 실행 결과에 따라 조건 플래그를 업데이트. 이후의 조건부 분기 명령어들이 실행 여부를 결정하는데 사용
  • Rn, Rd: 원본 레지스터, 결과 레지스터 등
  • Operand2: 즉시값이거나 다른 레지스터 + 쉬프트·로테이트 등으로 구성.
  • → RISC 원칙(고정 길이, 단순 디코딩)을 따르므로, 디코더가 비트 필드를 일정 위치에서 바로 해석할 수 있음.
  • 요소 (참고 https://twojun-space.tistory.com/99)
    • 연산코드 (operation code; opcode)
      • 어떤 작업을 수행할지 결정하는 코드
      • 매우 기초적인 연산만 수행
        • 레지스터의 두 숫자를 더하는 등 수학적 연산
        • 메모리와 레지스터 간 데이터를 전송
        • 조건부 분기를 통해 새로운 명령어 주소로 이동(분기처리)
      • 각 opcode는 고정된 비트패턴으로 표현. 이는 CPU가 해석하여 실행
    • 원천 오퍼랜드 참조(source operand reference)
      • 명령어가 연산 수행 시 사용되는 데이터의 원본 위치나 값을 지정
      • 값은 레지스터, 메모리주소 또는 상수일 수 있음
      • 두 개의 숫자를 더하는 명령어라면, 덧셈의 첫 번째 피연산자(operand)를 의미
    • 결과 오퍼랜드 참조(result operand reference)
      • 연산의 결과가 저장되거나 출력되는 위치를 지정
      • 보통 레지스터나 메모리 주소를 가리키며, 연산 후 결과값을 기록
      • 덧셈 명령어에서 두 숫자의 합이 저장될 레지스터나 메모리 위치를 뜻함
    • 다음 명령어 참조(next instruction reference)
      • 명령어 실행 후, CPU가 다음에 실행할 명령어의 주소를 지정
      • 기본적으로 모든 명령어에 명시적으로 포함되진 않고, PC(프로그램 카운터)에 의해 자동적으로 순차실행되어 다음 명령어 주소를 참조
      • 분기나 점프 명령어에서는 이 값이 수정되어 프로그램의 흐름 제어를 가능케함

기계 수준 프로그램에서는 가상주소를 메모리 주소로 사용

  • 기계어는 메모리를 단순히 큰 바이트 주소 배열로 본다 (추상화)
    • 프로그램은 실제 다양한 데이터 타입이나 구조체로 메모리를 다루지만, 기계어 수준에서는 메모리를 하나의 큰 연속된 배열(바이트 단위로 지정된 주소공간)으로 인식
      • 주소 추상화 / 공간 분리 : 각 프로세스는 가상 주소 공간을 가져, 독립실행됨. 이는 메모리 보호와 프로세스 간 격리를 가능케 함
      • 메모리 보호 : 가상 주소 공간은 페이지 테이블을 통해 물리 주소에 매핑, 페이지마다 접근권한을 설정하여 실수 또는 악의적으로 다른 프로세스의 메모리 침범 불가하도록 함
      • 효율적 관리 : 페이지 테이블과 MMU(메모리 관리 유닛)를 통해, 필요하지 않은 페이지는 디스크의 스왑 영역으로 이동시켜 물리 메모리를 효율적으로 관리
      • 시스템 안정성 및 보안 : 메모리 접근 오류나 버퍼 오버플로우로 인한 시스템 충돌을 방지

프로그램 메모리 구성

  • 실행 파일로 포함된 기계어 코드
  • 운영체제에 필요한 정보
  • 프로시저 호출과 반환을 관리하는 런타임 스택
  • malloc과 같은 함수를 통해 할당된 사용자 메모리 블록

기계 수준 프로그램의 메모리와 프로세서 (코드의 CPU 조작)

  • 프로그램 카운터(PC, %rip) : 실행될 다음 명령어의 메모리 주소를 나타냄
  • 정수 레지스터 파일
    • 16개의 64비트 레지스터로 구성, 주소(포인터)나 정수 데이터를 저장
    • 일부 레지스터는 함수의 인자, 지역 변수, 반환 값 등 중요한 데이터를 임시 저장
  • 조건 코드 레지스터 : 최근 수행된 산술 또는 논리 연산의 결과 상태를 저장하며, 조건부 제어 흐름(if, while 등)을 구현하는데 활용
  • 벡터 레지스터 : 여러 정수 또는 부동 소수점 값을 동시 저장

3.2.2 코드 예제

// C 파일 mstore.c
long mult2(long, long);

void multstroe(long x, long y, long *dest) {
        long t = mul2(x, y);
        *dest = t;
}

어셈블리 코드 생성 linux> gcc -Og -S mstore.c

-S 는 소스 파일을 어셈블리 코드로 변환 후, 어셈블리 파일(.s) 생성. (어셈블러 호출 직전 종료)

; Assembly 파일 mstore.s
multstore: ;label, 함수의 시작 주소
    pushq  %rbx ;레지스터 %rbx의 현재 값을 스택에 저장
movq   %rdx, %rbx ;%rdx 값을 %rbx로 복사
call   mult2 ;mult2 함수 호출
movq   %rax, (%rbx) ; %rax 값을 %rbx가 가리키는 메모리 주소에 저장
popq   %rbx ; 스택에서 값을 꺼내 %rbx 레지스터에 복원
ret
  1. pushq %rbx: 함수 호출 시, %rbx와 같은 칼에 저장된 값이 나중에 복원되어야 할 때 사용함. (함수 내에서 %rbx를 임시 데이터 저장 용도로 사용하므로, 원래 값을 보존함)
  2. movq %rdx, %rbx: 이 함수에서는 세 번째 인수(dest 포인터)가 %rdx에 전달되는데, 이후에 이 포인터를 사용하기 위해 %rbx에 저장함.
  3. call mult2: mult2 함수에서 두 값의 곱을 계산한 후, %rax에 결과를 반환하기 위함임.
  4. movq %rax, (%rbx): mult2 함수가 반환한 결과(곱의 값, %rax에 있음)를 dest 포인터(%rbx가 가리킴)를 통해 저장함.
  5. popq %rbx: 함수 호출 전에 pushq로 저장했던 원래의 %rbx 값을 복원하여, 호출자에게 영향을 주지 않도록 함.
  6. ret: multstore 함수의 실행을 마치고 호출한 함수로 복귀함.

바이너리 코드 생성 linux> gcc -Og -c mstore.c

-c 소스 파일 컴파일 후, 링크 과정 생략하고 오브젝트 코드(.o)만 생성

Object 파일 mstore.o
53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3

바이너리 코드의 역 어셈블러 수행 linux> objdump -d mstore.o

역 어셈블(disassembly)를 통해 처음 어셈블리 코드와 거의 흡사하게 생성

728x90

'CS:APP' 카테고리의 다른 글

오퍼랜드 식별자(operand specifier 어셈블리의 시작과 끝 - CS:APP 3.4.1  (0) 2025.04.07
레지스터가 뭔데 - CS:APP 3.4  (0) 2025.04.07
암달의 법칙 (Amdahl's law)  (0) 2025.04.07
캐시와 저장장치의 상관관계  (0) 2025.03.21
1.4 프로세서는 메모리에 저장된 인스트럭션을 읽고 해석한다  (0) 2025.03.20
'CS:APP' 카테고리의 다른 글
  • 오퍼랜드 식별자(operand specifier 어셈블리의 시작과 끝 - CS:APP 3.4.1
  • 레지스터가 뭔데 - CS:APP 3.4
  • 암달의 법칙 (Amdahl's law)
  • 캐시와 저장장치의 상관관계
ahpicl64
ahpicl64
in the clouds
  • ahpicl64
    구름
    ahpicl64
  • 전체
    오늘
    어제
    • 분류 전체보기 (95)
      • WIL (4)
      • Jungle (36)
      • AWS (2)
      • SQL (2)
      • CS:APP (17)
      • Algorithm (10)
      • K8s (7)
      • 자료 구조 (10)
      • Spring (4)
      • React (0)
      • 운영체제 (1)
      • 기타등등 (2)
      • 이야기 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    부하테스트
    어셈블리
    컴퓨터시스템
    queue
    github actions
    python
    CloudFront
    DevOps
    알고리즘
    자료구조
    Spring
    IAM
    Spring boot
    트러블슈팅
    DB
    S3
    k8s
    AWS
    EC2
    CSAPP
  • 02-21 08:19
  • hELLO· Designed By정상우.v4.10.3
ahpicl64
GCC는 C를 어떻게 기계어로 짜깁기할까? - CS:APP 3.2 요약과 인사이트
상단으로

티스토리툴바