CS

[CSAPP] 1챕터 - 컴퓨터 시스템에게로 떠나는 여행 (1) - 프로그램 번역 과정

Zeka_P 2025. 7. 28. 21:06

 7월 26일부로, 나만의 무기 프로젝트를 종료했습니다. 끝난 김에, 집에도 들르고 술도 마시고.. 랭크게임도 몇판 하고, 몬스터 헌터도 몇판 하고, 최애 굿즈 언박싱도 하고(아크릴 블록이 왔는데 너무 너무 너무 너무 예쁘더라고요) 라이브 티켓도 예매하고 정신을 차려보니 28일이네요. 알차게 이틀 보내고 왔습니다. 

 

사실상 모든 과정이 종료되어, 남은 이력서 기간 동안 이력서 대신에 그간 배운 내용들을 이렇게 좀 적으려고 합니다. 이력이래도 3월 10일부터가 이력 시작이기 때문도 있고.. 배우러 온 입장인데 다 까먹어버리면 정말 사고일테니 한번 더 복습한다 생각하고.. 

 

따라서 CSAPP 부터 차근차근, 공부한 내용을 정리해가보겠습니다. 이 앞부터는 CSAPP 도서의 치명적인 스포일러와, 컴퓨터는 그 시절 외계인을 고문해 고안한 기술이라 생각했던 (구)컴맹이 작성하여 오류가 존재할 수 있는 내용이 포함되어 있습니다. 양해부탁드립니다. 

 

추가적으로, 예를 들어 해당 글에선 1.3 컴파일 시스템이 어떻게 동작하는지 이해하는 것이 중요하다 가 생략되어 있습니다. 이런 가이드라인이나, 조언이 적혀있는 구간은 스킵하고, 저희가 실질적으로 공부하는 파트만 작성하였습니다. 양해 부탁드립니다. 

1. 정보는 비트와 컨텍스트(Context)로 이루어져 있다.

 컴퓨터도 결국은 전자기기입니다. 지금 제가 타자를 치고 있는 것도, 이걸 읽고 있는 것도, 터미네이터가 걸어다니는 것도 전부 가장 기초적인 규칙에 기반합니다. 전기 신호를 끊었다 올렸다를 반복하는 것입니다. 이를 0과 1로 주어 이진(binary)적으로 운영시키고, 그 어떤 복잡한 알고리즘이든, 또는 컴퓨터 기능이든 결국 이 "껐다 키기"가 미친듯이 동작하여 만들어지는 것입니다.(우리가 어렸을 때 방 전등 스위치를 미친듯이 껐다 키는 걸 반복했던 것도, 하나의 알고리즘을 만들기 위한 코딩 연습 중 하나가 아니었을까요)

 

우리는 이 0과 1의 신호값 하나를 비트(bit)라 부르고, 8개로 뭉쳤을 땐 바이트(byte)라고 부릅니다.

물론 사용자에게 노출 시킬 떈 그 목적에 맞춰 값을 변환합니다. 이런 언어의 출력에서는 아스키(ASCII) 표준 방식으로 값을 변환시키며, 이 아스키 문자로만 이루어진 파일이 텍스트 파일, 그 이외의 파일은 바이너리 파일 이라고 부릅니다. 

마지막으로, 이렇게 비트로만 이루어져 있다면, 예를 들어 01101001은 105인데, 이를 아스키 문자로 보면 'i'를 나타냅니다. 근데 단순히 이 01101001만 툭 던져줬을 땐, 이게 i 일수도, 그냥 숫자로 105일수도, 어떤 기계어 명령으로 66호 명령일수도 있는거지 않겠습니까. 즉, 명확한 구분점이 요구되는데 이를 컨텍스트(Context)로 판단합니다.

 

2. 프로그램은 다른 프로그램에 의해 다른 형태로 번역된다.

 앞서 말했듯, 사용자가 입력하거나 제공받게되는 파일은 대체로 텍스트 파일입니다. 하지만 스위치만 껐다 켰기만 할 줄아는(하지만 빠른) 우리의 컴퓨터는 바이너리 파일을 받아야 일을 할 수 있습니다. 따라서 저희가 어떤 프로그램을 작성하면, 이는 일종의 번역 과정을 거쳐 최종척으론 바이너리 파일로 컴퓨터가 받을 수 있어야 합니다. 이 일련의 과정, 시스템을 컴파일 시스템이라 부릅니다.

 

컴파일 시스템 요약도입니다. 우리의 명령이 정확히 전달되기 위해, 단순 번역만이 아닌 검수, 검열, 탐색 담당이 추가로 붙습니다.

 

C언어에서 printf("hello world"); 를 입력했다고 가정해볼까요. 단순히 printf 만 툭 던져주면, 이 이진 명령어가 뭘 의미하는지 조차 모를 것입니다. 그렇기에 이 printf 라는 함수가 정의된 stdio.h 헤더 파일을 동봉하여 보냅니다. 전처리 단계에서 해당 작업이 처리되며, 전처리기(cpp)가 담당합니다. 처음 파일이 hello.c 파일이었다면, 이 과정에서 동봉된 헤더파일을 포함하며 생긴 파일은 hello.i 파일이 됩니다.

 

이어서 가보겠습니다. 물론 hello.i 파일이 함수의 정의까지 잘 정의되어 있다고 할 수 있겠지만, 나열된 명령어들은 어떻게 보면 불규칙합니다. printf, scanf... 더 나아가면 청기 올리고 백기 내리는데 내리자마자 청기는 시계방향으로 돌리면서 공중제비를 돌아 같은 함수는요? 정렬을 해준다거나, 압축시키고 이진수로 넘겨준다면, 더 효율적일 것 같습니다. 하지만, 이진수로 바꾸기 전에, 어떻게 압축시킬까요? 어셈블리어 라고 하는, 약속된 명령어 정의가 존재합니다. 예를 들어 a랑 b를 더해줘 라는건 addq a b 같은 명령어로 정의됩니다. 전처리된 hello.i파일은 이진수로 바뀌기 전에, 이 약속된 명령어로 바뀌며 정렬되는 과정을 먼저 거칩니다. 이를 컴파일 단계라 하고, 단계 이름대로 컴파일러(ccl)가 담당합니다. 이 과정을 거치면, hello.i파일은 hello.s 파일로 변환됩니다. 그리고 이 단계로 만들어진 .s 파일까지만 텍스트 파일 성격을 유지합니다.

 

이제 진짜로, 이진수로 바꿀 차례입니다. 어셈블러(as)가 hello.s 파일을 hello.o 파일로 변환시키며, 이때 텍스트 파일이었던 프로그램이 이진수로 이루어진 바이너리 파일로 변환됩니다. 재배치 가능한 목적프로그램(object program)으로 변환되는데, 재배치 가능하다 라는 뜻을 쉽게 설명드리자면, 당장 값을 모르겠는 영역은, 마킹만 해두어 향후 재배치 가능하게 비워뒀다는 뜻입니다.

이건 또 무슨 뜻이냐, 위의 addq a b 를 예시로 들어볼까요? 저희 입장에선 a변수와 b 변수를 더하라는 간단한 명령어입니다. 하지만 이 a 변수가 어디 있을까요? a 변수가 정의되어 있는건 알겠습니다. 하지만 결국 a 변수가 가진 어떤 값이, 컴퓨터 안에 어떤 부품의 어딘가에는 저장이 되어 있어야 이를 꺼내서 더할 수 있겠죠. 그데 그게 어디에 저장되어 있는지는 모릅니다. 그래서 "지금 이 명령어의(addq) 이 자리(a) 는 변수 a의 자리야. 나중에라도 너가 변수 a의 위치를 알게되면 이 자리는 그 변수 a의 위치로 넣어줘" 라고 마킹만 해두고, 바로 다음 담당자에게 이 작업을 맡깁니다. 저희가 저장하는 변수도, 사실은 그리 편한 존재가 아니었던 겁니다.. 

 

마지막으로, 링커(ld)가 작업을 넘겨받아, 최종적인 실행가능 목적파일(=실행파일)로 완성시킵니다. 말 그대로 링크시키는 과정이 여기서 이루어지는데, 쉽게 생각하면 앞서 말씀드린 printf의 경우, 그 printf가 이미 컴파일된 printf.o 파일을 불러와 hello.o 파일과 링크시킵니다. 그리고 이 링크 과정에서, 주소를 알 수 없어 마킹만 되었던 a,b의 주소를 파악해 임시로 마킹만 되어있던 명령어를 최종으로 완성시킵니다. 그리고 링커까지 작업을 마치면, 이를 메모리에 적재시킨 뒤, 컴퓨터가 메모리에 적재된 프로그램을 실행해, 여러분 화면에 "hello, world!"를 노출 시킵니다. 

 

모든 내용을 완전히 외울 필요는 없습니다. 단순히 과정의 단계와 역할만 인지하고 넘어가셔도 무방합니다. 이렇게 자신있게 말할 수 있는 사유는, 어차피 뒤에서 한 과정당 1주일씩 투자하여 공부하게 됩니다. 이 앞, 절망 있다..