티스토리 뷰


* CHAPETER 3 - 32비트 모드 돌입과 C언어 도입



#1 IPL 만들기 (initial program loader, 초기 프로그램 로더)

 

 디스크의 맨 처음 512바이트는 부트섹터이므로 (이것은 2일째 OS 실습에서 정한 부분이다.), 그 다음의 512바이트를 읽어보자. projects / 03_day의 harib00a 안에 ipl.nas 는 기존의 ipl.nas에 추가된 부분이 존재한다.

/*************************************************
 ** day 03 ipl.nas, added part
*************************************************/

// 기존의 ipl.nas에서 추가된 부분이다.

; added part

        MOV        AX,0x0820
        MOV        ES,AX
        MOV        CH, 0            ; cylinder 0
        MOV        DH, 0            ; header 0
        MOV        CL, 2            ; sector 2

        MOV        AH, 0x02        ; AH=0x02 : disk read
        MOV        AL, 1            ; 1 sector
        MOV        BX,0
        MOV        DL, 0x00        ; A drive
        INT        0x13            ; call disk BIOS
        JC        error


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

 새로 나온 명령은 JC 뿐이다. JC라는 것은 'Jump if carry'의 약자로 캐리 플래그가 1이면 점프하라는 의미다. (뒤에 설명이 나온다.) INT 0x13 명령을 실행하면 어떤 일이 발생할까? disk BIOS의 구조를 살펴보자.


/* 디스크로부터 읽기, 디스크에 쓰기, 섹터의 검사(verify) 및 찾기 */

  • AH = 0x02 (읽을 때)

  • AH = 0X03 (쓸 때)

  • AH = 0X04 (검사를 할 때)

  • AH = 0X0C (찾기를 할 때)

  • AL = 처리할 섹터 수(연속된 섹터를 처리할 수 있음)

  • CH = 실린더 번호 & 0xFF

  • CL =  섹터 번호 (bit 0 ~ 5) | (실린더 번호 & 0x300) >> 2

  • DH = 헤드 번호

  • DL = 드라이브 번호

  • ES:BX = 버퍼 어드레스(검사 혹은 찾기를 할 때에는 이 값을 참조하지 않음)

  • 리턴 값:

  • FLAGS.CF == 0 : 에러 없음, AH == 0

  • FLAGS.CF == 1 : 에러 있음, AH에 에러코드(리셋 기능과 같음)


 위의 경우는 AH에는 0x02 값이 대입되므로, 읽기라는 걸 알 수 있다. '리턴 값'을 보면 FLAGS.CF라고 기술되어 있다. 이것이 캐리 플래그다. 즉, 이 함수를 호출하면 에러가 있을 경우에는 캐리 플래그가 0, 에러가 없을 경우에는 캐리 플래그가 1이 되는 것이다. 이것이 JC 명령의 존재이유다.


 캐리 플래그라는 것은 1비트밖에 기억할 수 없는 레지스터로, CPU에는 그 밖에도 몇 가지 이러한 1비트밖에 기억 못하는 레지스터들이 존재한다. 이러한 레지스터를 플래그 라고 한다. flag의 의미는 깃발이다. 즉, 들고 내리기 이 두 가지 상태만 기억할 수 있다는 의미로 받아들이면 된다. 1bit는 두 가지의 경우의 수(0 과 1)를 나타낼 수 있으므로, 1bit면 충분하다.


 캐리 플래그는 본래 캐리(carry)라는 상태를 나타내기 위해 사용되는 것이지만, CPU의 플래그 중에서 가장 다루기 쉬우므로 다른 용도에도 곧잘 사용된다. 이번에는 에러 유무의 보고에 사용한다.


 나머지를 살펴보자. CH, CL, DH, DL에는 각각 실린더 번호, 섹터 번호, 헤드 번호, 드라이브 번호라는 것을 대입해야만 한다. 이번 프로그램에서는 위의 ipl.nas 소스코드에 나온 값들이 각각 사용된다.




*디스크, 드라이브


 드라이브 번호라는 것은 플로피디스크 드라이브가 여러 개 연결 되었을 때, 어떤 드라이브의 디스크부터 읽어 들일까를 지정하기 위한 것이다. 요즘에는 PC에 플로피디스크 드라이브가 1개밖에 없지만, 예전에는 2개 정도 있었다. 지금은 한 개밖에 없으므로, 0번을 지정한다. 이것으로 어느 드라이브로부터 읽는지를 정했다. 다음은, 그 디스크의 어디부터 읽을까를 정할 차례다.



 디스크는 새카만 8cm의 CD 같은 원반이 들어 있다. 이 원반은 펄럭거리는 자기필름이다. 여기에 바깥쪽부터 실린더 0, 실린더 1, ... , 실린더 79라는 원이 모여있다. 생산이 이렇게 된다기 보단, 데이터를 기록하는 장치적 의미에서 보면 그렇게 생각할 수 있다는 거다. 디스크에는 전부 80개의 실린더(cylinder, 원통의 의미)가 있다. 아주 작은 원통이 동심원상으로 겹쳐져 있는 상태를 보고 지은 이름일 것이다.

 다음은 헤드인데, 자기 헤드를 의미한다. 자기 헤드를 위에 가져다 대면 헤드 0, 자기 헤드를 아래다 가져다 대면 헤드 1이다. 플로피디스크는 CD-ROM과는 달라 양면 기록 방식이다.

 마지막으로 섹터다. 실린더와 헤드를 정하면 그 원주를 따라 자기를 여러 비트 기록할 수 있으나 그렇게 하면 양이 너무 많아서 크게 몇 등분한다. 플로피디스크에서는 18등분되어 있다. (이 책은 플로피디스크를 기준으로 한다. 물론 구현은 가상으로 하지만.) 1개의 원주에는 18섹터(sector, 영역의 의미)가 있고, 이것은 섹터1, 섹터2, ..., 섹터18 이라 부른다.

 이상을 정리하면, 1장의 디스크에는 80실린더가 있고, 헤드는 2개 있고, 하나의 실린더에는 18섹터가 있다. 1섹터는 512 바이트이므로,

 80 x 2 x 18 x 512 = 1474560 바이트 = 1,440 KB

 가 된다. IPL이 들어 있는 부트섹터는 C0 - H0 - S1 이었지만 (이것은 실린더 0, 헤드 0, 섹터 1의 약자), 그 다음 섹터는 C0 - H0- S2 이다.



*버퍼 어드레스


 버퍼 어드레스에 관해 알아보자. 버퍼 어드레스는 디스크에서 읽은 데이터를 메모리의 어디에 저장할 것인지를 나타내는 주소 값이다. 보통, 번지는 1개의 레지스터로 나타내면 좋을 것 같지만, BX (16비트 레지스터)만으로는 0 ~ 0XFFFF 까지의 값밖에 표현할 수 없다. 즉, 0부터 65,535번지 까지밖에 나타낼 수 없다는 의미다. PC의 메모리는 대부분 64MB 이상일 텐데, 단지 64KB까지밖에 사용할 수 없다는 건 말도 안된다.


 따라서 이 점을 극복하기 위해, EBX 레지스터(32비트 레지스터)라는 것이 나중에 등장해 이것으로 4GB (2의 32승은 약 4GB)까지 다룰 수 있게 되었다. 이것은 CPU가 다룰 수 있는 최대 메모리 용량이므로, 문제 없다. 그러나 EBX가 사용될 수 있었던 건 훨씬 뒤의 이야기고, BIOS들이 설계된 시대의 CPU는 32비트 레지스터를 붙이는 것이 좀 어려웠기 때문에, 할 수 없이 보조적인 역할을 하는 세그먼트 레지스터라는 것을 만들었다. 그리고 메모리의 번지를 지정할 때 이 세그먼트 레지스터를 사용하게 된 것이다.


 ES:BX라는 표현이 바로 그것이며, MOV AL, [ES:BX]와 같이 사용한다. 이 경우 메모리 번지는 ES x 16 + BX(ES는 16비트 세그먼트 레지스터)로 계산하게 되어 있다. ES 레지스터로 대략적인 번지를 정한 후에 BX로 세세하게 지정한다. 이것으로 ES에 0xffff, BX에도 0xffff 를 넣어서 1,114,095 바이트, 즉 약 1MB의 메모리 번지를 지정할 수 있게 됐다.


 이 책에서 ES = 0x0820 이고, BX = 0이므로, 이 디스크의 데이터가 로드되는 곳은 0x8200(0x0820 * 16 + 0)번지부터 0x83ff번지까지가 된다. 0x8000 ~ 0x81ff 까지의 512바이트는 후에 부트섹터의 내용을 넣기 위해 남겨뒀다. OS개발 2일차에서 설명한 메모리 맵을 참고해 이 부분이 사용이 되지 않고 있다는 걸 알고 쓰는 거다. 0x7c00 ~ 0x7dff 는 부트섹터가 사용하고 있지만, 0x7e00 이후는 아무도 사용하지 않고 있고, 0x9fbff 까지는 OS가 마음대로 사용해도 된다.


 지금까지 세그먼트 레지스터라는 것은 전혀 생각하지 않고 있었지만, 실은 어떤 메모리라도 세그먼트 레지스터와 함께 번지수를 지정해야만 한다는 규칙이 있다. 거의 대부분의 경우가 DS:를 지정한 것이 된다.


 이제껏 MOV CX, [1234]라고 생각한 것은 MOV CX, [DS:1234]의 의미였던 것이다. MOV AL, [SI]MOV AL, [DS:SI]라는 의미다. 어셈블러에서는 매번 쓰는 것이 귀찮으므로 생략하게 되어 있다. 이러한 룰 때문에 DS를 0으로 해둬야만 한다. (DS x 16 + SI) 만약 DS가 0이 아니면, 그 16배수가 번지로 항상 더해지게 되므로, 어딘가 이상한 곳에 데이터를 읽고 쓰게 될지 모르기 때문이다.





#2 에러 방지를 위해 또 수정한 ipl.nas (실습 폴더 harib00b)


 이제 수정된 ipl.nas 코드를 살펴보자. 새로 나온 명령어 JNC(jump if not carry)라는 건 조건 점프 명령의 하나이다. 즉, 캐리 플래그가 0이라면 점프하라는 의미다. JAE 역시 조건 점프로 'jump if above or equal'의 약어이다. 즉, 크거나 같으면 점프하라는 의미다.
/*************************************************
 ** ipl.nas soure code in harib00b
*************************************************/

; read disk

		MOV		AX,0x0820
		MOV		ES,AX
		MOV		CH, 0			; cylinder 0
		MOV		DH, 0			; head 0
		MOV		CL, 2			; sector 2

		MOV		SI, 0			; count number of failure
retry:
		MOV		AH, 0x02		; AH=0x02 : disk read
		MOV		AL, 1			; 1 sector
		MOV		BX,0
		MOV		DL, 0x00		; A drive
		INT		0x13			; call disk BIOS
		JNC		fin			; if(!error) goto fin
		ADD		SI, 1			; SI += 1
		CMP		SI, 5			; SI == 5 ?
		JAE		error			; SI >= 5 goto error
		MOV		AH,0x00
		MOV		DL, 0x00		; A drive
		INT		0x13			; reset drive
		JMP		retry

/*************************************************
 ** End Line
*************************************************/
 에러가 발생했을 때의 처리는, 다시 읽기 전에 AH = 0x00, DL = 0x00 으로 INT 0x13을 하고 있다. 이것은 AT-BIOS 페이지에 의하면 '시스템 리셋'이다. 이것으로 드라이브를 리셋하여 처음부터 시작한다. 




#3 18섹터 까지 읽기 위한 코드 추가 (실습 폴더 harib00c), 시작 섹터는 2 (CL, 2)


 새로 나온 명령은 JBE(jump if below or equal)다. 즉, 작거나 같으면 점프하라는 의미다.
/*************************************************
 ** ipl.nas soure code in harib00c
*************************************************/

; read disk

		MOV		AX,0x0820
		MOV		ES,AX
		MOV		CH, 0			; cylinder 0
		MOV		DH, 0			; head 0
		MOV		CL, 2			; sector 2
readloop:
		MOV		SI, 0			; count number of failure
retry:
		MOV		AH, 0x02		; AH=0x02 : disk read
		MOV		AL, 1			; 1 sector
		MOV		BX,0
		MOV		DL, 0x00		; A drive
		INT		0x13			; call disk BIOS
		JNC		fin			; if(!error) goto fin
		ADD		SI, 1			; SI += 1
		CMP		SI, 5			; SI compare with 5
		JAE		error			; SI >= 5 goto error
		MOV		AH,0x00
		MOV		DL, 0x00		; A drive
		INT		0x13			; reset drive
		JMP		retry
next:
		MOV		AX, ES			; address += 0x200 (0x0020 * 16), ES = 16bit segment register
		ADD		AX,0x0020
		MOV		ES, AX			; cannot use command like 'ADD ES, 0x020', so split to 3step
		ADD		CL, 1			; CL += 1
		CMP		CL, 18			; CL compare with 18
		JBE		readloop		; CL <= 18 goto readloop

/*************************************************
 ** End Line
*************************************************/
 다음 섹터를 읽기 위해 필요한 것은 CL1을 더하는 것과, ES0x20만큼 늘리는 것이다. CL은 섹터 번호이고, ES는 읽어 낼 번지를 지정하기 위한 것이다. 0x20은 512를 16으로 나눈 값을 16진수 표기로 쓴 것이다. ADD AX, 512/16 라고 쓰는 편이 더 알기 쉽지만, 간단하게 나타내기 위해 이렇게 표현했다. (주소 번지를 계산할 때는, ES x 16 + BX 이므로, 한 섹터(512바이트)를 더 읽기 위해서, 즉 512를 정확하게 더하려면 ES에 512/16을 넣어줘야만 하는 것이다. ES에 512/16 을 수식에 대입하면 512/16 x 16 이므로 결과 값은 512 + BX가 된다.)

 BX에 512를 단순하게 더하는 것이 결과이므로, BX에 512를 더하는 방식을 채택해도 된다. 책에서는 계산 연습을 위해 의도적으로 이런 구조로 썼다고 한다. 그렇다면, 이제 코드를 다시한 번 분석해보자.

 이 코드는 readloop 이하 부분이 메인 루프로 반복해서 실행된다고 생각하면 된다. 오류발생 횟수를 초기화 해주는 MOV SI, 0 이하의 부분이 사실상 제일 중요한데, 즉 retry레이블을 주의 깊게 살펴보자.

 초기 부분에 AH(accumulator high)에 0x02 라는 값을 주고 (인터럽트 호출 시, 현재 디스크 읽기 기능을 사용할 것임을 명시) AL(accumulator low)에 1 값을 주고 있다. AL에 1값이 들어갈 때, 이 1의 의미는 하나의 디스크 섹터를 읽겠다는 뜻이다. 그리고 나서 다음 next레이블을 살펴보면, ES 레지스터에 0x200 값, 512 (bytes) 만큼을 더한다. 하나의 디스크 섹터를 읽었으니 다음 디스크 섹터로 이동하는 명령인 셈이다.

 그리고 나서, 카운팅을 전용으로 담당하는 CL(counter) 레지스터에 1을 더해준다. (하나를 읽었으니 카운터를 더 해 표시를 하는 것이다.) 그 후에 CL레지스터 값이 한계치인 18을 넘었는지 확인한다. 18을 넘지 않았으면, 아직 디스크 섹터 (2 ~ 18) 전부를 읽은 것이 아니므로 다시 readloop레이블로 돌아간다.

 그렇다면 여기서 한 가지 의문이 들 수 있는데, 애초에 retry레이블의 AL, 1 구문을 AL, 17 구문으로 대체하면 되지 않을까? 그렇게 하면 2 ~ 18 섹터에 해당하는 17개의 섹터를 한꺼번에 읽을 수 있는데 왜 굳이 loop 형식을 취해 반복을 하게 했을까? 이것은 책의 저자가 디스크 BIOS의 읽기 Function의 설명 부분인 '보충' 쪽을 신경 썼기 때문이다. 그 부분에는 이렇게 기재가 되어 있다.

  • 처리하는 섹터 수는 0x01 ~ 0xff의 범위에서 지정(0x02 이상을 지정할 때는 연속 처리할 수 있는 조건이 있을지도 모르므로 주의 - FD의 경우에는 아마 복수의 트랙에 걸쳐 있지 않고 64KB 경계를 넘어도 안 된다고 생각합니다.)

 즉, 결국 AL, 17 구문으로 대체해도 이상 없이 동작된다. 그러나, 다음에 작성될 프로그램에서 이러한 구문이 문제가 되기 때문에 일부러 1섹터씩 루프로 처리하였다. 이것으로 C0-H0-S2 (0번 실린더, 0번 헤드, 2번 섹터) ~ C0-H0-S18(0번 실린더, 0번 헤드, 18번 섹터)까지의 512 x 17 = 8,704바이트가 메모리 번지 0x8200 ~ 0xa3ff 로 읽혀지게 되는 것이다.

/* 0xa3ff - 0x8200 + 1 = 8,704 가 나옵니다. 정확하게 8,704바이트 만큼 주소가 잡혀 있는 것이죠. */





#4 10실린더 만큼 읽어보기 (실습 폴더 harib00d)


 C0-H0-S18 섹터의 다음은 디스크 뒷면으로 가서 C0-H1-S1이 된다. 18섹터까지가 0xa3ff 였으므로, 다음에 읽을 주소는 여기서 1을 더한 0xa400이다. 이 상태로 계속 읽어들여서 C9-H1-S18까지 읽어보도록 하자.

/************************************************* ** ipl.nas soure code in harib00d *************************************************/ ; read disk MOV AX,0x0820 MOV ES,AX MOV CH, 0 ; cylinder 0 MOV DH, 0 ; head 0 MOV CL, 2 ; sector 2 readloop: MOV SI, 0 ; count number of failure retry: MOV AH, 0x02 ; AH=0x02 : disk read MOV AL, 1 ; 1 sector MOV BX,0 MOV DL, 0x00 ; A drive INT 0x13 ; call disk BIOS JNC fin ; if(!error) goto fin ADD SI, 1 ; SI += 1 CMP SI, 5 ; SI compare with 5 JAE error ; SI >= 5 goto error MOV AH,0x00 MOV DL, 0x00 ; A drive INT 0x13 ; reset drive JMP retry next: MOV AX, ES ; address += 0x200 (0x0020 * 16), ES = 16bit segment register ADD AX,0x0020 MOV ES, AX ; cannot use command like 'ADD ES, 0x020', so split to 3step ADD CL, 1 ; CL += 1 CMP CL, 18 ; CL compare with 18 JBE readloop ; CL <= 18 goto readloop MOV CL,1 ADD DH,1 CMP DH,2 JB readloop ; DH < 2 goto readloop MOV DH,0 ADD CH,1 CMP CH,CYLS JB readloop ; CH < CYLS goto readloop /************************************************* ** End Line *************************************************/

 새로 나온 명령은 JB(jump if below)다. 작으면 점프하라는 의미다. 그리고 소스의 첫 부분에서 EQU라는 명령을 사용하고 있는데, 이것은 C언어로 말하자면 #define과 같은 것으로 정수로 선언하는 데에 사용된다. [CYLS EQU 10]이라는 것은 CYLS = 10이라는 의미다. EQU는 equal의 약어다. (; 주석문은 신경쓰지 않도록 하자. 일본어 표기로 되어 있다보니 깨져서 나오는 것 뿐이다.)


 왜 CYLS(cylinders) 부분만 정수로 했냐하면, 나중에 숫자를 변경할 수도 있기 때문이다. 지금은 10실린더 정도로 설정을 했지만, 나중에 크게 하거나 줄이거나 하는 조절을 할 예정이다. 이제 부트섹터는 거의 완성이 됐다. 처음에 시스템에 의해 로드된 부트섹터까지 포함하면, ,일단 이것으로 디스크 내의 최초 10 x 2 x 18 x 512 = 184,320바이트 = 180KB가 무사히 읽혀지게 된다. 'make install'로 디스크에 인스톨하여 실제 PC에서 실행시키면, 그만큼의 시간이 걸리는 것을 알 수 있다. 이것으로 메모리의 0x8200 ~ 0x34fff는 디스크에서 읽은 데이터로 꽉 차게 된다.




#5 OS 본체 작성하기


 책에 기술된 대로 실습을 진행해보자. 먼저, 처음에 HLT만을 하는 아주 짧은 프로그램 코드를 SCiTE 편집기로 작성해본다. 만드는 파일이름은 haribote.nas로 하여 기존의 실습하던 폴더 harib00d 폴더 안에 저장한다.


 haribote.nas 파일을 만들었으면 이제 어셈블할 차례다. 아래와 같이 !cons 콘솔창을 열어, haribote.sys 파일로 어셈블하는 명령어를 치면, 폴더 안에 성공적으로 haribote.sys 파일이 만들어진다.


다음으로 haribote.sys 파일을 디스크 이미지 haribote.img로 저장할 차례다. 디스크 이미지로 저장하는 작업은 다음과 같다.


  • 어떤 디스크 이미지 파일을 make install을 하여 디스크에 쓴다.
  • 디스크를 Windows로 열어서 haribote.sys를 평소와 같이 저장한다.
  • 디스크를 툴 등을 사용하여 디스크 이미지로 백업한다.

 자세한 설명은 뒤에 나온다. 그저 처음에도 디스크 이미지를 건드리고, 맨 나중 작업에서도 디스크 이미지를 쓴다는 걸 주목하면 된다. 다음으로 이 작업을 실제 디스크 혹은 Windows의 힘을 빌리지 않고, 디스크 이미지와 대상 파일만으로 만들어지면 편리하다는 사실 또한 알고 가야 한다. 이것이 디스크 이미지 저장의 의미다.

 이러한 작업을 해주는 툴은 많이 있지만, 여전히 이 책에서는 edimg.exe를 사용한다. (z_tools폴더에 들어 있음) 이제 다음으로 projecets/03_dayharib00e폴더를 살펴보자. 방금 설명한 haribote.nas 파일이 있는데, .sys파일은 따로 없으므로 위의 콘솔창에 친 명령과 동일한 명령을 줘서 현재 파일에도 haribote.sys파일을 만들어준다. 그 후 콘솔창에 make img 명령으로 haribote.img 파일을 만들어주자.
 


 이렇게 성공적으로 haribote.img 파일을 만들었다면, 이제는 이 파일을 들여다볼 차례다. 1일차에 우리가 받았던 바이너리 편집기로 이미지 파일을 연 후에, 책에 나와있는대로 0x002600 부근으로 쭈욱 내려보면 이렇게 내용이 나온다.


 haribote.sys라는 파일명이 디스크에 어떻게 기록되는지를 직접 눈으로 볼 수 있다. 내용을 좀 더 살펴보면 0x004200'F4 EB FD'를 발견할 수 있다. 이것이 바로 haribote.sys의 내용이다.



 어떻게 haribote.sys의 내용임을 알 수 있을까? 간단하다. haribote.sys를 바이너리 에디터로 열어보면 된다. 열어보면 이와 똑같이 3바이트로 되어 있다. 이상의 내용을 요약하자면 이렇다.


  • 빈 상태의 디스크에 파일을 평소대로 저장하면

    (1) 파일명은 0x002600 이후에 들어가는 듯하다.
    (2) 파일의 내용은 0x004200 이후에 들어가는 듯하다.

 이제 다음에 해야할 작업은 비교적 간단하다. OS 본체를 haribote.sys라는 이름으로 만들어 디스크 이미지로 보존하고, 부트섹터에서 이 haribote.sys를 실행시키면 된다.




#6 부트섹터에서 OS 본체 실행시키기

 

 디스크 이미지 상에서 0x004200에 위치하는 haribote.sys의 코드를 실행시키기 위해서는 어떻게 하면 좋을까? 우리는 앞서 0x8000 ~ 0x81ff 에 해당하는 512바이트를 부트섹터의 내용을 넣기로 사전에 설계했으므로, (교재 79페이지, 현재 게시물의 #1 *버퍼어드레스 파트 참조) 현재 부트섹터의 맨 앞이 메모리의 0x8000번지에 오도록 디스크를 메모리에 읽어들인 상태다. 따라서 0x8000 + 0x4200 = 0xc200 으로 읽어들일 거라는 걸 알 수 있다.


 위에서 작성했던 haribote.nas 파일에 새롭게 반영될 내용들을 추가해보자. 프로그램의 시작이 0xc200이므로, ORG 0xc200을 먼저 추가한다. 

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

 

 그리고 ipl.nas next레이블 맨 마지막 부분에 JMP 0xc200을 추가해보자. 이러한 작업이 완료되어 있는 폴더 harib00f에 들어가 !cons 콘솔창을 열어 make run 명령을 실행시켜 보자. 일단 이전과는 다른 뭔가 특별한 일이 일어나지는 않았다. 따라서 몇 가지 추가작업을 진행해보도록 한다.





#7 OS 본체의 동작 확인하기

 

 haribote.nas를 다음과 같이 수정해본다. Windows와 같은 화면을 만들기 위해 화면 모드를 전환시키는 기능을 추가하는 작업이다.

/*************************************************
 ** day 03 haribote.nas in harib00g
*************************************************/

// 바로 전에 작성했던 haribote.nas에 화면모드 전환 코드를 추가했다.

; haribote-os
; TAB=4

		ORG		0xc200			; where this program will be loaded
		
		MOV AL, 0x13	; VGA graphics, 320x200x8bit color
		MOV AH, 0x00
		INT 0x10
fin:
		HLT
		JMP		fin


/*************************************************
 ** End Line
*************************************************/
 화면 모드의 전환은 비디오 BIOS의 AH = 0x00으로 할 수 있다. AT-BIOS 는 이렇게 구성되어 있다.

  • 비디오 모드

    AH = 0x00
    AL = 모드: (권장하지 않는 화면 모드는 생략되어 있다.)
        0x03: 16색 텍스트, 80x25
        0x12: VGA 그래픽스, 640x480x4bit 컬러, 독자 영역 액세스
        0x13: VGA 그래픽스, 320x200x8bit 컬러, 패드 픽셀
        0x6a: 확장 VGA 그래픽스, 800x600x4bit 컬러, 독자 영역 액세스 (비디오카드에 의해서는 서포트되지 않는다.)

  • 리턴 값: 없음

 

 우선 화면 모드로 0x13을 선택했다. 8bit 컬러라는 것은 256색을 사용할 수 있다는 의미다. 화면 모드 전환이 잘 되면 화면은 새까만 색으로 될 것이다. 화면이 까맣게 되는지 여부로 실행이 잘 되었는지 확인할 수 있다. 그래픽 모드가 되므로 마우스 커서도 없어진다.


 우선 화면 모드로 0x13을 선택했다. 8bit 컬러라는 것은 256색을 사용할 수 있다는 의미다. 화면 모드 전환이 잘 되면 화면은 새까만 색으로 될 것이다. 화면이 까맣게 되는지 여부로 실행이 잘 되었는지 확인할 수 있다. 그래픽 모드가 되므로 마우스 커서도 없어진다.

 기존의 ipl.nas의 파일명은 ipl10.nas로 변경하였는데, 10실린더만큼만 읽어들일 것이라는 의미 때문에 그렇다. 기존의 ipl.nas에 몇 가지 소스코드도 추가 됐는데, 'JMP 0xc200' 앞에 메모리의 0x0ff0번지에 CYLS값을 써 넣는 명령이 추가됐다.

MOV [0x0ff0], CH




#8 32비트 모드 준비

 

 지금부터는 어셈블러 중심의 개발이 끝나고, C언어 중심으로 개발이 시작된다. 이 책에서 다룰 C컴파일러는 32비트 모드용의 기계어밖에는 생성할 수 없기 때문에, 32비트 모드를 이용하게 됐다.

 32비트 모드는 CPU 모드 중의 하나로, PC의 CPU에는 16비트 모드와 32비트 모드가 있다. 부팅 시 16비트 모드로 되어 있을 경우, AX라든가 CX라고 하는 16비트 레지스터는 매우 사용하기 편하겠지만, 32비트 레지스터, 즉 EAXECX는 사용하기 어렵다. 또한 16비트 모드와 32비트 모드에서는 기계어의 명령 번호가 변경되고, 같은 기계어라도 해석 방법이 달라 동작이 달라지므로, 16비트 모드용의 기계어는 32비트 모드에서는 동작하지 않는다. 그 반대도 마찬가지다.

 32비트 모드에서는 메모리도 1MB를 초과해서 사용할 수 있다. (위에 있는 *버퍼어드레스 파트 ES x 16 + BX 메모리 계산법을 그대로 적용하면 1MB가 나온다.) 또한 이상한 기계어를 만나도 오동작하지 않도록 하는 보호 기능도 사용할 수 있다. 장점이 많으므로 32비트 모드를 채택했다.

 그러나 32비트 모드에서는 BIOS를 이용할 수 없다. BIOS는 16비트용의 기계어로 쓰여 있기 때문이다. 그래서 BIOS를 사용해서 해 보고 싶은 일이 있다면, 일단 전부 해 보기로 한다. 32비트 모드가 된 후에는 할 수 없으므로, 그 전에 해보자는 의미다. (32비트 모드가 된 후 다시 16비트 모드로 돌아갈 순 있지만, 번잡하므로 이 책에서는 다루지 않는다고 한다.)

 그럼 BIOS를 사용해보자. 화면 모드 설정은 위에서 이미 했기때문에 키보드의 상태를 BIOS가 알려 주는 작업을 해보려 한다. (키보드의 상태의 예: NumLock이 ON인가 OFF인가)

 수정된 haribote.nas의 소스코드다. 프로그램을 보면 알겠지만, 화면 모드를 설정한 후 화면 모드에 관한 정보를 메모리에 메모해 두려고 했다.
/*************************************************
 ** day 03 haribote.nas in harib00h
*************************************************/

// 바로 전에 작성했던 haribote.nas에 화면모드 정보를 추가했다.

; haribote-os
; TAB=4

; BOOT_INFO 관계
CYLS	EQU		0x0ff0			; 부트섹터가 설정한다.
LEDS	EQU		0x0ff1
VMODE	EQU		0x0ff2			; 색 수에 관한 정보. 몇 비트 컬러인가?
SCRNX	EQU		0x0ff4			; 해상도 X(screen X)
SCRNY	EQU		0x0ff6			; 해상도 Y(screen Y)
VRAM	EQU		0x0ff8			; 그래픽 버퍼의 개시 번지

		ORG		0xc200		; 이 프로그램이 어디에 로딩되는가?

		MOV		AL, 0x13	; VGA 그래픽스. 320x200x8bit 컬러
		MOV		AH,0x00
		INT		0x10
		MOV		BYTE [VMODE], 8	; 화면 모드를 메모한다.
		MOV		WORD [SCRNX],320
		MOV		WORD [SCRNY],200
		MOV		DWORD [VRAM],0x000a0000

; 키보드의 LED 상태를 BIOS가 알려준다.

		MOV		AH,0x02
		INT		0x16 		; keyboard BIOS
		MOV		[LEDS],AL

fin:
		HLT
		JMP		fin



/*************************************************
 ** End Line
*************************************************/
 이것은 나중에 여러 가지 화면 모드를 이용할 경우를 대비하기 위한 것으로, 그때 어떤 모드로 설정했는지에 대한 정보가 필요해 질 것이라고 생각하기 때문이다. 일단 이러한 부팅시의 정보를 BOOT_INFO라고 부르기로 하자. INFO는 information의 약어이다.

 [VRAM]에 0xa0000을 넣어 두었지만, PC의 세계에서 VRAM이란 것은 비디오 램이라는 뜻으로, 'video RAM'이라고 쓰고 '화면용 메모리'라는 뜻이다. 이 메모리는 물론 데이터를 기억하는 것도 가능하다. 그러나 VRAM은 보통 메모리 이상의 존재로, 각각의 번지가 화면상의 화소에 대응하고 있고, 이것을 이용함으로써 화면에 그림을 표현할 수 있다.

 VRAM은 메모리 맵 안의 여러 군데에 존재한다. 화면 모드에 따라 화소 수가 다르기 때문이다. 화면 모드 a일 때는 이 VRAM을 사용하고 화면 모드 b일 때는 저 VRAM을 사용하는 식으로 사용되는 곳이 달라진다. 그래서 BOOT_INFO에 어느 VRAM을 사용할지를 메모해 두도록 하겠다.

 이번에는 그 값이 0xa0000이 된다. 이 정보는 AT-BIOS 페이지에서 가져온 것이다. INT 0x10 설명의 맨 뒷부분에, 이 화면 모드에서 'VRAM0xa0000~0xaffff의 64KB'라고 써 있다.

 그 외에도 화면의 화소 수나 컬러 수, BIOS가 알려 주는 키보드의 상태를 메모리에 저장하고 있다. 저장하고 있는 곳은 0x0ff0번지 주변이지만, 메모리 맵에 의하면 이 근처도 아무에게도 사용되지 않으므로 문제 없다.




#9 C언어 도입

 

 32비트 모드로 바꾸어 C언어를 동작시키는 작업을 진행한다. harib00i 폴더 안의 haribote.sys를 살펴보자. 이 파일은 어셈블러로 쓴 선두 부분과 C언어로 쓴 후반 부분으로 구성되어 있으므로, 지금까지의 haribote.nasasmhead.nas라는 이름으로 바꿨다. 그리고 C언어로 쓴 부분을 호출하기 위해 100행 정도가 추가 됐다.

 추가 100행에 대한 해석은 뒷부분에서 다루기로 하고, C언어 부분인 bootpack.c를 살펴보자. OS를 부팅하기 위한 여러가지 처리를 이 파일에 작성하고, 하나의 패키지로 만들 예정이다. 내용은 매우 짧다.
 


 1행은 함수를 작성하는 부분으로, 함수 이름은 HariMain이고, 어떤 데이터도 필요 없고(void), 어떤 데이터도 리턴하지 않는다는 의미다. 그리고 { }로 둘러싸인 범위가 함수의 내용이다.
 
 C언어에서 말하는 함수란 한 덩어리의 프로그램을 지칭하는 것으로, 수학에서 말하는 함수와 같이 변수 x를 받아 y를 돌려주는 일 등을 한다. 이번의 경우에는 변수가 없고 아무것도 리턴하지 않으므로 수학의 함수와는 다르지만, C언어에서는 이것 또한 함수라고 정의한다.

 goto명령의 경우, 어셈블러에서의 JMP와 동일하고 실제로 JMP 명령으로 해석된다. /* 와 */ 로 감싸진 부분은 주석으로, 여기 써져 있는대로 C언어에선 HLT를 사용할 수 없다. DB에 해당하는 명령이 없으므로, DB로 HLT의 기계어를 사용할 수가 없다.

 이제 이 bootpack.c를 어떻게 기계어로 만드냐는 문제가 남아 있다. 기계어로 바꾸는 순서는 아래와 같다.

  • 먼저, cc1.exe를 사용해서 bootpack.c로부터 bootpack.gas를 만든다.
  • 다음에, gas2nask.exe를 사용해서 bootpack.gas로부터 bootpack.nas를 만든다.
  • 그리고, nask.exe를 사용하여 bootpack.nas로부터 bootpack.obj를 만든다.
  • 또한, obj2bim.exe를 사용하여 bootpack.obj로부터 bootpack.bim을 만든다.
  • 마지막으로, bim2hrb.exe를 사용하여 bootpack.bim으로부터 bootpack.hrb를 만든다.
  • 이것으로 기계어가 되었으므로 copy 명령으로 asmhead.bin과 bootpack.hrb를 단순하게 붙여서
     haribote.sys를 만든다.

 많은 종류의 파일이 나왔는데, 소개하자면 이렇다. cc1C컴파일러로 C언어 프로그램에서 어셈블러의 소스 프로그램을 만들어준다. 그러나 이 C컴파일러는 gcc라는 컴파일러를 책의 필자가 개조한 것이며, gcc라는 컴파일러gas라는 이름의 어셈블러를 전제로 하고 있으므로 gas용의 소스프로그램을 출력한다. 이것은 nask로는 번역할 수 없다.

 그래서 nask로 변형할 수 있는 문법으로 변환하는 작업이 필요하다. 그것이 gas2nask다. 여기서 2는 단순히 영어의 'to'의 의미다. nas파일이 되면 작업이 훨씬 편해지며, nask로 어셈블하면 기계어가 될 것이다. nask를 사용하여 obj파일을 만든다. 오브젝트라는 것은 목적파일을 의미한다.

 우리들은 기계어를 얻기 위해 C언어 프로그램을 작성하는 것 뿐이고, 이것을 기계어로 번역한 것을 목적 파일(오브젝트 파일)이라고 부른다.

 오브젝트파일이라는 것은 다른 오브젝트 파일과 붙이기(링크라고 한다.) 위한 특별한 기계어 파일이다. 붙인다는 건 결국, C언어만으로는 프로그램 전체를 쓸 수 없다는 말이다. (위에서 봤듯이 C언어는 HLT를 쓸 수 없는 등의 제약이 존재한다.) 그래서 일부는 어셈블러로 써야만 한다.

/* 지금까지는 한 개의 소스 프로그램으로부터, 당연하다는 듯이, 기계어 파일을 직접 만들었으므로 오브젝트 파일이나 링크에 관한 것은 신경쓰지 않았지만 이제부터는 신경써야 한다. */

 그래서 오브젝트 파일은 다른 오브젝트파일과 링크시키기 위한 정보를 여분으로 가지고 있고, 단독으로는 온전한 기계어가 되지 못한다. 이것을 하나의 기계어로 만들기 위해서는 필요한 오브젝트 파일을 전부 링크시켜 주면 된다. 그것을 하는 것이 obj2bim이다. bim파일은 책의 저자가 고안한 것으로, 'binary image file'이라는 의미다. 2진수 이미지 파일이라는 의미다.

 이미지 파일이란 본래의 상태가 아닌 형식을 의미한다. 화상 통화를 떠올려 보면 이해가 쉬운데, 본인이 지금 여자친구와 화상통화를 하고 있다고 생각해보자. 화면에 나오는 여자친구는 만질 수가 없다. 실체가 아니기 때문에. 하지만 그럼에도 여자친구는 맞다. 즐겁게 대화할 수도 사랑을 표현할 수도 있기 때문에. 이미지 파일이란 그런 것이다.

 그래서 bim 파일은 '본래의 상태가 아닌 가짜 형식'이므로 완성품이 아니다. 이것은 전부 붙여서 기계어로 정리한 것 뿐이고 실제로 사용하기 위해서는 각각의 OS가 필요로 하는 형식에 맞춰 가공해야만 한다. 가공이란 식별용 헤더를 붙이거나 압축하는 것을 의미한다.

 'WindowsLinux로 C언어 프로그램을 만든 적은 몇 번 있지만 이렇게 많은 툴을 사용한 적은 없었고, 이렇게 많은 종류의 중간 파일을 다루지 않아도 됐었는데, 여긴 왜 이렇게 작업이 많은 걸까?'라는 의문을 가질 수 있다. 한마디로 말하면, 컴파일러가 잘 안되어 있기 때문이다.

 이 책에서 소개하는 컴파일러는 여러 가지 OS용으로 바꾸어 사용할 것을 전제로 하고 있으므로 조금 번잡하더라도 여러 가지 중간 파일을 출력하도록 하고 있다. 그 덕분에 이 컴파일러 만으로도 다양한 OS에 맞는 실행파일 또한 만들어 낼 수 있다.




#10 HLT 해 보기 (harib00j)

 

 HLT를 구현하기 위해 naskfunc.nas라는 프로그램을 만들었다. 이것은 어셈블러로 함수를 만든 것이다.
/*************************************************
 ** day 03 naskfunc.nas in harib00j
*************************************************/

; naskfunc
; TAB=4

[FORMAT "WCOFF"]				; 오브젝트 파일을 만드는 모드
[BITS 32]					; 32비트 모드용의 기계어를 만든다.


; 오브젝트 파일을 위한 정보

[FILE "naskfunc.nas"]				; 소스 파일명 정보

		GLOBAL	_io_hlt			; 이 프로그램에 포함된 함수명


; 이하는 실제의 함수

[SECTION .text]				; 오브젝트 파일에서는 이것을 쓴 후에 프로그램을 쓴다.

_io_hlt:	; void io_hlt(void);
		HLT
		RET


/*************************************************
 ** End Line
*************************************************/
 함수 명은 io_hlt로 였다. CPU 명령 중에서 HLT명령은 I/O 명령이라는 타입에 속하므로 이름을 이렇게 지었다. MOV는 전송 명령, ADD 등은 연산 명령이라 부른다.

 나중에 본체의 bootpack.obj와 링크시키기 위해 오브젝트 파일로 번역한다. 그래서 출력 포맷을 WCOFF라는 모드가 되도록 설정한다. 또, 32비트용의 기계어로 만들고 싶기 때문에 그것도 설정한다.

 nask의 오브젝트 파일 모드에서는 소스 파일명 정보를 설정해야만 한다. 그 다음에 이 프로그램 안에 포함된 함수명을 알려준다. 이때, 함수명 앞에 '_'를 붙여주게 되어 있다. C언어의 함수와 연계를 위해 쓰는 방법이다. 연계하고 싶은 함수명은 GLOBAL 명령으로 선언해둔다.

 그 다음에는 실제 함수를 쓴다. GLOBAL로 써둔 함수명과 같은 이름의 레이블을 만들어서, 거기서부터 쓰면 된다. 이번에 새로 나온 RET 명령은 C언어에서 말하는 return; 으로, '함수의 처리는 이것으로 끝났으니 돌아가라'라는 의미다. 이 함수를 C언어에서 사용하는 방법은 매우 간단하다. bootpack.c를 살펴보면 친절한 주석과 함께 코드가 나와 있다.
/*************************************************
 ** day 03 bootpack.c in harib00j
*************************************************/

/* 다른 파일로 만든 함수가 있으면 C 컴파일러에게 알려줌 */

void io_hlt(void);

/* 함수 선언인데, { } 없이 갑자기 ;을 쓰면
    다른 파일에 있다는 것을 나타낸다. */

void HariMain(void)
{

fin:
	io_hlt(); /* 이것으로 naskfunc.nas의 io_hlt가 실행된다. */
	goto fin;

}

/*************************************************
 ** End Line
*************************************************/
 소스 파일이 늘어났으므로 Makefile을 추가하고, 그것으로 'Make run'을 한다. 겉모습은 역시 새까만 창이지만, 잘 되고 있다. 아래는 Makefile의 내용이다.
/*************************************************
 ** day 03 Makefile in harib00j
*************************************************/

TOOLPATH = ../z_tools/
INCPATH  = ../z_tools/haribote/

MAKE     = $(TOOLPATH)make.exe -r
NASK     = $(TOOLPATH)nask.exe
CC1      = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet
GAS2NASK = $(TOOLPATH)gas2nask.exe -a
OBJ2BIM  = $(TOOLPATH)obj2bim.exe
BIM2HRB  = $(TOOLPATH)bim2hrb.exe
RULEFILE = $(TOOLPATH)haribote/haribote.rul
EDIMG    = $(TOOLPATH)edimg.exe
IMGTOL   = $(TOOLPATH)imgtol.com
COPY     = copy
DEL      = del

# 디폴트

default :
	$(MAKE) img

# 파일생성 규칙

ipl10.bin : ipl10.nas Makefile
	$(NASK) ipl10.nas ipl10.bin ipl10.lst

asmhead.bin : asmhead.nas Makefile
	$(NASK) asmhead.nas asmhead.bin asmhead.lst

bootpack.gas : bootpack.c Makefile
	$(CC1) -o bootpack.gas bootpack.c

bootpack.nas : bootpack.gas Makefile
	$(GAS2NASK) bootpack.gas bootpack.nas

bootpack.obj : bootpack.nas Makefile
	$(NASK) bootpack.nas bootpack.obj bootpack.lst

naskfunc.obj : naskfunc.nas Makefile
	$(NASK) naskfunc.nas naskfunc.obj naskfunc.lst

bootpack.bim : bootpack.obj naskfunc.obj Makefile
	$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
		bootpack.obj naskfunc.obj
# 3MB+64KB=3136KB

bootpack.hrb : bootpack.bim Makefile
	$(BIM2HRB) bootpack.bim bootpack.hrb 0

haribote.sys : asmhead.bin bootpack.hrb Makefile
	copy /B asmhead.bin+bootpack.hrb haribote.sys

haribote.img : ipl10.bin haribote.sys Makefile
	$(EDIMG)   imgin:../z_tools/fdimg0at.tek \
		wbinimg src:ipl10.bin len:512 from:0 to:0 \
		copy from:haribote.sys to:@: \
		imgout:haribote.img

# 커맨드

img :
	$(MAKE) haribote.img

run :
	$(MAKE) img
	$(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin
	$(MAKE) -C ../z_tools/qemu

install :
	$(MAKE) img
	$(IMGTOL) w a: haribote.img

clean :
	-$(DEL) *.bin
	-$(DEL) *.lst
	-$(DEL) *.gas
	-$(DEL) *.obj
	-$(DEL) bootpack.nas
	-$(DEL) bootpack.map
	-$(DEL) bootpack.bim
	-$(DEL) bootpack.hrb
	-$(DEL) haribote.sys

src_only :
	$(MAKE) clean
	-$(DEL) haribote.img


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




#11 후기

 

 포스팅을 시작할 때만 해도, 학습자들이 어려워 하거나 생소한 내용들을 위주로 다루고자 했다. 하지만 나 조차도 처음 접해보는 내용, 그리고 또 중요하다고 생각하는 내용들로만 가득 차 있었기에... 포스팅의 내용이 밑도 끝도 없이 길어져 버렸다. 포스팅의 내용을 보면, 교재 내용 + 나의 주석 (사람들이 어려워 할만한) 으로 되어 있다.


 아마 앞으로도 포스팅의 방향성은 위에서 말한 것과 크게 다르진 않을 것이다. 공부 내용을 정리하고, 설명이 미흡하다고 생각되는 부분에 나만의 주석을 달아 추가하는 것. 으로 될 것이다. 30일을 채우려면 아직 멀었지만, 한 권을 떼고 어서 새로운 책을 접하고 싶다.


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