ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OS 기초] 코드 분석
    미사용/##컴퓨터 기본 2016. 1. 18. 17:50

    [Operating System]

    참고 도서

    - 만들면서 배우는 OS 커널의 구조와 원리

    - OS구조와 원리 (OS개발 30일 프로젝트)

     

    이번에는 저번에 썼었던 어셈블리어를 한 줄 한 줄 동작을 분석해 본다. 준비해야 할 것은 어셈블리어 작성한 것과 컴파일한 boot.bin 의 디컴파일한 txt 파일이다.

     

    [그림 1]

     

    디컴파일은 [그림 1]과 같이 cmd 에서 명령어를 입력하면 disasm.txt 라는 파일에 boot.bin 을 디스어셈블 내용이 저장된다. –b16 옵션은 16bit 모드로 컴파일 된 바이너리기 때문에 이를 그대로 16bit 환경에서의 디스어셈블 결과를 얻기 위해 넣어준 옵션이다. " > disasm.txt " 을 빼면 파일로 저장이 안되고 cmd 화면에 디컴파일한 내용이 모두 출력 된다.

     

    [org 0] --> 프로그램이 메모리 몇 번지에서 실행해야 하는지 선언, offset 기준

    이 말을 이해하기 위해서 디스어셈블 텍스트를 열어서 확인 해 본다.

     

    00000000 EA0500C007 jmp word 0x7c0:0x5

    00000005 8CC8 mov ax,cs

     

    disasm.txt 를 열어보면 위와 같은 형식으로 출력이 되어있는데 왼쪽부터 차례로 명령어 주소 | 기계어 | 어셈블리어 순이다.

     

    우리가 작성한 어셈블리어는 디스크의 첫 512byte 에 쓰여지는데 그 때 0번지부터 명령어가 나열되어 있는 것이다. 이 주소가 제일 왼쪽 명령어 주소 부분이다. 그리고 두 번째에 있는 값은 실제 명령어의 바이너리값이 16진수로 출력되어 있는데 실제로는 2진수로 저장되어있다. 마지막 값은 바이너리 값에 해당하는 어셈블리어가 출력 되어 있다. EA0500C007 == jmp word 0x7c0:0x5 라고 생각하면 된다.

     

    여기서 하나 확인해야 하는 부분은 jmp 0x07C0:start 가 jmp 0x07C0:0x5 로 바뀌었다는 점이다. 우리가 작성한 어셈블리어에서 :start는 사람이 보기 편하도록 사용한 것이나 컴퓨터는 start가 뭔지 모르기 때문에 :start에 해당하는 실제 값으로 어셈블러가 넣어준 것이다.

     

    여기서 0x5 가 어떻게 나온 값인지는 다음 명령어줄을 보면 명령어의 주소가 00000005 인 것을 알 수 있다. 결국 00000005 번지로 jmp 하라는 것이다. 이 때 이 두 번째 명령어의 주소가 00000005인 이유는 첫 번째 명령어가 EA 05 00 C0 07로 각 byte가 00000000 00000001 00000002 00000003 00000004 번지에 저장되어 있기 때문에 다음 명령어는 00000005에 저장되어 있고 때문에 jmp 0x7c0:0x5 로 어셈블이 된 것이다.

     

    그럼 0x7c0:0x5 에서 0x7c0가 의미하는 것에 대해서도 알아보아야 한다. 일단 컴퓨터는 기본적으로 특정 주소로 부터 상대적으로 메모리에 접근한다는 사실을 기억해두자. [주소]:[값]은 어셈블리어에서 주소를 표시하는 방식인데 콜론(:) 앞의 값은 메모리의 실제 물리 주소이고 뒤의 값은 상대 주소 즉, 오프셋 값을 의미한다. 이 코드가 cpu에서 실행될 때는 이 값을 계산에 의해 완전한 물리 주소로 변환해서 사용한다.

     

    0x7c0:0x5를 변환하는 과정은 우선 세그먼트에 있는 값(cpu에는 RAM의 특정 공간에 접근하기 위해 그 기준이 되는 물리 주소를 저장하는 공간이 있는데 그 공간을 세그먼트라고 한다. 여기서는 0x7c0) 에 16을 곱하여 (2진수에서 왼쪽으로 4bit만큼 shift) 거기에 오프셋 값을 더한다. 16진수에서 16을 곱하면 끝에 0을 하나 더 추가하는 것과 같다.

     

    0x7c0:0x5 => 0x7c00 + 0x5 = 0x7c05 와 같이 계산된다. 이런 계산법은 Real Mode 에서만 적용되는 계산법이므로 실제 운영체제에서는 Protected Mode로 동작되기 때문에 전혀 다른 방법으로 계산된다. Real Mode와 Protected Mode에 대한 설명은 다른 글에서 다루도록 한다.

     

    0x07c0의 의미를 알기 전에 잠깐 앞의 내용을 복습해보면 BIOS의 부트 스트랩(부트 프로그램)이 POST과정을 거치고 디스크 첫 512byte(MBR 영역)를 읽어서 RAM에 로드한다고 했다. 이 때 RAM에서 올릴 때 RAM의 0x07c00 번지에 로드한다. 우리가 짠 코드 역시 마찬가지로 RAM의 0x07c00 번지로 로드되기 때문에 물리 주소값을 0x7c0으로 지정해준 것이다.

     

    이제 원래 목표였던 [org 0] 의 의미를 다시 보자. 프로그램이 실행 될 메모리 번지를 지정하는 명령어라고 했는데 이 말은 곳 프로그램이 처음 실행될 때 00000000 번지에부터 시작한다는 의미다. 만약 [org 10]으로 바꿔서 어셈블하고 다시 디스어셈블해서 코드를 보면

    00000000 EA0500C007 jmp word 0x7c0:0xf 로 바뀌어 있는 것을 볼 수 있다. 0x5가 0xf 로 10 만큼 값이 증가했다. 즉 프로그램 실행 시 시작 위치가 0000000A 로 되면서 상대 주소(오프셋) 역시 10 만큼 증가하게 된 것이다. 하지만 이렇게 되면 실제로 다음에 실행해야할 명령어가 있는 00000005 를 실행하지 않고 엉뚱한 곳을 실행하게 되어 괴상하게 실행이 되어 버린다.

     

    [bits 16] 16비트 시스템이라고 선언하는 부분이다.

     

    이제는 jmp 0x07C0:start 이 코드가 실행되는 과정을 좀 더 자세히 알아 본다.

     

    BIOS가 디스크의 MBR 영역을 RAM의 0x07C00 번지로 로드한다. 앞에서 RAM으로 접근은 상대적으로 접근을 한다고 했다 즉 0x07C00으로 로드할 때에는 0x0000:7C00 방식으로 접근을 한다. 이 때 CS(Code Segment – 코드 영역의 기준 주소)에는 0x0000가 저장되고 IP 레지스터(Program Counter – CPU가 이 레지스터에 있는 주소에 저장되어 있는 명령어를 실행한다.)에는 0x7C00이 저장된다.

     

    이 상황은 막 jmp 0x07C0:0x5 가 실행되기 직전이고 CPU가 IP 레지스터를 보고 해당 명령어를 실행하게 되면 CS에는 0x07C0이 들어가고 IP에는 0x5가 저장된다. 이 때 부터 오프셋을 0부터 시작해서 편하게 메모리에 접근할 수 있게 된다.

     

    이 동작은 [org 0]을 [org 0x7C00]으로 바꾸고 jmp 0x07C0:start 를 지우는 면 같은 동작이 된다. 왜 같은지는 직접 생각 해보는 것이 여러 모로 도움이 많이 될 것 같다.

     

    start:

        mov ax, cs

        mov ds, ax

    • 현재 CS 에 들어있는 값을 DS에 복사.. 코드 세그먼트와 데이터 세그먼트를 같은 값으로 사용

    CS 레지스터에 있는 값을 DS 레지스터에 넣는 과정이다. DS는 Data Segment 로 프로그램에서 정의된 상수나 데이터가 저장되는 메모리 공간의 기준 주소가 저장된다. 여기서는 CS와 DS를 같은 영역으로 사용하는 것이다. ax는 cpu에서 사용하는 범용 레지스터로 cpu에 있는 저장 공간이다. 세그먼트 끼리 값을 복사하기 위해서는 반드시 레지스터를 이용해서 중간 저장을 해 주어야 한다. 세그먼트 끼리 복사가 바로 되지 않는다.

     

        mov ax, 0xB800 // ax 레지스터에 0xB800 주소값 저장

        mov es, ax // es에 b800 저장

        mov di, 0 // di에 0 저장

        mov ax, word [msgBack] // ax 레지스터에 msgBack 에 있는 값 저장

        mov cx, 0x7FF // cx에 7ff 저장 // cx = 반복 카운터

     

    코드를 보면 0xB800 을 ES 에 저장한다. 이어서 di 범용 레지스터에 0을 저장하고 ax 범용 레지스터에 msgBack 에 있는 값을 저장한다. 그리고 바로 이어서 cx 레지스터에 0x7FF 를 저장하는데 cx 레지스터는 cpu가 반복 카운터로 사용한다. 즉 cx레지스터에 저장되어 있는 값 만큼 특정 구간을 반복 하는데 사용한다.

     

    이 부분을 설명하기 위해서는 또 다시 몇 가지 개념적인 부분을 다뤄야 한다. 우선 대체 0xB800 이 무슨 값이기에 ES(Extra Segment) 에 저장하는 것인가.

     

    메모리의 일부 영역은 모니터에 글자나 그림을 나타내기 위해 사용하는 영역이 있다. 그래픽 모드 비디오 메모리, 흑백 텍스트 모드 비디오 메모리, 컬러 텍스트 모드 비디오 메모리 이렇게 3가지 영역이 바로 그 것인데 우리가 사용할 모드는 컬러 텍스트 모드이며 나머지 영역에 대한건 책에서 설명하고 있지 않다. 이 컬러 텍스트 모드 비디오 메모리가 바로 0xB800:0000 ~ 0xB800:FFFF 의 한 개의 세그먼트 영역을 사용한다.

     

    모니터 한 화면에 가로 80개 세로 25개의 문자를 나타내고, 글자수는 총 2000자인데 이는 0xFFFF 보다 훨씬 작은 값이다. 따라서 페이지를 나누어서 0xB800:0000 ~ 0xB800:1000, 0xB800:1001 ~ 0xB800:2000 ... 와 같이 여러 페이지를 사용하도록 되어 있다. 그런데 보통 첫 페이지만 사용하기 때문에 이 책에서도 첫 페이지만 사용하도록 설명한다.

     

    글자를 나타낼 때 2byte를 사용하는데 처음 1byte는 글자의 ASCII 코드를 지정하고, 두 번째 byte는 글자의 배경색과 글자 색을 지정한다. 두 번째 byte에서 앞 4bit는 글자 색을 나타내고 뒤 4bit는 배경색이다.

     

    글자 색과 배경 색을 나타내는 코드는 정해져있다.

    0x0(0000) – 검은색

    0x1(0001) – 파란색

    0x2(0010) – 녹색

    0x3(0011) – 하늘색

    0x4(0100) – 빨간색

    0x5(0101) – 보라색

    0x6(0110) – 갈색

    0x7(0111) – 흰색

    이다. 위 4bit중 가장 왼쪽 최상단 bit가 1인 경우 깜박이는 효과를 줄 수 있다.

     

    mov ax, 0xB800 // ax 레지스터에 0xB800 주소값 저장

    mov es, ax // es에 b800 저장

    즉 이 부분은 컬러 모드 비디로 메모리 세그먼트를 설정해서 사용하기 위해 설정해주는 과정이다.

     

    mov di, 0 // di에 0 저장

    mov ax, word [msgBack] // ax 레지스터에 msgBack 에 있는 값 저장

    mov cx, 0x7FF // cx에 7ff 저장 // cx = 반복 카운터

     

    di 레지스터에0을 저장하는 것은 이 다음 코드에서 사용하기 때문에 저장한 것이다. mov ax, word [msgBack]을 이해하기 위해 코드 아래에 msgBack 부분을 보자.

    msgBack db '.', 0xE7 이 여기서 db는 메모리 공간을 1byte 만큼 두 개의 공간을 할당하고 각각 '.'과 0xE7 로 초기화 한다. 그리고 이 공간에 msgBack 라는 이름을 부여 한다. (이부분은 정확한게 아니라서 조심스럽다) 그리고 ax 레지스터에는 msgBack 에 있는 word(2byte) 만큼을 복사한다.

     

    여기까지가 다음 paint 부분을 실행하기 위한 설정 단계 였고 이제 본격적으로 모니터에 우리가 원하는 값을 출력한다.

     

    paint:

        mov word [es:di], ax // es:di 주소에 ax 값(msgBack 에 있는 값) 저장

        add di, 2 // di + 2

        dec cx // cx 감소

        jnz paint // ZF가 0이면 paint 로 jmp

    es에 있는 주소에서 부터 di 만큼 떨어진 공간에 ax 에 저장된 값을 복사한다. 한 번 반복 할 때 마다di 에 있는 값은 2씩 증가하고 cx는 1씩 감소한다. cx 에 값이 0이 되면 ZF(Zero Flag)가 1로 Set되고 jnz 가 False가 되면서 반복을 중단한다. 즉 cx에 있는 0x7FF 만큼 반복하면서 비디오 메모리 0xB800 부터 약 2000글자를 msgBack 에 있는 값으로 채운다.

     

    여기까지는 화면의 배경을 먼저 갈색 배경에 '.' 글자로 채우는 과정이고 이 다음 과정은 우리가 원하는 문자 출력하는 코드다.

        mov edi, 0 // edi 레지스터에 0 저장

    다시 edi 레지스터에 0을 저장한다. edi 레지스터는 di 레지스터가 확장된 레지스터이다. 앞에서 di 레지스터를 사용하고 이번에 edi 레지스터를 사용한 이유는 아직 확실하게 아는게 아니기 때문에 잠시 넘어가도록 한다.

     

        mov byte [es:edi], 'A' // es:edi 주소에 'A' 저장

        inc edi // edi 값 증가

        mov byte [es:edi], 0x06 //es:edi 주소에 0x06 저장

     

    edi 가 가리키는 주소는 es 세그먼트 기준으로 0번지에 'A' 에 해당하는 아스키 값이 저장된다. 이어서 edi 값을 1 증가시키고 0x06을 1번지에 저장한다. 앞에서 컬러 모드 비디오 메모리는 글자를 나타낼 때 2byte를 사용한다고 했다. 여기서 앞에 1byte는 'A'고 뒤 1byte는 0x06으로 검정색 글자에 갈색 배경을 나타낸다.

        inc edi // edi 증가

        mov byte [es:edi], 'B'

        inc edi

        mov byte [es:edi], 0x06

        inc edi

        mov byte [es:edi], 'C'

        inc edi

        mov byte [es:edi], 0x06

        inc edi

        mov byte [es:edi], '1'

        inc edi

        mov byte [es:edi], 0x06

        inc edi

        mov byte [es:edi], '2'

        inc edi

        mov byte [es:edi], 0x06

        inc edi

        mov byte [es:edi], '3'

        inc edi

        mov byte [es:edi], 0x06

     

    여기까지 연속적으로 글자를 입력한다.

     

        jmp $ // 현재 이 줄에 있는 명령어로 계속 점프.. 무한루프

     

    이 코드는 while(1) {} 와 같은 코드로 무한루프를 돌게 하는 코드로 글자를 모두 출력한 다음에 다른 작업 하지 않고 대기하도록 한다.

     

    msgBack db '.', 0xE7

     

    이 코드는 앞에서도 설명 했던 코드인데 1byte만큼 두 개의 공간을 만들고 '.'과 0xE7로 초기화를 한다고 했다. 이 코드는 어떤 커널에 어떤 동작을 하게 하는 코드가 아니라 기계어에 단순히 바이트를 넣는 코드이다.

     

    times 510-($-$$) db 0

     

    times는 명령행을 N 번 반복하게하는 명령어로 510-($-$$) 번 반복한다. $는 현재 줄의 주소, $$는 세그먼트의 주소로 총 512 바이트중 앞에서 쓴 부분을 제외한 나머지 부분을 0으로 채워주기 위해 사용했다.

     

    dw 0xAA55

     

    MBR은 마지막이 55AA로 끝이 나야한다. 그렇지 않으면 MBR에 이상이 있다고 판단하고 에러 메세지를 출력한다. 그래서 dw(word = 2byte) 만큼 공간을 할당하고 0xAA55를 넣어준 것이다. cpu가 데이터를 읽을 때 리틀엔디언 방식으로 읽기 때문에 0x55AA 가 아닌 0xAA55 로 넣어준 것이다.

     

    이렇게 되면 MBR에 넣을 기계어가 완벽하게 작성이 되었고 이걸 부팅 디스크로 만들어서 부팅하면 이전에 보았던 화면이 출력되는 것이다.

    댓글

Designed by Tistory.