크래프톤정글/CS:APP

CH01 컴퓨터 시스템으로의 여행

아람2 2024. 9. 14. 14:10
반응형

 

컴퓨터 시스템은 하드웨어와 시스템 소프트웨어로 구성되며, 이들이 함께 작동하여 응용프로그램을 실행한다
모든 컴퓨터 시스템은 유사한 기능을 수행하는 유사한 하드웨어와 소프트웨어 컴포넌트를 가지고 있다

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

소스 프로그램은 0 과 1 로 표시되는 비트들의 연속이며, Byte 라는 8 bit 단위로 구성된다 

각 바이트는 프로그램의 텍스트 문자를 나타낸다 

아스키 코드들로만 구성된 프로그램 == 텍스트 파일, 다른 모든 파일은 바이너리 파일이라고 부른다 

 

hello.c 프로그램은 연속된 바이트들로 파일에 저장되고,

각 바이트들은 특정 문자에 대응되는 정수 값을 가진다 (ex. 문자 i 는 아스키 코드 105)

# hello.c Program

#include <stdio.h>
int main(){
	printf("Hello, World!\\n");
	return 0;
}

모든 시스템 내부의 정보 - 디스크 파일, 메모리 상의 프로그램, 데이터, 네트워크를 통해 전송되는 데이터 - 는

비트들로 표시된다, 서로 다른 객체들을 구분하는 유일한 방법은 이들을 바라보는 컨텍스트에 달려 있다 

일례로 다른 컨텍스트에서는 동일한 일련의 바이트가 정수, 부동소수, 문자열 또는 기계어 명령을 의미할 수 있다 

 

C 프로그래밍 언어의 기원

1. C 는 유닉스 운영 체제와 밀접하게 연결되어 있다

2. C 는 작고 간단한 언어다 ( 한 명에 의해 관리 )

3. C 는 실용적 목적으로 설계되었다

 

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

컴파일 시스템

Preprocessing 전처리 단계 - 전처리기 cpp 는 본래의 C 프로그램을

# 문자로 시작하는 디렉티브 directive 에 따라 수정한다, hello.i 생성

    본격적으로 컴파일하기 전에 처리할 작업들, 외부에 선언된 다양한 소스 코드, 라이브러리 포함 (ex. #include)

    프로그래밍의 편의를 위해 작성된 매크로 변환 (ex. #define), 컴파일할 영역 명시 (ex. #if, #ifdef, ...)

    linux> gcc -E hello.c -o hello.i

Compiling 컴파일 단계 - 컴파일러 cc1 는 텍스트파일 hello.i 를 텍스트파일 hello.s 로 번역하며,

이 파일에는 어셈블리어 프로그램이 저장된다 (어셈블리어는 기계어와 일대일 대응이 되는 저급 언어)

그리고 main 함수의 정의를 포함한다

    전처리 완료된 소스 코드를 저급 언어 (어셈블리 언어) 로 변환

    linux> gcc -S hello.i -o hello.s

Assembling 어셈블리 단계 - 어셈블러 as 가 hello.s 를 기계어 인스트럭션으로 번역하고,

이들을 재배치가능 목적프로그램의 형태로 묶어서 hello.o 목적 파일에 그 결과를 저장한다

    어셈블리어를 기계어로 변환, 목적 코드 (Object File) 를 포함하는 목적 파일이 된다 

    linux> gcc -o hello.o hello.s

링크 단계 - hello 프로그램에 있는 printf 함수는 이미 컴파일된 별도의 목적파일인 printf.o 에 들어있으며,

이 파일과 hello.o 파일은 어떤 형태로든 결합되어야 한다, 링커 프로그램 ld 이 이 통합작업을 수행하고

 그 결과인 hello 파일은 실행가능 목적파일 (즉, 실행파일) 로 메모리에 적재되어 시스템에 의해 실행된다

목적 파일 VS 실행 파일
목적 파일과 실행 파일은 둘 다 기계어로 이루어진 파일이지만
목적 파일은 링킹 (Linking) 을 거친 이후에야 실행 파일이 된다

 

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

문제1) 프로그램 성능 최적화하기 컴파일러가 어떻게 C 문장들을 기계어 코드로 번역하는지 알 필요가 있다,

ex) switch 문은 if-else 문을 연속해서 사용하는 것보다 언제나 더 효율적일까?

함수 호출 시 발생하는 오버헤드는 얼마나 되는가? while 루프는 for 루프보다 더 효율적일까?

포인터 참조가 배열 인덱스보다 더 효율적일까? (답은 3장, 5장, 6장)

 

문제2) 링크 에러 이해하기

큰 규모의 소프트웨어 시스템을 빌드하려는 경우 링커가 어떤 참조를 풀어낼 수 없다고 할 때는 무엇을 의미하는가?

정적 변수와 전역 변수의 차이는 무엇인가? 왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는걸까? (답은 7장)

 

문제3) 보안 약점 Security Hole 피하기

인터넷과 네트워크상의 보안 약점의 주요 원인인 버퍼 오버플로우는 프로그래머들이 신뢰할 수 없는 곳에서 획득한

데이터의 양과 형태를 주의 깊게 제한하지 않기 때문에 발생한다, 안전한 프로그래밍은 무엇인가? (답은 3장)

 

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

- 커맨드라인 인터프리터로 프롬프트를 출력하고 명령어 라인을 입력 받아 그 명령을 실행한다 

만일 명령어 라인이 내장 쉘 명령어가 아니면 쉘은 실행파일 이름으로 판단하고 그 파일을 로딩해서 실행해 준다 

linux> ./hello # 쉘 프로그램이 각각의 문자를 레지스터에 읽어 들인 후 메모리에 저장한다 
hello, world # 키보드에서 Enter 를 누르면 명령 입력을 끝마쳤다는 사실을 쉘이 알게 된다 
# 그러면 쉘은 파일 내의 코드와 데ㅣ터를 복사하는 일련의 인스트럭션을 실행하여 실행 파일 hello 를
# 디스크에서 메인 메모리로 로딩한다 (데이터 부분에 "hello, world\n" 포함) 
# 데이터는 프로세서를 거치지 않고 디스크에서 메인 메모리로 직접 이동한다 
# hello 목적파일의 코드와 데이터가 메모리에 적재된 후, 프로세서는 hello 프로그램의 main 루틴의
# 기계어 인스트럭션을 실행하기 시작한다, 이 인스트럭션들은 "hello, world\n 스트링을
# 메모리로부터 레지스터 파일로 복사하고, 거기로부터 디스플레이 장치로 전송하여 화면에 글자들이 표시된다 
linux>

버스 Buses - 시스템 내를 관통하는 전기적 배선군, 컴포넌트들 간에 바이트 정보들을, Word 단위로 전송한다

입출력 장치 - 시스템과 외부 세계와의 연결을 담당, ex. 입력용 키보드, 출력용 디스플레이, 디스크 드라이브 등 

각 입출력 장치는 입출력 버스와 컨트롤러나 어댑터를 통해 연결된다 

컨트롤러와 어댑터는 패키징 Packaging 의 차이에 있다, 어댑터 - 마더보드의 슬롯에 장착되는 카드
컨트롤러 - 디바이스 자체가 칩셋이거나 시스템의 인쇄기판 (마더보드) 에 장착

메인 메모리 - 프로세서가 프로그램을 실행하는 동안 데이터와 프로그램을 모두 저장하는 임시 저장 장치,

물리적으로 DRAM Dynamic Random Access Memory 으로 구성되어 있다

프로세서 - 주처리장치 CPU, 메인 메모리에 저장된 인스트럭션들을 해독(실행)하는 장치,

프로세서의 중심에는 워드 크기의 저장장치 (or Register) 인 프로그램 카운터 PC 가 있다

 

워드 Word - 컴퓨터 설계 시 정해지는 메모리 기본 단위

 

인스트럭션의 요청에 의해 CPU 가 실행하는 단순한 작업의 예시들 

* 적재 Load - 메인 메모리에서 레지스터에 한 바이트 또는 워드를 이전 값에 덮어쓰는 방식으로 복사

* 저장 Store - 레지스터에서 메인 메모리로 한 바이트 또는 워드를 이전 값에 덮어쓰는 방식으로 복사

* 작업 Operate - 두 레지스터의 값을 ALU 로 복사하고 두 개의 워드로 수식연산을 수행한 뒤,

결과를 덮어쓰기 방식으로 레지스터에 저장한다

* 점프 Jump - 인스트럭션 자신으로부터 한 개의 워드를 추출하고, 이것을 PC 에 덮어쓰기 방식으로 복사한다

 

1.5 캐시가 중요하다

시스템이 정보를 한 곳에서 다른 곳으로 이동시키는 일에 많은 시간을 보내는데

프로세서-메모리 간 격차가 지속적으로 증가함에 따라 (프로세서 성능 >>> 메모리 성능)

격차에 대응하기 위해 보다 작고 빠른 캐시 메모리라고 부르는 저장 장치를 고안하여

프로세서가 단기간에 필요로 할 가능성이 높은 정보를 임시로 저장할 목적으로 사용한다 

캐시는 SRAM Static Rancom Access Memory 를 이용하여 구현한다 

 

프로그램이 특정 코드나 데이터를 집중적으로 액세스하는 경향을 지역성 Locality 라고 부르며

이를 캐시 시스템이 잘 활용하여 시스템이 매우 크고 빠른 메모리 효과를 얻을 수 있다

* 캐시는 실제로 큰 메모리가 아니라 작은 크기의 고속 메모리다 

자주 액세스할 가능성이 높은 데이터를 캐시가 보관하도록 설정하면

빠른 캐시를 이용해서 대부분의 메모리 작업을 수행할 수 있게 된다

+ 비유로 얘기하자면, 내가 경기도에 거주하고 있고, 택배 저장소가 제주도에 있는데

택배 저장소 위치를 서울로 옮겨서 택배를 빨리 받을 수 있게 만드는 것이 캐시 Cache 

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

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

계층의 꼭대기에서부터 밑으로 이동할수록 저장 장치들은 더 느리고, 더 크고, 바이트 당 가격이 저렴해진다

 

메모리 계층 구조에서, 한 레벨의 저장 장치가 다음 하위 레벨 저장 장치의 캐시 역할을 한다 

ex. L1 캐시는 L2 캐시의 데이터를 캐싱하고, L2 캐시는 L3 캐시의 데이터를 캐싱한다 

네트워크 시스템과 같은 분산 파일 시스템을 사용하는 경우

로컬 디스크는 다른 시스템의 디스크에 저장된 데이터의 캐시 역할을 수행한다 

로컬 디스크는 원격 네트워크 서버에서 파일들을 가져와 캐시처럼 보관할 수 있다 

 

1.7 운영 체제는 하드웨어를 관리한다

hello 예제로 다시 돌아가서, 쉘 프로그램이 hello 프로그램을 로드, 실행, 출력할 때 

프로그램은 디스크나 메모리를 직접 액세스하지 않는다

대신, 운영 체제 Operation System 가 제공하는 시스템 호출 System Calls 과 같은 서비스를 사용하여

디스크에서 프로그램을 읽고, 메모리에 적재하고, 실행하며, 출력 결과를 화면에 표시한다

운영 체제는 메모리 관리, 프로세스 스케줄링, I/O 처리를 담당하여

프로그램이 하드웨어에 직접 접근하는 대신, 안전하고 효율적으로 실행될 수 있도록 중간 역할을 한다 

운영 체제 Operation System 의 목적

1. 제멋대로 동작하는 응용 프로그램들이 하드웨어를 잘못 사용하는 경우를 막기 위해
2. 응용 프로그램들이 단순하고 균일한 매커니즘을 사용하여
    복잡하고 매우 다른 저수준 하드웨어 장치들을 조작할 수 있도록 하기 위해 

 

운영 체제는 추상화를 통해 이 두 가지 목표를 달성하고 있다 

* 추상화란, 복잡한 시스템 및 개념을 단순화하는 것

 

 

1.7.1 프로세스

프로세스는 실행 중인 프로그램에 대한 운영체제의 추상화다

다수의 프로세스는 동일한 시스템에서 동시에 실행될 수 있으며, 각 프로세스는 하드웨어를 배타적으로 사용하는 것처럼 느낀다

(동시에 concurrently 라는 말은 한 프로세스의 인스트럭션들이 다른 프로세스의 인스트럭션들과 섞인다는 것을 의미한다)

이러한 착각 (책에서는 환상이라고 함) 은 프로세스라고 하는 개념에 의해서 만들어진다 

 

운영 체제는 문맥 전환 Contect Switching 이라는 방법을 사용해서 이러한 교차 실행을 수행한다

또한 운영 체제는 프로세스가 실행하는 데 필요한 모든 상태 정보 (컨텍스트) 의 변화를 추적한다 

현재 프로세스에서 다른 새로운 프로세스로 제어를 옮기려고 할 때, 운영 체제는 현재 프로세스의 컨텍스트를 저장하고

새 프로세스를 복원시키는 문맥 전환을 실행하여 제어권을 새 프로세스로 넘겨준다

새 프로세스는 이전에 중단했던 바로 그 위치에서 다시 실행된다 

하나의 프로세스에서 다른 프로세스로의 전환은 운영 체제 커널 kernel 에 의해 관리된다

커널은 운영 체제 코드의 일부분으로 메모리에 상주하고, 모든 프로세스를 관리하기 위해 시스템이 이용하는 코드와 자료 구조의 집합이다 

응용 프로그램이 운영 체제에 의한 어떤 작업을 요청하면, 컴퓨터는 파일 읽기나 쓰기와 같은 특정 시스템 콜 System call 을 실행해서 커널에 제어가 넘겨준다, 그러면 커널은 요청된 작업을 수행하고 응용 프로그램으로 리턴한다 

1.7.2 쓰레드 Thread 

최근의 시스템에서는 프로세스가 실제로 Thread 라고 하는 다수의 실행 유닛으로 구성되어 있다 

각각의 쓰레드는 해당 프로세스의 컨텍스트에서 실행되며 동일한 코드와 전역 데이터를 공유한다

쓰레드는 다수의 프로세스들에서보다 데이터의 공유가 더 쉽고, 프로세스보다 더 효율적이다 

1.7.3 가상메모리

가상메모리는 각 프로세스들이 메인 메모리 전체를 독점적으로 사용하고 있는 것 같은 환상을 제공하는 추상화이다 

리눅스에서, 주소 공간의 최상위 영역은 모든 프로세스들이 공통으로 사용하는 운영 체제의 코드와 데이터를 위한 공간이고,

주소 공간의 하위 영역은 사용자 프로세스의 코드와 데이터를 저장한다 (그림 위쪽으로 갈수록 주소가 증가한다)

가상 메모리가 작동하기 위해서는 하드웨어와 운영 체제 소프트웨어 간의 복잡한 상호 작용이 필요하다 

기본적인 아이디어는 프로세스의 가상 메모리 내용을 디스크에 저장하고 메인 메모리를 디스크의 캐시로 사용하는 것이다

1.7.4 파일

파일은 그저 연속된 바이트들이다, 모든 입출력 장치는 파일로 모델링한다 

시스템의 모든 입출력은 유닉스 I/O 라는 시스템 콜들을 이용하여 파일을 읽고 쓰는 형태로 이루어진다

파일 개념은 응용 프로그램 시스템에 들어 있는 입출력 장치들의 통일된 관점을 제공한다 

 

1.8 시스템은 네트워크를 사용하여 다른 시스템과 통신한다

개별 시스템의 관점에서 볼 때, 네트워크는 또 다른 입출력 장치로 볼 수 있다 

시스템이 메인 메모리로부터 네트워크 어댑터로 일련의 바이트를 복사할 때, 데이터는 로컬디스크 드라이브 대신에 네트워크를 통해서 다른 컴퓨터로 이동된다, 마찬가지로 다른 컴퓨터로부터 받은 데이터를 읽어서 메인 메모리에 복사할 수 있다 

hello 예제로 돌아가서, telnet 응용을 사용하여 hello 프로그램을 다른 곳에 위치한 컴퓨터에서 실행할 수 있다

telnet 클라이언트를 사용하여 로컬 컴퓨터를 원격 컴퓨터의 telnet 서버로 연결하려 한다고 하면

원격지 컴퓨터에 로그인하고 쉘을 실행시킨 후에 원격지 쉘은 입력 명령을 기다린다

1. telnet 클라이언트에 "hello" 스트링을 입력하고 Enter 키를 누르면

2. 클라이언트 프로그램은 이 스트링을 telnet 서버로 보낸다 

3. Telnet 서버가 네트워크에서 스트링을 받은 후에, 원격 쉘 프로그램에 이들을 전달한다

4. 원격 쉘은 hello 프로그램을 시행하고 출력을 다시 telnet 서버로 전달한다

5. telnet 서버는 네트워크를 거쳐 출력 스트링을 telnet 클라이언트로 전달하고

    클라이언트 프로그램은 출력 스트링을 자신의 로컬 터미널에 표시한다 

Telnet 텔넷과 SSH
인터넷을 통해 원격지의 호스트 컴퓨터에 접속할 때 지원되는 인터넷 표준 프로토콜
TCP/ IP 기반의 프로토콜이며, TCP 23번 Port 를 사용한다 
Telnet 클라이언트와 Telnet 서버는 NVT (Network Virtual Terminal) 를 통해 연결되어 통신이 가능하다 
주로 네트워크 장비나 서버의 원격 관리를 위해 사용되며, 텍스트 기반의 명령어를 쉽게 주고 받을 수 있기 때문에
간단한 명령어 입력이 필요한 작업에 유용하다, 그러나 텍스트 이외의 데이터 전송에는 적합하지 않다
또한 데이터를 암호화하지 않고 평문으로 전달하기 때문에 패킷 스니핑에 취약하다

이러한 보안 문제를 해결하기 위해 비대칭 암호화 방식을 사용한 SSH (Secure Shell, TCP Port 22) 가 등장했다 
SSH 는 키 기반 인증 방식을 통해 무단 접속을 방지할 수 있으며, Telnet 에 비해 보안성이 뛰어나다 
또한 SFTP (Secure File Transfer Protocol) 를 이용한 암호화된 파일 전송이 가능하고,
포드 포워딩을 이용한 데이터나 애플리케이션 통신을 안전하게 전달할 수 있다,
이를 통해 웹 트래픽, 데이터베이스 통신 등 다양한 데이터를 암호화하여 전송할 수 있다 

 

반응형