[CSAPP] Chapter 1 - A Tour of Computer Systems

nogosan·2025년 8월 10일

CSAPP

목록 보기
1/1

이 글은 전문가가 아닌 학부생이 개인 공부 목적으로 작성하는 글인 만큼, 내용상 오류번역 오류가 있을 가능성이 있습니다. 참고용으로만 봐 주세요!

2025-08-15 WIP (~46p)

Chapter 1 - A Tour of Computer Systems

  • 컴퓨터 시스템 : 하드웨어 + 시스템 소프트웨어
  • 모든 컴퓨터 시스템은 비슷한 기능을 하는, 비슷한 소프트웨어 및 하드웨어 요소들로 구성되어 있음
  • Chapter 1은 아래의 hello 프로그램의 작성부터 실행까지의 과정을 살펴보며 이후에 등장할 개념들을 소개하는 구성임
#include <stdio.h>

int main()
{
	printf("hello, world\n");
	return 0;
}


1.1 Information is Bits + Context

  • hello 프로그램은 프로그래머가 에디터로 작성하고 텍스트 파일(hello.c)로 저장한 소스 프로그램(소스 파일)로 시작함
  • 소스 프로그램은 0 또는 1의 값을 가지는 비트(bit)의 배열이며, 이 비트들은 8개씩(8비트씩) 묶여 1개의 바이트(byte) 단위로 구성됨

  • 대부분 컴퓨터 시스템들은 ASCII 표준으로 문자를 나타내는데, 이때 하나의 문자는 1바이트 크기의 숫자로 표현됨
  • hello.c는 ASCII에 따라 아래의 표의 기준대로 저장됨

  • 모든 바이트는 어떠한 문자(character)에 대응하는 정수 값을 저장하고 있음
  • 이때 모든 줄은 사용자에게 보이지 않는, 10의 정수값을 가지는 개행문자(newline) \n로 끝남
  • hello.c와 같이 ASCII 문자로만 구성되어 있는 파일을 텍스트(text) 파일이라고 하며, 다른 모든 파일을 바이너리(binary) 파일이라고 함.

  • 컴퓨터 시스템의 모든 정보는 비트 덩어리로 구성되어 있음
  • 데이터 덩어리들을 구분하는 유일한 기준은 그 데이터를 보는 맥락(context)
  • 같은 바이트의 배열도 맥락에 따라 정수를 나타내거나, 부동소수점을 나타내거나, 문자열을 나타내거나, 기계어 명령 등을 나타낼 수 있음.




1.2 Programs Are Translated by Other Programs Into Different Forms

  • hello 프로그램은 인간이 읽을 수 있는 형태의 고수준(high-level) C 프로그램으로 시작함
  • hello.c를 컴퓨터에서 실행하려면 C언어 문장들이 다른 프로그램에 의해 저수준(low-level)기계어(machine-language) 명령들로 변환되어야 함
  • 번역된 명령들은 실행 파일(executable object program)로 변환되어 바이너리 파일로 저장됨
linux> gcc -o hello hello.c
  • UNIX 시스템에서의 소스 파일 -> 실행 파일(오브젝트) 변환은 컴파일러 드라이버(compiler driver)가 실행함
  • gcc 컴파일러 드라이버는 소스 파일(hello.c)를 읽고 실행 파일 hello로 변환함

  • 전처리기(preprocessor), 컴파일러(compiler), 어셈블러(assembler), 링커(linker) 총 4계의 단계를 거치며, 이 4단계를 합쳐서 컴파일러 시스템(compilation system)이라고 함

전처리기 과정 (Preprocessing phase)

  • 전처리기(cpp)는 코드를 #으로 시작하는 지시문에 따라 수정함
    • #include <stdio.h> 지시는 전처리기에 시스템 헤더 파일 stdio.h의 내용을 프로그램에 삽입하라는 지시임
  • 이 과정으로 수정된 C 프로그램 (.i로 끝남)이 생성됨

컴파일러 과정 (Compilation phase)

  • 컴파일러(cc1)은 hello.ihello.s로 변환하는데, 이때 hello.s어셈블리어 프로그램을 포함하는 텍스트 파일임
  • main 함수의 경우 다음과 같이 변환됨
main:
	subq $8, %rsp
	movl $.LC0, %edi
	call puts			// puts 함수 호출
	movl $0, %eax
	addq $8, %rsp
	ret
  • subq ~ ret는 모두 각각 1개의 기계어 명령을 텍스트 형태로 나타낸 것임

어셈블리 과정 (Compilation phase)

  • 어셈블러(as)는 hello.s를 기계어 명령들로 변환한 후 오브젝트 파일(relocatable object program)이라는 형태로 포장해 hello.o로 저장함
  • hello.omain 함수의 명령들을 저장하는 바이너리 파일임

링크 과정 (Compilation phase)

  • hello.c는 C 표준 라이브러리의 printf 함수를 호출하는데, 이때 printf 함수는 printf.o라는 미리 컴파일되어 있는 다른 파일에 저장되어 있음
  • 프로그램이 작동하려면printf.ohello.o랑 합쳐야 하는데, 링커(ld)가 이 합치는 과정을 담당하며, 결과물로 hello라는 실행 파일(executable)을 생성함
  • hello 파일은 이제 메모리에 탑재해 실행할 수 있음


1.3 It Pays to Understand How Compilation Systems Work

  • 간단한 프로그램에서는 컴파일러가 보통 알아서 잘함
  • 그러나 프로그래머들이 컴파일러 시스템의 작동 원리를 이해해야 하는 중요한 이유들이 있음

프로그램 성능 최적화 (Optimizing program performance)

  • 현대의 컴파일러들은 대부분의 경우 좋은 코드를 출력하고, 굳이 우리가 컴파일러의 내부 구조를 알 필요는 없음
  • 그러나, C 프로그램을 만들 때 올바른 결정을 내리기 위해서는 기계어 및 컴파일러 과정을 이해할 필요가 있음
  • 프로그래밍에서 가장 어렵고 이해하기 어려운 오류 중 하나는 링커 오류임
  • 일부 링커 오류는 프로그램 실행 전까지 나타나지 않기도 함

보안 실수 피하기 (Avoiding security holes)

  • 버퍼 오버플러우(buffer overflow) 취약점은 수년 동안 네트워크 및 인터넷 분야에서 보안 이슈가 되었음
  • 신뢰하지 않는 출처에서 온 데이터는 조심스럽게 접근하고 다룰 필요가 있음



1.4 Processors Read and Interpret Instructions Stored in Memory

  • 위의 과정을 통해 소스 프로그램 hello.c는 실행 가능한 hello 오브젝트 파일로 바뀜
  • UNIX 시스템에서는 실행을 하기 위해 shell(셸)이라는 프로그램에 프로그램 이름을 입력함
linux> ./hello
hello, world
linux>
  • shell(셸)은 명령어 해석기로, 사용자의 명령을 입력받아 수행함
  • 명령의 첫 번째 단어가 셸의 내장 명령이 아니면 해당 단어가 실행 파일 이름이라고 간주함
  • 셸은 다음과 같은 과정을 반복함
1. 특정 프롬프트를 출력 (linux>)
2. 입력 대기 -> 입력 받음 (./hello)
3. 입력된 명령 수행 (hello, world)
4. -> 다시 1번부터 시작함 (>linux)

1.4.1 Hardware Organization of a System

  • hello 프로그램을 실행했을 때 어떤 일이 일어나는지 이해하기 위해서 일반적인 컴퓨터 시스템의 하드웨어 구조를 알아야 함
  • 아래의 구조는 최신 Intel 시스템의 구조를 따른 것으로, 모든 컴퓨터 시스템들은 대체로 비슷한 구성을 가짐
  • 아래 그림은 대략적인 요약임 (실제로는 더 복잡하다)

Buses (버스)

  • 버스(bus)는 시스템 구성요소 사이에서 정보를 전달하는 전기 회로임
  • 버스는 word(워드)라고 하는 정해진 크기의 바이트들을 단위로 정보를 전달하며, 이때 워드 1개 안에 있는 바이트 수 (= 워드 크기, word size)는 매우 중요한 시스템 변수임
  • 현대 컴퓨터 시스템의 워드 크기는 시스템에 따라 다르지만 보통 4바이트(32비트) 또는 8바이트(64바이트)임

cf. 32비트, 64비트

  • 컴퓨터 프로그램을 설치하다 보면 종종 설치 파일이 32비트/64비트 (혹은 x86/x64) 구분이 되어 있는 것을 볼 수 있다.
  • 이는 워드 크기의 차이로 인한 것으로, 32비트 워드를 가진 시스템에는 32비트 설치 파일을, 64비트 시스템에는 64비트 설치 파일을 이용해야 한다.
    • (Windows와 같은 운영 체제에서는 64비트 시스템에서도 호환성을 위해 지원한다.)

I/O Devices (입출력 장치)

  • Input/Output Devices (I/O Devices, 입출력 장치)는 시스템을 외부(사용자)와 연결함
  • 예를 들어, 위 그림에서 I/O 장치는 마우스, 키보드, 디스플레이, 디스크 드라이브 총 4개임
  • 각각의 I/O 장치는 controller(컨트롤러) 또는 adapter(어댑터)에 의해 I/O 버스에 연결되며, 컨트롤러/어댑터는 I/O 버스와 I/O 장치 간 정보 전달을 가능하게 함
  • 컨트롤러와 어댑터의 차이는 패키징의 여부임
    • 컨트롤러는 장치에 붙어있거나 시스템의 메인 회로 (메인보드)에 장착되어 있음
    • 어댑터는 메인보드에 꽂을 수 있는 형태의 장치임

Main Memory (메인 메모리)

  • 메인 메모리(Main Memory)는 임시 저장 공간으로, 프로그램이 실행되는 동안 프로그램 그 자체및 프로그램이 다루는 데이터를 저장함
  • 물리적으로, 메인 메모리는 DRAM(Dynamic Random Access Memory) 칩들로 구성되어 있음
  • 논리적으로, 메인 메모리는 바이트들의 배열로, 이때 모든 칸들은 0부터 시작하는 고유한 주소(배열 인덱스)를 가짐
    • (ex) [ 0x0 | 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | ... ]
    • 간단하게 말하자면, 프로그래밍 언어에서의 배열(array)와 같은 구조를 생각하면 됨
  • 프로그램을 구성하는 기계어 명령은 일정한 바이트 수를 차지함
  • 자료형에 따라 서로 다른 공간을 메모리에서 차지함 (char는 보통 1바이트, int는 보통 4바이트...)

Processor (프로세서)

  • 프로세서(Processor/CPU)는 메인 메모리에 저장된 명령들을 해석(실행)함
  • 프로세서의 중심에는 프로그램 카운터(Program Counter, PC)가 있음
    • PC는 레지스터(register)라고 불리는 워드 사이즈 크기의 저장 장치임
    • PC는 항상 메인 메모리에 저장되어 있는 어떤 기계어 명령의 주소값을 저장함
  • 시스템이 시작될 때부터 종료될 때까지, PC는 저장되어 있는 주소값에 있는 명령을 수행한 후, 다음 명령을 수행할 수 있도록 PC에 저장된 주소 값을 업데이트하는 과정을 반복함
  • 프로세서는 ISA(Instruction Set Architecture)라는 간단한 명령 모델에 따라 작동한다고 볼 수 있음
    • 여기서 명령은 일정한 순서에 따라 실행되며 하나의 명령을 실행하려면 다음의 과정을 거쳐야 함
      1. 프로세서는 프로그램 카운터의 주소를 가지고 메모리에서 명령을 읽고 해석함
      2. 명령에서 지시하는 연산을 실행함
      3. 프로그램 카운터가 다음 명령를 지시하도록 주소값을 업데이트함
    • 연산은 메인 메모리, 레지스터 파일(register file), 산술 논리 장치(ALU, Arithmetic/Logic Unit)을 통해 실행됨
      • 레지스터 파일은 워드 사이즈만큼의 크기를 가지는 레지스터 여러 개로 구성된 저장 장치로, 각각의 레지스터는 고유한 이름을 가짐
      • ALU는 데이터 및 메모리 주소값을 계산함
    • 일반적으로 다음과 같은 연산이 시행됨
      • 로드(Load) : 데이터를 바이트/워드만큼 메인 메모리에서 레지스터로 복사함 (레지스터에 기존에 있던 데이터는 지워짐)
      • 저장(Store) : 데이터를 바이트/워드만큼 레지스터에서 메모리 공간으로 복사함 (메모리 상에 기존에 있던 데이터는 지워짐)
      • 계산(Operate) : 레지스터 2개의 값을 ALU로 복사해서 수학적 연산을 시행한 후 결과값을 어떠한 레지스터에 저장함 (해당 레지스터에 기존에 있던 데이터는 지워짐)
      • 점프(Jump) : 명령어 자체에서 데이터를 워드만큼 추출해서 프로그램 카운터로 복사함 (PC의 이전 주소값은 지워짐)
    • 예시) A레지스터에 'n'의 값을 로드하고, B레지스터에 'm'의 값을 로드하고, A와 B의 값으로 계산을 한 결과를 C레지스터에 저장...
  • 위에 나온 ISA는 간소화된 모델로, 실제로 최근의 프로세서는 매우 복잡한 작동 원리를 이용해 프로그램의 실행을 가속함
  • 실제 프로세서의 구현 방식을 설명하는 것을 마이크로아키텍처(microarchitecture)라고 함.

cf. ISA란?
텍스트에서 간략하게 설명한 ISA를 조금 더 알아보기

  • ISA는 하드웨어와 소프트웨어 사이의 인터페이스 역할을 하며, 프로세서가 무엇을 할 수 있으며 어떻게 일을 하는지 기술함
  • ISA는 프로세서가 지원하는 데이터형, 프로세서의 레지스터, 하드웨어가 메모리를 관리하는 방식, 주요 프로세서 기능 (ex. 가상 메모리), 프로세서가 실행할 수 있는 명령 등을 기술함.
  • "프로그래머의 메뉴얼"이라고 할 수 있음
  • 마이크로아키텍쳐는 "ISA의 구현"이라고 요약할 수 있겠음
    참고: ARM 단어 목록 (Glossary)

1.4.2 Running the hello Program

  • 이제 위의 설명을 바탕으로 hello 프로그램이 어떻게 작동하는지 알 수 있음

    1. 이 입력을 받기 위해 대기 중임
    2. ./hello라는 명령을 입력하면 셸은 글자를 읽어 레지스터로 이동한 후 메모리에 저장함
    3. 엔터 키를 누르면 셸이 명령을 다 입력한 것으로 인식함
    4. 셸은 hello 실행 파일을 로딩하며, 이때 hello 오브젝트 파일에 있는 코드와 데이터를 디스크에서 메모리로 복사함
      • 이때 데이터는 DMA(Direct Memory Access)라는 방법을 통해 CPU를 거치지 않고 디스크에서 메모리로 바로 이동함
    5. 프로세서는 메모리에 있는 hello 프로그램의 기계어 명령을 실행함
    6. hello, world 스트링은 메모리에서 레지스터 파일로 복사된 후 다시 출력 장치 (모니터)로 보내짐


1.5 Caches Matter

  • wip

cf. 컴파일러와 printf

  • hello.c의 어셈블리어 출력을 보면 call puts라는 부분을 볼 수 있음

Q)hello.c에서는 분명 printf 함수를 사용했는데 왜 어셈블리어 출력에는 puts가 있는 걸까?
A) 컴파일러의 코드 최적화

  • puts는 문자열(char*)를 받아 그대로 출력함
  • printf 또한 문자열을 출력하지만, format specifier를 확인하기 위해 주어진 문자열을 파싱함 - 즉, 더 복잡한 과정을 거침
  • 따라서, hello.c에서 hello, world\n와 같이 format specifier가 없고 개행문자로 끝나는 문자열이 출력되는 경우 컴파일러에서 자동으로 puts로 바꾸는 것
    https://stackoverflow.com/questions/60080021/compiler-changes-printf-to-puts
profile
nogosan.exe > nogosan.log

0개의 댓글