[공부를 시작하기에 앞서]
앞으로 쓸 내용들은 윤성우 저자의 '뇌를 자극하는 Windows 시스템 프로그래밍' 서적을 기반으로 작성하였습니다.
꽤 오래 전에 나온 책인지라 요즘 내용과 일치하지 않는 부분들이 있으면 제가 임의로 수정할 수도 있습니다.
많이 미흡하겠지만, 공부하는 사람의 입장에서 쓰는 글인만큼 부족한 부분은 지적해주시면 감사히 수용하겠습니다.
<컴퓨터 구조에 대한 첫 번째 이야기>
[시스템 프로그래밍의 이해와 접근]
[시스템 프로그래밍이란?]
시스템 프로그래밍을 공부하기에 앞서서, 우선 '시스템 프로그래밍이 무엇일까'에 대해서 생각해볼 필요가 있다.
막연하게 '시스템 프로그래밍'이라고 하면 도대체 뭘 하는건지 모르기 때문이다.
지금 이 글을 쓰고 있는 나도 그랬던 것처럼.
가장 먼저 '시스템'이라는 것을 뭘 말하는 것일까?
쉽게 요약하면 다음과 같이 말할 수 있을 것 같다.
"하드웨어(H/W) + 운영체제(OS)"
예를 들면 A라는 시스템은 Intel계 CPU를 사용하고 운영 체제는 Windows를 쓴다.
여기서 Intel CPU가 하드웨어가 되고, Windows는 운영체제가 된다.
그리고 '시스템 프로그램'은 다음과 같이 말할 수 있다.
"컴퓨터 시스템을 동작시키는 프로그램"
그리고 우리가 흔히 알고 있는 Windows나 Linux, UNIX와 같은 운영체제도 시스템 프로그램의 일종이다.
그래서 운영체제 개발자들을 '시스템 프로그래머'라고 할 수 있다.
어셈블리 언어나 C언어를 이용해서 하드웨어를 직접 제어하는 개발자들도 시스템 프로그래머라고 한다.
여기서 더 나아가면 Windows나 UNIX 같은 운영체제에서 제공하는 라이브러리를 사용하여 프로그램을 개발하는 개발자도 시스템 프로그래머라고 볼 수 있다.
여기서 Windows에서 제공하는 라이브러리를 활용하여 프로그램을 개발하면 Windows 시스템 프로그래머가 되는 것이고, UNIX에서 제공하는 라이브러리를 활용하여 프로그램을 개발하면 UNIX 시스템 프로그래머가 되는 것이다.
위에서 한 이야기를 요약하자면 다음과 같다.
컴퓨터 시스템 = 하드웨어(H/W) + 운영체제(OS)
Windows 시스템 프로그래밍 = Windows 운영체제 기반의 컴퓨터에게 일을 시키기 위한 프로그램을 구현하는 것
Windows 시스템 프로그램 작성을 위한 라이브러리는 Windows 운영체제에서 제공한다.
[컴퓨터 시스템의 주요 구성요소(Main Components)]
컴퓨터 시스템을 구성하는 요소는 크게 다음과 같이 나눌 수 있다.
이를 나누면 CPU와 캐시에 대한 내용은 컴퓨터 구조로 분류가 된다.
그리고 메인 메모리와 하드 디스크에 대한 내용은 운영체제쪽으로 분류가 된다.
실제로 대학을 다니면서도 이 둘에 대해서는 따로 배웠었고, 하나라는 생각을 하지 못했다.
하지만 이 둘은 하나로 인식해야 전체 시스템을 이해하기에 좋다.
이 책의 저자도 '큰 그림에 대한 이해'가 중요하다고 했으니 보다 넓게 보는 것이 중요할 것이다.
[컴퓨터 하드웨어의 구성]
컴퓨터의 하드웨어 구성은 간략하게 표현하면 다음과 같이 표현할 수 있다.
여기서는 각각의 구성요소에 대해서 정리를 한다.
[CPU (Central Processing Unit)]
흔히 말하는 중앙처리장치가 바로 CPU다.
많이들 알고 있겠지만 컴퓨터에서 연산을 담당하는 것이 CPU다.
실제로 CPU는 컴퓨터 프로그램의 실행에 있어서 핵심적인 역할을 담당한다.
그래서 사람으로 치면 머리에 해당하는 부분이라고 볼 수 있다.
[메인 메모리 (Main Memory)]
램(RAM)이라는 저장장치로 구성되는 메인 메모리는 컴파일이 완료된 프로그램 코드가 올라가서 실행되는 영역이다.
쉽게 말하면, 내가 게임을 다운로드 받아서 설치를 하면 하드 디스크에 저장이 된다.
그리고 그 게임을 실행시키면 그 게임 프로그램은 '메인 메모리'에 올라가서 실행된다.
따라서 쉽게 말하면 메인 메모리는 프로그램 실행을 위해 존재하는 메모리라고 보면 된다.
[입/출력 버스(Input/Output Bus)]
입/출력 버스는 컴퓨터를 구성하는 구성요소 사이에서 데이터를 주고 받기 위해 사용되는 경로다.
주고 받는 데이터의 종류와 역할에 따라서 크게 어드레스 버스, 데이터 버스, 컨트롤 버스로 나뉘어 진다.
버스가 셋으로 나뉘는 것보다 중요한 것은 버스가 하는 역할이다.
위의 그림에서도 보면 하드 디스크부터 메모리에 CPU까지 모두 버스에 연결이 되어있다.
그래서 버스 시스템을 기반으로 하드 디스크에 있는 데이터가 메인 메모리에 전송할 수 있고 그 반대도 가능하다.
메인 메모리와 CPU 사이에서의 데이터 입/출력도 담당하는 것이 버스 시스템이다.
[CPU에 대한 이해]
[ALU(Arithmetic Logic Unit)]
프로그램이 실행되는 곳이 어디냐고 물으면 대부분은 CPU라고 대답한다.
틀린 대답은 아니다.
실제로 덧셈이나 뺄셈과 같은 연산을 진행하는 주체가 CPU이기 때문이다.
그 중에서 CPU 내부의 블록들 중 ALU가 실제로 연산을 담당한다.
나머지 블록들은 연산을 하는데 도움을 주는 블록이다.
ALU에서 처리하는 기본적인 연산은 크게 두 가지다.
하나는 덧셈이나 뺄셈같은 산술 연산이고, 나머지 하나는 AND나 OR와 같은 논리 연산을 수행한다.
그래서 실제로 복잡한 프로그램을 작성해도 CPU의 입장에서는 대부분 이 두 가지의 형태의 연산으로 이뤄진다.
[컨트롤 유닛(Control Unit)]
우리가 실제로 프로그램을 실행할 때, 실행 파일에는 CPU에게 일을 시키기 위한 명령어들이 저장되어 있다.
이 명령어가 CPU로 전해져서 일을 처리하게 되는데, ALU는 산술, 논리연산만 처리한다.
다시 말해서 명령어를 해석하는 역할은 없는 것이다.
이 명령어를 해석하는 구성요소가 바로 컨트롤 유닛이다.
컨트롤 유닛은 CPU가 처리해야 할 명령어들을 해석하는 것이다.
그리고 해석된 결과에 따라서 적절한 신호를 CPU 내의 다른 블록에 보내는 일을 수행한다.
그래서 쉽게 말하면 컨트롤 유닛은 CPU의 총사령관이라고 볼 수 있다.
[CPU 내 레지스터들 (Register Set)]
실제로 CPU에 덧셈 연산을 수행하라고 명령어를 보냈다고 가정해보자.
그러면 연산자와 피연산자 2개가 CPU 내부로 전달이 된다.
덧셈 명령어는 컨트롤 유닛에 의해서 덧셈을 하라는 명령을 잘 해석했다고 치자.
그럼 피연산자는 어떻게 처리해야 할까?
현재 ALU는 연산을 수행하고 있고, 컨트롤 유닛은 앞서 들어온 명령어를 해석하고 있는 상황이라고 치자.
다시 말해서 바쁜 상황이라면 피연산자들은 어딘가에 저장해뒀다가 처리할 상황이 되면 그 때 가져다 쓰면 된다.
그래서 CPU 내부에도 임시적으로 데이터를 저장하기 위한 작은 메모리 공간을 필요로 한다.
그게 바로 레지스터다.
레지스터는 2진 데이터(Binary Data) 저장을 위한 저장 장치다.
CPU에 따라서 16, 32, 64비트 정도의 데이터를 저장한다.
그리고 위에서 레지스터'들'이라고 한 것처럼 CPU 내부에 여러개가 존재한다.
이는 CPU의 종류에 따라서 갯수와 형태가 다양하다.
그리고 레지스터는 각각의 용도가 정해져있는 것이 일반적이고, CPU가 연산을 수행하기 위해서 반드시 필요하다.
한참 뒤에 이야기를 할 부분이기도 하지만, 레지스터의 갯수나 크기는 명령어를 디자인하는데에도 큰 영향을 준다.
[버스 인터페이스(Bus Interface)]
실제로 명령어가 CPU까지 잘 전달된 것은 버스 인터페이스가 있어서이다.
앞에서 봤던 큰 그림처럼 컴퓨터는 CPU, 메인 메모리, 하드 디스크 등등 많은 장치들이 붙어있다.
이들은 서로 독립적으로 동작하는 것이 아니라 데이터를 주고 받으며 동작한다.
그리고 서로 데이터를 주고 받기 위해서는 어떤 매개체가 필요한데 그것이 입출력 버스다.
위의 그림에서 보면 CPU와 입출력 버스 사이로 데이터가 입출력이 가능한 것으로 묘사가 되어 있다.
하지만 실제로는 입출력 버스의 통신방식을 모르면 데이터를 송/수신하는 것이 불가능하다.
그래서 CPU 내에는 입출력 버스의 통신방식을 이해하고 있는 무엇인가가 필요하다.
그 역할을 수행하는 것이 버스 인터페이스다.
버스 인터페이스 장치는 버스가 어떻게 데이터를 전송하는지, 그에 대한 프로토콜 또는 통신방식을 알고 있다.
따라서 CPU는 버스 인터페이스를 통해 CPU 내의 레지스터에 저장된 데이터를 보내기도 하고 받을 수 있는 것이다.
+ 추가 사항
CPU 외에도 하드디스크, 그래픽 카드와 같은 입출력 버스에 연결되는 모든 장치(Device)들은 인터페이스가 필요하다.
그래서 인터페이스는 연결되는 장치에 따라서 컨트롤러(Controller) 또는 어댑터(Adapter)라고도 한다.
[클럭 신호(Clock Pulse)]
클럭 신호 자체는 CPU를 구성하는 요소도 아니고 위의 그림에 표현되어 있지도 않다.
하지만 CPU를 구성하는 구성 요소에 제공이 되어야 하는 신호로 중요한 역할을 가지고 있다.
클럭 신호는 '타이밍(Timing)'을 제공하기 위해서 필요하다.
예를 들어 CPU의 클럭 속도가 1.6Mhz라고 치자.
그러면 메인보드에 있는 클럭발생기는 1초당 160만번의 클럭을 발생시키게 된다.
그리고 CPU는 매 클럭이 발생할 때마다 그 클럭에 맞춰서 일을 하게 된다.
쉽게 말하면, 클럭 속도가 1.6Mhz인 CPU는 초당 160만번의 연산을 한다는 것이다.
그래서 CPU의 클럭 속도가 높으면 초당 처리하는 명령어의 수가 많아지므로 컴퓨터의 성능이 좋아지는 것이다.
그런데 왜 클럭 신호에 맞춰서 연산을 수행할까?
여기에 대한 답은 "컴퓨터 시스템은 동기화를 필요로 한다" 라고 할 수 있다.
대충 이런 장치가 있다고 치자.
현재 input1과 input2가 계속 들어오고 있는 상황이다.
그런데 + 연산 장치가 연산하는 속도와 출력장치가 데이터를 가져가는 속도가 일치하지 않는다면?
어느쪽이 빠르거나 느리던 문제가 발생한다.
그래서 이를 해결하는 방법은?
속도가 느린 장치에 맞춰서 동작하면 되는 것이다.
만약 + 연산 장치의 속도가 출력장치보다 느리다고 가정하자.
그러면 클럭 신호의 발생속도를 + 연산 장치를 기준으로 맞춰서 데이터를 이동시키면 된다.
이와 같은 이유로 클럭 신호는 필요한 요소 중 하나이다.
[프로그램의 실행 과정]
[폰 노이만 구조(Architecture)]
오늘날의 컴퓨터의 모델은 폰 노이만에 의해 만들어졌다.
폰 노이만은 제시한 모델은 아래의 그림과 같다.
여기서 핵심은 '메모리와 프로그램'이다.
이 모델이 나오기 전까지는 프로그램은 메모리에 적재되어 실행되는 구조가 아니었다.
하지만 프로그램을 컴퓨터 내부에 저장되는 구조를 생각해낸 모델이 바로 '폰 노이만 아키텍처'다.
"프로그램이라는 것이 있고, 이 프로그램은 컴퓨터 내부에 '저장'되어서 순차적으로 실행되어야 한다."
이 구조는 지금까지 쓰이고 있다.
그리고 폰 노이만 구조는 "실행되어야 할 프로그램이 컴퓨터 내부에 저장된다"는 것이 특징이다.
그래서 'Stored Program Concept'라고도 한다.
[프로그램의 실행 과정]
프로그램이 실행되기에 앞서 실행파일이 생성되는 과정을 간략히 정리하면 다음과 같다.
C를 기준으로 한다면 다음과 같이 실행파일이 생성된다.
1) 소스 또는 헤더파일(*.c 또는 *.h) → 전처리기(Preprocessor) → 전처리가 된 소스 또는 헤더파일(*.i 또는 *.ii)
프로그래머가 작성한 코드에서 매크로에 대한 전처리를 수행한다.
#define이나 #include와 같은 # 지시자에 대해서 사전처리를 하여 컴파일러가 해석할 수 있도록 만드는 과정이다.
이 과정을 통해서 전처리가 완료된 소스파일이 나오게 된다.
2) 전처리가 된 소스 또는 헤더파일(*.i 또는 *.ii) → 컴파일러(Compiler) → 어셈블리 코드(*.s 또는 *.S)
전처리가 끝난 소스나 헤더파일은 컴파일러에 의해 어셈블리어로 바뀌게 된다.
컴파일러에 의해 수행되는 작업을 컴파일(Compile)이라고 하며, C언어로 작성된 코드가 어셈블리어로 번역이 된다.
만약 소스코드나 헤더파일에 문법적인 오류가 있을 경우 에러가 발생한다.
여기서 발생하는 에러를 '컴파일 에러(Compile Error)' 라고 한다.
3) 어셈블리 코드(*.s 또는 *.S) → 어셈블러(Assembler) → 오브젝트 파일(*.o 또는 *.obj)
어셈블리 코드는 어셈블러에 의해서 기계어(Machine Language)로 번역이 되어 바이너리 파일을 작성한다.
기계어파일은 오브젝트(object) 파일이라고 하며, 해당 파일은 데이터와 기계어를 포함하고 있다.
대부분의 서적이나 설명에서는 컴파일과 어셈블링을 하는 과정을 묶어서 컴파일이라고 하기도 한다.
4) 오브젝트 파일(*.o 또는 *.obj) - 링커(Linker) - 실행파일(.exe)
마지막으로 생성된 오브젝트 파일들을 하나로 연결을 하는 과정을 거치는데, 그 작업을 수행하는 것이 링커(Linker)다.
링커는 프로그램 내에서 참조하는 변수나 함수, 라이브러리를 하나하나 연결하고 묶는(Mapping) 작업을 수행한다.
여기서도 에러가 발생할 수 있다.
참조하고자 하는 파일의 위치가 맞지 않거나, 함수를 정의하는 부분에서 정의하지 않았거나 다른 경우에 발생한다.
이 경우는 컴파일은 됐지만 링커가 선언된 함수와 정의를 맵핑할 때 일치하지 않는 것을 확인하면 에러가 발생한다.
이와 같은 에러를 '링킹 에러(Linking Error)'라고 한다.
결론은 실행파일 안에는 컴퓨터가 실행해야 할 '바이너리 코드(기계어)'가 존재한다.
그리고 이 바이너리 코드와 폰 노이만의 Stored Program Concept를 하나로 묶으면 다음과 같은 그림이 된다.
실행파일에 의해 생성된 바이너리 코드는 메모리 공간에 적재된 다음 CPU에서는 명령어를 순차적으로 실행한다.
해당 명령어들은 메모리 내에서 실행이 되는 것이 아니라 CPU 내부로 하나씩 이동하여 실행하게 된다.
이를 3단계로 나누면 다음과 같다.
1) Fetch: 메모리 상에 있는 명령어를 CPU로 가져온다.
2) Decode: 가져온 명령어를 CPU가 해석하는 단계다.
3) Execution: 해석된 명령어의 명령대로 CPU가 실행하는 단계다.
프로그램이 실행되는 과정은 Fetch, Decode, Execution의 세 단계를 거쳐서 수행된다.
[하드웨어 구성의 재접근]
[데이터 이동의 기반이 되는 버스 시스템]
데이터를 이동하는데 있어서 사용되는 전송 경로를 버스 시스템(Bus System)이라고 한다.
실제로 입출력 버스에는 CPU를 포함한 여러 장치들이 연결되어 있다.
입출력 버스는 데이터를 주고 받기 위한 통로의 역할을 한다.
메모리에서 Fetch를 통해 명령어를 가져올 수도 있었지만, 반대로 CPU에서 메모리로 데이터를 저장하는 것도 가능하다.
위 그림은 CPU와 메모리를 기준으로 구성된 버스 시스템을 예로 들었다.
버스 시스템은 주고 받는 데이터의 종류에 따라서 다음과 같이 구성된다.
1) 데이터 버스(Data Bus)
이름 그대로 데이터의 이동을 위한 버스다.
데이터는 명령어가 될 수도 있고, 피연산자가 될 수도 있다.
2) 어드레스 버스(Address Bus)
마찬가지로 주소값을 이동하기 위해 필요한 버스다.
주소값을 이동해야 하는 경우는 예를 들면 다음과 같다.
CPU가 0x1024에 저장되어 있는 데이터 4바이트를 읽으려고 한다고 가정하자.
그러면 메모리 영역에 주소값 0x1024를 전달하는데 이 때 사용하는 것이 어드레스 버스다.
이후 CPU는 메모리에서 0x1024번지에 있는 4바이트의 데이터를 데이터 버스를 통해 전달받게 된다.
3) 컨트롤 버스(Control Bus)
CPU가 원하는 바가 있을 때 메모리에 전달하는 용도로 사용한다.
쉽게 말하면 CPU와 메모리가 특별한 사인(sign)을 주고 받는 용도로 사용된다고 보면 된다.
앞서 말했던 것처럼 CPU는 메모리에서 데이터를 가져오기도 하지만, 메모리에 데이터를 저장할 수도 있다.
그래서 CPU와 메모리 사이에는 데이터를 보낼 것인지, 받을 것인지에 대한 적절한 사인이 필요하다.
지금까지 정리했던 내용으로 폰 노이만이 구상했던 컴퓨터 구조를 좀 더 구체화하면 다음과 같은 그림이 된다.