티스토리 뷰

 

* CHAPETER 2 - 어셈블러 학습과 Makefile 입문



#1 레지스터 내용 정리


 *16비트 레지스터 (= 기억회로)


 CPU에는 레지스터라는 기억회로가 있는데, 이것은 기계어의 변수이다. 변수라는 건 데이터를 담는 공간, 그릇이라는 의미로 이해하면 된다. 대표적인 레지스터로는 다음의 8개가 있는데, 이들은 고유의 이름과 기능을 가지고 있다.


  • AX - 어큐물레이터(accumulator: 누적 연산기라는 의미) // X의 의미는 확장(extend)의 의미다.
  • CX - 카운터(counter: 수를 세는 기계라는 의미)
  • DX - 데이터(data: 데이터라는 의미)
  • BX - 베이스(base: 기초, 기점이라는 의미)
  • SP - 스택 포인터(stack pointer: 스택용 포인터)
  • BP - 베이스 포인터(base pointer: 베이스용 포인터)
  • SI - 소스 인덱스(source index: 읽기 인덱스)
  • DI - 데스티네이션 인덱스(destination index: 쓰기 인덱스)

    /* 레지스터의 이름이 알파벳 순이 아닌 이유는 기계어에 따라 레지스터 번호순으로 나열했기 때문이다. */


 이것들은 모두 16비트(= 8바이트) 레지스터로 16자리의 2진수를 기억할 수 있다. 읽을 때는 '에이엑스 레지스터' 등과 같이 알파벳 그대로 읽는다. 고유의 기능을 가지고 있다고 위에서 서술했는데, 예를 들면 이렇다. AX는 연산을 할 때 사용되는데 각종 연산에 AX 레지스터를 사용하면 프로그램이 한 층 더 간결해진다. 아래 예시를 보자.


 ADD CX, 0x1234 (16진수 1234, 10진수로는 4660)는 기계어로 81 C1 34 12 라는 4바이트의 명령이지만,

 ADD AX, 0x1234는 기계어로 05 34 12 라는 3바이트의 명령이 된다. (기계어가 한 바이트 더 줄었다.)


/* ADD CX, 0x1234는 CX 레지스터에 0x1234 를 더해라 라는 의미다. 즉, CX += 0x1234 로 해석할 수 있다. */


 이 예에서 보다시피, 프로그램이 간결해진다는 건 여기서 기계어가 좀 더 짧아진다는 의미다. 어셈블러 명령어의 단어 수에는 전혀 차이가 없다. 이처럼 각각의 레지스터에는 고유의 기능이 있는데, CX의 경우 횟수를 세는 데 사용하면 편리하게 되어 있고 BX는 메모리의 번지 계산의 기준 주소로 사용하면 편리하게 되어 있다.


 레지스터 이름 뒤에 보면, X와 P가 붙어 있는데 X의 경우는 확장(extend)의 의미로 쓰이고 있다. 예전에는 CPU의 레지스터가 8비트 였는데, 그 때 16비트가 등장하면서 기존의 레지스터 보다 확장됐다는 의미로 저렇게 이름이 붙여졌다. P의 경우는pointer (주소를 가리킨다)를 의미한다.


 한편, CPU에는 8비트의 레지스터도 8개가 존재한다. 이름이 위의 16비트 레지스터와 닮아보이는 건 그만한 이유가 있다.


  • AL - 어큐물레이터 로우 (low: 낮다의 의미)
  • CL - 카운터 로우
  • DL - 데이터 로우
  • BL - 베이스 로우
  • AH - 어큐물레이터 하이 (high: 높다의 의미)
  • CH - 카운터 하이
  • DH - 데이터 하이
  • BH - 베이스 하이


 AX 레지스터의 16비트 중 아래쪽 0 ~ 7비트 (0부터 센다)의 8개비트를 AL라고 한다. 또한 8 ~ 15비트 까지는 AH라고 한다. 단지 AX, CX ... 를 두 개의 이름으로 나누어 둔 것이므로 추가적인 저장공간을 확보했다고 해석하면 안된다. 여전히 16바이트밖에 기억할 수 없다.

 BP, SP, SI, DI 는 L과 H로 구분되어 있지 않다. 인덱스와 포인터에 해당하는 부분들인데, 이들은 절대로 둘로 나눌 수 없다. 이유는 인텔이 애초에 설계할 때 이런 구조로 만들었기 때문이다.



*32비트 레지스터 (= 기억회로)

 그렇다면, 32비트씩 처리되는 레지스터는 어떻게 표현이 될까? 매우 간단한데, 저 위의 레지스터들의 이름 앞에다가 'E' 만 붙여주면 된다.
EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI

 그렇다면, 32비트씩 처리되는 레지스터는 어떻게 표현이 될까? 매우 간단한데, 저 위의 레지스터들의 이름 앞에다가 'E' 만 붙여주면 된다. E의 의미는 여전히 확장(extend) 이다. 16비트 시대 입장에서 32비트는 또 다른 확장이니까.


 구조를 보자면 EAX는 32비트(=4바이트) 레지스터지만, AX와 일부 공통으로 되어 있고, 32비트 중 하위 16비트 (0~15비트)가 AX 그 자체다. 상위 16비트 (16~31비트) 는 이름도 레지스터 번호도 없다. 즉, EAX를 16비트 2개로 나누어 사용하는 것은 가능하지만, 간단하게 사용할 수 있는 것은 하위 비트(0~15, AX) 뿐이고, 상위 비트를 꼭 사용하고 싶다면 16비트만큼 밀어내는 명령을 사용하여 상위 16비트를 하위로 내려야만 한다. 마찬가지로 32비트의 CPU는 32바이트만 기억할 수 있다. (4바이트(32비트) * 8개 = 32바이트)


 32비트 레지스터를 설명할 때, 세그먼트 레지스터 또한 설명이 필요하다. 위의 16비트의 경우 8비트의 레지스터가 존재했듯이 32비트의 경우 16비트 레지스터 또한 존재한다.


  • ES - 엑스트라 세그먼트 (extra segment: 덤 세그먼트)
  • CS - 코드 세그먼트 (code segment)
  • SS - 스택 세그먼트 (stack segment)
  • DS - 데이터 세그먼트
  • FS - 명칭 없음 (덤 세그먼트 2번째)
  • GS - 명칭 없음 (덤 세금너트 3번째)





#2 어셈블러 해석, CPU와 메모리 내용 정리

 

 MOV SI, msg 의 의미를 살펴 보면 MOV는 대입의 의미이므로 SI = msg; 라는 의미이다. 여기서 msg는 레이블이라 부르는데 그 정체는 주소다.


 JMP entry라는 명령 (entry로 점프하라, entry로 가라)을 보며 의미를 더 명확히 해보자. entry는 프로그램이 시작되는 부분을 의미하는 레이블로, 그 값을 0x7c50 이라고 가정해 보자. 그럴 때 JMP entry 라는 문장을 JMP 0x7c50 라고 해석해도 전혀 문제 될 게 없다. 어셈블러에서 레이블은 단지 주소를 의미하는 숫자에 불과하다. 어떤 숫자가 되는지는 ORG 명령에 의해 번지 값을 계산한 후, 그 값을 레이블 값으로 정해주는 것이다. 

 

/* ORG는 프로그램이 실행 시에 메모리 내 몇 번지에 로딩되는지를 알려주는 명령이다. origin의 의미다. */


그래서 만약 MOV AX, entry 라고 쓰면 AX에는 0x7c50이 대입되는 것이다. entry 이후에 프로그램의 모든 실행 소스코드가 AX에 저장된다고 해석하는 건 곤란하다. 단지 프로그램의 시작 주소값을 저장하는 것에 불과하다.


 다음은 MOV AL, [SI]을 보자. [ ]의 의미는 '메모리'를 의미한다. 메모리란 기억 소자로 만들어진 대규모 아파트 단지 같은 것인데, 꽤나 규칙적으로 기억 소자들이 나열되어 있다. 위에서 레지스터를 설명하면서 CPU가 얼마나 변변 찮은 기억공간을 가지고 있는지를 설명했는데, 이를 위해 메모리를 따로 준비해 줄 필요가 있는 것이다.


 MOV 명령은 전송이 도착하는 쪽이나 전송을 주는 쪽에 레지스터나 정수 뿐만 아니라 메모리를 지정하는 것도 가능하다. 그 때 [ ] 기호를 사용한다.




 메모리CPU 외부에 존재한다. 즉, 외부기억 장치라고 할 수 있다. CPU는 자신의 단자 일부를 이용해 메모리에게 '메모리야, 5678 번지에 있는 데이터를 나에게 보내주라.' 라는 전기 신호를 보낸다. (정확히는 칩셋이라는 제어 소자가 들어간다.) CPU가 메모리의 데이터(내용)를 읽거나 쓸 때에는 이렇게 주고받는 동작을 반복하는 것이다.


 메모리와 교신하는 것은 데이터를 주고받을 때뿐만이 아니다. 프로그램 자체도 메모리의 어딘가에 반드시 들어가 있어야 한다. 일반적으로 프로그램이라 하는 것은 상당히 커서, 전부 합쳐봐야 44바이트밖에 안 되는 레지스터에 들어갈 수 없다. 따라서 여기에는 반드시 메모리에 둔다는 규칙이 존재한다. 이는 CPU가 기계어 (프로그램을 구성하고 있는)를 실행할 때는 반드시 메모리로부터 프로그램을 1명령씩 읽어 들여 차례로 실행하기 때문이다.


 메모리는 CPU와 꽤 떨어진 곳에 위치하고 있다. CPU의 반도체 안 쪽과 비교한다면 멀다 (10cm라고 가정)고 표현할 수 있다. 그래서 데이터를 주고 받을 때 응답시간이 꽤 걸릴 수밖에 없다. (CPU는 매우 고속으로 동작하므로, 10cm라는 짧은 거리의 전기 신호가 전해지는 속도조차 문제가 된다.) 그리고 메모리는 레지스터보다 엄청나게 많은 것을 기억할 수 있지만 메모리를 이용하면 레지스터보다 몇 배나 느리다.


 위에서 설명한 MOV의 사용법에 추가로 BYTE, WORD, DWORD 라는 영어도 사용한다. 예를 들면 아래와 같다.


 MOV BYTE [678], 123 (여기서 숫자는 컴퓨터 입장에서 2진수(010101 ... 등)로 받아들여 진다. ON과 OFF의 나열이다.)


 해석을 하자면 이렇다. 일단 전기 신호가 들어오면 잠깐 회로의 어딘가(678번지)에 8개의 기억 소자 (소자 당 1bit)가 반응할 수 있는 회로(678번지)에 123을 ON과 OFF로 표시한 전기 신호를 기억하는 것이다. 왜 8개의 기억소자냐 하면, BYTE로 지정했기 때문이다.


 MOV WORD [678], 123 (WORD는 2개의 번지를 이용할 수 있다. (번지 당 1바이트 = 8bit))


 이 경우는 메모리 단지의 678번지와 그 옆의 679번지의 기억 소자가 반응한다. 합계 16비트인데, 이 경우 123은 16비트의 수치로 해석되어 저장된다. 본래 123은 111 1011로 7비트만으로 표현할 수 있지만, 16비트로 해석되므로 앞에 0으로 채워지면서 0000 0000 0111 1011 로 저장된다. (편의상 4bit 단위로 공백을 줬다.)

 한 번지당 1바이트(8비트)가 저장되므로 678번지에는 앞의 0000 0000 이 저장되고, 옆의 679번지에는 0111 1011 이 저장된다. 아래의 그림처럼 저장된다고 생각하면 된다.


 지정한 번지수에 하위비트(0~7비트)에 해당하는 0111 0111 이 저장되고, 그 옆 번지(주소 1증가)에 상위비트(8~15비트)에 해당하는 0000 0000 이 저장된다. 이처럼 어셈블러에서 메모리를 저장할 때는,

 "데이터의 크기[번지]"


 처럼 사용한다. 이것은 하나의 문법으로, 데이터의 크기로 BYTE를 지정하면 지정한 메모리만 대상이 된다. (메모리 하나당 1바이트) WORD를 지정하면 옆 번지도 대상이 되고, DWORD를 지정하면 옆과 그 옆과 그 다음 번지도 대상이 된다. (4바이트 이므로) 여기서 말하는 옆은 지정한 번지로부터 번지가 증가하는 방향을 의미한다.

 메모리의 번지를 지정할 때는 정수를 지정하는 방법 외에도 레지스터를 사용하는 방법도 가능하다. 예를 들어 BYTE[SI] 라든가, WORD[BX] 라든가 하는 식이다. SI에 987이 들어 있으면, BYTE[SI]BYTE[987] 로 해석되어 987번지의 메모리를 지정한 것이 된다. 여기서 사용할 수 있는 레지스터는 BX, BP, SI, DI 뿐이다. (스택과 포인터에 해당하는 레지스터)

 MOV 명령에는 비트 수가 같은 것끼리만 대입한다는 규칙이 있어서 어떤 경우에는 데이터의 크기를 생략하는 문법도 가능하다. 예를 들어

 MOV AL, BYTE [SI] MOV AL, [SI] 라는 표현이 된다. 이것은 메모리의 SI 번지의 1바이트 내용을 AL로 읽어라는 의미다.

 이 밖에도 다양한 명령들이 존재한다. ADD SI, 1 이라는 명령은 SI = SI + 1로 해석되고, CMP a, 3 이라는 명령은 a와 3을 비교하라는 의미다. JE (Jump if Equal) 명령은 조건 점프 명령의 하나로 해석되고, 그 예는 아래와 같다.

 CMP AL, 0
 JE fin

 위 2개의 명령은 if (AL == 0) { goto fin; } 이라는 의미다. 여기서 fin이라는 레이블은 이 책에서 '끝'이라는 의미로 사용된다.




#3 BIOS (basic input output system), 기본적인 입출력에 시스템(프로그램)

 

 PC에는 BIOS 라는 프로그래밍이 있는데, PC의 기판 상에 있는 ROM (read only memory, 전원을 차단해도 내용이 없어지지 않는 읽기 전용 메모리) 이라는 소자에 들어 있다. BIOS는 OS 제작자가 자주 사용할 것 같은 프로그램을 PC 제조사가 미리 준비해 둔 매우 고마운 존재다. 


 INT (interrupt)라는 명령으로 이 함수들을 호출하여 사용할 수 있다. INT 뒤에는 숫자를 쓰는데, 이 번호에 따라 BIOS 의 어느 함수를 호출할지를 결정할 수 있다. 0x10 (10진로 16)번 함수를 호출하는 예를 보자. 이것은 비디오카드 제어에 대한 함수다. (관련 코드를 아래 첨부합니다.)
/*************************************************
 ** helloos.nas source code (교재의 소스 코드 일부)
*************************************************/

entry:
        MOV        AX, 0            ; initialize register
        MOV        SS,AX        ; SS (stack segment) = AX (accumulator)
        MOV        SP,0x7c00 ; SP (stack pointer) = 0x7c00
        MOV        DS,AX        ; DS (data segment) = AX (accumulator)
        MOV        ES,AX        ; ES (extra segment) = AX (accmulator)

        MOV        SI,msg         ; msg means label
putloop:
        MOV        AL,[SI]
        ADD        SI, 1           ; SI + 1
        CMP        AL,0            ; AL = character code
        JE        fin              ; fin label means final
        MOV        AH, 0x0e        ; one char expression possible
        MOV        BX, 15          ; color code
        INT        0x10            ; call video BIOS
        JMP        putloop
fin:
        HLT                       ; stop cpu
        JMP        fin            ; Endless Loop


/*************************************************
 ** End Line
*************************************************/
 '1문자씩 표시하려고 하면 구체적으로 어떻게 해야 그 기능을 사용할 수 있을까?' 라는 예시다. 우선, 문자 표시이므로 비디오카드와 관련이 있을 것이다. INT 0x10를 떠올릴 수 있다. 여기에서 정보들을 찾아보면,

한 문자 표시
  • AH = 0x0e;
  • AL = 캐릭터 코드;
  • BH = 컬러 코드;
  • 리턴 값: 없음
  • 주: 힙, 백스페이스, CR (carriage retturn, 개행에 사용된다.), LF (line feed, 개행에 사용된다.)는 제어코드로 인식된다.

 위의 정보들을 발견할 수 있다. 그래서 여기에 써 있는 대로 레지스터에 각종 값을 대입한 후, INT 0x10 을 실행하면 1문자가 나오게 된다. 

/* 관련 설명은 현재 OS 교재 55p에 있는 helloos.nas 파일의 코드를 바탕으로 하고 있습니다. (위의 코드 첨부) */


 새로 나온 명령의 마지막은 HLT 명령 (=halt 의 의미)이다. 이 명령은 CPU를 정지시키는 명령이다. 정지란 대기 상태를 의미한다. (완전한 정지는 전원을 끄는 것만이 방법이다.) 이 때 키보드를 누른다던가, 마우스를 움직인다던가 하는 외부의 변화가 있으면 CPU는 계속해서 프로그램을 실행한다. 결국 HLT 명령 없이도 JMP fin 이 무한루프이기 때문에 굳이 HLT를 쓰지 않아도 된다.

 하지만, 이 경우 CPU는 무의미한 공회전을 계속할 수 있다. HLT 없이 JMP 명령을 실행시키면, CPU의 부하는 100%가 되어 계속 전기를 사용하게 된다. HLT를 넣는 것만으로 CPU는 거의 가만히 있게 되고, 전기를 그다지 사용하지 않게 된다. 따라서 HLT를 이용하는 습관은 중요하다. 

 위의 어셈블러 코드를 C언어 식으로 다시 써보면 이렇다.
/*************************************************
 ** helloos.nas source code (교재의 소스 코드 일부)
*************************************************/

entry:
        AX = 0;
        SS = AX;
        SP = 0x7c00;
        DS = AX;
        ES = AX;
        SI = msg;

putloop:
        AL = BYTE [SI];
        SI = SI + 1;
        if (AL == 0) { goto fin; }
        AH = 0x0e;
        BX = 15;
        INT 0x10;
        goto putloop;
fin:
        HLT
        goto fin;


/*************************************************
 ** End Line
*************************************************/
 이 프로그램 덕분에 msg에 써져 있는 데이터(hello, world)가 1문자씩 표시되고, 데이터가 0이 되면 HLT의 무한루프에 들어간다. 이런 구조로 'hello, world' 가 표시되는 것이다.




#4 메모리 부연 설명


 메모리에는 우리가 마음대로 사용할 수 없는 부분이 존재한다. 그건 바로 BIOS가 사용하는 영역이다. 0xf0000번지 부근에는 BIOS 자체가 들어 있어서 이 곳은 건드리면 프로그램 근간이 흔들릴 수 있다. 이 외에도 사용하면 안 되는 메모리 영역이 여기저기 있어서, OS 제작자는 반드시 신경을 써야한다.

 보통 Windows나 Linux를 사용할 때에는 이러한 것들을 생각하지 않아도 되지만, 우리는 OS를 만들고 있기 때문에 반드시 신경써야 한다. 이렇게 신경쓰는 일을 효율적으로 해야할 때 만드는 것이 바로 메모리 맵이다. 책의 저자가 책에 기술해 놓은 페이지는 현재 접속할 수 없으므로, 타 사이트를 참고하여 아래 작성해 놓았다.

IBM PC AT 호환기종의 메모리 맵핑 (가독성을 위해 16진수 대문자를 사용, 본래 소문자로 표현되어 있다.)

/* PCI 장치 혹은 PC에 장착하는 카드를 사용할 때, 각 장치가 메모리에 맵핑되는 상태를 설명한다. */
  • 0x00000000 ~ 0x0009FFFF : RAM
  • 0x000A0000 ~ 0x000BFFFF : 비디오 카드 접근 영역
  • 0x000C0000 ~ 0x000C7FFF : 비디오 BIOS
  • 0x000C8000 ~ 0x000DFFFF : 각종 카드의 ROM 영역
    /* 0X000
    D0000 ~ 0x000DFFFF 영역은 대부분 비어 있다. */
  • 0x000E0000 ~ 0x000FFFFF : 확장 BIOS
  • 0x000F0000 ~ 0x000FFFFF : BIOS
  • 0x00100000 ~ 0x00EFFFFF : RAM
  • 0x00F00000 ~ 0x00FFFFFF : RAM 혹은 홀 (BIOS의 설정에 의해서 변경될 수 있다.)
    /* 286의 경우는 0x00FFFFF0 로부터의 16바이트에 리셋트 점프 명령이 있을 수도 있다. */
  • 0x01000000 ~ 메모리의 끝 : RAM
  • 메모리의 끝 ~ 0xFFFFFFEF : PCI 장치 등의 메모릴 맵핑 I/O에 이용 가능한 영역
  • 0xFFFFFFF0 ~ 0xFFFFFFFF : 386이후에서는 여기에 리셋트 점프 명령이 있다.

소프트웨어에 따른 용도 구분
  • 0x00000000 ~ 0x000003FF : 리얼모드용 인터럽트 벡터
    /* 물론 IDT를 변경하면 변경할 수 있지만, 기본적으로는 이 주소가 사용된다. */
  • 0x00000300 ~ 0x000003FF : BIOS용 스택
  • 0x00000400 ~ 0x000004FF : BIOS가 사용하는 영역
  • 0x00007C00 ~ 0x00007DFF : 부트섹터가 로딩되는 주소
  • 0x0009FC00 ~ 0x0009FFFF : ACPI 영역

 위에 빨간색으로 표시된 부분은 놓칠 수 없는 부분이다. 이 책에서는 ORG 값에 이 숫자를 사용했다. 그리고 이 숫자를 사용했기 때문에 정상작동 되는 것이다. IBM에서 그렇게 정했다.

 한 번 정해지면 이것을 전제로 각종 OS가 만들어지기 시작한다. 만약, 후에 부트섹터가 로딩되는 주소를 변경한다면 지금까지의 OS를 개조하지 않으면 동작하지 않게 된다. 결국 호환성이 나쁘다는 평을 들을 수밖에 없다. 이런 것들은 모두 IBM이나 인텔에서 정하는 것들로 이 부분이 궁금하다면 각 회사의 소프트웨어 개발 철학이나 서적을 들여다보면 된다.




#5 실습 시 문제가 발생할 수 있는 부분 (69p 부트섹터 ~ 72p Makefile)

 

 실습을 하다 보면, 뭔가 꼬일 때가 있다. 아마도 책이 그렇게 완벽하지 않다는 것의 반증일 것이다. 이번 장에서의 실습을 성공적으로 하기 위해서는 69p의 3번 <부트섹터만 정리> 파트를 진행하지 않고 4번 <이후를 위한 Makefile 도입> 부분부터 실행해야 한다.


 69p 하단부 부터 Makefile 설명이 시작된다. Makefile은 SciTE 편집기를 통해 정확하게 작성해야하는데, 이 부분이 상당히 헷갈린다. 아래의 내용과 같이 작성해 보면, 다들 문제 없이 실행이 가능할 것이다. 소스 코드이며 아래 사진까지 첨부해놨다.

/*************************************************
 ** Makefile 내용
*************************************************/

// 여기서 중간 중간 공백은 space 키를 누름으로써 작성하시면 됩니다.
/* 엔터키로 개행을 하신 후에 탭 키를 누르셔서 저렇게 줄 간격을 맞춰줍니다. */

ipl.bin : ipl.nas Makefile 
	../z_tools/nask.exe ipl.nas ipl.bin ipl.lst 

helloos.img : ipl.bin Makefile
	..\z_tools\edimg.exe	imgin:../z_tools/fdimg0at.tek \
		wbinimg src:ipl.bin len:512 from:0 to:0	imgout:helloos.img

/*************************************************
 ** End Line
*************************************************/


 이후의 실습 또한 문제 없이 진행할 수 있을 것이다. 파일의 내용을 조금 다르게하는 것으로, 공백이나 개행을 정확하게 하지 않았을 경우 매우 곤란해질 수 있다는 걸 깨달은 오늘이다.


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday