OS를 만듭시다. 어때요~ 참 쉽죠? (4)
OS. 영어로 풀어서 Operating System. 한자어로 번역해서 운영체제. 이것을 만든다고 하면 사람들은 굉장히 어렵게 생각한다. 리눅스나 윈도우같은 OS를 비교대상으로 본다면 OS를 만드는것은 정말 어렵고 힘들고, 개인이 만들기엔 어쩌면 불가능에 가까운 도전일지도 모른다. 하지만 OS의 기본 개념들은 대학교 학부과정에서 가르칠 정도로 이미 보편화 되어 있고 그 개념 자체들은 그다지 어렵지 않다. 개념 구현을 중심으로 동작하는 것 자체에 의미를 둔 OS를 만드는 것은 어쩌면 도전 해 볼만 한 가치가 있는 시도가 아닐까.
4. 인터럽트 핸들러 만들기
일반적으로 인터럽트가 발생해서 인터럽트 서비스루틴의 코드가 수행되고 나면 사람들은 당연히 인터럽트가 발생한 그 시점으로 되 돌아 오기를 기대 한다. 당연한것이다. 인터럽트라는 말 자체가 중간에 끼어든다는 뜻 아닌가. 중간에 끼어들었으니 일단 끼어든 녀석을 처리 해 주고 나면 원래 하던 일을 해야 한다.
그럴려면 일종의 컨택스트스위칭과 비슷한 작업을 해 주어야 한다. 물론 다중테스크의 컨텍스트를 전환해주는 완성된 컨텍스트스위칭이 아니라, 사용자테스크와 ISR(Interrupt Service Routine)사이의 컨텍스틑 전환해주는 1:1 전환이다. 그래서 아직은 별도의 자료구조를 사용하지 않고 스택만을 이용해 컨텍스트스위칭을 구현 할 수 있다.
여기서 집고 넘어가야 할 것이 있다. 바로 '''Context Switching''' 이 대체 뭐냐는 것이다. 알고 있는 사람도 있을 것이고 모르는 사람도 있을 것이다. 아는 사람은 한번 더 읽어보고 모르는 사람은 일단 대충 읽어보고 검색해서 보다 정확하고 자세히 설명한 자료를 보기 바란다. 이 글은 어디까지나 구현에 목적을 둔 글이기 때문에 세세한 개념에 대해서는 자세히 설명하지 않는다.
CPU에는 레지스터라는 것이 있다. CPU의 종류에 따라서 이름도 갯수도 다르지만 아무튼 레지스터라는 것은 있다. 레지스터없이 만들어진 CPU도 있겠지만 필자가 아는 한도내에서 CPU는 모두 레지스터라는 것을 가진다. 이전 강좌에서 설명했듯이 CPU가 하는 일은 메모리에 저장되어 있는 명령어와 값을 읽어가지고 와서 수행(계산)하는 일이 전부이다. 그럼 메모리에 저장되어 있는 값을 어디로 가져 오는 걸까? 바로 그곳이 레지스터이다.
CPU가 현재 수행중인 명령어가 존재하는 메모리 주소를 가르키고 있는 것을 PC(Program Counter)라고 부른다고 했었다. 그래서 브랜치 명령어를 사용하면 해당 주소로 PC의 값이 바뀌고 아니면 직접 '''mov pc, 주소''' 이런식으로 PC 값을 바꾸어서 프로그램 흐름을 점프 할 수 있다고도 하였다. 이 PC 가 바로 명령어의 주소를 가지고 있는 레지스터 이다. 또한 메모리 주소 어딘가에 있는 100 이라는 값과 또 다른 메모리 어딘가에 있는 200이라는 값을 더해서 300이라는 값을 만들기 위해서 CPU 는 메모리에서 100을 읽어다가 레지스터에 넣고 또 200을 읽어다가 다른 레지스터에 넣은다음 그 결과를 다시 레지스터에 넣는다. 즉 CPU는 레지스터에 있는 값밖에 취급할 수 없다. 뭔가 CPU로 값을 읽어올려면 일단 메모리의 값을 레지스터로 먼저 옮겨야 한다.
x86 계열 칩인 경우에는 EAX, EBX, ECX.. 뭐 이런식으로 범용레지스터가 존재하고 스택포인터 베이스포인터, PC 등등의 특수목적 레지스터가 존재 한다. 본 강좌에서 만드는 나빌눅스는 ARM칩 기반이므로 x86에서 뭐가 있는지는 논외로 하겠다. ARM 에서는 31개의 범용레지스터와 6개의 상태레지스터가 있다. 굉장히 많은 편이다. 하지만 31개의 범용레지스터를 전부 다 쓸수 있는 건 아니고, ARM 칩의 각 동작모드별로 r0~r15 까지의 16개의 레이블만 할당되어 있다.
http://www.segger.com/gif/jlink/arm_registers.gif
위 그림에서와 같이 ARM은 r0~r12 까지의 데이터처리용 레지스터를 가지고 sp(StackPointer) 와 lr(LinkRegister, Return Address를 저장함), pc(ProgramCounter) 를 각각 r13, r14, r15 레이블에 할당한다. 그리고 모드 전환시에 cpsr값을 해당모드의 spsr 에 복사 한다. cpsr 은 현재 ARM프로세서의 상태를 가지고 있는 레지스터 인데, 정확히 무슨역할을 하고 어떻게 사용하는 지는 역시 검색을 이용하면 좋은 자료들이 많다. 본 강좌에서는 필요한 부분만 그때그때 설명하도록 하겠다.
프로세서에서 동작하는 프로그램들은 OS건 응용프로그램이건 관계없이 스택을 사용한다. 메모리에 값을 넣을때 sp(r13)을 기준으로 해서 메모리주소가 증가하는 방향 아니면 메모리 주소가 감소하는 방향으로 sp를 바꾸면서 값을 메모리에 넣는다. 나빌눅스를 빌드하는데 사용하는 arm-linux-gcc는 로컬변수를 스택에 할당할때 메모리 주소가 감소하는 방향으로 sp를 변화 시킨다. 아래 와 같은 코드가 있다고 가정하자
void 어떤함수() { int a; int b; int c; }
그럼 이 함수 안에 있는 로컬변수 a,b,c 의 주소는 어떻게 할당될까? 어떤함수() 안으로 실행흐름이 진입했을때 sp의 값이 0xA0008010 이라면
a | 0xA0008010 |
b | 0xA000800C |
c | 0xA0008008 |
위와 같이 각 변수에 할당되는 주소의 값은 변수의 크기만큼 감소 하면서 할당된다. 변수의 위치는 저렇게 sp를 기준으로 주소가 감소하는 방향으로 자라면서 스택에 할당된다. 그러면 실제 변수를 사용할때는 어떻게 될까. 당연히 r0~r12까지의 범용레지스터를 사용한다. 우선 가장먼저 생각나는 함수의 매개변수 부터 순서대로 r0, r1, r2... 로 사용하고, 이후 나오는 로컬변수들은 매개변수로 사용된 다음 레이블 부터 사용된다.
void myfunc(int a, int b) { int c; int d; c = a + b; d = c -1; printf("%d, %d", c,d); } int main(void) { myfunc(3,4); return 0; }
위의 코드조각을 컴파일 해서 전 강좌에서 사용했던 arm-linux-objdump 로 역어셈블 해보면 아래와 같은 코드를 볼 수 있다. 참고로 일반 PC에서 리눅스 실행파일은 objdump 명령으로 역어셈블 해보면 x86 프로세서에 맞는 어셈블리 코드를 볼 수 있다.
a0009a94 <myfunc>: a0009a94: e0803001 add r3, r0, r1 a0009a98: e59f0008 ldr r0, [pc, #8] ; a0009aa8 <myfunc+0x14> a0009a9c: e2432001 sub r2, r3, #1 ; 0x1 a0009aa0: e1a01003 mov r1, r3 a0009aa4: eafffbfb b a0008a98 <printf> a0009aa8: a0009b50 andge r9, r0, r0, asr fp a0009aac <main>: a0009aac: e52de004 str lr, [sp, -#4]! a0009ab0: e3a00003 mov r0, #3 ; 0x3 a0009ab4: e3a01004 mov r1, #4 ; 0x4 a0009ab8: ebfffff5 bl a0009a94 <myfunc> a0009abc: e3a00000 mov r0, #0 ; 0x0 a0009ac0: e49df004 ldr pc, [sp], #4
myfunc() 함수는 두개의 매개변수를 가지고 있다. 위에서 설명했듯이 매개변수가 우선이고 그다음 로컬변수 순서로 r0 부터 레이블이 사용된다. 아래쪽에 보면 r0 에 3을, r1 에 4 를 넣는 부분이 보인다. 그리고 myfunc 함수를 호출한다. 즉
a0009ab0: e3a00003 mov r0, #3 ; 0x3 a0009ab4: e3a01004 mov r1, #4 ; 0x4 a0009ab8: ebfffff5 bl a0009a94
위 세줄은
myfunc(3,4);
이 한줄의 C 언어 명령을 어셈블리 명령으로 바꾼것이다. 그리고 다시 myfunc 의 어셈블리 코드를 보면 r0와 r1을 더해서 r3에 넣고, r3 에서 1을 빼서 r2 에 넣는다. 즉, 변수 c 가 r3 에, 변수 d 가 r2 로 사용 되는 것이다. 함수의 구조와 내용에 따라서 r0~r12까지 중 일부만 사용할 수도 있고 전부 다 사용할 수도 있다. 그리고 r13(sp)는 지금 동작중인 프로세스의 스택top 주소를 항상 유지하고 있다. r14(lr) 은 현재함수가 끝났을때 돌아가야 할 주소를 가지고 있다. ARM 은 프로세서 동작 모드가 변할때 cpsr 을 자동으로 해당 모드의 spsr 에 복사한다고 위에 설명하였다. 더불어 이전 모드의 pc 는 바뀌는 모드의 lr 에 자동 복사가 된다.
정리하면 동작모드가 바뀔때 스택에 r0 부터 r14까지를 스택에 백업하고, ISR에 들어갔다가 ISR을 나와서 다시 r0~r13은 그대로 해당레지스터에 복구 하고, r14는 r15로 복구하면 ISR에 진입하기 이전의 상태로 돌아 갈 수 있다. 비슷한 원리로 컨텍스트 스위칭도 구현된다. 컨텍스트 스위칭에 대해서는 이후에 설명할 것이니 프로세스의 상태 보존에 대한 위의 개념만 잘 이해 하고 있으면 된다.
드디어 나빌눅스의 소스를 고쳐 보자. entry.S 를 아래와 같이 고친다.
.globl _ram_entry _ram_entry: bl main b _ram_entry b Core_swiHandler b Core_irqHandler .global Core_swiHandler Core_swiHandler: stmfd sp!,{r0-r12,r14} mrs r1,spsr stmfd sp!,{r1} bl swiHandler ldmfd sp!,{r1} msr spsr_cxsf,r1 ldmfd sp!,{r0-r12,pc}^ .global Core_irqHandler Core_irqHandler: bl irqHandler
다른곳은 바뀐데 없고, Core_swiHandler 만 바뀌었다. 이 몇줄을 설명하기 위해 필자는 위에서 부터 역어셈블을 해가며 그렇게 타자를 쳐 댔던 것이다. stmfd 나 mrs, bl, ldmfd, msr 등의 명령은 인터넷에 널려 있는 ARM 아키텍쳐 레퍼런스 메뉴얼을 참고 하기 바란다. 본 강좌에서는 그냥 뭐하는 녀석이다 라고 언급만 할뿐 개별 어셈블리 명령에 대해서는 따로 서술하지 않겠다.
r0~r12 까지의 범용레지스터와 r14(lr) 레지스터를 스택에 저장한다. stmfd 명령의 인덱스로 sp 를 사용했기 때문에 14개의 해당 레지스터는 스택에 저장된다. 그리고 spsr을 r1에 임시로 저장한다음 그것도 스택에 저장한다. 그럼 스택에는 순서대로 r0~r12,lr,spsr 이 저장되어 있다. 다시 한번더 말하지만 Exception 이 발생하여 칩의 동작모드가 바뀌는 순간 pc 는 lr 로, cpsr 은 spsr 로 저장된다. 즉, 이 두 레지스터를 백업하는 일은 아주 중요하다. 그리고서 C언어로 작성된 ISR에 들어갔다가 나온다. 이후 나오는 명령은 백업한걸 복구하는 명령이다. 스택에 저장하였으므로 복구는 저장의 역순이다. 먼저 r1 에 처음 4바이트를 가져온다. 여기에는 spsr 이 들어 있다. 이것을 spsr에 cxsf 마스크를 줘서 복사 한다. spsr_cxsf 이 구문에 대해서 궁금한 독자들 역시 검색을 이용하시라, 아주 친절하게 잘 설명 되어 있다. 그리고서 14개의 word 를 순서대로 r0~r12까지에 복구하고, 마지막에 lr을 pc로 복구한다. 그럼 복구가 끝남과 동시에 이전 실행위치로 되돌아가 간다. 그러면서 알아서 spsr 은 cpsr로 복구 된다.
ISR 에 들어갔다가 다시 이전위치로 돌아올 수 있게 수정하였으므로 무한루프를 돌게 만들면 멈추지 않는 OS 라는 첫번째 목표에 도달 할 수 있을 것이다. navilnux.c 에 있는 main() 함수를 수정한다.
int main(void) { while(1){ __asm__("swi 77"); msleep(1000); } return 0; }
이전 강좌에 있던거에서 while(1) 무한루프와 msleep() 으로 약간의 간격을 주었을 뿐 똑같다. 빌드하고 이지부트를 이용해 이미지를 올린다음 실행(부팅) 해 보자
system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 system call 2684387336 : :
짜잔~ 무한루프가 잘 돈다. 그러나 아직 만족 스럽지 않다. 우리는 swi 77 이라고 분명히 소프트웨어 인터럽트 번호를 77이라고 주었다. 그런데 2684387336 라는 얼토당토 않은 숫자가 찍힌다. ISR 로 넘어가는 매개변수에 정확한 값을 주어야 한다. 그 작업을 해 보자.
여기서 잠깐! 강좌를 세심히 읽어본 사람만이 맞출 수 있는 질문. swiHandler() 함수에는 매개변수로 int형의 syscallnum 이라는 값을 받는다. 어셈블리 코드에서 이 매개변수를 넘길려면 어떻게 해야 할까. 10초간의 생각 할 시간을 주겠다.
:
:
:
생각 났는가? 그렇다. r0 에 값을 넣은 후에 bl 로 브렌치 하면 된다. 그렇다면 r0 에 넣어야 할 77 이란 값은 어떻게 가져오는가. ARM 은 현재 실행될 명령어 (1 word)를 읽어들인 다음에는 자동으로 pc를 4바이트(1 word)증가 한다. 즉, 지금 명령이 실행되고 있으면 pc는 항상 그 다음 명령을 가르키고 있다는 것이다. 위에서 썻듯이 swi 77 로 소프트웨어 인터럽트를 발생시키고 나면 pc 는 svc모드의 lr로 복사 된다. 그럼 lr 에는 swi 77 의 바로 아랫줄에 있는 어셈명령어의 주소가 들어가게 된다. 이 lr에서 4를 빼면? swi 77 에 해당하는 명령의 주소가 나온다. 이 주소의 값을 읽어서 77에 해당하는 값을 추출해 내면 된다.
swi 명령은 4바이트중 최상위 1바이트를 차지하고 나머지 3바이트는 인자로 넘어오는 숫자 값이다. swi 77 어셈블리 명령을 기계어로 쓰면
ef00004d
이다. ef 가 swi 이고 이후 00004d 말 그대로 77 이 매개변수로 넘어간다. 어셈코드에서 뒤의 3바이트를 마스킹하여 r0 에 넣어준다음 swiHandler 함수로 브렌치하면 시스템콜 번호를 가져 올 수 있을 것이라 예상된다. entry.S를 수정하자.
.globl _ram_entry _ram_entry: bl main b _ram_entry b Core_swiHandler b Core_irqHandler .global Core_swiHandler Core_swiHandler: stmfd sp!,{r0-r12,r14} mrs r1,spsr stmfd sp!,{r1} ldr r10,[lr,#-4] bic r10,r10,#0xff000000 mov r0,r10 bl swiHandler ldmfd sp!,{r1} msr spsr_cxsf,r1 ldmfd sp!,{r0-r12,pc}^ .global Core_irqHandler Core_irqHandler: bl irqHandler
자세히 보면 알겠지만 딱 세줄이 추가 되었다.
ldr r10,[lr,#-4] bic r10,r10,#0xff000000 mov r0,r10
아주 명료한 문장이다. lr 에서 4를 빼서 r10 에 저장하고 r10에서 하위 3바이트를 마스크하여 다시 r10 에 넣고, 그 값을 r0 에 복사한것이다. 앞에서도 몇번 반복해서 설명했듯이 r0 에 값을 넣고 C언어 함수로 점프할때, C언어 함수에 매개변수가 있으면 컴파일러는 해당 매개변수를 r0 에서 부터 순서대로 선택하는 코드를 생성한다.
entry.S를 위와 같이 고치고 컴파일 해서 이지부트를 이용해 이미지를 보드에 올리고 '''부팅''' 해 보자
system call 77 system call 77 system call 77 system call 77 system call 77 system call 77 system call 77 system call 77 system call 77 : :
우리가 목표로 했던 결과 값이 나온다. 이렇게 해서 이번에도 얼렁뚱땅 Software interrupt 의 사용법에 대해 구렁이 담넘어가듯 설명하고 넘어갔다. 인터럽트도 처리하고 뭔가 조금씩 OS 스러워 지고 있는 것 같기도 하다. 다음회에서는 ARM범용적이거나 OS범용적이기 보단 플랫폼에 종속적인 부분을 설명할것이다. 이지보드에서 사용하고 있는 pxa255칩에서 OS timer를 사용하여 entry.S에서 외로이 아무내용도 가지고 있지 않는 Core_irqHandler 에 생명을 불어 넣어 보도록 할 생각이다.
강좌가 허접해서 그런지 따라하시다가 에러가 난다는 분들이 계신다. 그래서 필자가 작업하고 있는 소스를 같이 올린다. 기본적으로 본 강좌가 목표로 하는 OS 는 다 만들어 놓은 상태에서 강좌를 쓰면서 코드를 다시 쓰고 있기에, 앞으로는 해당 강좌가 끝나면 그 부분까지 작업한 소스를 같이 올리도록 노력해 보겠다.
이 글은 http://raonlife.com/navilera/blog/view/78/ 에 동시 연재 됩니다.
첨부 | 파일 크기 |
---|---|
navilnux_chap4.tgz | 24.04 KB |
댓글
계속 부탁드립니다.
Not at all....
농담이고요 ^^
잘 읽고 있습니다.
이렇게 올려주셔서 감사하고요, 앞으로도 좋은 자료 부탁할께요!
e^(pi*i) + 1 = 0 이 얼마나 아름다운 공식인가?!
잘 보고 있습니다!
...
---------------------------------
Life is not fair, Get used to it.
늦게나마 잘 보고있어요 ^^
ㅎㅎ 깨알같은 설명입니다.!
다시봐도 재미있네요 ㅎ
다시봐도 재미있네요 ㅎ
요즘도 강의 계속해주세요 ㅎ
댓글 달기