컴퓨터 구조 + 운영체제/컴퓨터 구조

혼자 공부하는 컴퓨터 구조 + 운영체제 1주차 (컴퓨터 구조, 데이터, 명령어, 진수, 스택과 큐)

dash758 2024. 7. 8. 02:17

 

 

 

 

 포너블을 공부하면서 어셈블리 코드 실행 흐름과 그에 따른 바이너리의 메모리 할당 변화를 분석하는데에 큰 어려움을 겪어 메모리 구조를 공부하고, 나아가 컴퓨터 구조까지 배울 생각으로 이 책을 읽기로 결심하였습니다. 등하교 시간에 틈틈이 책을 읽던 중 혼공학습단에 대해 알게 되었고 참가 신청을 하여 3회독 이상을 목표로 잡게 되었습니다.

 

 

도서 정보

 

책 제목: 혼자 공부하는 컴퓨터 구조 + 운영체제

저자: 강민철

출판사: 한빛미디어

책 정보 및 구매 사이트: https://www.hanbit.co.kr/store/books/look.php?p_code=B9177037040

 

혼자 공부하는 컴퓨터 구조+운영체제

어려운 컴퓨터 구조와 운영체제의 원리를 누구나 쉽게 이해할 수 있도록 용어와 개념은 한 번 더 풀어쓰고, 적절한 예시와 이해하기 쉬운 그림으로 재미있게 구성했다. 또한 일상 소재를 활용한

www.hanbit.co.kr

저자의 유튜브 강의: https://www.youtube.com/playlist?list=PLYH7OjNUOWLUz15j4Q9M6INxK5J3-59GC

 

혼자 공부하는 컴퓨터구조 + 운영체제

 

www.youtube.com

 

 

 

 

 

목차

1. 컴퓨터 구조 시작하기

1.1 컴퓨터 구조를 알아야 하는 이유

1.2 컴퓨터 구조의 큰 그림

 

2. 데이터

2.1 0과 1로 숫자를 표현하는 법

2.2 0과 1로 문자를 표현하는 법

 

3. 명령어

3.1 소스 코드와 명령어

3.2 명령어의 구조

 

부록- 기본 숙제

- 추가 숙제: 스택과 큐의 개념을 설명하기

 

 

 

 

1. 컴퓨터 구조 시작하기

1.1 컴퓨터 구조를 알아야 하는 이유

 사실  컴퓨터 구조를 알 이유는 없습니다. 하지만 더 실력있는 개발자가 되고 싶다면 컴퓨터 구조를 알아야 하는 것은 필수 입니다. Intel, ARM, AMD, MAC 등 세상에는 다양한 CPU가 있고, 같은 코드일지라도 각각의 환경에서 에러가 나타날 수 있기 때문입니다. 이러한 문제가 아닐지라도 프로그래밍 혹은 개임 게발, 웹 어플리케이션 개발 등 여러가지 문제가 발생하기도 합니다. 대표적으로 메모리 관리를 잘 하지 못하여 발생하 메모리 누수 현상이 그 예시 일 것입니다. 이러한 사고에 유연한 대처를 위해서라도 컴퓨터 구조를 익히는 것은 큰 도움이 된다는 뜻 입니다. 개발자의 입장에서 이 책은

"컴퓨터 구조를 이해하면 우리는 컴퓨터를 미지의 대상에서 분석의 대상으로 인식하게 됩니다."

라고 내걸고 있으니 실력있는 개발자가 되기 위해서라면 컴퓨터 구조를 아는 것은 필수입니다.

 

 또한 웹 서버를 개발하여 운영하기 위한 서버 컴퓨터를 찾을 때에도 컴퓨터 구조 지식은 유리하게 사용됩니다. 요즘에야 아마존 웹 서비스 같은 클라우드 서비스가 잘 되어있긴 하지만, 그럼에도 자신이 운영할 서버에 맞는 CPU의 성능, 기억장치의 용량 등 필요에 따라 알맞게 선택해야 할 상황이 오기 때문입니다. 이는 개인용 컴퓨터를 맞추는 곳에서도 확연하게 중요성이 드러나게 됩니다. 자신이 웹 서핑을 할때 크롬 창 수십개를 띄어놓고 쓴다거나, 유튜브 편집을 위하여 포토샵, 프리미어 프로, 애프터 이펙트 등 메모리를 많이 잡아먹는 프로그램을 동시에 돌려야 하는 경우 적은 메모리(RAM) 용량으로는 컴퓨터가 아주아주 느려질 것 입니다. 따라서 일반인들 역시 전문가 수준의 컴퓨터 구조 지식을 알 필요는 없지만 각각의 컴퓨터 부품들이 어떤 역할을 하는지는 알 필요가 있습니다.

  

1.2 컴퓨터 구조의 큰 그림

 컴퓨터 구조는 컴퓨터가 이해하는 정보와, 컴퓨터의 네 가지 핵심 부품으로 크게 두 가지로 나눌 수 있습니다. 그 중에서 컴퓨터가 이해하는 정보는 데이터명령어로, 데이터는 컴퓨터가 이해하는 숫자, 문자, 이미지, 동영상 등과 같은 0과 1로 표현되는 정보들을 일컫는 말이고, 명령어는 이 데이터들을 처리하고 컴퓨터를 작동시키는 정보라고 할 수 있습니다. 예를 들어 컴퓨터에 'minecraft.exe를 실행시켜줘'라고 했을 때 'minecraft.exe'는 데이터가 되고, minecraft.exe를 '실행'시켜줘 는 명령어가 되는 것입니다.

 다음으로 컴퓨터의 4가지 핵심 부품입니다. 컴퓨터는 크게 CPU, 주기억장치(RAM), 보조기억장치(SSD), 입출력장치로 구성되어 있습니다.

컴퓨터 전체 부품(수작업)

메모리

먼저 메모리는 현재 실행되는 프로그램의 명령어와 데이터를 저장하는 부품입니다. 프로그램이 실행되기 위해서 반드시 메모리에 필요한 명령어와 데이터가 저장되어야 합니다. 프로그램의 빠른 실행을 위해서 CPU가 메모리에 있는 정보를 편하고, 효율적으로 가져와야 합니다. 그렇기 때문에 메모리는 주소라는 개념을 사용하여 프로그램에 필요한 데이터들을 순차적으로 정돈하여 저장합니다. 즉, CPU는 프로그램의 실행을 위해서 원하는 위치의 메모리의 주소로 접근하여 필요한 정보들을 가져와 처리합니다.

 

CPU

다음으로 CPU입니다. CPU는 컴퓨터의 두뇌 역할로, 메모리에 저장된 명령어를 읽어들이고, 읽어들인 명령어를 해석하며 실행하는 부품입니다. CPU는 크게 ALU(Arithmetic Logic Unit, 산술논리연산장치), 제어장치(CU. Control Unit), 레지스터로 이루어 집니다.

- ALU: 이름 그대로 계산'만'을 하는 장치입니다. 컴퓨터 내부에서 수행되는 대부분의 계산은 ALU의 역할입니다.

-제어장치: 컴퓨터의 각 부품을 제어하는 '제어 신호'라는 전기 신호를 보내어 명령어를 해석하는 장치입니다.

* CPU가 메모리에 저장된 값을 읽어야 할 때에는 메모리에게 '메모리 읽기'라는 제어 신호를 보냅니다.

* CPU가 메모리에 어떤 값을 저장해야 할 때에는 메모리에게 '메모리 쓰기'라는 제어 신호를 보냅니다.

-레지스터: CPU 내부의 작은 임시 저장 장치로 프로그램을 실행하는 데 필요한 값들을 임시로 저장합니다. CPU 안에는 여러가지 각기 다른 역할의 레지스터들이 존재합니다.

 

CPU가 메모리에 저장된 두 값을 더하는 과정을 설명하겠습니다.

위 그림에서 메모리에 위에서부터 차례대로 1번지, 2번지, 3번지, 4번지 라고 합시다. 머릿속으로 그려보세요

그리고 각 메모리 주소에는 다음과 같이 명령어와 데이터가 저장되어 있습니다.

 

--------------------------------------------------------------------------

-1번지: 더하라. 3번지와 4번지를

-2번지: 저장하라. 연산 결과를

-3번지: 120

-4번지: 100

--------------------------------------------------------------------------

 

01. 제어 장치는 1번지에 저장된 명령어를 읽기 위해 메모리에 시스템 버스를 통하여 '메모리 읽기' 제어 신호를 보냅니다. (시스템 버스의 어떤 버스를 이용하여 신호를 보내는 지는 아래에서 살펴보도록 하겠습니다. 명령어나 값을 건네주거나 이동되었다는 표현이 나온다면 그것들은 전부 시스템 버스를 통하여 값들이 전달 된 것입니다.)

 

02-1. 메모리는 1번지에 저장된 명령어를 CPU에 건네주고, 이 명령어는 레지스터에 저장됩니다.

02-2. 제어장치는 읽은 명령어를 해석한 뒤 3번지와 4번지에 들어있는 데이터들이 필요하다고 판단합니다.

02-3. 제어장치는 3번지와 4번지에 저장된 데이터를 읽기 위하여 메모리에게 '메모리 읽기' 제어 신호를 보냅니다.

 

03-1. 메모리는 3번지와 4번지에 저장된 데이터를 CPU에 건네주고, 이 데이터들은 각자 다른 레지스터에 저장됩니다.

03-2. ALU는 읽어 들인 데이터로 120과 100을 더합니다.

03-3.계산의 결과값인 220은 레지스터에 저장됩니다.

이로써 1번지의 명령어는 실행이 완료 되었습니다.

 

04-1. 제어장치는 2번지에 저장된 다음 명령어를 읽기 위해 메모리에 '메모리 읽기' 제어 신호를 보냅니다.

04-2. 메모리는 2번지에 저장된 명령어를 CPU에 건네주며, 이 명령어는 레지스터에 저장됩니다.

04-3. 제어장치는 이 명령어를 해석한 뒤 메모리에 계산 결과를 저장해야 한다고 판단합니다.

04-4. 제어장치는 계산 결과를 저장하기 위하여 메모리에 '메모리 쓰기' 제어 신호와 함께 계산 결과값인 220을 전송합니다.

04-5. 메모리가 계산 결과를 저장하면서 2번지의 명령어도 실행이 완료 되었습니다.

 

보조기억장치

주기억장치는 전원이 꺼지면(=전기 공급이 멈춘다면) 저장된 정보가 전부 날아갑니다. 또한 가격이 비싸며 용량이 적다는 단점이 존재합니다. 이러한 문제점을 보완하기 위하여 보조기억장치가 등장하게 되었습니다. 하드디스크, SSD, USB, DVD 등과 같은 저장 장치들이 보조기억장치의 일종입니다. 

 

입출력장치

입출력장치는 입력장치와 출력장치로 나뉘며 마이크, 마우스, 키보드 등이 입력장치가 되겠고, 스피커, 모니터 등이 출력장치가 되겠습니다. 이들은 컴퓨터 외부에서 동작하여 컴퓨터 내부와 정보를 주고받는다는 특징이 있습니다.

 

보조기억장치와 입출력장치를 '주변장치'라고도 일컫는다고 합니다.

 

시스템 버스

시스템 버스는 위에서 언급했던 컴퓨터의 4대 구성 요소에 포함되어 있진 않지만 메인보드와 주변장치들 사이에서 정보를 주고받을 수 있도록 도와주는 통로 역할을 하는 장치이기 때문에 살펴보고 넘어가겠습니다.

시스템 버스는 주소 버스, 데이터 버스, 제어 버스로 구성되어 있습니다.

주소 버스는 CPU가 읽어 들인, 혹은 필요하다고 판단되는 정보들을 원하는 메모리 주소에 접근할 수 있도록 도와주는 버스입니다.

데이터 버스는 주소 버스에서 원하는 메모리 주소에 접근했을 때 그 안에 있는 값을 운반하는 버스에 해당합니다.

제어 버스는 제어 장치를 통해 신호가 전달되며, 메모리 쓰기, 메모리 읽기와 같은 신호들을 보내는데 사용되는 버스입니다.

위에서 살펴보았던 CPU가 메모리에 저장된 두 값을 더하는 과정의 04-1번 순서에서 제어 버스를 통해 메모리 읽기 신호를 건네고, 주소 버스를 통해 2번지 라는 정보를 제공합니다. 이후 메모리로 부터 2번지에 저장된 명령어가 CPU에 전달 되면서 이러한 원리로 버스와 메인보드와의 관계가 사용되는 것입니다.

 

 

 

2. 데이터

2.1 0과 1로 숫자를 표현하는 방법

 컴퓨터는 0(꺼짐)과 1(켜짐)밖에 이해하지 못합니다. 그렇다면 2 이상의 숫자, 혹은 여러분들이 지금 읽고 있는 문자들은 어떻게 컴퓨터가 이해하여 우리에게 보여주고 있는 것 일까요?

 0과 1밖에 표현할 수 없는 전구를 예시로 들었을 때 한개의 전구는 꺼짐(0)과 켜짐(1) 상태밖에 표현할 수 없습니다. 하지만 전구를 2개 놓았을 때에는 다음과 같이 4가지 정보로 표현할 수 있습니다.

 

-------------------------------------------------------------------------------

전구1     |     꺼짐     |     꺼짐     |     켜짐     |     켜짐

전구2     |     꺼짐     |     켜짐     |     꺼짐     |     켜짐

-------------------------------------------------------------------------------

 

이와 같이 많은 정보를 표현하기 위해서는 전구를 n개 이어 붙일 수록 그 정보의 표현의 가짓수는 2의 n제곱개로 늘어나게 됩니다.

MBTI를 예시로 들었을 때 그 가짓수가 2의 4제곱 이므로 16개가 되는 것과 비슷하다고 설명할 수 있곘네요.

 

이때, 하나의 전구가 0과 1의 상태만 있듯이 정보의 표현에서 가장 작은 정보 단위를 비트(bit)라고 합니다. 만약 전구가 2개 붙어 있다면 2비트 라고 표현하고, 8개 붙어 있으면 8비트 라고 표현합니다. 옛날에는 32비트 컴퓨터를, 현대에는 64비트 컴퓨터를 사용한다고 하죠? 이 뜻은 컴퓨터가 표현할 수 있는 정보의 가짓수가 2의 64제곱 이라는 것과 같은 말입니다.

 

여러분들은 32비트를 어디선가 한 번쯤은 경험해보셨을 것 입니다. '메이플스토리' 라는 게임을 예시로 들어보겠습니다. 옛날 메이플스토리는 게임 내 재화인 '메소'의 제한이 21억 메소 였습니다. 그 이유는 당시 메이플스토리는 32비트를 사용하고 있었고, 그렇기 때문에 2의 31제곱이 2,147,483,648였던 이유로 최대 메소 제한이 21억 이었습니다. (*바로 앞 문장에서 32비트를 사용한다면서 2의 31제곱 값이 메소 제한 이라는 것에 의문을 갖는 분들도 계시기 마련입니다. 그 이유는 2의 32제곱으로 계산할 때 맨 앞, 즉 첫 번째 비트는 양수, 음수를 표현하는 '부호 비트' 이기 때문에 2의 32제곱인 4,294,967,296이 최대 메소 제한이 아니었던 이유입니다.)

 

정보의 크기 단위는 다음과 같이 정리됩니다.

 

-------------------------------------------------------------------------------

1바이트(byte) = 8비트

1킬로바이트(KB) = 1,000바이트

1메가바이트(MB) = 1,000킬로바이트

1기가바이트(GB) = 1,000메가바이트

1테라바이트(TB) = 1,000기가바이트

-------------------------------------------------------------------------------

*니블(Nibble) 이라는 정보 단위도 있습니다. 1니블 = 4비트의 크기 이지만, 많이 쓰이진 않는 단위입니다.

 

2.2 0과 1로 문자를 표현하는 방법

 지금까지 숫자를 표현하는 방법에 대해 알아보았다면, 이번에는 문자를 표현하는 방법에 대하여 알아보도록 하겠습니다. 먼저 컴퓨터는 자신이 이해하고, 인식할 수 있는 문자 집합(Character Set)이 있습니다. 만약 컴퓨터에 A부터 Z까지 알파벳이 문자 집합에 속해 있다고 했을 때 'ㄱ'과 같은 다른 문자는 컴퓨터는 이해할 수 없습니다. 물론 막연하게 문자 집합에 속한 문자 중 'A' 라는 단어를 입력했을 때 컴퓨터가 곧이 곧대로 이해하는 것이 아닙니다. 이 문자를 0과 1로 변환해야 비로소 컴퓨터가 이해할 수 있게 됩니다. 이 과정을 문자 인코딩 이라고 합니다. 반대로 컴퓨터가 이해한, 0과 1로 이루어진 문자를 다시 사람이 이해할 수 있도록 변환하는 과정이 필요합니다. 이 과정을 문자 디코딩 이라고 합니다.

 

아스키 코드

 위 사진은 아스키 코드의 전체를 표기한 아스키 코드 표 입니다. 아스키란 알파벳과 숫자, 일부 특수 문자들을 포함한 초창기 문자 집합 중 하나 입니다. 아스키 코드는 특정 문자를 각각 10진수 혹은 16진수로 변환 시킨 것과 일대일로 대응됩니다. 대문자 'A'를 예시로 들었을 때 이를 10진수로 변환 시키면 65, 16진수로 변환시키면 41로 인코딩됩니다. 하지만, 세상에는 수많은 문자들이 존재합니다. 이들을 전부 표현하기에는 128개의 숫자로는 턱없이 부족합니다. 이를 보완하기 위해 확장 아스키가 등장하긴 했지만 그마저도 256개의 문자 밖에 표현할 수 없어서 한참 부족했습니다.

 

 세상에는 영어를 포함한 한글, 한자, 일본어 등 아아아아주 많은 문자들이 존재합니다.그렇기 때문에 전 세계의 문자들을 포함해 인코딩 방식을 통일시켜 새로운 문자 집합인 유니코드를 만들게 됩니다. 유니코드는 전 세계의 문자들을 포함해서 특수문자, 심지어 이모티콘까지 포함시켜 코드로 표현할 수 있는 문자 집합입니다. 이는 현대 문자를 포현할 때 가장 많이 사용되는 표준 문자 집합이며, 문자 인코딩의 세계에서 매우 중요한 역할을 맡고 있습니다.

 

 문자 집합에 포함되지 않아 컴퓨터가 이해할 수 없는 글자라면 인코딩을 할 수도 없어 당연히 컴퓨터가 표현할 수도 없습니다. 하지만 현대에는 유니코드를 사용하기 때문에 어떤 문자라도 표현이 가능합니다. 꿻, 쌼, 뚋 같은 글자들도요.

 

 

 

3. 명령어

3.1 소스 코드와 명령어

 2000년대생, 혹은 그 이전세대의 우리들은 명령어에 아주 익숙했을 것 입니다. 바로 '마인크래프트'라는 게임 덕분일 것 이라고 생각됩니다. 명령어를 사용하여 사용자의 게임 환경을 자유자재로 변형하여 쾌적하게 게임할 수 있도록 도와주었습니다. 컴퓨터의 명령어도 비슷할 것이라고 생각합니다. 사용자가 더 편하고 쾌적하게 컴퓨터를 이용할 수 있도록 도와주는 수단이고, 컴퓨터는 명령어를 처리하는 기계이기 때문입니다.

 

 하지만 이 명령어는 Python, C, JAVA와 같은 프로그래밍 언어와 같은 소스코드를 말하는 것이 아닙니다. 정확히 얘기하자면 이들을 컴퓨터가 이해할 수 있도록 변환되는 명령어, 기계어를 말하는 것입니다. 소스 코드와 같은 인간이 이해하기 쉬운 언어를 '고급 언어' 라고 하고, 컴퓨터가 이해하기 쉬운 언어를 '저급 언어' 라고 합니다. 고급 언어는 위와 같은 파이썬, C, 자바와 같은 언어를 말하는 것이고, 이들이 변환되어 저급 언어가 됩니다. 저급 언어의 종류로는 '기계어'와 '어셈블리어'가 있습니다. 기계어는 0과 1의 명령어 비트로 이루어진 언어입니다.

 

이진수로 표현된 명령어

 

 위 사진이 명령어 입니다. 저게 무슨 소리일까요 저도 모릅니다. 8비트로 표현된 기계어들을 계속해서 나열한다면 사람이 이해하기도 쉽지 않은걸 보기도 싫게 만들어주겠죠. 그래서 가독성을 위해 8비트 2진수로 표현된 명령어를 16진수로 표현하기도 한다고 합니다. .?.

(역사적인 천재 폰 노이만이 저걸로 코딩헀다는 이야기가 있다고 합니다.)

 

 어차피 기계어는 컴퓨터만을 위해 만들어진 언어이기 때문에 어차피 사람이 보라고 만든게 아닙니다. 그래서 새로운 저급 언어가 등장하게 되는데 어셈블리어가 등장하게 됩니다. 기계어를 읽기 편하라고 어셈블리어를 만들었다는데, 한 번 보시죠. 다음은 81p에 나와있는 어셈블리 코드 전문입니다.

push		rbp												
mov		rbp, rsp
mov		DWORD PTR [rbp-4], 1
mov		DWORD PTR [rbp-8], 2
mov		edx, DWORD PTR [rbp-4]
mov		eax, DWORD PTR [rbp-8]
add		eax, edx
mov		DWORD PTR [rbp-12], eax
mov		eax, 0
pop		rbp
ret

 

 대충 변수 1, 2를 저장한 변수 두개를 만들어서 둘을 더한 후 저장하는 코드를 어셈블리어로 표현한 것 같습니다. 물론 이를 한줄한줄 이해할 필요는 없습니다. (하지만 리버싱, 포너블, 임베디드 등을 공부하고 싶다면 반드시 알아야 합니다....)

기계어로 코딩한 폰 노이만의 제자들이 어셈블리어로 코딩했다고 전해집니다. 폰 노이만이 그 광경을 보고 요즘 애들은 코딩 참 편하게 한다고 했다고 하네요.

 

프로그래밍 언어는 실행 방식에 따라 인터프리터 언어와 컴파일 언어로 분류됩니다. 먼저 인터프리터 방식이란 코드를 한줄한줄 컴퓨터에게 실시간 번역을 해줌으로써 실행되는 방식입니다. 이 방식을 사용하는 언어 중 대표적으로 파이썬과 자바스크립트가 있습니다. 컴파일 방식은 소스 코드가 컴파일러에 의해 저급 언어로 변환되어 실행되는 방식입니다. 컴파일 방식을 사용하는 프로그래밍 언어는 대표적으로 C와 C++이 있습니다. 자바와 C#은 둘 다 사용하는 언어입니다.

 

 컴파일러를 사용하는 언어가 컴파일되는 방식은 소스 코드가 컴파일러를 통해 컴파일 되면서 목적 코드(.obj)로 변환되고, 링커를 통해 파일들을 모으며 실행파일(.exe)로 변환됩니다. 리눅스의 gcc 컴파일러를 통하여 실제로 확인해 봅시다.

 

 

 vi 편집기로 두 개의 변수 a, b에 각각 1과 2를 저장하여 변수 c에 이 둘을 더한 값을 저장하여 출력하는 프로그램입니다.

 

 

gcc 명령어의 -o 옵션을 사용하여 test.c 파일을 컴파일 하여 ./test 명령어로 출력한 모습입니다. test 파일을 실행한 결과

1 + 2 = 3이 정상적으로 출력된 것을 확인할 수 있습니다.

 

 

gcc 명령어의 -c 옵션을 사용하여 test.c 파일을 test.o라는 목적 코드로 변환한 모습입니다. 읽을 수 없습니다.

 

 

gcc의 명령어 중 -S 옵션을 사용하여 test.c 파일을 기계어 코드로 변환한 모습입니다.

 

물론 위 과정들을 이해하거나 이해하려고 할 필요 없습니다. 그냥 이런게 있구나~~ 하고 보셔도 됩니다.

 

3.2 명령어의 구조

 명령어도 결국 언어인지라 무엇을 지칭하는지, 목적이 무엇인지는 명확합니다. 즉 구조는 명확하다는 이야기입니다. 명령어는 연산 코드(Operation Code)오퍼랜드(operand)로 구성되어 있습니다. 

-연산 코드: 명령어가 수행할 연산이며, 연산자로써 연산 코드가 담기는 영역을 '연산 코드 필드'라고 합니다.

-오퍼랜드: 연산에 사용할 데이터 혹은 연산에 사용할 데이터가 저장된 위치. 피연산자이며, 오퍼랜드 코드가 담기는 영역을 '오퍼랜드 필드'라고 합니다.

 

위 어셈블리 코드 중 일부로 예시를 들어보겠습니다.

push		rbp
mov		rbp, rsp

 

 

 위 코드의 mov    rbp, rsp에서 mov는 연산 코드, rbp, rsp는 오퍼랜드에 해당됩니다. 어셈블리 언어에 대해서는 추후에 포스팅 하겠습니다.

 두 번째 줄에 있는 코드를 직역하자면 rsp의 값을 rbp에 대입하라는 말과 같습니다. 처음보시는 분들은 rsp가 무엇이고 rbp가 무엇인지 역시 모르실 것 입니다. 이 내용은 추후 어셈블리에 관한 내용에서 다루고, 명령어의 구조를 쉽게 파악하기 위하여 다음과 같이 비유를 해 보겠습니다.

 

------------------------------------------------------------------------------------------------

더하라      100과 | 120을

빼라          메모리 32번지 안의 값과 | 메모리 33번지 안의 값을

저장해라   10을 | 메모리 128번지에

------------------------------------------------------------------------------------------------

 

 배경이 초록색인 부분은 연산 코드에 해당하고, 배경이 파란색인 부분은 오퍼랜드에 해당합니다. 명령어는 위와 같이 직관적인 모습을 띄고 있습니다. 이런게 연속해서 상당히 길게 붙어있는게 문제지

 

연산자

 연산자에 대하여 조금 더 자세히 알아보겠습니다. 연산자는 위 명령어에서 동사에 해당하는 부분으로, 더해라, 빼라, 저장해라와 같은 역할을 하는 것이 연산 코드입니다. 연산 코드는 다음과 같이 크게 4가지로 분류할 수 있습니다.

1. 데이터 전송

2. 산술/논리 연산

3. 제어 흐름 변경

4. 입출력 제어

연산자는 이와 같이 크게 4가지 유형으로 분류되지만 연산 코드와 CPU에 따라 생김새가 달라집니다. 저희는 대중적으로 가장 많이 사용되는 x86-64 아키텍쳐의 어셈블리 언어를 아주 간략하게 알아보겠습니다.

 

1. 데이터 전송

데이터 전송 명령어는 어떤 값을 특정 메모리나 레지스터 주소에 옮기거나 저장하도록 지시하는 명령어 입니다.

-mov: move의 약어로 데이터를 옮길 때 사용합니다.

-lea: Load Effective Address의 약어로 주소 계산 결과를 레지스터에 저장합니다.

 

2. 산술/논리 연산

명령어에 따라 산술/논리 연산을 수행합니다.

-add: 덧셈

-sub: 뺄셈

-mul: 곱셈

-div: 나눗셈

-inc: 오퍼랜드에 1 더하기

-dec: 오퍼랜드에 1 빼기

-and/or/xor/not: 각각의 연산을 수행합니다.

-cmp(compare): 오퍼랜드를 빼서 비교합니다.

 

3. 제어 흐름 변경

프로그래밍 언어에서 반복, 조건, 함수 호출 등에 해당합니다.

-jmp: 특정 주소로 점프합니다.

-call: 함수를 호출합니다.

-ret(return): 함수를 호출하고 원래 흐름으로 돌아와야 하므로 반환 주소(Return Address)를 스택에 저장하여 반환 주소로 돌아가 원래의 실행 흐름으로 복귀합니다. 요약하여 call 다음으로 쓰이며 함수 호출 이전으로 복귀합니다.

 

C언어를 하셨던 분들이라면 call과 ret 명령어에 익숙하실 것 입니다.

4. 입출력 제어

-in: 데이터를 읽어옵니다.

-out: 데이터를 내보냅니다.

5. 스택

보너스로 알아두시면 좋은 명령어 입니다. 스택에 관한 내용은 이번 글의 마지막 내용에 설명하겠습니다.

-push: 데이터를 스택의 최상단에 쌓습니다.

-pop: 스택 최상단에 있는 데이터를 꺼냅니다.

오퍼랜드

 다음으로 오퍼랜드에 대해 조금 더 자세히 알아보겠습니다. 오퍼랜드는 상술했듯 연산에 사용할 데이터 혹은 연산에 사용할 데이터가 저장된 위치를 의미한다고 하였습니다. 하지만 오퍼랜드 필드에는 데이터가 직접적으로 명시되어있기 보다는 데이터가 담겨있는 메모리 주소나 레지스터의 이름이 담겨있는 경우가 더 많습니다. 그렇기 때문에 오퍼랜드 필드를 '주소 필드' 라고도 부릅니다. 오퍼랜드는 명령어 안에 아예 존재하지 않을 수도 있고, 한 개, 두 개 혹은 세 개 등 여러 개 존재할 수 있습니다. 오퍼랜드가 얼마나 있느냐에 따라 다음과 같이 정의할 수 있습니다.

*0-주소 명령어: 오퍼랜드가 하나도 없는 경우

*1-주소 명령어: 오퍼랜드가 한 개인 경우

*2-주소 명령어: 오퍼랜드가 두 개인 경우

*3-주소 명령어: 오퍼랜드가 세 개인 경우

 

 

 

부록

기본 숙제

p.51의 확인 문제 3번, p.65의 확인 문제 3번 풀고 인증하기

 

먼저 p.51의 문제 3번 입니다.

더보기

프로그램이 실행되려면 반드시 (________)에 저장되어 있어야 합니다.

정답은 메모리입니다. 프로그램을 실행하기 위해 CPU가 연산을 해야 합니다. 그러기 위해선 CPU가 빠르게 정보를 가져올 수 있도록 주기억장치에 데이터들이 들어 있어야 하죠. 이는 폰 노이만 구조와 비슷한 내용입니다.

 

p.65 3번 문제

더보기

1101(2)의 음수를 2의 보수 표현법으로 구해 보세요.

아이고 젠장 2진수, 2의 보수 설명을 안했네

간략하게 2진수와 2의 보수에 대해 설명해 드리겠습니다.

2진수는 0과 1로 수를 표현하는 방법입니다. 진짜 0과 1만 쓰는데, 먼저 10진수에 관하여 설명해 드리겠습니다.

 

10진수(Decimal)는 사람들이 숫자를 표현하는 방법입니다. 0부터 9까지 표현할 수 있는 사람들이 평범하게 '숫자세기'하면 떠올리는 그 방법이요. 10진수에서 첫 번째 자리는 10의 0제곱 단위 입니다. 두 번째 자리는 10의 1제곱 단위이며, 세 번째 자리는 10의 제곱 단위입니다.

 

예를 들어서 10진수로 이루어진 '256'이라는 숫자가 있다고 생각해봅시다. 우리가 '백의 자리' 라고 생각하는 세 번째 자리는 10의 제곱의 자릿수 입니다. 즉 256은 200 + 50 + 6으로 쪼갤 수 있으며, 이를 풀어서 쓰면

2 * 100^2 + 5 * 10^1 + 6 * 10^0 = 256과 같습니다.

 

또한 10진수를 왜 10진수라고 이야기하는지 알아봅시다. 10진수에는 0부터 9까지 총 10개의 숫자가 존재합니다. 일의 자리에서 0부터 9까지의 수 중 마지막 수인 9에서 1을 더하면 일의 자리는 0이 되고, 10의 자리는 1이 됩니다. 쉽게 말해서 9+ 1 = 10을 설명한 것 입니다.

 

2진수(Binary)에 대해 설명하겠습니다. 10진수는 사람이 쓰는 것이라면 2진수는 컴퓨터가 쓰는 숫자 표현 방법입니다. 보통 영화나 만화 등 다른 매체에서 컴퓨터를 표현할때 00111001 과 같이 0과 1로만 표현하는 것을 접해보셨을 것 입니다. 실제로 2진수는 0과 1로만 이루어져 있는데, 이 0과 1로 존재하는 모든 수를 표현할 수 있습니다.

 

앞서 10진수는 각 자릿수마다 10의 n제곱으로 표현한다고 이야기 하였습니다. 2진수 역시 각 자릿수마다 2의 n제곱으로 표현합니다. 첫 번째 자리는 2의 0제곱, 두 번째 자리는 2의 1제곱, 세 번째 자리는 2의 3제곱과 같은 식으로 표현합니다.

 

10진수로 18을 표현했을 때 2진수로는 1 0010와 같이 표현할 수 있습니다. 18(10) = 1 0010(2) 괄호 안의 수는 그것이 나타내는 수가 어떤 진수로 쓴 것인지 표기한 것입니다.

 

1 0010이 왜 18이 되는지 알아봅시다. 1 0010은 총 다섯자리 수로 되어 있습니다.

1 * 2*4 + 0 * 2*3 + 0 * 2^2 + 1 * 2^1 + 0 * 2^0 = 18

 

이렇게 하면 조금이라도 이해가 되셨을까요 10진수는 0~9 중 마지막 수인 9에서 1을 더한다면 그 다음 자릿수에 1을 더한다고 말하였습니다. (9 + 1 = 10) 2진수 역시 마찬가지입니다. 2진수는 0과 1로만 이루어져 있다고 하였으므로, 0부터 1까지의 숫자 중 마지막 수인 1에서 1을 더한다면 그 다음 자릿수에 1을 더하게 됩니다. (0001(2) 에 1을 더하면 0010(2) 가 됩니다.)

 

그렇다면 여기서 '어떻게 음수를 표현하는가'에 대한 의문이 생길 것입니다. 그를 표기하기 위한 방법이 바로 '2의 보수(Compliment)'입니다. 2진수를 2의 보수로 변환하는 방법은 여러가지 있지만 그 중 가장 쉬운 방법을 문제에서 주어진 1101(2)로 소개해드리겠습니다.

1 1 0 1

가장 오른쪽에서 부터 처음으로 나오는 1까지를 그대로 두고 나머지 왼쪽 2진수를 모두 반전시켜 버리는 방법입니다.

 

1. 첫 번째 자릿수부터 두 번째, 세 번째 자릿수로 이동하면서, 즉 가장 오른쪽에서 처음으로 나오는 1은 무슨색일까요?

 

2. 바로 파란색 1이죠. 이 파란색 1을 기준으로 왼쪽의 2진수를 전부 반전시킵시다.

 

3. 그렇다면 답은 0011이 됩니다.

 

 

예리하신 분들은 아마 이상함을 느끼셨을 지도 모르겠습니다. 앞서 설명했던 비트를 표현하는 것과 2진수를 표현하는 것과 같음을 느끼셨을 것입니다. 정확합니다. 비트는 0과 1로 '없음'과 '있음'을 나타낸다고 하였습니다. 2진수도 마찬가지 입니다. 각 자릿수에 1이 있다면 그건 '있다'는 뜻 입니다.

 

하지만 이 2진수에는 단점이 있습니다. 바로 숫자가 커질수록 2진수로 표현하는 길이가 길어진다는 점입니다. 예를 들어 10진수 128을 2진수로 표현하였을때는 1000 0000(2) 입니다. 앞서 저희는 '아스키 코드'에 대하여 알아보았습니다. 알파벳을 각 진수로 변환하여 표기할 수 있다는 내용이었는데, 'Computer Structure' 를 아스키에 따라 2진수로 변환하면 다음과 같습니다.

01000011 01101111 01101101 01110000 01110101 01110100 01100101 01110010 00100000

01010011 01110100 01110010 01110101 01100011 01110100 01110101 01110010 01100101

 

굉장히 길어져서 공간 낭비를 유발하기 때문에 사람들은 새로운 진법인 '16진수(hexadecimal)'를 고안하였습니다. 16진수는 0부터 알파벳 f까지 사용하는 진법으로 0~9까지는 10진수와 같으나 이후 a = 10, b = 11, c = 12 ------ f = 15와 같은 식으로 숫자를 새는 방식입니다. Computer Structure를 이번엔 16진수로 변환하여 봅시다.

 

43,6f,6d,70,75,74,65,72,20,53,74,72,75,63,74,75,72,65

 

굉장히 깔끔하며 공간이 많이 절약된 모습입니다.

 

 

 

추가 숙제 - 스택과 큐의 개념 설명하기

 스택(Stack), 큐(Queue)는 메모리에서 쓰이는 개념입니다. 스택은 보통 U자의 형태로 많이 설명하는데. 이는 아래쪽 공간이 막혀있는 형태입니다. 이 공간에 물건을 넣는다면 먼저 들어간 물건은 가장 아래에 있고, 마지막에 넣은 물건일 수록 위쪽에 위치하게 되겠죠. 이 U자형 상자에 담긴 물건을 빼낼때에도 가장 나중에 담긴 물건이 가장 먼저 꺼내집니다. 이를 후입 선출(LIFO - Last In First Out)이라고 합니다. 이 때 위에서 배웠던 어셈블리 명령어 중에서 push 명령어와 pop 명령어가 쓰입니다. push 명령어를 통하여 상자에 물건을 넣고, pop 명령어를 통해 물건을 꺼내는 방식이죠.

 

 보통 스택은 '아래로 자란다'라는 말을 많이 합니다. 리눅스의 메모리 구조를 예시로 설명해 보겠습니다. 리눅스의 스택 세그먼트(Stack Segment)에서 프로그램이 실행될 때 얼마 만큼의 스택 공간을 차지할 지 모릅니다. 그렇기 때문에 운영체제는 프로그램에게 스택 공간을 작게 할당해주고, 필요할 때마다 공간을 더 확장해 주는데, 이 때 기존 주소보다 낮은 주소로 스택 공간을 확장해주기 때문에 '스택은 아래로 자란다' 라고 많이들 표현을 하는 것입니다.

 

 스택은 후입 선출이라는 것과 스택은 아래로 자란다는 말을 합쳐 프로그래밍에 적용해보면 먼저 들어온 물건(먼저 선언된 변수)은 가장 아래에 위치한다.(가장 높은 메모리 주소를 할당받는다.)와 같습니다. 이는 늦게 선언된 변수일수록 낮은 메모리 주소를 할당받는다는 이야기와 같습니다. 실제로 그런지 확인해봅시다.

 

 

리눅스로 4개의 a, b, c, d와 같이 4개의 변수를 선언하여 그 메모리의 주소값과 그 주소에 들어있는 값을 출력하는 프로그램입니다. (C언어의 포인터에서 쓰이는 개념입니다.) 이를 아까처럼 리눅스의 gcc컴파일러에서 -o 옵션을 사용하여 컴파일 해주고 실행하면

 

0x41f1e26c가 변수 a의 메모리 주소입니다

다음과 같이 나중에 선언된 변수일수록 낮은 메모리 주소를 할당 받는다는 사실을 알 수 있습니다. int형으로 선언했기에 4바이트씩 줄어드는 것도 잘 구현 되었네요.

 

*윈도우의 Visual Studio에서 실행한다면 나중에 선언된 변수가 높은 메모리 주소를 할당 받거나, int형으로 선언했음에도 불구하고 10바이트 혹은 20바이트로 크기가 할당되는 현상이 보이는 것과 같이 사진과 다른 내용이 출력될 수도 있습니다. 그 이유는 컴퓨터가 이상한게 아닌 운영체제 자체에서 예비 공간을 할당한 것 이기 때문에 걱정하실 필요는 없습니다.

 

 다음으로 큐의 개념을 설명하겠습니다. 큐는 아래가 막혀있는 U자형 상자와 달리 위와 아래 전부 뚫려있는 ㅣㅣ와 같은 형태입니다. 그렇기 때문에 큐는 선입선출이 가능해지며, 가장 먼저 넣는 물건을 가장 먼저 꺼내는 것이 가능합니다.

 

 스택의 장점은 쉬운 구현이나 메모리 효율성이 좋은것, 큐의 장점은 데이터가 입력된 순서대로 들어가고 빠져나가니 신뢰성이 있고, 순차적으로 처리해야 하는 작업에는 효율적이겠네요.

 단점은 둘 다 중간에 있는 데이터에 접근하기 힘들다는 정도가 되겠습니다.