2025. 10. 1. 01:18ㆍCSAPP
어.. 이번 부분은 좀 어렵기도 하고... 내용도 좀 많은 편이다.
그래서 이번부분은 솔직히 내가 개드립을 치기도 애매하고.. 쉽게 설명하기도 힘들다.(사실 쉬운것도 쉽게 설명을 못하긴 한다.)
이번 부분 부터는 조금씩 어셈블리어를 이해해야 하는 부분이 많다.
필자도 어셈블리어에 대한 이해가 없으나, 최대한 열심히는 써봤다.
본 글은 필자가 CS:APP의 3.2장을 읽고, 3.2장의 순서에 상관 없이 필자가 이해한대로 정리한 글이기에,
책의 내용이 뒤죽박죽 나오고 생략된 부분 또한 존재한다.
이를 염두해두고 본 글을 읽어주길 바란다.
3장은 전체적으로 프로그램이 기계 수준에서 어떻게 작동하는지에 대해 자세히 다룬 파트인데,
3.2장은 여기서 기계어의 내부 구조에 대해
크게 아래의 3가지를 다룬다.
- 기계어의 정적 구조
- 어셈블리어와 기계어의 대응
- 실행과 프로세서 상태
1. 기계어의 정적 구조
우리는 1.2장에서 컴파일러의 동작 순서인
전처리 → 컴파일 → 어셈블링 → 링킹에 대해 간략히 배웠었다.
이 부분은 이 컴파일러가 만든 실행파일(=기계어 코드)과 실행에 대해 다룬다.
(1) 실행 파일 만들기
linux> gcc -Og -o p p1.c p2.c
이 리눅스 CLI 명령어는
"p1.c와 p2.c라는 c언어로 작성된 코드 파일을 g단계로 컴파일 하여 p라는 실행파일을 만드세요."
라는 뜻으로,
단어별로 자세히 알아보자면,
linux>는 다 알겠지만, 리눅스 CLI의 프롬프트이다.(이제 이걸 모르면 당신은 인간이 아니다..)
우선 gcc는 리눅스에 내장되어 있는 GNU C Compiler라는 C언어 컴파일러의 약자로,
이 gcc 컴파일러를 호출하는 명령어 이다.
-Og는 디버깅 친화적 최적화 단계로, -O뒤에 최적화 단계를 써주면 된다.
-Og 은 디버깅 친화적 단계이고(매우 낮은 최적화 단계 이다.),
-O1 -O2 -O3 등의 -O뒤에 숫자가 있는 단계들이 일반적인 최적화 단계로,
-O뒤에 뒤의 숫자가 커질수록 최적화 수준이 높아진다.
다만 최적화 수준이 높아질수록, 원본 코드와 최적화된 코드간의 관계가 파악하기 어려워진다.
즉, 최적화 수준이 높아질수록 생성된 코드를 이해하기 어려워진다.
-o p 는 p라는 이름의 실행파일로 만들라는 말이고,
p1.c p2.c 는 p1과 p2라는 이름의 2개의 c언어로 작성된 파일을 컴파일 하라는 소리다.
(2) 실행파일의 생김새
저 명령어에서 만들어진 실행파일 p의 내부는 어떻게 되어있을까?
우선 당연하게 저 코드는 기계어 코드로 구성되어있다.
이 기계어 코드는 0,1로만 표시하는 바이너리 표현으로만 구성되어있고,
이 바이너리 표현은 CPU가 바로 읽을 수 있다.
이 컴파일/링크 과정에서 전역변수들의 논리적 주소는 결정되지만,
실제 메모리에 배치는 프로그램 실행 시 OS가 담당한다.
우리가 알아두어야 할 것은, 이 실행파일은 하나의 설계도 일뿐,
이 설계도를 보고 직접 실행하는 것은 OS에 지시를 받은 CPU라는 점이다.
2. 어셈블리어와 기계어의 대응
이제 우리가 이 기계어의 생김새와 생성 과정을 알아봤으니,
이게 어셈블리어와 어떻게 대응되어 바이너리 표현으로 바뀌는지 알아보자.
(1) ISA
기계어 코드의 여러 동작들(레지스터에 저장 / 삭제, 연산 등)은 명령어 집합(ISA, Instruction Set Architecture)이라는
소프트웨어와 하드웨어(특히 CPU) 사이의 약속을 통해 구현된다.
ISA는 마치 우리가 같은 언어와 문법을 써야 대화가 가능하듯,
CPU와 프로그램도 이 공용 규칙을 따라야만 올바르게 실행된다.
구체적으로 프로세서의 상태,
인스트럭션의 형식,
인스트럭션이 상태에 미치는 효과
를 규정한다.
대부분의 ISA는 마치 명령어가 하나 하나 순서대로 실행되는 것 처럼 보이는 순차 실행 모델을 제공한다.
이 순차 실행 모델은 실제로는 하드웨어상에서는 여러 명령어가 동시에 처리(=병렬로 처리)되지만,
순차적으로 실행하는 것처럼 보이게 해준다.
(2) 코드 예제
기계어는 앞서 설명한 ISA 규칙에 따라 CPU가 이해할 수 있는 명령어로 변환된다.
이제, C 코드가 실제로 어떻게 어셈블리 코드
그리고 최종 실행 파일로 변환되는지 예제를 통해 살펴보자.
long mult2(long, long);
void multstore(long x, long y, long *dest) {
long t = mult2(x, y);
*dest = t;
}
우선 이렇게 위와 같이 C로 작성된 곱셈 함수가 있다. → 이름을 mstore.c로 가정한다.
linux> gcc -Og -S mstore.c
mstore.c 를 디버깅 친화적 최적화 단계로 어셈블리 단계 까지 컴파일러를 실행하여, mstore.s를 만든다.
(이름을 지정해주지 않으면 기존 c파일과 같은 이름의 어셈블리파일이 생성된다.)
multstore:
pushq %rbx # %rbx를 스택에 저장
movq %rdx, %rbx # dest를 %rbx에 복사
call mult2 # mult2 호출
movq %rax, (%rbx) # 반환값을 *dest에 저장
popq %rbx # %rbx 복원
ret # 함수 종료
위와 같이 바뀐 어셈블리 코드는 각각 ISA에 정의된 특정 인스트럭션과 1:1로 대응한다.
pushq %rbx 에서의 pushq는 레지스터 값을 스택 에 저장하는 push,
movq %rdx, %rbx 에서의 movq는 값을 지정한 곳으로 복사하는 mov,
call mult2 에서의 call은 함수를 호출하고 리턴 주소를 스택에 저장하는 call,
popq %rbx 에서의 popq는 스택에서 값을 꺼내 지정한 곳에 저장하는 pop,
ret에서 함수 종료 후 리턴 주소로 이동하는 ret
으로 각각 1:1로 대응한다.
이후,
linux> gcc -Og -S mstore.c
위의 명령어 대신
linux> gcc -Og -o mstore mstore.c
위의 명령어를 통해 컴파일하여 mstore.c 파일을 mstore라는 이름 실행 파일로 만들 수 있다.
3. 실행과 프로세서 상태
위에서 실행파일이 만들어지는 과정과, 실행파일의 생김새들을 알아봤으니,
만들어진 실행파일이 실제로 메모리상에서 어떻게 동작하는지,
어떻게 CPU가 인스트럭션들을 처리하는지를 알아보겠다.
(1) 프로세서 상태
CPU가 인스트럭션들을 처리하기 위해 내부적으로 유지하는 정보들을 프로세서 상태라고 한다.
프로세서 상태의 대표적 4가지는 아래와 같다.
- 레지스터 → CPU 내부의 초고속 저장 공간으로, 계산과 데이터 이동에 필요한 정보들이 저장된다.
- 프로그램 카운터(= PC) → CPU가 다음에 실행할 인스트럭션의 메모리 주소를 가리킨다.
- 메모리 → 실행 파일의 실행에 필요한 전역변수, 동적 할당 변수, 스택, 인스트럭션 등이 위치하는 저장 공간이다.
- 스택 → 함수 호출 시, 함수 내부의 지역변수, 리턴 주소, 계산에 필요한 임시 데이터 등을 저장한다.
여기서 레지스터는 대표적으로 3가지로 또 나뉘게 되는데,
- 정수 레지스터 → 일반적인 산술/논리연산, 주소 계산에 사용되는 정보들을 저장한다.
- 백터 레지스터 → 여러 데이터를 동시에 처리하는 연산(= SIMP연산)들에 사용되는 정보들을 저장한다.
- 조건 코드 레지스터 → 비교 연산의 결과를 기록하여 분기 명령어(if, while문 등) 실행에 사용되는 정보를 저장한다.
이들은 전부 프로세서(=CPU)를 구성하는 핵심요소들이다.
(2) 가상주소
실행 파일 실행 시, 프로그램이 사용하는 메모리의 일부 공간(= 프로그램 메모리 → 아래에서 다룬다.)을 가상주소로 관리된다.

프로그램 내의 모든 포인터, 주소 계산(인덱스 등을 통한)은 가상주소를 기준으로 이루어지며,
이 가상주소의 실제 메모리 상에서 어디에 위치할지는 프로그램 실행 시에 OS가 결정하고,
CPU는 MMU(Memory Management Unit)라는 CPU 내부 하드웨어를 통해 가상주소를 실제 물리주소로 변환하고,
이를 통해 가상주소에 있는 데이터들에 접근한다.
이 가상주소는 프로그램이 다른 프로그램에서 사용되는 메모리를 간섭하지 않도록 보호하고,
하나의 큰 배열 처럼 큰 연속 메모리를 사용할 수 있게 하고,
프로그램이 사용하지 못하는 빈공간을 줄여준다.
즉, 메모리 단편화를 최소화 해준다.
이러한 메모리의 효율성과 보호 라는 장점들 때문에 프로그램의 모든 데이터는 가상 주소 안에서 관리된다.
(3) 프로그램 메모리
프로그램 메모리는 프로그램의 실행 가능한 기계어 코드,
OS에서 필요한 정보(예 :전역 변수를 배치하기 위해 필요한 논리적 주소),
함수 호출과 반환을 관리하는 런타임 스택,
사용자가 할당한 메모리(예 : c에서의 malloc) 등을 포함하는
프로그램이 사용하는 메모리 전체를 뜻한다.
이 메모리는 위에서 설명한 것처럼, 가상주소를 하여 접근되며,
특정 시점에 유요한 가상주소 범위(=실행중인 프로그램 하나에게 할당된 가상주소 메모리만) 사용 가능하다.
오늘은 글이 좀 길었다. 다들 이 부분을 잘 이해해서 cs적 지식을 향상시키길 바란다.
다들 긴글을 읽어주어 고맙고 고생했다.
오늘의 추천곡은 spica - Rokudenashi 이다.

즐코딩.
'CSAPP' 카테고리의 다른 글
| [Week6 - CSAPP] 3.4 - 여친 만드는 법 : 어셈블리어 배우기 (3) | 2025.10.18 |
|---|---|
| [Week5 - CSAPP] 3.3 - 데이터의 형식 (0) | 2025.10.17 |
| [Week3 - CSAPP] 3.1 - 역사적 관점(근데 약간 이상한) (0) | 2025.09.19 |
| [Week2 - CSAPP] 1.9 - 중요한 주체들 (0) | 2025.09.15 |
| [Week1 - CSAPP] 1.5 - 캐시가 중요하다. (0) | 2025.09.08 |