[CSAPP] 1. 컴퓨터 시스템으로의 여행 1

이도윤·2022년 11월 22일
0

0. 서론

컴퓨터 시스템은 하드웨어와 시스템 소프트웨어로 구성되며, 이들이 함께 작동하여 응용프로그램을 실행한다.






1. 컴퓨터 시스템 소개

//
#include <stdin.h>

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

1.1 정보는 비트와 컨텍스트로 이루어진다

  hello 프로그램은 프로그래머가 에디터로 작성한 소스 프로그램(또는 소스파일)으로 생명을 시작하며, hello.c라는 텍스트 파일로 저장된다. 소스 프로그램은 0 또는 1로 표시되는 비트들의 연속이며, 바이트라는 8비트 단위로 구성된다. 각 바이트는 프로그램의 텍스트 문자를 나타낸다.

  대부분의 컴퓨터 시스템은 텍스트 문자를 아스키(ASCII) 표준을 사용하여 표시한다. hello.c처럼 오로지 아스키 문자들로만 이루어진 파일들은 텍스트 파일이라고 부른다. 다른 모든 파일들은 바이너리 파일이라고 한다.




1.2 프로그램은 다른 프로그램들에 의해 다른 형태로 해석된다

프로그램은 다른 형태로 다른 프로그램에 의해 번역된다.
hello 프로그램은 고수준 언어(high-level)로 사람이 읽을 수 있고 이해할 수 있다.
hello.c 프로그램을 실행시키려면, 각각의 C 명령어가 저수준 기계 언어(low-level machine language)로 번역된다.
이러한 과정을 거치면 실행 가능한 파일 혹은 프로그램 형태로 만들어진 후에 이진 디스크 파일에 저장된다.


  • 전처리 (Preprocessor)

전처리 구간에서는 '#' 문자로 시작하는 지시문에 따라 원래 C 프로그램를 수정한다.
예를 들어, #include <stdio.h>의 첫 번째 명령줄이 전처리기에게 말해 전처리기가 시스템 헤더 파일인 stdio.h의 내용을 읽는다.
내용을 읽은 후에 프로그램 텍스트에 직접 삽입(insert)하도록 지시한다.
전처리 구간을 거치면 hello.i 파일의 결과를 만든다.


  • 컴파일 (Compilation)

컴파일 구간에서는 hello.i 파일을 hello.s 텍스트 파일로 바꾼다.
hello.s는 어셈블리어 프로그램(assembly-language program)이다.
어셈블리어는 다른 고수준 언어를 위한 다른 컴파일러들에 대해 같은 출력 언어를 제공해주기 때문이다.

ex) C 컴파일러와 Fortran 컴파일러는 같은 어셈블리어 언어 출력 파일을 생성한다.


  • 어셈블리 (Assembly)

어셈블러는 hello.s를 기계어 명령어로 번역하고, 재배치 가능한 오브젝트 프로그램(relocatable object program)으로 만든 후에 hello.o 오브젝트 파일을 만든다.


  • 링킹 (Linking)

hello 프로그램은 모든 C 컴파일러에서 제공되는 표준 C 라이브러리의 printf 함수를 호출한다.
printf 함수는 printf.o라고 이미 컴파일된 오브젝트 파일에 있으며 hello.o 프로그램에 합쳐진다.
링커가 이 합쳐지는 과정을 핸들링 하며 hello라는 파일을 만든다.




1.3 컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다

프로그래머가 컴파일 시스템이 어떻게 작동되어야 하는 지 이해해야 하는 이유 :


  • 프로그램 성능 최적화

현대 컴파일러들은 좋은 코드를 만드는 정교한 도구이다.
C 프로그램에서 좋은 코드를 만드려면, 기계 수준 코드의 기본적인 이해와 어떻게 컴파일러가 다른 C 명령어들을 기계 코드로 바꾸는 지 이해해야 한다.


  • 링크 에러 이해

프로그래밍을 하면서 대부분의 어이 없는 에러는 링커와 관련되어 있다.

예를 들어 링커가 어떤 참조를 풀어낼 수 없다고 할 때는 무엇을 의미하는가? 정적변수와 전역변수의 차이는 무엇인가? 만일 각기 다른 파일에 동일한 이름의 두 개의 전역변수를 정의한다면 무슨 일이 일어나는가? 왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는 걸까?

이러한 질문들에 대한 대답은 7장에서 이어진다.


  • 보안 약점 피하기

네트워크와 인터넷 서버에서 버퍼 오버플로우 취약성은 많은 보안 허점을 보여주었다.
보안 프로그래밍을 배우려면 데이터 및 정보가 프로그램 스택에 저장되는 방식의 결과를 이해해야 한다.




1.4 프로세서는 메모리에 저장된 인스트럭션을 읽고 해석한다

linux> ./hello
hello, world
linux>

유닉스 시스템에서 실행 가능한 파일을 실행하기 위해 쉘이라는 이름을 가진 어플리케이션 프로그램에게 우리 프로그램의 이름을 알려줘야 한다.

쉘은 커맨드-라인(명령줄) 인터프리터이다. 프롬프트를 출력하고, 프롬프트 입력을 대기하고 커맨드를 실행한다. 커맨드 라인의 첫번째 단어가 쉘 빌트인 커맨드와 충돌하지 않는다면 실행가능한 파일로 간주하고 불러온 뒤 실행한다.


1.4.1 시스템의 하드웨어 조직

hello 프로그램을 실행시킬 때 어떤 일이 벌어지는 지 알려면 시스템의 하드웨어 구조를 이해해야 한다.

  • Buses

시스템 전체를 실행하는 것은 버스(Bus)라고 불리는 일종의 도관의 모음

버스는 각 구성 요소 간의 바이트를 전달시키면 워드(word)라고 불리는 고정된 사이즈의 바이트 모음을 전달시킨다.

word에 바이트 수는 근본적인 시스템 매개변수이다.

오늘날 대부분 기계들은 4바이트(32비트) or 8바이트(64비트)로 이루어져있다.


  • I/O Devices

I/O(입출력) 장치는 외부 세계와 연결하는 시스템

ex) 키보드, 마우스, 화면, 디스크 드라이브 등등

각각의 I/O 장치는 컨트롤러 혹은 어댑터를 통해 I/O 버스로 연결한다.


  • Main Memory

메인 메모리는 프로그램과 데이터를 가지고 있는 임시 저장소

메인 메모리는 DRAM 칩의 모음으로 구성되어 있다.

메모리는 선형 배열로 이루어져있으며 0으로 시작하는 유일한 주소를 가지고 있다.


  • Processor

CPU 혹은 프로세서는 메인 메모리에 있는 명령어를 실행하거나 해석하는 엔진

CPU 코어는 PC(program counter)라고 불리는 word 크기의 저장 장치 또는 레지스터가 있다.

PC는 메인 메모리에 있는 기계어 명령어를 가리킨다.

프로세서는 PC가 가리키는 명령어를 반복적으로 실행하고 PC를 다음 명령을 가리키게끔 업데이트한다.

프로세서는 명령어 집합 구조(instruction set architecture)에 의해 정의된 매우 단순한 명령어 실행 모델에 따라 작동한다.

프로세서는 PC가 가리키는 메모리에 명령어를 읽고, 명령어의 비트를 해석하고, 명령어에 의해 간단한 작업을 수행하고 그 후에는 PC를 다음 명령어로 가리킨다.


이러한 간단한 작업은 메인 메모리, 레지스터 파일, 그리고 산술/논리 장치(ALU) 주변으로 반복된다.

레지스터 파일은 word 크기의 레지스터들을 구성되어 있는 작은 저장 장치이다.

ALU는 새로운 데이터와 주소 값을 계산한다.


다음은 인스트럭션의 요청에 의해 CPU가 실행하는 단순한 작업의 예이다.

- Load
메인 메모리의 바이트 또는 word를 레지스터로 복사하여, 레지스터 이전 내용을 덮어쓴다.

- Store
레지스터의 바이트 또는 단어를 메인 메모리의 위치로 복사하여 해당 위치의 내용을 덮어쓴다.

- Operate
두 레지스터의 내용을 ALU로 복사하여 두 words에 대한 산술 작업을 수행하고, 레지스터에 결과를 저장하고 해당 레지스터의 이전 내용을 덮어쓴다.

- Jump
명령어 자체로 부터word를 추출하고 PC로 복사하고 PC의 이전 값을 덮어쓴다.


1.4.2 hello 프로그램의 실행

처음에 쉘 프로그램은 자신의 인스트럭션을 실행하면서 사용자가 명령을 입력하기를 기다린다. "./hello"를 입력하면 쉘 프로그램은 각각의 문자를 레지스터에 읽어들인 후, 그림 1.5(hello 명령을 키보드에서 읽어들이는 과정)와 같이 메모리에 저장한다.

키보드에서 엔터(Enter) 키를 누르면 쉘은 명령 입력을 끝마쳤다는 것을 알게 된다. 그러면 쉘은 파일 내의 코드와 데이터를 복사하는 일련의 인스트럭션을 실행하여 실행파일 hello를 디스크에서 메인 메모리로 로딩한다. 데이터 부분은 최종적으로 출력되는 문자 스트링인 "hello, world\n"을 포함한다. 직접 메모리 접근(DMA, 6장)이라고 알려진 기법을 이용해서 데이터는 프로세서를 거치지 않고 디스크에서 메인 메모리로 직접 이동한다. 이 과정은 그림 1.6(실행파일을 디스크에서 메인 메모리로 로딩하는 과정)에 나타나 있다.

hello 목적파일의 코드와 데이터가 메모리에 적재된 후, 프로세서는 hello 프로그램의 main 루틴의 기계어 인스트럭션을 실행하기 시작한다. 이 인스트럭션들은 "hello, world/n" 스트링을 메모리로부터 레지스터 파일로 복사하고, 거기로부터 디스플레이 장치로 전송하여 화면에 글자들이 표시된다. 이 과정은 그림 1.7(출력 스트링을 메모리에서 화면으로 기록하는 과정)에 나타나 있다.




1.5 캐시가 중요하다

시스템은 한 곳에서 다른 곳으로 정보를 옮기는 데 많은 시간을 소비한다.

hello 프로그램에 있는 기계 명령어는 원래 디스크에 저장된다.
프로그램이 로드되면, 메인 메모리로 복사된다.
프로세서가 프로그램을 실행하면, 명령어들은 메인 메모리에서 프로세서로 복사된다.

시스템 설계자는 이러한 복사하는 작업들을 빠르게 실행해야 하게 만들어야 한다.
큰 저장 장치는 작은 저장 장치보다 느리고, 빠른 저장 장치는 느린 저장 장치보다 비싸다.
프로세서는 메인 메모리보다 데이터를 거의 100배 빠르게 읽는다.
반도체 기술이 빠르게 발전하면서, 프로세서-메모리 갭(processor-memory gap)이 점점 증가한다.

시스템 설계자들은 이러한 프로세서-메모리 갭을 해결하기 위해 작고 빠른 캐시 메모리(cache memories)를 만든다.

캐시 메모리는 프로세서가 가까운 미래에 필요한 정보에 대한 임시 영역이다.
L1 캐시와 L2 캐시 SRAM 반도체로 구현된다.

캐싱에 대한 기본 개념은 프로그램이 지역화된 지역의 데이터와 코드에 액세스하려는 지역성(locality)을 활용하여 시스템이 매우 큰 메모리와 매우 빠는 메모리의 효과를 얻는 것이다.

캐시에 대해 아는 애플리케이션 프로그래머는 캐시를 활용하여 프로그램의 성능을 향상시킬 수 있다.




1.6 저장장치들은 계층구조를 이룬다

모든 컴퓨터 시스템의 저장 장치들은 메모리 계층 구조로 되어 있다.

위에서 아래로 내려갈 수록, 장치들은 커지고, 느려지고, 바이트당 가격이 싸진다.

레지스터는 계층 구조에서 가장 위에 있다. (L0 or level 0)

메모리 계층 구조의 주요 개념은 한 계층에 저장 장치가 다음 낮은 레벨에 대한 저장 장치의 캐시 역할을 한다.

분산 파일 시스템으로 되어있는 시스템에서는, 로컬 디스크가 다른 시스템의 로컬 디스크에 저장되어 있는 데이터에 대한 캐시 역할을 한다.





profile
Java 백엔드 개발자

0개의 댓글