짱해커가 되어보자

[#2] Makefile제작해보자 본문

프로젝트/OS 개발!

[#2] Makefile제작해보자

Spadework 2017. 7. 20. 23:52

OS 개발 프로젝트 #2



저번 강의에 이어서 글을 적어보겠다.


시작하기에 앞서, 저번에 봤던 어셈블리어 코드에 대한 설명과 새로운 어셈블리어 개념을 알아보고 가겠다.

(어셈블리어 공부하는 데에 많은 시간을 투자할 것 같다.)





; hello-os
; TAB=4

		ORG		0x7c00			; 이 프로그램이 어디에 read되는가

; 이하는 표준적인 FAT12 포맷 플로피 디스크를 위한 기술

		JMP		entry
		DB		0x90
		DB		"HELLOIPL"		; boot sector이름을 자유롭게 써도 좋다(8바이트)
		DW		512			; 1섹터 크기(512로 해야 함)
		DB		1			; 클러스터 크기(1섹터로 해야 함)
		DW		1			; FAT가 어디에서 시작될까(보통 1섹터째부터)
		DB		2			; FAT 개수(2로 해야 함)
		DW		224			; 루트 디렉토리 영역의 크기(보통 224엔트리로 해야 한다)
		DW		2880			; 드라이브 크기(2880섹터로 해야 함)
		DB		0xf0			; 미디어 타입(0xf0로 해야 함)
		DW		9			; FAT영역 길이(9섹터로 해야 함)
		DW		18			; 1트럭에 몇 개의 섹터가 있을까(18로 해야 함)
		DW		2			; 헤드 수(2로 해야 함)
		DD		0			; 파티션을 사용하지 않기 때문에 여기는 반드시 0
		DD		2880			; 드라이브 크기를 한번 더 write
		DB		0,0,0x29		; 잘 모르지만 이 값으로 해 두면 좋은 것 같다
		DD		0xffffffff		; 아마, 볼륨 시리얼 번호
		DB		"HELLO-OS   "		; 디스크 이름(11바이트)
		DB		"FAT12   "		; 포맷 이름(8바이트)
		RESB	18				; 우선 18바이트를 비어 둔다

; 프로그램 본체

entry:
		MOV		AX, 0			; 레지스터 초기화
		MOV		SS,AX
		MOV		SP,0x7c00
		MOV		DS,AX
		MOV		ES,AX

		MOV		SI,msg
putloop:
		MOV		AL,[SI]
		ADD		SI, 1			; SI에 1을 더한다
		CMP		AL,0
		JE		fin
		MOV		AH, 0x0e		; 한 글자 표시 Function
		MOV		BX, 15			; 칼라 코드
		INT		0x10			; 비디오 BIOS 호출
		JMP		putloop
fin:
		HLT					; 무엇인가 있을 때까지 CPU를 정지시킨다
		JMP		fin			; Endless Loop

msg:
		DB		0x0a, 0x0a		; 개행을 2개
		DB		"hello, world"
		DB		0x0a			; 개행
		DB		0

		RESB	0x7dfe-$			; 0x7dfe까지를 0x00로 채우는 명령

		DB		0x55, 0xaa

; 이하는 boot sector이외의 부분을 기술

		DB		0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
		RESB	4600
		DB		0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
		RESB	1469432


;

어셈블리어에서의 주석(실제로 컴파일 안 되는 코드)

:

함수의 시작점을 나타냄

[ ]

메모리를 의미함(나중에 설명)

ORG

Origin의 약자로, 실행 시에 PC의 메모리 내 어디로 로딩되는지를 나타냄

JMP

jump의 의미로, 메모리로 이동시키는 역할이다

MOV

어셈블리어에서 가장 많이 사용하는 명령어로, 이동시키는 것이 아니라 '대입'의 의미이다. (Move의 약자이다)

ADD

더하는 명령어이다

CMP

두 값이 같은지 비교(CoMPare)

JE

조건이 성립할 때 점프(Jump if Equal)

INT

간단히 말해서, 함수 호출 명령의 일종(interrupt, 방해하다의 의미)

HLT

CPU를 정지(대기)시키는 명령어


위의 것들이 이번에 알아볼 명령어이며, 추가로 레지스터에 대해 설명하겠다.






처음엔 레지스터와 메모리 개념을 설명하고, 넘어가겠다.


일단 레지스터는, 위의 코드에서 봤을 때


MOV	AX, 0


'AX' 인데, 앞의 'MOV'의 역할은 대입이다.

그러면 'AX'안에 0을 넣는다는 소리인데, 그렇게 보면 AX는 변수의 역할을 해준다.

그러나, 일반적인 변수와는 다르다. 어떤 점이 다르냐면, 일반 변수는 '메모리'에 저장되지만, 레지스터는 CPU 내부에 존재한다.


이가 무엇을 의미하는지는 컴퓨터의 구조로 보면 알 수 있다.

코딩한다고 가정할 때, 메모리에 변수를 넣고 사용하면, CPU가 실행할 때 메모리에서 가져와서 사용하게 된다.

이를 CPU의 측면에서 보면, CPU는 고속연산을 하는 중에 메모리의 변수를 가져와야 하는데, 메모리와의 거리가 몇 센티미터가 떨어져 있다.

우리가 보기엔 얼마 안 떨어져 있지만, 엄청나게 빠른 연산을 하는 CPU에서는 이를 기다리는 시간이 매우 길게 느껴진다. 그래서 상대적으로 느리다고 볼 수 있다.


하지만 레지스터 같은 경우, CPU의 내부에 존재하므로 빠른 속도로 참고할 수가 있다. 

컴퓨터 프로그램이 레지스터만 사용한다면, 매우 빨라지지 않을까?

이렇게 생각할 수 있는데, 레지스터만 사용한다면, 매우 빨라질 수 있는 것이 사실이다.

그러나 레지스터의 개수는 한정적이기 때문에, 레지스터로만 개발하기엔 무리가 많을 것이다.(64비트는 16개) 


서론이 길었는데, 다시 돌아와서 범용 레지스터의 종류에 대해 설명해보겠다.


범용 레지스터 명 용도
AX 산술 연산 수행 시의 Accumulator (누적 연산기라는 의미)
BX

Data Address 지정 시 Data Pointer로 사용 (base: 기초, 기점이라는 의미)

CX Loop 혹은 문자열의 Index Counter (counter: 수를 세는 기계라는 의미)
DX

I/O Address 지정 시 산술연산을 보조 (data: 데이터라는 의미)

SI

문자열 작업 진행 시 원본 문자열의 Index (source index: 읽기)

DI

문자열 작업 진행 시 목적 문자열의 Index (destination index: 쓰기)

SP

Stack Pointer : 스택용 포인터

BP

Stack의 Data 접근 시 Data Pointer (base pointer: 베이스용 포인터)


이것들은 모두 16비트 레지스터로, 16자리의 2진수를 기억할 수 있다.

이것들의 이름은 일반적으로 '에이엑스 레지스터', '에스아이 레지스터' 등과 같이 알파벳 그대로 읽게 되며, 뒤에 붙은 X는 확장(extend)의 의미로 사용된다.(당시에는 대단한 확장이었나 보다)

그리고 각자의 목적에 맞게 사용하면, 프로그램을 간결하게 쓸 수 있다.

AX는 연산에, BX는 메모리 번지 계산의 기준 주소로, CX는 회수를 세는 데 사용하도록 편리하게 되어 있다.

AX, BX, CX, DX 레지스터는 두 개로 나눌 수가 있다.


무슨 말인가 하면, AX 레지스터가 16비트인데, 이를 8비트 씩 쪼개서, 앞의 0부터 7비트까지를 AL, 8비트부터 15비트까지, 남은 8비트 부분을 AH라 할 수 있다. 공간이 늘어난 것이 아니고, 위와 같이 하나의 레지스터를 쪼개서 사용할 수 있다.

AX뿐만 아니라, BX, CX, DX 레지스터도 쪼개서 사용할 수 있으며, 여기서 L과 H는, Low와 High의 약자이다.


그리고, 나머지 범용 레지스터인, SI, DI, SP, BP는 나눌 수 없다.


그러면, 32비트와 64비트의 레지스터는 없는지 궁금할 수가 있다.

32비트 같은 경우, 위의 레지스터 명 앞에 'E'가 또다시 붙는다.(ex. EAX)

이 'E'역시 확장(extend)인데, 이렇게 두 개를 쓴 것을 보면, 엄청난 확장이었던 것 같다.

EAX에서도 AX처럼 쪼개서 사용하고 싶을 수가 있는데, 여기서 하위 16비트가 AX이고, 상위 16비트는 이름이 없다.

상위 16비트를 사용하려면, 하위의 16비트를 밀어내야 한다.

그리고 64비트는, 'E' 대신, 'R'이 붙는다. 책에서는 64비트 레지스터가 사용되는 모드는 사용하지 않으므로, 생략하겠다.



이번에는, 세그먼트 레지스터에 대해 알아보겠다.


범용 레지스터 명 용도
AX 산술 연산 수행 시의 Accumulator
BX Data Address 지정 시 Data Pointer로 사용
CX Loop 혹은 문자열의 Index Counter
DX I/O Address 지정 시 산술연산을 보조
SI 문자열 작업 진행 시 원본 문자열의 Index
DI 문자열 작업 진행 시 목적 문자열의 Index
SP Stack Pointer
BP Stack의 Data 접근 시 Data Pointer


위와 같은 것들이 있는데, 자세한 설명은 다음에 하겠다.


그리고, 메모리라는 말이 여러 번 나오는데, 이 메모리는 우리가 아는 그 메모리가 맞다.

책에서는 이 메모리를, '기억소자로 이루어진 대규모 아파트'라는 표현으로 서술하였는데, 이 메모리가 필요한 이유로는, 레지스터의 부족한 기억능력이 있다. 레지스터는 기억 소자가 적어, 부트 섹터도 실행할 수 없다.

그래서 이러한 메모리가 존재하는 것인데, 메모리는 기억소자가 많아 저장할 수 있는 용량이 크다. 이를 CPU가 사용하기 위해서, '어이, 5678의 메모리를 나에게 보내줘'라는 전기 신호를 보내게 된다. CPU가 메모리의 데이터를 읽고 쓸 때에는 이렇게 주고받는 동작은 반복하게 되는 것이다.(메모리는 CPU의 외부에 있기 때문에 레지스터보다 상대적으로 느리다)


그리고 메모리의 역할은 이것만 있는 것이 아니다.

원래 프로그램 자체도 메모리의 어딘가에 반드시 들어가 있어야 한다. 이를 반드시 메모리에 넣는다는 규칙이 있다. 이는 CPU가 기계어를 실행할 때 메모리로부터 프로그램을 1명령씩 읽어 차례대로 실행하기 때문이다. 


이것도 나중에 필요에 따라 추가로 설명하겠다.






그러면 이제 어셈블리어 명령어를 알아보도록 하겠다. 


먼저 ';' 같은 경우, 개발자가 메모를 달기 위해서나, 코드를 컴파일되지 않게 만들어서 테스트를 해보는 용도로 사용되기도 한다.


':' 는 함수, 어셈블리어에서는 프로시저라 부르는 것을 만들 때 사용된다.

어셈블리어에서는 프로시저의 이름을 지어주고, ':'를 붙여 프로시저의 공간을 구분한 다음, 프로시저 내부의 코드는 탭으로 구분하게 된다(파이썬과 같다). 이해가 안 된다면, 맨 처음에 나온 코드를 보면 될 것 같다.



'[ ]'는 '메모리'를 의미한다. 무슨 말인가 하면, 예를들어 '123'은 그냥 숫자지만, '[123]'을 하게 되면 이는 123번지를 가리키게 된다. 그리고 '[ ]'의 앞에 BYTE, WORD, DWORD를 붙여서 사용하는데, 간단히 설명하게 되면, BYTE를 붙이면 8메모리 주소 하나만을 참고하며, WORD를 붙이면 그 메모리와 다음 메모리까지, DWORD는 그 메모리와 그 옆의 옆의 옆까지도 지정되는 것이다(Byte에 맞춰서 지정).


그리고 '[ ]'를 사용할 때, 정수를 사용하는 방법 외에도, 레지스터를 사용하는 방법도 된다.

그러나 사용할 수 있는 레지스터는 BX, BP, SI, DI로 한정된다. 이에 대한 이유로는, CPU가 그런 처리를 위한 회로를 가지고 있지 않기 때문이다. 그래서 주소를 참고할 때엔 위의 4개를 활용하면 된다.


그리고 MOV 배울 때, 나오겠지만 MOV는 비트 수가 같은 것끼리만 대입할 수밖에 없는 규칙이 있다

'메모리의 SI번지의 1바이트의 내용을 AL에 넣어라' 같은 경우

MOV Al, BYTE [SI]

이런 식으로 표현할 수 있다. 그리고 MOV의 규칙이 있어, 같은 것끼리만 대입이 가능하므로

MOV Al, [SI]

위와 같이 BYTE는 생략할 수가 있다.



'ORG'는 'origin'의 약자이다. 이는 기계어가 실행할 때, 메모리의 어디에 로딩되는지를 알려주기 위한 명령어이다.

(메모리에 왜 로딩하는가에 대한 내용은 메모리를 설명하는 부분에 잠깐 나온다)

ORG		0x7c00

이 코드를 설명하고 넘어가겠다.

여기서 왜 '0x7c00'으로 굳이 설정했는지가 궁금할 수 있다. 

프로그램을 실행할 때, 메모리에 저장된다고 말을 했었다. 그러므로, 'INT'명령 부분에 나오는 'BIOS' 같은 경우, 할당되어 있는 메모리가 있으며, 기타 프로그램도 있을 수 있다.

만약 우리가 이미 할당되어 있는 곳을 사용하려 하면, 오작동이 일어난다. 윈도우나 리눅스 같은 경우 OS가 알아서 해주지만, 만들려는 OS는 이 편의를 직접 제공해야 한다.


[필자가 작성한 메모리맵 : http://osguru.net/index.php/AT-MemoryMap]

*7-20 기준 웹 사이트를 찾을 수 없다



'JMP'는 'jump'의 약자로, 말 그대로 원하는 메모리로 이동하는 것이다.

근데 위의 코드를 봐보면, 

JMP	putloop

위와 같이, 프로시저(함수)로 점프하는 것을 볼 수 있다.

이는 프로그램을 실행할 때, 번역한 기계어가 메모리에 저장되므로, 'putloop'라는 프로시저도 메모리의 어딘가에 저장이 될 것이다.

그래서 'JMP'는 'putloop'의 시작 메모리를 찾아내어, 해당 메모리로 이동을 시켜준다.


'MOV'는 어셈블리어에서 많은 비중을 차지한다. 책의 저자는 'MOV를 알면, 어셈블리어의 반을 안 것이다'라는 말을 하며, 'MOV'의 중요성을 언급하였다. 'MOV'는 'move'의 약자이면서, 역할은 대입이다.

레지스터에 값을 대입할 수도 있고, 레지스터의 값을 다른 레지스터로 옮길 수도 있고, 메모리에 있는 데이터를 옮길 수도 있다. 이 'MOV'에 대해서는 우리가 여태까지 열심히 보았으므로, 더 설명하지는 않겠다.



'ADD'는 더한다는 의미라 보면 된다.

ADD	SI, 1

여기서는, 'SI = SI + 1'의 의미로 사용되었다.



'CMP'는 'compare'의 약자로, 두 값을 비교하게 된다.

예를들어, 'CMP a, 3'이라 놓으면, a가 3과 같은지를 비교하게 되는 것이다.



'JE'는 'jump if equal'의 약자로, 비교 명령에 의하여 점프할지 말지를 결정하게 된다.



'INT'는 'interrupt'로, 방해하다의 약자이다. 이는 소프트웨어의 인터럽트란 명령어지만, 설명하기엔 복잡하므로 지금은 함수 호출 명령의 일종이다는 것만 알고 넘어가겠다.


PC에서는 BIOS라는 프로그래밍이 있는데, 이것은 PC의 기관 상에 있는 ROM이라는 소자에 들어 있다. 이 BIOS라는 것은 OS 제작자가 자주 사용할 것 같은 프로그램을 PC 제조사가 미리 준비해둔 것이다. BIOS는 'basic input output system'의 약어로, 기본적인 입출력에 관한 시스템이 된다.

그래서 INT가 이러한 함수들을 호출하기 위해 사용하는 명령인 것이다. INT의 뒤에는 숫자를 쓰는데, 숫자에 따라 BIOS의 어떤 함수를 호출할지를 선택할 수 있다. 위의 코드에서는 '0x10'(16번)함수를 호출하는데, 이는 비디오카드 제어에 관한 함수이다.


[필자가 작성한 사용법 : http://osguru.net/index.php/AT-BIOS]

*7-20 기준 웹 사이트를 찾을 수 없다



'HLT'는 CPU를 정지시키는 명령어이다. 그러나 완전히 정지시키는 것은 아니고, 대기 상태로 만든다(외부 변화가 있으면 CPU는 계속해서 프로그램 실행). HLT이 없어도, JMP fin이 무한루프이기 때문에, HLT를 굳이 쓰지 않아도 된다.

하지만 책의 저자는 자원의 효율성 때문에, HLT를 사용해야 한다고 주장을 하였다.


여기까지가 오늘 나온 명령어에 대한 설명이다.



그러면, 이제 Makefile을 도입해보겠다.

먼저 이 Makefile을 배치파일인데, 여기서 배치파일은 '.bat'파일로, cmd에서 실행되는 명령어들을 정리한 것이라 보면 된다. 배치파일을 실행하면 그 안에 적혀있는 명령어들이 실행되는 것이다.


# 파일 생성 규칙

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


여기서 '#'은 주석의 역할이다.

그리고 'ipl.bin : ipl.nas Makefile'은 조건이며,

'만약 ipl.bin을 만들고 싶으면, 먼저 ipl.nas라는 파일과 Makefile이라는 파일이 갖추어 한다,' 라는 뜻이라 볼 수 있다.

그리고 조건이 맞으면, 밑의 명령어를 실행하게 된다.

*ipl.nas는 여태까지 코딩한 부트섹터의 뒷부분을 없앴으며, 이는 부트섹터만의 소스가 되므로 파일명을 ipl로 정했다.


그리고 '\'는 1행에 모든 내용이 들어가지 않아, 다음 행으로 이어진다는 뜻의 표시이다.


..\z_tools\make.exe %1 %2 %3 %4 %5 %6 %7 %8 %9


이런 식으로, Makefile을 실행할 수 있게 한다.

여기서 '%'은 인자를 받는다는 의미로 보면 된다. 뒤의 숫자는 순서이다.


이런 방법으로 필요한 내용을 작성한다.

# 디폴트 동작

default :
	../z_tools/make.exe img

# 파일 생성 규칙

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

# 커맨드

asm :
	../z_tools/make.exe -r ipl.bin

img :
	../z_tools/make.exe -r helloos.img

run :
	../z_tools/make.exe img
	copy helloos.img ..\z_tools\qemu\fdimage0.bin
	../z_tools/make.exe -C ../z_tools/qemu

install :
	../z_tools/make.exe img
	../z_tools/imgtol.com w a: helloos.img

clean :
	-del ipl.bin
	-del ipl.lst

src_only :
	../z_tools/make.exe clean
	-del helloos.img

이렇게, Make파일을 만들 수가 있다.

수정해야 하는 내용은 수정하고, 필요 없는 내용은 빼면 될 것이다.





여기까지가 이번 글의 내용이다.

어셈블리에 대해 알아보는 내용이 많아 힘들었던 것 같다.

다음 글까지 고생하면, 초반부의 힘든 부분은 넘어갈 것 같다.


이번 글에서는, 레지스터와 메모리 맵, BIOS를 알아두면 좋을 것 같다.

[32비트 돌입과 C언어 도입]

'프로젝트 > OS 개발!' 카테고리의 다른 글

[#1.5] 시작해보자  (0) 2017.07.17
[#1] 시작해보자  (0) 2017.07.17
[#0] 들어가기 전  (0) 2017.07.14
Comments