ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 혼자 공부하는 컴퓨터구조 + 운영체제 - 컴퓨터 구조 기본
    책책책 책을 읽읍시다/프로그래밍 2023. 1. 15. 00:37

    저자 : 강민철

     

    들어가며


    만 7년된 개발자이지만 부끄럽게도 컴퓨터 구조와 운영체제쪽은 잘 몰라 위기감에 사 읽은 책이다. 고수준 언어인 자바는 JVM이 OS별로 인터프리팅해주므로 운영체제니 CPU 레지스터니 하는 영역은 몰라도 되는줄 알았다. 대학생 때 광고홍보학과를 복수 전공했는데, 졸업 전까지 개발자가 될줄은 몰랐기 때문에 저수준 강의는 자료구조 빼면 하나도 듣지 않았다 ㅎㅎㅎ... 그런데 작년에 단순 반복문 돌리는 코드에서 코드상의 논리적 오류가 없음에도 불구하고 기대했던 대로 동작하지 않는 일이 발생했다. 이 에피소드는 오래된 기억을 상기시키느라고 문제 진단과 해결방법이 정확치 않으므로 양해 바란다. 매번 났으면 테스트 과정에서 알 수 있었을텐데 몇천분의 일 정도의 확률도 나기 때문에 상용 환경에서 버그 리포트가 계속 들어왔다. 팀장이 몇시간 고민 끝에 JVM이 데이터를 메모리에 로드하고 연산중인 컬렉션을 CPU 캐시에 올려 사용하는데, 반복문이 돌면서 CPU 캐시든 메모리이든 원래 참조했던 곳이 아닌 다른쪽을 참조하는 것 같아 발생하는 것 같다고 가설을 세웠다. 우선 실제 환경에서처럼 무지막지한 데이터 배열을 만들고 반복문을 돌리는 테스트 코드로 재현이 되는지 알아봤는데 정말 재현이 되었다. 가설이 맞았는지는 모르지만 가설에서 맞닥드린 상황을 회피하도록 코드 구성을 조금 바꾸자 테스트 코드에서 문제가 재현되지 않고 정상적인 흐름이 이어졌다. 수십번 테스트를 해서 다시 운영 배포하였고, 같은 유형의 오류는 발생하지 않았다. JVM의 뒷면은 보지 못하는 반쪽짜리 개발자였음을 느낀 순간이었다.

    그 후로 이 귀찮은 영역을 어떻게 공부할까 생각만 하고 있다 우연히 페북 광고로 이 책을 접했다. 딱딱하지 않은 컨셉의 책이라 큰 에너지를 쏟지 않고 부담없이 읽을 수 있을 것 같아 바로 구매했다. 그리고 다 읽고 난 뒤에는 대만족이었다. 나처럼 기본기가 취약한 개발자들 특히 비전공자인데 개발자로 전향한 사람들 중 이쪽은 한번도 안들어봤다 하는 사람들은 꼭 읽어보길 바란다.

     

    발췌 정리


    01 컴퓨터 구조 시작하기


    컴퓨터가 이해하는 정보

    컴퓨터는 0과 1로 표현된 정보만을 이해한다. 그리고 이렇게 0과 1로 표현되는 정보에는 크게 두 종류가 있는데, 데이터와 명령어이다. 컴퓨터가 이해하는 숫자, 문자, 이미지, 동영성과 같은 정적인 정보를 가리켜 데이터(data)라고 한다. 명령어는 컴퓨터를 실직적으로 작동시키는 역할을 한다. 명령어는 컴퓨터를 작동시키는 정보이고, 데이터는 명령어를 위해 존재하는 일종의 재료라고 보면 된다.

    컴퓨터의 4가지 핵심 부품

    중앙처리장치(CPU : Central Processing Unit), 주기억장치(main memory), 보조기억장치(secondary storage), 입출력장치(input/output device)가 컴퓨터의 핵심 4가지 부품이다.

    • 메모리 : 현재 실행되는 프로그램의 명령어와 데이터를 저장하는 부품이다. 즉, 프로그램이 실행되려면 반드시 메모리에 저장되어 있어야 한다.
    • CPU : 메모리에 저장된 명령어를 읽어 들이고, 읽어 들인 명령어를 해석하고, 실행하는 부품이다. 중요하게 알아둬야 할 구성 요소는 산술논리연산장치(ALU : Arithmetic Logic Unit), 레지스터(register), 제어장치(CU: Control Unit)가 있다. ALU는 계산만을 위해 존재하는 부품이고, 컴퓨터 내부에서 수행되는 대부분의 계산은 ALU가 맡는다. 레지스터는 CPU 내부의 작은 임시 저장 장치이다. 프로그램을 실행하는 데 필요한 값들을 임시로 저장한다. 제어장치는 제어 신호(control signal)라는 컴퓨터 부품들을 관리하고 작동시키기 위한 전기 신호를 내보내고 명령어를 해석하는 장치이다.
    • 보조기억장치 : 메모리는 가격이 비싸고 용량이 적으며 전원이 꺼지면 저장된 내용을 잃는 치명적인 단점이 있다. 메모리보다 크기가 크고 전원이 꺼져도 저장된 내용을 잃지 않는 메모리를 보조할 저장 장치가 보조기억장치이다.
    • 입출력장치 : 마이크, 스피커, 프린터, 마우스, 키보드처럼 컴퓨터 외부에 연결되어 컴퓨터 내부와 정보를 교환하는 장치를 의미한다.

    위에 설명한 컴퓨터의 핵심 부품들은 모두 마더보드(mother board)라고도 부르는 메인보드(main board)라는 판에 연결된다. 메인보드에는 앞서 소개한 부품을 비롯한 여러 컴퓨터 부품을 부착할 수 있는 슬롯과 연결 단자가 있다. 메인보드에 연결된 부품들은 서로 정보를 주고 받을 수 있는데, 이는 메인보드 내부에 버스(bus)라는 통로가 있기 때문이다. 컴퓨터 내부에는 다양한 종류의 통로, 즉 버스가 있다. 하지만 여러 버스 가운데 컴퓨터의 네 가지 핵심 부품을 연결하는 가장 중요한 버스는 시스템 버스(system bus)이다. 시스템 버스는 주소 버스, 데이터 버스, 제어 버스로 구성되어 있다. 주소 버스(address bus)는 주소르 주고받는 통로, 데이터 버스(data bus)는 명령어와 데이터를 주고받는 통로, 제어 버스(control bus)는 제어 신호를 주고받는 통로이다.

     

    02 데이터


    정보 단위

    0과 1을 나타내는 가장 작은 정보 단위를 비트(bit)라고 한다. 1비트는 0 또는 1, 두 가지 정보를 표현할 수 있다. 2비트는 자릿수가 2개이니 4(2의 2승)가지 정보를 3비트는 3자릿수니 8(2의 3승)가지 정보를 나타낸다. 그리고 8비트가 모여 1바이트(byte)가 되고 1,000개씩 모아 kB(kilobyte), MB(megabyte), GB(gigabyte), TB(terabyte) 단위로 표현한다. 1kB는 1,024byte, 1MB는 1,024kB 이런식으로 표현하는 것은 잘못된 관습이다. 1,024개씩 묶어 표현한 단위는 kB, MB, GB, TB가 아닌 kiB, MiB, GiB, TiB이다.

    문자 집합과 인코딩

    컴퓨터가 인식하고 표현할 수 있는 문자의 모음을 문자 집합(character set)이라고 한다. 컴퓨터는 문자 집합에 속해 있는 문자를 이해할 수 있고, 반대로 문자 집합에 속해 있지 않은 문자는 이해할 수 없다. 예를 들어 문자 집합이 {a, b, c, d, e}인 경우 컴퓨터는 이 다섯 개의 문자는 이해할 수 있고, f나 g같은 문자는 이해하지 못한다.

    문자 집합에 속한 문자라고 해서 컴퓨터가 그대로 이해할 수 있는 건 아니다. 문자를 0과 1로 변환해야 비로소 컴퓨터가 이해할 수 있다. 이 변환 과정을 문자 인코딩(character encoding)이라 하고 인코딩 후 0과 1로 이루어진 결과값이 문자 코드가 됩니다. 같은 문자 집합에 대해서도 다양한 인코딩 방법이 있을 수 있다. 인코딩의 반대 과정, 즉 0과 1로 이루어진 문자 코드를 사람이 이해할 수 있는 문자로 변환하는 과정은 문자 디코딩(character decoding)이라고 한다.

    아스키 코드

    아스키(ASCII: American Standard Code for Information Interchange)는 초창기 문자 집합 중 하나로, 영어 알파벳과 아라비아 숫자, 그리고 일부 특수 문자를 포함한다. 아스키 문자 집합에 속한 문자들은 각각 7비트로 표현되는데, 7비트로 표현할 수 있는 정보의 가짓수는 128(2의 7승)개이다.

    매우 간단하게 인코딩되는 장점이 있지만, 단점도 있다. 한글과 같은 아스키 문자 집합 외의 문자, 특수문자등을 표현할 수 없다. 근본적으로 아스키 문자 집합에 속한 문자들은 128개보다 많은 문자를 표현하지 못하기 때문이다.

    EUC-KR

    한글 인코딩을 이해하려면 우선 한글의 특수성을 알아야 한다. 알파벳을 쭉 이어 쓰면 단어가 되는 영어와는 달리, 한글은 각 음절 하나하나가 초성, 중성, 종성의 조합으로 이루어져 있다. 그래서 한글 인코딩에는 두 가지 방식, 완성형(한글 완성형 인코딩)과 조합형(한글 조합형 인코딩)이 존재한다.

    완성형 인코딩은 초성, 중성, 종성의 조합으로 이루어진 완성된 하나의 글자에 고유한 코드를 부여하는 인코딩 방식이다. 예를 들어 '가'는 1, '나'는 2, '다'는 3으로 말이다.

    조합형 인코딩 방식은 초성을 위한 비트열, 중성을 위한 비트열, 종성을 위한 비트열을 할당하여 그것들의 조합으로 하나의 글자 코드를 완성하는 방식이다. 즉, 초성, 중성, 종성에 해당하는 코드를 합하여 하나의 글자 코드를 만든다.

    EUC-KR은 KS X 1001, KS X 1003이라는 문자 집합을 기반으로 하는 대표적인 완성형 인코딩 방식이다. 즉, EUC-KR 인코딩은 초성, 중성, 종성이 모두 결합된 한글 단어에 2바이트 크기의 코드를 부여한다. 한 글자에 2바이트 코드가 부여된다고 했으니 이를 표현하려면 16비트가 필요하다. 그리고 16비트는 네 자리 16진수로 표현할 수 있다. 즉 EUC-KR로 인코딩된 한글은 4자리 16진수로 나타낼수 있다. '가'의 경우 b0a1로 인코딩되고, '거'의 경우 b0c5로 인코딩된다.

    EUC-KR 인코딩 방식으로 총 2,350개 정도의 한글 단어를 표현할 수 있다. 아스키 코드보다 표현할 수 있는 문자가 많아졌지만, 사실 이는 모든 한글 조합을 표현할 수 있을 정도로 많은 양은 아니다. 그래서 문자 집합에 정의되지 않은 '쀍', '쀓', '믜'같은 글자는 EUC-KR로 표현할 수 없다. 이는 떄때로 크고 작은 문제를 유발한다. EUC-KR 인코딩을 사용하는 웹사이트의 한글이 깨진다든지, 이 방식으로는 표현할 수 없는 이름으로 인해 은행, 학교 등에서 피해랄 받는 사람이 생겨나기도 했다. 이러한 문제를 조금이나마 해결하기 위해 등장한 것이 마이크로소프트의 CP949(Code Page 949)인데, EUC-KR의 확장된 버전으로 더욱 다양한 문자를 표현할 수 있지만 이마저도 한글 전체를 표현하기에 넉넉한 양은 아니다.

    유니코드와 UTF-8

    EUC-KR이 모든 한글을 표현할 수 없는 한계를 가지고 있고, 언어별로 인코딩을 나라마다 해야 한다면 다국어를 지원하는 프로그램을 만들 때 각 나라의 언어의 인코딩을 모두 알아야 하는 번거로움이 있을 것이다. 그런데 만약 모든 나라 언어의 문자 집합과 인코딩 방식이 통일되어 있다면, 다시 말해 모든 언어를 아우르는 문자 집합과 통일된 표준 인코딩 방식이 있다면 언어별로 인코딩하는 수고로움을 덜 수 있을 것이다. 그래서 등장한 것이 유니코드(unicode) 문자 집합이다. 유니코드는 한글을 포함하여 대부분 나라의 문자, 특수문자, 화살표나 이모티콘까지도 코드로 표현할 수 있는 통일된 문자 집합이다. 때문에 현대 문자를 표현할 때 가장 많이 사용된다.

    유니코드 문자 집합에는 아스키 코드나 EUC-KR과 같이 각 문자마다 고유한 값이 부여된다. 예를 들어 '한'에 부여된 값은 D55C, '글'에 부여된 값은 AE00이다. 간혹 유니코드 글자에 부여된 값 앞에 U+D55C, U+AE00처럼 U+라는 문자열을 붙이기도 하는데, 이는 16진수 유니코드를 표현할 떄 사용하는 표기이다.

    아스키 코드나 EUC-KR은 글자에 부여된 값을 그대로 인코딩 값으로 삼았다면 유니코드는 조금 다르다. 글자에 부여된 값 자체를 인코딩된 값으로 삼지 않고 이 값을 다양한 방법으로 인코딩한다. 이런 인코딩 방법에는 크게 UTF-8, UTF-16, UTF-32 등이 있다. UTF는 Unicode Transformation Format의 약어로 유니코드를 인코딩하는 방법이다.

    UTF-8은 통상 1바이트부터 4바이트까지의 인코딩 결과를 만들어 낸다. 인코딩한 결과가 몇 바이트가 될지는 유니코드 문자에 부여된 값의 범위에 따라 결정된다.

    • 1바이트 : 0부터 007F까지
    • 2바이트 : 0080부터 07FF까지
    • 3바이트 : 0800부터 FFFF까지
    • 4바이트 : 10000부터 10FFFF까지

    그렇다면 '한글'은 몇 바이트로 구성될까? '한'에 부여된 값은 D55C, '글'에 부여된 값은 AE00이었다. 두 글자 모두 0800과 FFFF 사이에 있어 3바이트가 된다.

     

    03 명령어


    고급 언어와 저급 언어

    우리가 프로그램을 만들 떄 사용하는 프로그래밍 언어는 컴퓨터가 이해하는 언어가 아닌 사람이 이해하고 작성하기 쉽게 만들어진 언어이다. 컴퓨터는 이 언어를 이해하지 못한다. 이렇게 '사람을 위한 언어'를 고급 언어(high-level programming language)라고 한다. 우리가 알고 있는 대부분의 프로그래밍 언어가 고급 언어에 속한다. 반대로 컴퓨터가 직접 이해하고 실행할 수 있는 언어를 저급 언어(low-level programming language)라고 한다. 저급 언어는 명령어로 이루어져 있다. 컴퓨터가 이해하고 실행할 수 있는 언어는 오직 저급 언어뿐이다. 그래서 고급 언어르 작성된 소스 코드가 실행되려면 반드시 저급 언어, 즉 명령어로 변환되어야 한다.

    저급 언어에는 기계어와 어셈블리어 2가지가 있다. 기계어(machie code)란 0과 1의 명령어 비트로 이루어진 언어이다. 오로지 컴퓨터만을 위해 만들어진 언어이기 때문에 사람이 읽으면 그 의미를 이해하기 어렵다. 그래서 등장한 저급 언어가 어셈블리어(assembly langugae)이다. 0과 1로 표현된 기계어(명령어)를 읽기 편한 형태로 번역한 언어가 어셈블리어이다. 예를 들어 기계어 '0101 0101'은 어셈블리어 'push rbp'와 같이 번역된다.

    컴파일 언어와 인터프리터 언어

    컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어이다. 대표적인 컴파일 언어로는 C가 있다. 컴파일 언어로 작성된 소스 코드는 코드 전체가 저급 언어로 변환되는 과정을 거친는데 이 과정을 컴파일(complie)이라고 한다. 그리고 컴파일을 수행해 주는 도구를 컴파일러(compiler)라고 한다. 컴파일러는 개발자가 작성한 소스 코드 전체를 쭉 훑어보며 소스 코드에 문법적인 오류는 없는지, 실행 가능한 코드인지, 실행하는 데 불필요한 코드는 없는지 등을 따지며 소스 코드를 처음부터 끝까지 저급 언어로 컴파일한다. 이때 컴파일러가 소스 코드 내에서 오류를 하나라도 발견하면 해당 소스 코드는 컴파일에 실패한다. 컴파일러를 통해 저급 언어로 변환된 코드를 목적 코드(object code)라고 한다.

    인터프리터 언어는 인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어이다. 대표적인 인터프리터 언어로 Python이 있다. 소스 코드 전체가 저급 언어로 변환되는 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 차례로 실행한다. 이를 수행하는 도구를 인터프리터(interpreter)라고 한다. 인터프리터 언어는 컴퓨터와 대화하듯 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없다. 그리고 소스 코드 내에 오류가 하나라도 있으면 컴파일이 불가능했던 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 N번째 줄에 문법 오류가 있더라도 N-1번째 줄까지는 올바르게 수행한다. 

    일반적으로 인터프리터 언어는 컴파일 언어보다 느리다. 컴파일을 통해 나온 결과물, 즉 목적 코드는 컴퓨터가 이해하고 실행할 수 있는 저급 언어인 반면, 인터프리터 언어는 소스 코드 마지막에 이를 때 까지 한 줄씩 저급 언어로 해석하며 실행해야 하기 때문이다.

    C, C++과 같이 컴파일 언어인지 인터프리터 언어인지 명확하게 구분할 수 있는 언어도 있지만 경계가 모호한 경우가 많다. 대표적인 인터프리터 언어인 Python도 컴파일 하지 않는 것은 아니며, Java도 컴파일과 인터프리팅 둘 다 수행한다. 즉, 하나의 프로그래밍 언어가 반드시 둘 중 하나의 방식으로 작동한다고는 생각하지 말아야한다.

    연산 코드와 오퍼랜드

    명령어는 연산 코드와 오퍼랜드로 구성되어 있대, '명령어가 수행할 연산'을 연산 코드(operation code)라 하고, '연산에 사용할 데이터' 또는 '연산에 사용할 데이터가 저장된 위치'를 오퍼랜드(operand)라고 한다. 연산 코드는 연산자, 오퍼랜드는 피연산자라고도 부른다.

    연산코드 오퍼랜드1 오퍼랜드2
    더해라 100과 120을
    저장해라 10을 메모리 128번지에

    오퍼랜드 필드에는 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올수 있다. 다만 오퍼랜드 필드에는 숫자나 문자와 같이 연산에 사용할 데이터를 직접 명시하기보다는, 많은 경우 연산에 사용할 데이터가 저장된 위치, 즉 메모리 주소나 레지스터 이름이 담긴다. 그래서 오퍼랜드를 주소 필드라고 부르기도 한다.

    연산 토드 종류는 매우 많지만, 가장 기본적인 연산 코드 유형은 트게 네 가지로 나눌 수 있다.

    1. 데이터 전송
      • MOVE : 데이터를 옮겨라
      • STORE : 메모리에 저장하라
      • LOAD(FETCH) : 메모리에서 CPU로 데이터를 가져와라
      • PUSH : 스택에 데이터를 저장하라
      • POP : 스택의 최상단 데이터를 가져와라
    2. 산술/논리 연산
      • ADD / SUBTRACT / MULTIPLY / DIVIDE : 덧셈 / 뺄셈 / 곱셉/ 나눗셈을 수행하라
      • INCREMENT / DECREMENT : 오퍼랜드에 1을 더하라 / 1을 빼라
      • AND / OR / NOT : AND / OR / NOT 연산을 수행하라
      • COMPARE : 두 개의 숫자 또는 TRUE / FALSE 값을 비교하라
    3. 제어 흐름 변경
      • JUMP : 특정 주소로 실행 순서를 옮겨라
      • CONDITIONAL JUMP : 조건에 부합할 때 특정 주소로 실행 순서를 옮겨라
      • HALT : 프로그램의 실행을 멈춰라
      • CALL : 되돌아올 주소를 저장한 채 특정 주소로 실행 순서를 옮겨라
      • RETURN : CALL을 호출할 때 저장했던 주소로 돌아가라
    4. 입출력 제어
      • READ(INPUT) : 특정 입출력 장치로부터 데이터를 읽어라
      • WRITE(OUTPUT) : 특정 입출력 장치로 데이터를 써라
      • START IO : 입출력 장치를 시작하라
      • TEST IO : 입출력 장치의 상태를 확인하라

    주소 지정 방식

    명령어 길이 때문에 오퍼랜드에 연산에 사용할 데이터를 직접 넣지 않고 메모리나 레지스터의 주소를 담아 표현할 수 있는 데이터 크기를 늘릴 수 있다. 어떤 방식으로 오퍼랜드를 사용하느냐에 따라 주소 지정 방식이 달라진다. 연산 코드에 사용할 데이터가 저장된 위치, 즉 연산의 대상이 되는 데이터가 저장된 위치를 유효 주소(effective address)라고 한다. 주소 지정 방식은 이 유효 주소를 찾는 방법을 일컫는다.

    1. 즉시 주소 지정 방식(immediate addressing mode) : 연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시하는 방식이다. 가장 간단한 형태의 방식이고 표현할 수 있는 데이터의 크기가 작아지는 단점이 있지만, 연산에 사용할 데이터를 메모리나 레지스터로부터 찾는 과정이 없기 때문에 다른 주소 지정 방식들보다 빠르다.
    2. 직접 주소 지정 방식(direct addressing mode) : 오퍼랜드 필드에 유효 주소를 직접적으로 명시하는 방식이다. 오퍼랜드 필드에서 표현할 수 있는 데이터의 크기는 즉시 주소 지정 방식보다 더 커졌지만, 여전히 유효 주소를 표현할 수 있는 범위가 연산 코드의 비트 수만큼 줄어들었다. 다시 말해 표현할 수 있는 오퍼랜드 필드의 길이가 연산 코드의 길이만큼 짧아져 표현할 수 있는 유효 주소에 제한이 생길 수 있다.
    3. 간접 주소 지정 방식(indirect addressing mode) : 유효 주소의 주소를 오퍼랜드 필드에 명시한다. 직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 더 넓어졌지만, 두 번의 메모리 접근이 필요하기 때문에 앞서 설명한 주소 지정 방식들보다 일반적으로 느리다.
    4. 레지스터 주소 지정 방식(register addressing mode) : 직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방식이다. 일반적으로 CPU 외부에 있는 메모리에 접근하는 것보다 CPU 내부에 있는 레지스터에 접근하는 것이 더 빠르다. 그러므로 레지스터 주소 지정 방식은 직접 주소 지정 방식보다 빠르게 데이터에 접근할 수 있다. 다만, 레지스터 주소 지정 방식은 직접 주소 지정 방식과 비슷한 문제를 공유한다. 표현할 수 있는 레지스터 크기에 제한이 생길 수 있다는 점이다.
    5. 레지스터 간접 주소 지정 방식(register indirect addressing mode) : 연산에 사용할 데이터를 메모리에 저장하고, 그 주소(유효 주소)를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법이다. 유효 주소를 찾는 과정이 간접 주소 방식과 비슷하지만, 메모리에 접근하는 횟수가 한 번으로 줄어든다는 차이이자 장점이 있다. 메모리에 접근하는 것이 레지스터에 접근하는 것보다 더 느리기 때문에 레지스터 간접 주소 지정 방식은 간접 주소 지정 방식보다 빠르다.

    댓글

Designed by Tistory.