정글사관학교 개발일지/자료구조&알고리즘

정글사관학교 14일차 TIL: 컴퓨터 시스템 1장 - A Tour of Computer Systems (1-1 ~ 1-4)

Woonys 2021. 11. 15. 01:22
반응형

1. A Tour of Computer Systems

  • 컴퓨터 시스템: 하드웨어 + 소프트웨어가 함께 동작해서 응용 프로그램을 돌리는 개념.

이 책에서 배울 것은

  1. Practical skills such as how to avoid starage numerical errors
  2. How to optimize my C codes by using clever tricks
  3. compiler

등. (원서로 보다보니 100% 이해하는데 부족함이 있는지라 내용에 오류가 있다면 꼭 말씀해주세요..)

 

1-1. Information is Bits + Context

여기 C로 짠 프로그램이 하나 있다. 이를 hello라고 하자.

#include <stdio.h>

int main() {
   printf("Hello, World!");
   return 0;
}

Hello 프로그램을 위와 같이 짜게 될텐데, 이렇게 컴퓨터에 전달하기 위한 소스로서 사람이 알아볼 수 있는 형태로 짠 프로그램을 소스 프로그램(source program)이라고 한다. hello.c는 이렇게 소스 프로그램으로 만들어진다. 이 소스 프로그램은 프로그래머가 에디터로 작성한 다음 hello.c라는 이름의 텍스트 파일로 저장한다. 소스 프로그램은 a sequence of bits로, 0 or 1의 value를 가짐. 얘네를 8-bit 뭉치인 byte로 만들어서 데이터화한다.

 

하나의 바이트는 text character와 일대일 대응을 이룬다. 즉, 문자 하나를 의미한다. 대부분 컴퓨터 시스템은 텍스트 문자를 아스키 코드로 표현한다. 따라서 각 바이트 값이 문자에 대응하게 된다.

 

ex) #은 35바이트와 대응

 

hello.c 표현에서 우리가 알 수 있는 컴퓨터의 근본 아이디어는 다음과 같다.

시스템 내 모든 정보 - 디스크 파일이나 메모리에 저장된 프로그램, 혹은 유저 데이터나 네트워크로 전송되는 데이터 등- 이 모든 게 비트의 묶음이다!

서로 다른 데이터 객체임을 구분 가능하게 하는 유일한 요소는 사람이 바라보는 맥락에서 결정된다. 위의 아스키 코드를 보면, 컴퓨터 입장에서는 숫자만 다르지 다 비트로 보이지만 사람 입장에서는 정수, 부동소수, 문자 등 맥락에 따라 다르게 취급한다.

 

1-2. Programs are translated by other programs into different forms

hello.c 프로그램은 사람이 읽고 쓰기가 가능하게끔 하기 위해 고수준(상대적으로 사람이 알아먹기 쉬운) C 프로그램으로 되어 있다. 하지만 hello.c 프로그램을 실행하기 위해서는 다른 프로그램을 이용해 low-level의 machine language(기계어)로 반드시 번역해줘야 한다. 이러한 번역 지시는 executable object program이라고 하는 형태로 패키징되어서 binary disk file에 저장된다.

 

UNIX에서 source file(앞에서 hello.c)을 object file로 번역하는 과정은 compiler driver로 수행한다.(이게 바로 그 말로만 듣던 컴파일러..)

 

리눅스에서 다음과 같이 코드를 친다.

 

"gcc -o hello hello.c"

 

  • gcc compiler가 source file "hello.c"를 읽는다.
  • 그 다음 실행 가능한 object file인 "hello"로 번역
  • 번역은 위의 fig 1.3에 표시된 4단계를 거쳐서 수행. 이 4단계를 합쳐서 Compilation system이라고 한다.

1. Preprocessing phase(CPP)

CPP는 original C program을 수정하는데, 이때 #뒤에 오는 문자열 지시를 받아서 수정한다. 위의 예시인 hello.c에서는 #include <stdio.h>가 들어간다.

 

이 #include <stdio.h>라는 커맨드는 preprocessor에다가 시스템 헤더 파일 "stdio.h" 콘텐츠를 읽으라고 알려준 다음 얘를 program text에다가 삽입한다. 이에 대한 결과로 다른 C 프로그램이 만들어지는데, 얘는 뒤에 접미사로 i가 붙는다. (hello.i)

 

2. Compilation phase(CC1)

컴파일러(cc1)은 hello.i를 텍스트 파일 hello.s로 한 번 더 번역한다. 이 때, 번역하는 언어는 어셈블리어로 된 프로그램을 포함한다. 이 프로그램은 function main을 어셈블리어로 포함하는데, 아래 이미지의 function main에 대해 라인 2부터 라인 7까지는 저수준 기계어 지시사항을 텍스트 형식으로 나타낸 것이다.

어셈블리어는 서로 다른 고수준 언어에 대한 서로 다른 컴파일러에 대해 공통의 아웃풋 언어를 제공한다는 점에서 유용하다. 이게 무슨 말이냐, C와 포트란을 비교해보자. (포트란은 프로그래밍 언어 이름이다.) C와 포트란은 언어와 컴파일러가 각각 다를테지만 동일한 실행에 대해  동일한 어셈블리어로 구성된 파일을 생성한다.

 

3. Assembly phase

다음으로, 어셈블러는 어셈블리어로 되어 있는 hello.s 파일을 기계어 지시사항으로 번역한다. 이 때, 기계어 지시를 패키징하는데 relocatable object program이라는 형태로 번역한다. 이 패키징 결과를 object file인 hello.o로 저장한다.

 

이 파일은 function main에 들어있는 지시사항을 코드로 담은 17 bytes를 포함하고 있는 binary file이다. 이 hello.o부터는 text editor로 뜯어서 보면 우리는 뭔 말인지 1도 알아볼 수가 없다.

 

4. Linking phase

앞서 hello 프로그램의 코드를 보면 printf function을 호출했다. 이 printf는 C 컴파일러에서 제공해주는 기본 C 라이브러리에 속한 일부이다.

 

printf function은 미리 컴파일되어있는 object file인 printf.o 안에 있다. 얘는 미리 만들어둔 object file이라고 생각하면 된다. 얘가 위에서 짰던 hello.c 프로그램과 합쳐져야 된다. linker(ld)라는 애는 이렇게 우리가 짠 코드에서 내장 라이브러리를 호출했을 때 이 라이브러리를 우리의 코드와 합쳐주는 역할을 한다.

 

이렇게 만들어진 최종적인 결과는 hello 파일인데 요놈은 executable object file이다. 한마디로 이제 실행 가능한 파일이라고 생각하면 된다. 그래서 비로소 메모리로 불러올 준비가 되고 시스템이 파일을 실행할 준비를 마친다.

 

 

1-3. It pays to understand how compilation system work

<프로그래머가 컴필레이션 시스템이 어떻게 동작하는지 이해해야 하는 이유 세 가지>

 

1. Optimizing program performance: 프로그램 성능을 최적화하기 위해

현대 컴파일러는 좋은 코드를 생성하는 정교한 툴이다. 따라서 프로그래머가 더 효율적인 코드를 짜겠답시고 컴파일러 내부에서 어떻게 돌아가는지까지 알 필요는 없다. 하지만, C program에서 좋은 코드 자체를 만들기 위해서는 기계어 레벨까지 내려가는 저수준 코드에 대한 기본적인 이해, 그리고 컴파일러가 어떻게 다른 C statement를 기계어로 번역하는지를 알 필요가 있다.

 

여기서 statement라는 게 무엇을 가리키는지 잘 이해되지 않아 검색해보니 "문(文)"이라고 한다. 그러니 위의 문장은 while문이 for문보다 효율적인지, if-else문보다 switch문이 더 효율적인지에 대해 알려면 컴파일러가 이런 문을 어떻게 기계어로 번역하는지에 대한 동작 원리를 이해할 필요는 있어야 한다는 말이다.

컴퓨터 프로그래밍에서 문(文)은 명령형 프로그래밍 언어의 가장 작은 독립 요소이다. 프로그램은 하나 이상의 문이 연결되어 형성된다. 문은 과 같은 내부 요소를 포함할 수 있다.

문의 종류
1. 단순 문
대입문: A = A + 1;
함수: CLEARSCREEN();
리턴문: return 5;
GOTO문: GOTO 1;
선언문: int i;

2. 복합문
구문 블록
IF문
switch문
While 루프
Do 루프
For 루프

- 위키백과

 

2. Understanding link-time errors

몇몇 복잡한 프로그래밍 에러는 linker의 동작과 연관된 경우가 다수 있다. (linker는 위에서 얘기했듯 우리가 만든 프로그램 실행할 때 내부 라이브러리에 있는 함수 불러오면 이 둘을 merging하는 애)

 

특히 large size의 소프트웨어 시스템을 만들 때 이런 케이스가 왕왕 일어난다. 따라서 컴필레이션 시스템이 어떻게 동작하는지 이해해야 한다.

 

3. Avoiding security holes

buffer overflow vurnerabilities라는 개념이 있다. 이 링크에서 자세한 설명이 있으니 참고. 여기서는 간단하게 요약하자면, 프로그램에다가 너무 많은 데이터를 던져주면 버퍼에 오버플로우가 일어난다. 이 초과 데이터는 메모리 내 근처 공간에 있는 다른 데이터를 바꿔버린다. 결과적으로, 프로그램이 에러를 뱉거나 원래 계획된 행동과 다른 행동을 하게 된다. 이런 취약성을 buffer overflow라고 한다.

 

"A buffer overflow vulnerability occurs when you give a program too much data. The excess data corrupts nearby space in memory and may alter other data. As a result, the program might report an error or behave differently. Such vulnerabilities are also called buffer overrun."

 

buffer overflow를 설명하는 짤.

이런 취약성으로 인해 네트워크 혹은 인터넷 서버에 보안 구멍이 발생한다. 보안을 잘 알기 위해서도 컴필레이션 시스템을 이해할 필요가 있다.

 

1-4. Processors read and interpret instructions stored in memory

 

hello.c 소스 프로그램은 컴필레이션 시스템으로 번역되어 executible object file인 hello로 바뀌어 disk에 저장된다. 이 hellofmf UNIX system에서 실행하려면 우리는 shell에 대해 알 필요가 있다.

 

UNIX shell

Shell은 command-line interpreter라고 해서 명령어를 입력하면 이를 해석해 프로그램을 실행하도록 설계된 인터프리터이다. shell을 실행하면 위와 같은 이미지의 프롬프트를 화면에 출력한다. 사용자가 커맨드 라인에 명령어를 입력할 때까지 깜빡이며 기다렸다가 사용자가 명령어를 입력하고 엔터를 치면 그 명령을 수행한다.

 

input으로 들어오는 단어가 built-in command와 대응하지 않으면(예컨대 ls, cd, mkdir 와 같이 내장된 명령어가 아니라면) shell은 이를 실행파일의 이름으로 인식해 해당 파일을 불러와 실행한다.

 

예를 들어, shell에 "./hello"라고 입력하면 hello 파일을 불러와서 실행하고 종료될 때까지 기다린다.

 

1.4.1 Hardware organization of a system

 

시스템의 하드웨어 조직 구성은 아래 이미지와 같이 되어 있다. 하나씩 간략히 살펴보자.

<Buses>

시스템에서 프로그램을 실행하는 건 bus라 불리는 전기적 통로의 집합을 통해 byte가 이동하면서 일어난다. 이 버스라는 놈은 바이트를 각 컴포넌트(위에서 각 요소) 사이로 옮겨주는 역할을 하는 통로이다. 버스는 고정된 크기의 바이트 묶음(=word)을 이동시키는데, 이 word의 사이즈는 컴퓨터 OS에 내재된 파라미터이다. 즉, 애초에 설계할 때부터 지정된 것이라 바꿀 수 없다. 이 word 사이즈가 4bytes(=32-bit)냐 8bytes(=64-bit)냐에 따라 달라진다.

 

우리가 컴퓨터 프로그램 설치할 때 이 컴퓨터가 32bit인지 64bit인지 확인해야 했는데, 이때 그 bit가 바로 버스를 오고가는 word의 사이즈였던 것이다!(신기)

 

<I/O devices>

Input/Output device는 외부 세계와 시스템 간의 연결고리이다. 위의 예시 이미지에서는 총 4개의 I/O 디바이스가 존재한다.

 

- 키보드 (Input)

- 마우스 (Input)

- 디스플레이 (Output)

- 디스크 드라이브 (Long-term으로 데이터&프로그램 저장)

 

각 I/O 디바이스는 I/O bus와 연결되어 있다. 이때 연결하는 방식은 controller(각 I/O 디바이스 내에 칩셋으로 구성)냐 Adapter(마더보드 슬롯에 끼우는 카드 형식)냐의 두 가지가 있는데, 무엇이든 목적은 I/O device & I/O bus 사이에 정보를 전송하는 역할이다.

 

<Main memory>

프로세서가 프로그램을 실행하는 동안 데이터, 프로그램을 임시로 저장하는 디바이스가 메인 메모리이다 우리가 익히 잘 아는 DRAM이 바로 이것.

 

물리적으로 메인 메모리는 DRAM의 집합으로 구성되어 있다. 한편, 논리적으로 메모리는 byte의 linear array로 구성되어 있다. 각 array는 특정한 주소값을 가진다(0부터 시작). 일반적으로 프로그램을 구성하는 machine instruction은 다양한 수의 byte로 구성된다.

 

<Processor>

얘가 바로 CPU. 메인 메모리에 저장된 지시사항을 해석하는 엔진이다. CPU의 코어에는 word(32/64bit) 사이즈의 저장소가 있는데, 이를 레지스터(Register)라고 한다. 얘를 Program Counter(PC)로 보낸다.

 

요구하는 지시사항에 대해 CPU가 수행하는 동작은 다음의 네 가지가 있다.

 

1. Load

CPU에서 연산을 수행하기 위해 메인 메모리에서 레지스터로 byte나 word를 복사해온다. 이때 레지스터에 이전 콘텐츠가 남아있다면 그 위에 덮어씌운다.

2. Store

Load와 반대로, CPU에서 연산이 끝나면 레지스터에서 메인 메모리 내 어떤 한 위치로 바이트나 워드를 카피해온다. 역시 해당 위치에 정보가 있다면 그 위에 덮어씌운다.

3. Operate

CPU에는 두 개의 레지스터가 있다. 각 레지스터에서 정보를 ALU로 카피해온 뒤 ALU에서 동작을 수행하고 결과값을 레지스터 위에 덮어씌우기까지의 작업이 Operate.

4. Jump

지시사항으로부터 word를 뽑아서 Program Counter(PC)에다가 넘겨준다.

 

 

반응형