Bryant O'Hallaron의 <컴퓨터 시스템>을 읽고 요약한 글입니다. C 언어 기준으로 작성되었습니다.
들어가기 전에
컴퓨터 시스템은 하드웨어와 소프트웨어로 구성되며 이들이 함께 작동하여 응용프로그램을 실행한다. 하드웨어와 소프트웨어는 상호 의존적으로 작동한다. 소프트웨어가 하드웨어에 명령을 내리고, 하드웨어는 이 명령을 실행하여 결과를 만들어낸다. 이러한 상호작용을 통해 사용자가 원하는 작업을 수행할 수 있게 된다.
- 하드웨어:
- 물리적인 장치들을 포함한다.
- 주요 구성요소로는 CPU(중앙처리장치), RAM(메모리), 하드 디스크, 마더보드 등이 있다.
- 입력장치(키보드, 마우스)와 출력장치(모니터, 프린터)도 포함된다.
- 소프트웨어:
- 컴퓨터에서 실행되는 프로그램들이다.
- 운영체제(Windows, macOS, Linux 등)와 응용프로그램(워드프로세서, 웹 브라우저 등)으로 나눌 수 있다.
1. 1 정보는 비트와 컨텍스트로 이루어진다.
프로그램은 프로그래머가 에디터로 작성한 소스 프로그램(또는 소스파일)으로 생명을 시작하며 텍스트 파일로 저장된다. 소스 프로그램은 0 또는 1로 표시되는 비트들의 연속이며, 바이트라는 8비트 단위로 구성된다. 각 바이트는 프로그램의 텍스트 문자를 나타낸다.
대부분의 컴퓨터 시스템은 텍스트 문자를 아스키 표준을 사용하여 표시한다. 아스키 표준은 각 문자를 바이트 길이의 정수 값으로 나타낸다. hello.c 처럼 오로지 아스키 문자들로만 이루어진 파일들은 텍스트 파일이라고 부른다. 다른 모든 파일들은 바이너리 파일이라고 한다.
hello.c의 표시방법은 기본개념들을 분명히 보여준다 : 모든 시스템 내부의 정보- 디스크 파일, 메모리상의 프로그램, 데이터, 네트워크를 통해 전송되는 데이터-는 비트들로 표시된다. 서로 다른 객체들을 구분하는 유일한 방법은 이들을 바라보는 컨텍스트에 의해서다. 일례로 다른 컨텍스트에서는 동일한 일련의 바이트가 정수, 부동소수, 문자열 또는 기계어 명령을 의미할 수 있다.
컴퓨터 내에서의 숫자 표현은 실제 수학적 개념과 차이가 있다. 이로 인해 발생할 수 있는 문제들을 프로그래머들이 이해하고 대비해야 한다. 컴퓨터는 정수를 고정된 비트 수로 표현한다. (예 : 32비트 또는 64비트) 이로 인해 표현할 수 있는 정수의 범위가 제한된다. 오버플로우(overflow)나 언더플로우(underflow)가 발생할 수 있다. 실수는 IEEE 754 표준을 따르는 부동소수점 형식으로 표현된다. 이는 실수의 근사값을 저장하는 방식이다. 정밀도의 한계로 인해 반올림 오차가 발생할 수 있다. 부동소수점 연산에서 결과가 직관과 다를 수 있다. 예를 들어, 0.1 + 0.2가 정확히 0.3이 되지 않을 수 있다.
- 정수 오버플로우:
- 8비트 부호 있는 정수에서 127에 1을 더하면 -128이 되는 현상을 보여준다.
- 이는 가장 큰 양수 값에서 오버플로우가 발생하면 가장 작은 음수가 되는 것을 나타낸다.
- 부동소수점 정밀도 문제:
- 0.1과 0.2를 더했을 때 정확히 0.3이 되지 않는 현상을 보여준다.
- 이는 이진 부동소수점 표현의 한계로 인한 것이다.
- 숫자 표현 범위:
- 32비트 정수와 부동소수점 숫자의 표현 가능한 범위를 보여준다.
- 부동소수점의 경우 정밀도에 제한이 있음을 나타낸다.
1. 2 프로그램은 다른 프로그램에 의해 다른 형태로 번역된다.
hello 프로그램은 인간이 그 형태로 바로 이해하고 읽을 수 있기 때문에 고급 C 프로그램으로 일생을 시작한다. 그러나 hello.c를 시스템에서 실행시키려면, 각 C 문장들은 다른 프로그램들에 의해 저급 기계어 인스트럭션들로 번역되어야 한다. 이 인스트럭션들은 실행 가능 목적 프로그램이라고 하는 형태로 합쳐져서 바이너리 디스크 파일로 저장된다. 목적 프로그램은 실행가능 목적 파일이라고도 부른다.
컴파일러 드라이버는 유닉스 시스템에서 다음과 같이 소스파일에서 오브젝트 파일로 번역한다.
linux > gcc -o hello hello.c
여기서 GCC 컴파일러 드라이버는 소스 파일 hello.c를 읽어서 실행 파일인 hello로 번역한다. 번역은 4개의 단계를 거쳐서 실행된다. 이 네 단계를 실행하는 프포그램들(전처리기, 컴파일러, 어셈블러, 링커)을 합쳐서 컴파일 시스템이라고 부른다.
컴파일 과정의 4단계
- 전처리 단계 (Preprocessing): 소스 파일 hello.c의 텍스트를 전처리기(cpp)가 수정한다. #include 지시어를 실제 헤더 파일 내용으로 대체한다. 결과물은 확장된 소스 프로그램이다 (보통 .i 확장자).
- 컴파일 단계 (Compilation): 수정된 소스 코드를 컴파일러(cc1)가 어셈블리어 프로그램으로 번역한다. 결과물은 hello.s 파일이다.
- 어셈블리 단계 (Assembly): 어셈블러(as)가 hello.s를 기계어 인스트럭션으로 번역한다. 결과물은 재배치 가능 목적 파일 hello.o이다.
- 링크 단계 (Linking): 링커(ld)가 hello.o와 필요한 시스템 목적 파일들을 결합한다. 최종 결과물은 실행 가능한 목적 파일 hello이다.
GCC 컴파일러 드라이버를 사용한 "Hello, World!" 프로그램의 컴파일 과정
- 소스 파일 (hello.c) : 프로그래머가 작성한 C 소스 코드 파일이다.
- 전처리기 (cpp): 소스 파일을 전처리하여 hello.i 파일을 생성한다. 주로 #include와 같은 전처리기 지시문을 처리한다.
- 컴파일러 (cc1): 전처리된 파일을 어셈블리 코드(hello.s)로 변환한다.
- 어셈블러 (as): 어셈블리 코드를 기계어로 변환하여 재배치 가능 목적 파일(hello.o)을 생성한다.
- 링커 (ld): 목적 파일과 필요한 시스템 목적 파일들을 결합하여 최종 실행 파일(hello)을 생성한다.
이 과정의 주요 특징
- 각 단계는 이전 단계의 출력을 입력으로 사용한다.
- 전체 과정은 GCC 컴파일러 드라이버에 의해 관리된다.
- 시스템 목적 파일은 링크 단계에서 추가되어 최종 실행 파일에 포함된다.
- 각 중간 단계의 파일(.i, .s, .o)은 일반적으로 임시 파일로 생성되며, 컴파일 완료 후 삭제된다.
1. 3 컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다.
1. 4 프로세서는 메모리에 저장된 익스트럭션을 읽고 해석한다.
지금까지 hello.c 소스 프로그램은 컴파일 시스템에 의해 hello 라는 실행 가능한 목적파일로 번역되어 디스크에 저장되었다. 이 실행 파일을 유닉스 시스템에서 실행하기 위해서 쉘이라는 응용 프로그램에 그 이름을 입력한다.
linux> ./hello
hello, world
linux>
쉘은 커맨드 라인 인터프리터로 프롬프트르를 출력하고 명령어 라인을 입력 받아 그 명령을 실행한다. 만일 명령어 라인이 내장 쉘 명령어가 아니면 쉘은 실행파일의 이름으로 판단하고 그 파일을 로딩해서 실행해 준다. 그래서 이 경우에 쉘은 hello 프로그램을 로딩하고 실행한 뒤에 종료를 기다린다. hello 프로그램은 메시지를 화면에 출력하고 종료한다. 쉘은 프롬프트를 출력해주고 다음 입력 명령어 라인을 기다린다.
"./hello" 명령어가 입력되었을 때 일어나는 과정
- 사용자가 "./hello"를 입력한다.
- 쉘이 이 입력을 파싱하고 "hello" 파일을 확인한다.
- 쉘이 exec() 시스템 콜을 사용하여 운영 체제에 실행을 요청한다.
- 운영 체제가 "hello" 프로그램을 메모리에 로드한다.
- 운영 체제가 "hello" 프로그램의 실행을 시작한다.
- "hello" 프로그램이 "hello, world"를 출력한다.
- "hello" 프로그램이 exit()를 호출하여 종료를 요청한다.
- 운영 체제가 프로그램의 자원을 해제하고 제어를 쉘로 반환한다.
- 쉘이 다시 프롬프트를 출력하고 다음 명령을 기다린다.
이 과정에서 주목할 점들:
- 쉘, 운영 체제, 사용자 프로그램 간의 복잡한 상호작용이 이루어진다.
- 운영 체제는 프로그램 실행의 중재자 역할을 한다.
- 시스템 콜(exec(), exit())이 사용자 프로그램과 운영 체제 간의 인터페이스 역할을 한다.
- 프로세스의 생성, 실행, 종료가 체계적으로 관리된다.
1. 4. 1 시스템의 하드웨어 조직
hello 프로그램을 실행할 때 무슨 일이 일어나는지 설명하기 위해서는 전형적인 시스템에서의 하드웨어 구성을 이해할 필요가 있다.
하드웨어 구성 요소와 각 구성 요소의 역할
주요 구성 요소 | 하위 구성 요소 | 설명 |
CPU (중앙처리장치) | Register file (레지스터 파일) | 데이터를 임시 저장 |
PC (Program Counter) | 다음 실행할 명령어의 주소를 저장 | |
ALU (Arithmetic Logic Unit) | 산술 및 논리 연산 수행 | |
Bus interface (버스 인터페이스) | 시스템 버스와 연결 | |
I/O bridge (입출력 브릿지) | - | CPU와 메인 메모리, 그리고 I/O 장치들 사이의 통신을 관리 |
Main memory (메인 메모리) | - | 프로그램과 데이터를 저장하는 주 기억장치 |
버스 시스템 | System bus (시스템 버스) | CPU와 I/O 브릿지 연결 |
Memory bus (메모리 버스) | I/O 브릿지와 메인 메모리 연결 | |
I/O bus (입출력 버스) | 다양한 I/O 장치들과 연결 | |
I/O 장치들 | USB controller | 마우스, 키보드 등 연결 |
Graphics adapter | 디스플레이 연결 | |
Disk controller | 디스크 드라이브 관리 | |
Expansion slots (확장 슬롯) | - | 네트워크 어댑터 등 추가 장치를 위한 슬롯 |
Disk (디스크) | - | hello 실행 파일이 저장된 저장 장치 |
1. 4. 2 hello 프로그램의 실행
단계 | 주요 구성 요소 | 역할 | 데이터 흐름 |
1. 프로그램 로딩 | 디스크 드라이브 | hello 프로그램 저장 | 디스크 → 메인 메모리 |
메인 메모리 | 프로그램 로드 및 저장 | ||
2. 명령어 실행 | CPU - 제어 장치 | 명령어 해석 및 실행 | 메인 메모리 → CPU |
메인 메모리 | 명령어 제공 | ||
3. 데이터 처리 | CPU - ALU | 연산 수행 | ALU → 레지스터/메모리 |
CPU - 레지스터 | 임시 데이터 저장 | ||
메인 메모리 | 데이터 저장 | ||
4. 출력 | CPU | 출력 데이터 생성 | CPU → 시스템 버스 → 디스플레이 |
시스템 버스 | 데이터 전송 | ||
디스플레이 | 문자열 표시 | ||
5. 프로그램 종료 | CPU | 제어 반환 | CPU → 운영 체제 |
운영 체제 | 제어 수신 |
'TIL' 카테고리의 다른 글
알고리즘 그래프 탐색 - BFS(너비 우선 탐색) (0) | 2024.09.21 |
---|---|
[TIL] 그래프 1편 - 그래프 정의, 특징, 종류, 구성 요소 (2) | 2024.09.20 |
[TIL] Python 문자열(String)의 불변성(Immutability) (1) | 2024.09.10 |
[TIL] 자료 구조와 배열 (Python) 5편 - 리스트와 튜플 활용하기 (1) | 2024.09.10 |
[TIL] 자료 구조와 배열 (Python) 4편 - 뮤터블과 이뮤터블 객체의 대입 (0) | 2024.09.10 |