- 1.1 정보는 비트와 컨텍스트로 이루어진다.
- 1.2 프로그램은 다른 프로그램에 의해 다른 형태로 번역된다.
- 1.3 컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다.
- 1.4 프로세서는 메모리에 저장된 인스트럭션을 읽고 해석한다.
- 1.4.1 시스템의 하드웨어 조직
- 1.4.2 hello 프로그램의 실행
- 1.5 캐시가 중요하다.
- 1.6 저장장치들은 계층구조를 이룬다.
- 1.7 운영체제는 하드웨어를 관리한다.
- 1.8 시스템은 네트워크를 이용하여 다른 시스템과 통신한다.
- 1.9 중요한 주제들
- 1.10 요약
안녕하세요! caution입니다. 오늘의 컨텐츠는 컴퓨터 시스템 제 3판의 1장 내용입니다.
1.1 정보는 비트와 컨텍스트로 이루어진다.
#include <studio.h>
int main()
{
printf("hello, world\n");
return 0;
}
위의 hello 프로그램은 프로그래머가 에디터로 작성한 소스프로그램이며, hello.c
라는 텍스트 파일로 저장됩니다. 소스 프로그램은 0 또는 1로 표시되는 비트들의 연속이며, 바이트라는 8비트 단위로 구성됩니다.
대부분의 컴퓨터 시스템은 텍스트 문자를 ASCII 표준을 사용하여 표시합니다. hello.c
프로그램은 연속된 바이트들로 파일에 저장되며, 각 바이트는 특정 문자에 대응되는 정수값이 존재합니다. 예를 들어 첫 번째 바이트는 35인데, 이는 문자 ‘#’에 대응됩니다. 이처럼 오로지 아스키 문자들로만 이루어진 파일들을 텍스트 파일이라고 부르며, 다른 모든 파일들은 바이너리 파일이라고 합니다.
모든 시스템 내부의 정보는 비트들로 표시됩니다.
1.2 프로그램은 다른 프로그램에 의해 다른 형태로 번역된다.
hello 프로그램은 인간이 읽을 수 있는 언어로 작성되어있습니다. 하지만 시스템에서 이 프로그램을 실행시키려면 각 문장을 저급 기계어 인스트럭션들로 번역되어야 합니다. 이 인스트럭션들은 “실행가능 목적 프로그램(Executable object Program)”이라고 하는 바이너리 디스크 파일로 저장됩니다. 유닉스 시스템에서 소스파일을 오브젝트 파일로 번역하는 방법은 다음과 같습니다.
linux> gcc -o hello hello.c
GCC 컴파일러 드라이버는 소스파일 hello.c
를 읽어서 실행파일인 hello 로 번역합니다. 번역은 다음 4개의 단계를 거쳐서 실행됩니다.
각각의 단계를 수행하는 프로그램들 - 전처리기, 컴파일러, 어셈블러, 링커 - 을 합쳐서 컴파일 시스템이라고 합니다.
- 전처리 단계 : 전처리기는 본래의 C 프로그램을 # 문자로 시작하는 디렉티브에 따라 수정합니다. 우리의
hello.c
파일 첫 줄의#include<studio.h>
는 전처리기에게 시스템 헤더파일인studio.h
를 프로그램 문장에 직접 삽입하라고 지시합니다. 이 결과는.i
로 끝나는 프로그램이 생성됩니다. - 컴파일 단계 : 컴파일러는
hello.i
를hello.s
로 번역하며, 이 파일에는 어셈블리어 프로그램이 저장됩니다. 어셈블리어는 상위수준 컴파일러를 위한 공통의 출력언어를 제공함으로 유용합니다.main: subq $8, %resp movl $.LCO, %edi call puts movl $0, %eax addq #8, %rsp ret
- 어셈블리 단계 : 어셈블러가
hello.s
를 기계어 인스트럭션으로 번역하고, 재배치가능 목적프로그램의 형태로 묶어서hello.o
라는 목적 파일에 저장합니다. 이 파일은 main 함수의 인스트럭셔들을 인코딩하기 위한 17바이트를 포함하는 바이너리 파일입니다. - 링크 단계 : hello 프로그램에서 호출하는 printf 함수는 표준 C 라이브러리에 포함되어 있으며, 이미 컴파일된 별도의 목적파일인
printf.o
에 들어있습니다. 링커는print.o
파일과hello.o
파일을 연결해줍니다. 이 결과 실행가능 목적파ㅇㅣㄹㄹㅗ 메모리에 적재되어 시스템에 의해 실행됩니다.
1.3 컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다.
프로그램 성능 최적화하기
최신 컴파일러들은 복잡한 도구로 대개 우수한 코드를 생성하고 최적화를 지원합니다. 하지만 프로그래머로서 효율적인 코드를 작성하기 위해서는 기계어 수준 코드에 대한 기본적인 이해를 할 필요가 있으며 컴파일러가 어떻게 C 문장들을 기계어 코드로 번역하는지 알 필요가 있습니다. 예를 들어 switch문은 if-else 문을 연속해서 사용하는 것보다 언제나 더 효율적일까? 함수 호출 시 발생하는 오버헤드는 얼마나 되는가? while 루프는 for 루프보다 효율적일까? 포인터 참조가 배열 인덱스보다 더 효율적인가? 합계를 지역변수에 저장하면 참조형태로 넘겨받은 인자를 사용하는 것보다 왜 루프가 더 빨리 실행되는가? 수식 연산시 괄호를 단순히 재배치 하기만 해도 함수가 더 빨리 실행되는 이유는 무엇인가?
링크 에러 이해하기
링커가 어떤 참조를 풀어낼 수 없다고 할 때는 무엇을 의미하는가? 정적변수와 전역변수의 차이는 무엇인가? 만일 각기 다른 파일에 동일한 이름의 두 개의 전역변수를 정의한다면 무슨 일이 일어나는가? 정적 라이브러리와 동적 라이브러리의 차이는 무엇인가? 컴파일 명령을 쉘에서 입력할 때 명령어 라인의 라이브러리들의 순서는 무슨 의미가 있는가? 가장 겁나는 질문인, 왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는 걸까?
보안 약점 피하기
오랫동안 버퍼 오버플로우 취약성이 인터넷과 네트워크상의 보안 약점의 주요 원인으로 설명되었다. 이 취약성은 프로그래머들이 신뢰할 수 없는 곳에서 획득한 데이터의 양과 형태를 주의 깊게 제한해야 할 필요를 거의 인식하지 못하기 때문에 생겨난다. 안전한 프로그래밍을 배우는 첫 단계는 프로그램 스택에 데이터와 제어 정보가 저장되는 방식 때문에 생겨나는 영향을 이해아흔 ㄴ것입니다.
1.4 프로세서는 메모리에 저장된 인스트럭션을 읽고 해석한다.
hello.c
소스 프로그램은 컴파일 시스템에 의해 hello라는 실행 가능한 목적파일로 번역되어 디스크에 저장되었습니다. 이 실행파일을 유닉스 시스템에서 실행하기 위해서는 쉘에서 그 이름을 입력하면 됩니다.
linux> ./hello
hello, world
linex>
입력된 명령어 ./hello
는 내장 쉘 명령어가 아니기 때문에 실행파일의 이름으로 판단하고 파일을 로딩하고 실행해 주고, 이 프로그램이 종료되기를 기다립니다. hello 프로그램은 메세지를 화면이 출력하고 종료됩니다. 쉘은 프롬프트를 출력해주고 다음 입력 명령어 라인을 기다립니다.
1.4.1 시스템의 하드웨어 조직
hello 프로그램을 실행할 때 무슨 일이 일어나는지 설명하기 위해서는 전형적인 시스템에서의 하드웨어 조직을 이해할 필요가 있습니다.
버스
시스템 내를 관통하는 전기적 배선군을 버스라고 하며, 컴포넌트들 간에 바이트 정보들을 전송한다. 버스는 일반적으로 word(32비트 : 4바이트 /64비트 : 8바이트)단위로 데이터를 전송하도록 설계된다.
입출력 장치
입출력 장치는 시스템과 외부세계와의 연결을 담당한다. 예제 시스템은 네 개의 입출력 장치를 가지고 있다. 마우스, 키보드, 디스플레이, 디스크 드라이브. hello 실행파일은 디스크에 저장되어 있다. 각 입출력 장치는 입출력 버스와 컨트롤러나 어댑터를 통해 연결된다. 이 두 장치의 차이는 패키징에 있다. 컨트롤러는 디바이스 자체가 칩셋이거나 메인보드에 장착된다. 어댑터는 메인보드의 슬롯에 장착되는 카드이다.
메인 메모리
메인 메모리는 프로세서가 프로그램을 실행하는 동안 데이터와 프로그램을 모두 저장하는 임시 저장장치다. 물리적으로 메인 메모리는 DRAM 칩들로 구성되어 있다. 논리적으로 메모리는 연속적인 바이트들의 배열로 0부터 시작해서 각 고유의 주소를 가진다.
프로세서
주처리장치(CPU) or 프로세서는 메인 메모리에 저장된 인스트럭션들을 해독하는 엔진이다. 프로세서의 중심에는 word 크기의 저장장치(Register)인 프로그램 카운터(PC)가 있다. 프로세서는 PC가 가리키는 곳의 메모리로부터 인스트럭션을 읽어오고, 비트들을 해석하여 지정된 동작을 실행한다. 그리고 PC를 다음 인스트럭션 위치로 업데이트 한다.(이 새로운 위치는 이전의 인스트럭션과 메모리 상에서 연속적일 수도 있고, 그렇지 않을 수도 있다.) 프로세서는 메인 메모리, 레지스터 파일, ALU(수식/논리 처리기) 주위를 순환한다. 레지스터 파일은 각각 고유의 이름을 갖는 word 크기의 레지스터 집합으로 구성되어 있다. ALU는 새 데이터와 주소 값을 계산한다.
- 적재(Load) : 메인 메모리에서 레지스터에 1 byte or 1 word를 이전 값에 덮어쓰는 방식으로 복사한다.
- 저장(Store) : 레지스터에서 메인 메모리로 1 byte or 1 word를 이전 값을 덮어쓰는 방식으로 복사한다.
- 작업(Operate) : 두 레지스터의 값을 ALU로 복사하고 두 개의 워드로 수식연산을 수행한 뒤, 결과를 덮어쓰기 방식으로 레지스터에 저장한다.
- 점프(Jump) : 인스트럭션 자신으로부터 1 word를 추출하고 이를 PC에 덮어쓰기 방식으로 복사한다.
1.4.2 hello 프로그램의 실행
앞 서 작성했던 hello 프로그램을 실행시켰을 때 무슨 일이 일어나는지 자세히 살펴보자.
Step 1
- 쉘 프로그램을 실행시키고 “./hello”를 입력한다.
- 메인 메모리에 “./hello”가 저장된다. USB Controller - Keyboard » I/O bridge » Bus Interface » Register file » Bus Interface » I/O bridge » Main Memory
Step 2
- 엔터를 누른다. 파일의 코드와 데이터를 복사하는 인스트럭션을 실행한다.
- hello를 디스크에서 메인 메모리로 로딩한다.
- 출력 문자열인 “hello, world\n”이 메인 메모리에 포함된다. Disk » Disk Controller » I/O bridge » Main Memory
Step 3
- hello 프로그램의 main 루틴의 인스트럭션을 실행한다.
- “hello, world\n” 문자열을 메인 메모리로부터 레지스터로 복사한다.
- 문쟈열을 디스플레이 장치로 전송하여 화면에 글자들이 표시된다. Main memory » I/O bridge » Bus Interface » Register file » Bus Interface » I/O bridge » Graphics adapter
1.5 캐시가 중요하다.
위의 시나리오를 보면 시스템이 정보를 이동시키는 일에 매우 많은 시간을 보낸다는 것이다. 이러한 복사과정들이 프로그램의 실제 작업을 느리게 하는 오버헤드이다. 그래서 시스템 설계자들은 이러한 복사과정을 가능한 빠르게 동작하도록 설계하려고 한다. 물리학의 법칙 때문에 더 큰 저장장치들은 더 작은 저장장치들보다 느린 속도를 갖는다. 하지만 더 빠른 장치를 만드는 것은 더 많은 비용이 든다.(당연하게도)
시스템 드라이브는 메인 메모리보다 1,000 배 크기가 크지만 프로세서가 디스크에서 1 word의 데이터를 읽어드리는 데는 천만 배 더 오래걸릴 수 있다. 레지스터 파일은 수백 바이트를 저장하지만 메인 메모리는 십억 바이트를 저장한다. 프로세서는 레지스터 파일의 데이터를 읽는데 메모리보다 100배 이상 빨리 읽을 수 있다.
프로세서-메모리 간 속도 격차가 지속적으로 증가함에 대응하기 위해서 작고 빠른 캐시 메모리를 고안하여 프로세서가 단기간에 필요로 할 가능성이 높은 정보를 임시로 저장할 목적으로 사용한다.
1.6 저장장치들은 계층구조를 이룬다.
모든 컴퓨터 시스템의 저장장치들은 다음과 같은 메모리 계층구조로 구성되어 있다. 계층의 곡대기에서부터 맨 밑바닥까지 이동할수록 저장장치들을 더 느리고, 크고, 비용이 싸진다. 레지스터 파일은 가장 최상위인 레벨 0에 해당하며 L1-L3의 캐시를 가진다. 메인 메모리는 다음 계층에 속한다. 이러한 메모리 계층 구조의 주요 아이디어는 한 레벨의 저장장치가 다음 하위레벨 저장장치의 캐시 역할을 안하는 것이다. L1과 L2의 캐시는 L2와 L3이며, 디스크의 캐시는 메인 메모리다.
1.7 운영체제는 하드웨어를 관리한다.
셀 프로그램이 hello 프로그램을 로드하고 실행할 때, 또 hello 프로그램이 메시지를 출력할 때, 프로그램이 키보드나 디스플레이, 디스크나 메인 메모리를 직접 엑세스하지 않고 운영체제가 제공하는 서비스를 활용한다. 운영체제는 응용프로그램이 하드웨어를 잘못 사용하는 것을 막고 단순하고 균일한 메커니즘을 사용하여 복잡한 저수준 하드웨어 장치들을 조작할 수 있도록 하기 위해 이를 추상화한다.
1.7.1 프로세스
셀 프로그램이나 hello 같은 프로그램이 실행될 때 운영체제는 시스템에서 이 한 개의 프로그램만 실행되는 것 같은 착각에 빠지도록 해준다. 프로세서가 프로그램 내의 인스트럭션들을 다른 방해 없이 순차적으로 실행하고, 프로세서, 메인 메모리, 입출력장치들을 이 프로그램이 모두 독차지 하는 것처럼 보인다. 이러한 동작이 가능한 것은 프로그램에 대한 운영체제의 추상화인 프로세스라는 개념 때문이다. 다수의 프로세스들은 동일한 시스템에서 동시에 실행될 수 있으며, 하드웨어를 배타적으로 사용하는 것처럼 느껴진다. 대부분의 시스템에서 CPU의 수보다 실행가능한 프로세스 수가 더 많다. 이는 프로세서가 프로세스들을 교차하면서 실행시키는 방식(context switching)으로 CPU가 다수의 프로세스를 동시에 실행하는 것처럼 보이게 해준다. 운영체제는 프로세스가 실행하는 데 필요한 모든 상태정보의 변화를 추적한다. 이러한 상태정보들을 context라고 부르는데, Context에는 PC, register file, main memory의 현재 값을 포함하고 있다. context switching이 발생하면 운영체제는 현재 프로세스의 Context를 저장하고 새 프로세스의 컨텍스트를 복원시키며 제어권을 새 프로세스로 넘겨준다. 이러한 프로세스 전환은 운영체제 커널에 의해 관리된다. 커널은 운영체제 코드의 일부분으로 메모리에 상주하고 있다. 응용프로그램이 운영체제에 어떤 작업을 요청하면, 특정한 시스템 콜을 실행해서 커널에 제어를 넘겨준다. 그러면 커널이 요청된 작업을 수행하고 다시 응용프로그램으로 리턴한다. 커널은 별도 프로세스가 아니라 모든 프로세스를 관리하기 위해 시스템이 이용하는 코드와 자료구조의 집합이다.
1.7.2 스레드
프로세스는 스레드라 불리는 다수의 실행 유닛으로 구성되어 있다. 각각의 스레드는 해당 프로세스의 컨텍스트에서 실행되며 동일한 코드와 전역 데이터를 공유한다. 그렇기 때문에 스레드간 데이터 공유가 쉽다. 스레드는 독립적인 실행 흐름이기 때문에 독립적으로 함수 호출이 가능해야 한다. 그러기 위한 최소 조건으로 독립된 스택을 할당해야한다. 스택에는 함수 호출 시 전달되는 인자, 되돌라갈 주소값, 함수내에서 선언하는 변수 등을 저장하고 있다. 스레드는 CPU를 할당받았다가 스케줄러에 의해 다시 선점당하기 때문에 어디까지 수행되었는지 기억해야 한다. 그렇기 때문에 PC 레지스터를 독립적으로 할당한다.
멀티 스레딩 시 주의해야할 점
멀티 프로세스 기반으로 프로그래밍 할 때는 프로세스 간 공유자원이 없기 때문에 동일한 자원에 동시접근할 일이 없었지만 멀티 스레딩 시에는 데이터와 힙 영역을 공유하기 때문에 어떤 스레드가 다른 스레드에서 사용중인 변수나 자료구조에 접근하여 엉뚱한 값을 읽어오거나 수정할 수도 있다. 이로 인한 동기화작업이 필요하다. 동기화를 통해 작업 처리 순서를 컨트롤하고 공유 자원에 대한 접근을 컨트롤 해야한다. 하지만 이 또한 병목현상이 발생할 수 있기 때문에 과도한 락을 걸지 않도록 주의해야 한다.
멀티 스레드 vs 멀티 프로세스
멀티 스레드는 멀티 프로세스보다 적은 메모리 공간을 차지하고 context switching이 빠르다는 장점이 있지만 오류로 인해 하나의 스레드가 종료되면 다른 스레드에 영향을 줄 수 있다는 점과 동기화 문제를 안고 있다. 반면 멀티 프로세스는 한 프로세스가 죽더라도 다른 프로세스에 영향을 끼치지 않고 정상적으로 수행되지만 멀티 스레드에 비해 많은 메모리 공간과 CPU를 차지한다는 단점이 존재한다.
1.7.3 가상메모리
가상메모리는 각 프로세스들이 메인 메모리 전체를 독점하고 있는 것 같은 환상을 제공하는 추상화이다. 각 프로세스는 가상주소 공간을 가지는데 이는 다음과 같은 메모리 구조를 가진다. 리눅스에서 주소공간의 최상위 영역은 모든 프로세스들이 공통으로 사용하는 운영체제의 코드와 데이터(커널) 을 위한 공간이다. 주소공간의 하위영역은 사용자 프로세스의 코드와 데이터를 저장한다.
프로그램 코드와 데이터
코드는 모든 ㅡ로세스들이 같은 고정 주소에서 시작하며, 그 다음 C 전역 변수에 대응되는 데이터 위치들이 따라온다.
힙(Heap)
코드와 데이터 영역 다음으로 런타임 힙이 따라온다. 힙은 프로세스가 실행되면서 malloc
이나 free
를 호출하면서 런타임에 동적으로 그 크기가 늘었다 줄었다 한다.
공유 라이브러리
주소공간의 중간에 C 표준 라이브러리나 수학 라이브러리와 같은 공유 라이브러리의 코드와 데이터를 저장하는 영역이 있다.
Stack
사용자 가상메모리 공간의 맨 위에 컴파일러가 함수 호출을 구현하기 위해 사용하는 사용자 스택이 위치한다. 힙과 마찬가지로 사용자 스택은 프로그램이 실행되는 동안에 동적으로 늘어났다 줄어들었다 한다. 함수를 호출할 때마다 스택이 커지며, 함수에서 리턴될 때에는 줄어든다.
Kernel Virtual Memory
주소공간의 맨 윗부분은 커널을 위해 예약되어 있다. 응용프로그램들은 이 영역의 내용을 읽거나 쓰는 것이 금지되어 있으며, 커널 코드 내에 정의된 함수를 직접 호출하는 것도 금지되어 있다.
1.7.4 파일
파일은 연속된 바이트들로, 디스크, 키보드, 디스플레이, 네트워크까지 포함하는 모든 입출력장치는 파일로 모델링한다. 시스템의 모든 입출력은 유닉스I/O라는 시스템 콜들을 이용하여 파일을 읽고 쓰는 형태로 이루어진다. 이를 통해 프로그래머는 디스크 기술에 대해서는 몰라도 된다.
1.8 시스템은 네트워크를 이용하여 다른 시스템과 통신한다.
네트워크는 또 하나의 입출력 장치로 볼 수 있다. 이를 hello 예제에 적용해보자. telnet을 이용하여 hello 프로그램을 다른 곳에 위치한 컴퓨터에서 실행할 수 있다. telnet 클라이언트를 사용하여 로컬 컴퓨터를 원격 컴퓨터의 telnet 서버와 연결하고 쉘 프로그램을 실행시킨다. “hello” 스트링을 telnet 클라이언트에 입력하고 엔터를 누르면 클라이언트는 이 스트링을 telnet 서버로 보낸다. telnet 서버가 네트워크에서 스트링을 받은 후, 원격 쉘 프로그램에 이를 전달한다. 다음으로 원격 쉘 프로그램이 hello 프로그램을 실행하고 출력을 telnet 서버로 전달한다면, 네트워크를 통해 다시 telnet 클라이언트로 전달하고 클ㅅ이다.라이언트 프로그램은 이를 로컬 터미널에 표시한다.
1.9 중요한 주제들
컴퓨터 시스템이란 단지 하드웨어 그 이상으로 응용프로그램의 실행이라는 궁극의 목적을 달성하기 위해 협ㄴ력해야 하는 하드웨어와 시스템 소프트웨어가 서로 연결된 것을 말한다.
1.9.1 Amdahl의 법칙
Gene Amdahl은 시스템의 일부 성능 개선의 효율성에 대해 간단하지만 직관적인 관찰을 하였다. 우리가 어떤 시스템의 한 부분의 성능을 개선할 때, 전체 시스템 성능에 대한 효과는 그 부분이 얼마나 중요한가와 이부분이 얼마나 빨라졌는지에 관계한다는 것이다.
T(new) = (1 - a)*T(old) + (a * T(old))/k = T(old)[ (1 - a) + a/k ]
- T(old) : 실행 시간
- a : 전체에서 이 부분이 걸리는 시간
- k : 성능을 높이려는 배수
여기에서 개선된 속도는 다음과 같이 구할 수 있다.
S = 1 / ((1 - a) + a/k)
1.9.2 동시 병렬성
- 동시성 : 시스템에서 다수의 동시에 벌어지는 것
- 병렬성 : 동시성을 사용해서 시스템을 보다 빠르게 동작하도록 하는 것
쓰레드 수준 동시성
- 단일 프로세서 시스템 : 쓰레드를 사용하여 하나의 프로세스 내에서 실행되는 다수의 제어흐름을 가지는 것
- 멀티 프로세서 시스템 : 여러 개의 프로세서를 가지고 하나의 운영체제 커널의 제어 하에 동작하는 경우
- 여러 개의 CPU 코어를 가진다.
- CPU 코어는 각각 L1, L2 캐시를 가진다.
- 각 CPU 코어는 메인 메모리 인터페이스와 상위 수준 캐시를 공유한다.
- 멀티쓰레딩 : 하나의 CPU가 여러 개의 제어 흐름을 실행할 수 있게 해주는 기술
- 매 사이클마다 실행할 쓰레드를 결정한다. 현재 쓰레드가 대기해야 한다면 다른 쓰레드를 실행시킨다.
인스트럭션 수준 병렬성
- 사이클당 한 개 이상의 인스트럭션을 실행할 수 있는 것을 말합니다.
싱글 인스트럭션, 다중 데이터 병렬성(SIMD)
- 한 개의 인스트럭션이 다수의 연산을 수행할 수 있는 특수 하드웨어를 가지는 것
- 영상, 소리, 동영상 데이터 처리를 위한 응용프로그램의 속도를 개선하기 위해 제공됩니다.
1.9.3 컴퓨터 시스템에서 추상화의 중요성
추상화를 통해서 내부 동작을 고려하지 않으면서 코드를 사용할 수 있다.
- 입출력 장치의 추상화 : 파일
- 프로그램 메모리의 추상화 : 가상 메모리
- 실행 중인 프로그램의 추상화 : 프로세스
- 운영체제, 프로세서, 프로그램 모두의 추상화 : 가상머신
1.10 요약
컴퓨터 시스템은 응용프로그램을 실행하기 위해 함께 동작하는 하드웨어와 시스템 소프트웨어로 구성된다. 컴퓨터 내의 정보는 상황에 따라 다르게 해석되는 비트들의 그룹으로 표시된다. 프로그램은 ASCII 문자로 시작해서 컴파일러와 링커에 의해 바이너리 실행파일로 번역되는 방식으로, 다른 프로그램들에 의해 다른 형태로 번역된다.
프로세서는 메인 메모리에 저장된 바이너리 인스트럭을 읽고 해석한다. 컴퓨터가 대부분의 시간을 메모리, 입출력장치, CPU 레지스터 간에 데이터를 복사하는 데 쓰고 있으므로 시스템의 저장장치들은 계층 구조를 형성하여 CPU 레지스터가 최상위에, 하드웨어 캐시 메모리, DRAM 메인 메모리, 디스크 저장장치 등이 순차적으로 위치한다. 계층 구조의 상부에 위치한 저장장치들은 하부의 장치들보다 비트당 단가가 더 비싸고, 더 빠르게 동작한다.
계층구조 상부의 저장장치들은 하부의 장치들을 위한 캐시 역할을 수행한다. 프로그래머들은 이러한 메모리 계층구조를 이해하고 활용해서 자신이 작성한 C 프로그램의 성능을 최적화할 수 있다.
운영체제 커널은 응용프로그램과 하드웨어 사이에서 중간자의 역할을 수행한다. 운영체제는 세 가지 근본적인 추상화를 제공한다 (1) 파일은 입출력장치의 추상화다. (2) 가상메모리는 메인 메모리와 디스크의 추상화다. (3) 프로세스는 프로세서, 메인 메모리, 입출력 장치의 추상화다.
끝으로 네트워크는 컴퓨터 시스템이 서로 통신할 수 있는 방법을 제공한다. 특정 시스템의 관점으로 볼 때, 네트워크는 단지 또 하나의 입출력장치다.