ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • NASM
    임베디드 SW/어셈블리어 2023. 8. 12. 17:01

    글의 참고

    - https://www.nasm.us/xdoc/2.13.03/html/nasmdoc6.html

    - https://www.nasm.us/xdoc/2.13.03/html/nasmdoc7.html

    - https://forum.osdev.org/viewtopic.php?f=1&t=27678 

    - https://nasm.us/doc/nasmdoc2.html


    글의 전제

    - 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.

    - 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.

    - `글의 참조`에서 빨간색 볼드체로 체크된 링크는 이 글을 작성하면 가장 많이 참조한 링크다.

    - `운영체제 만들기` 파트에서 퍼온 모든 참조 글들과 그림은 반드시 `이 글과 그림을 소스 코드로 어떻게 구현을 해야할까` 라는 생각으로 정말 심도있게 잠시 멈춰서 생각해봐야 실력이 발전한다.


    글의 내용

     

    : NASM은 `Netwide Assembler`의 약자이다. 

     

     

    - BIN 포맷

    : NASM을 사용하면 BIN 포맷에 대해 아는 것이 상당히 중요하다. BIN 포맷에서만 사용할 수 있는 많은 지시어들이 있다. 아래의 ORG 지시어는 BIN 포맷에서만 사용가능한 지시어다.

     

    - 지시어

    : BITS

    " BITS 지시어는 해당 프로세서의 모드를 명시한다. 여기서 말하는 모드는 16비트, 32비트, 64비트를 말한다. 근데 사실 이 지시어는 명시적으로 사용할 필요가 없다고 한다. 왜냐면, NASM은 출력 포맷이 정해지면 자기 알아서 프로세서의 모드를 설정해서 어셈블하기 때문이다. 예를 들어, ELF64가 출력 포맷이면 64비트로 어셈블하고 WIN32면 32비트 모드로 어셈블하고 WIN64 면 64비트 모드로 어셈블하기 때문이다. 

     

    : BITS 지시어를 사용하는 대부분의 경우는 플랫 바이너리 파일에서 32비트 혹 64비트 코드를 작성하기 위해서다. 이게 무슨 말이냐면, 흔히 플랫 바이너리 파일의 포맷은 *.bin이다. 즉, bin 파일은 헤더가 존재하지 않는다. 순수한 바이너리 파일 그 자체다. 대개 실행 파일들은 많은 헤더들을 가지고 있다. 그래서 실행 파일을 실행시키기 위해서는 그 실행 파일의 헤더를 해석해야 실행을 할 수 있다. 그러나 bin 파일은 내가 작성한 코드로만 이루어진 파일이기 때문에, 그냥 앞에서 부터 실행하면 된다.

     

    : 그런데, NASM은 이렇게 출력 포맷이 bin 파일이면 16비트 모드로 어셈블한다는 것이다. 그래서 BITS 지시어를 사용해서 베어 메탈로 프로그래밍을 하는 개발자들에게는 bin 포맷의 출력 파일을 32비트 혹 64비트 파일로 어셈블 해준다.

     

    : ORG

    " ORG 지시어는 간단하게 NASM에게 현재 프로그램이 메모리의 어디 주소에서 시작될 지를 알려주는 명령어다. 이렇게 하면 무슨 효과가 있을까? ORG가 지정된 섹션은 내부적으로 ORG로 명시된 값으로 오프셋이 적용된다. 예를 들어, 아래와 같은 코드가 있을 경우 label 심볼은 0x00000104 에서 생성될 것이다. 왜냐면, `dd label`은 명령어이고, 실제 label의 심볼의 메모리가 할당되는 위치는 `label:` 에서이다. 참고로, dd는 `Define Double word`의 약자이다. 

        org     0x100 
        dd      label 
    label:

     

     

    : EXTERN

    " extern 지시어는 C언어의 extern 키워드와 동일한 기능을 한다. 즉, 다른 어셈블리언어로 작성된 심볼 및 C언어로 작성된 변수 및 함수를 파일 내부에서 사용하기 위해서 사용되는 지시어다. 주의 사항이 있다. 이 지시어는 해당 심볼이 외부에 존재한다는 가정하기 때문에, 빌드시에 에러가 발생하지 않는다. 그래서 반드시 이 파일은 외부 파일 어딘가에 선언이 되어 있어야 한다. 만약 존재하지 않으면, 런타임에 큰 문제가 발생할 수 있다. 그리고 대게 이런 문제들은 디버깅도 굉장히 어렵다. 이 지시어는 BIN 파일 포맷에서는 사용할 수 없다.

     

     

    : $와 $$

    " NASM에서 현재 어셈블리의 위치 관련해서 지원하는 2가지 특별한 기능이 있다. 바로 `$`과 `$$`이다. $은 현재 어셈블리의 위치를 나타낸다. 그래서 `jmp $` 와 같은 명령은 제 자리에서 무한 루프를 돌게 만든다. `$$`은 현재 섹션의 위치를 나타낸다. 자주 쓰이는 표현이 있다.

    $ - $$

    " 위의 표현은 현재 섹션에서 얼마나 떨어져 있는지를 나타낸다. 쉽게 파일의 크기를 나타낸다고 볼 수 있다. 물론, 섹션이 최상단에 하나만 있을 때를 전제한다. BIOS MBR 부트 섹터에서 뒤쪽 영역을 0으로 패딩할 때, 자주 사용하는 패턴이다. 또 다른 예시가 있다.

     

    " 그리고 $$은 현재 파일의 섹션을 의미한다. 즉, 링커 스크립트를 기준으로 하는 섹션값이 아니다. 예를 들어, NASM 파일에 `SECTION .text` 라고 선언한다. 그리고 링커 스크립트 `.text` 섹션안에 NASM 파일의 .text가 들어갈 것이다. 근데, 여기서 $$은 링커를 통해 나오는 실행 파일의 섹션의 시작을 의미하는게 아니라, 자기가 포함되었던 입력 파일에 섹션의 시작 주소를 의미한다. 그래서 위의 표현이 현재 파일의 크기를 나타낸다고 말한 것이다.

     

    msg db 'KingKong\n', 0
    size equ $-msg

    " msg의 길이는 `KingKong\n`으로 9글자, 0인 널 문자해새 총 10 글자로, size 레이블에 들어오는 값은 10이된다. 

     

     

    : TIMES

     

    : ALIGN

    " `align` 지시어는 첫 번째 인수로 전달된 값에 경계로 주소를 할당해준다. 형태는 `align <n>, <padding>` 형태를 갖는다.

    align   4               ; align on 4-byte boundary 
    main:
    	xor ax, ax
    
    align   16              ; align on 16-byte boundary 
    	dw 0x0000
    
    align   8,db 0          ; pad with 0s rather than NOPs

    : `main` 심볼이 갖게되는 주소는 4로 나누어 떨어지는 주소를 할당받게 된다. `dw 0x0000`으로 선언된 데이터의 주소는 16로 나누어 떨어지는 주소를 할당받게 된다.

     

    : `align 8, db 0`는 8로 나누어로 떨어지는 주소를 주고 패딩은 0으로 채운다. `align` 지시어의 기본 패딩값은 `NOP(0x90)`이다.

     

     

    : RES[B|W|D|Q|T|O|Y|Z]

    " 초기화되지 않은 데이터들을 선언하는데 사용된다. NASM에서는 이 명령어는 `BSS` 섹션을 위해 만들어졌다고 명시하고 있다. 

    buffer:         resb    64              ; reserve 64 bytes 
    wordvar:        resw    1               ; reserve a word 
    realarray       resq    10              ; array of ten reals 
    ymmval:         resy    1               ; one YMM register 
    zmmvals:        resz    32              ; 32 ZMM registers

     

    : %include 

    : C언어의 헤더 파일과 비슷한 역할을 한다. 그래서 C언어에서 자주 사용하는 헤더 파일 중복 선언을 방지하고자, 파일 앞에 매크로 선언문도 동일하다.

    %ifndef MACROS_MAC
        %define MACROS_MAC
        ; now define some macros
    %endif

    : 그런데 어셈블리언어에서 인클루드문은 생각보다 의미가 없을 수 있다. 

     

     

    - 커맨드 라인

    : nasm 커맨드 라인의 기본 형태는 다음과 같다.

     

    nasm -f <format> <filename> [-o <output>]

     

    : 출력 파일의 이름은 생략이 가능하다. 이름을 생략하면 입력 파일에서 확장자를 제거한 파일로 출력 결과가 나온다. 출력 파일 포맷도 생략이 가능하다. 파일 포맷을 생략하면 기본적으로 BIN 파일 포맷으로 만든다. 

     

    : nams -f { elf, bin, win32, win64 }

    " `-f` 옵션은 파일 포맷을 지정한다. 

    " 예를 들어, `nasm -f elf test.asm -o test.elf`을 하면 test.elf 라는 파일이 나온다. `file test.elf`를 통해 test.elf 파일의 포맷을 확인해 볼 수 있다. `nasm -f bin test.asm`을 치면, `test` 라는 바이너리 파일이 생성된다. .bin 파일은 확장자가 기본적으로 생략된다. 참고로, elf32 도 있는데, elf와 동일하다. `nasm -h`를 통해 파일 포맷을 확인할 수 있다.

     

    : nams -o { output name }

    " 파일 이름을 지정한다.

    " 예를 들어, `nasm -f bin program.asm -o program.com`

     

    : nams -D

    " 해당 파일에 DEFINE 매크로를 삽입해준다. 

    " 예를 들어, `nasm myfile.asm -dF00=100`은 myfile.asm의 제일 앞에 `%define FOO 100`을 선언하는 것과 동일한 기능을 한다. 대개 이 기능은 테스트용으로 자주 사용한다. 그래서 %ifdef와 짝을 이룬다. 예를 들면, `-dDEBUG` 형식을 사용한다.

     

    : .elf 형식의 부트 로더는 ORG, BITS 지시어를 사용할 수 없다. 앞에 지시어들은 .bin 포맷의 부트 로더에서만 사용가능한 지시어다. 그래서 베어 메탈 프로그래밍을 하다가, GDB 사용을 위해 .elf로 빌드해야 하면 `-dDEBUG` 옵션을 추가해서 부트 로더를 디버깅용으로 바꿀 수 있다.

     

    : nams -g

    " 출력 파일에 디버깅 정보를 함께 생성한다. `nams -f bin boot.asm -o boot.bin`은 단순 바이너리 파일을 만든다. 그런데, 앞에 명령어에 `-g` 옵션을 추가하면 출력 파일 포맷에 맞는 디버깅 정보를 생성해준다. 즉, `nams -f elf -g boot.asm -o boot.elf` 라면 ELF 포맷에 맞는 디버깅 정보를 생성해준다. 그런데, 만약에 출력 파일 포맷에 맞는 디버깅 정보가 없다면 `-g` 옵션은 무시된다. 예를 들어, `nams -f bin -g boot.asm -o boot.bin` 와 같은 문장을 실행하면 디버깅 정보가 생성되지 않는다. 왜냐면, BIN 파일은 애초에 디버깅 정보를 가지는 파일 포맷이 아니기 때문이다. 여기서 중요한 건 이름이 아니다. `-f` 옵션에 들어가는 출력 파일 폼새이 중요하다. 예를 들어, `nams -f elf -g boot.asm -o boot.axf` 명령어는 boot.axf 파일에 ELF 디버깅 정보를 생성하지만, `nams -f bin -g boot.asm -o boot.elf` 명령어는 boot.elf 파일에 ELF 디버깅 정보를 생성하지 않는다.

     

    : nams -F

    " 위에서 `-g` 옵션은 `-f` 옵션에 의존한다. 그러나, `-F` 옵션을 사용하면, `-f` 옵션보다 우선한다. `-F` 옵션은 `nasm -h` 명령을 통해 확인이 가능하다. 아래와 같은 목록들이 있는데, 주로 `dwarf`를 사용한다.

    : 예를 들어, `nasm -f elf -g -F dwarf boot.asm -o boot.o` 같은 형식으로 사용한다. 참고로, dwarf는 디버깅 데이터 포맷중 하나로 ELF 실행 파일 포맷에서 쓰는 디버깅 포맷이다.

     

    : nams -E

    " NASM에서 매크로를 자주 사용하는데, 매크로의 결과는 디버깅시에도 확인이 안된다. 전처리 옵션을 붙여서 전처리가 처리된파일로 확인해야 한다. nasm -E -f elf64 -g3 -F dwarf  lbl.asm -o lbl.o 명령어를 통해 `lbl.o` 전처리 파일은 만든다.

    align 4096
    page_fhigh_pd: ; 1GB
    %assign i 0 
    %rep 512 
        dd 0x00000083+i
        dd 0x00000000                                                                                 
    %assign i i+1 
    %endrep

    : 위의 문장은 아래와 같이 전처리된다.

     

    [sectalign 4096]
    %line 31+0 lbl.asm
    times (((4096) - (($-$$) % (4096))) % (4096)) nop
    %line 32+1 lbl.asm
    page_fhigh_pd:
    %line 35+1 lbl.asm
     dd 0x00000083+0
     dd 0x00000000
    %line 35+1 lbl.asm
     dd 0x00000083+1
     dd 0x00000000
    %line 35+1 lbl.asm
     dd 0x00000083+2
     dd 0x00000000
    %line 35+1 lbl.asm
     dd 0x00000083+3
     dd 0x00000000
    ...
    ...
    %line 35+1 lbl.asm
     dd 0x00000083+509
     dd 0x00000000
    %line 35+1 lbl.asm
     dd 0x00000083+510
     dd 0x00000000
    %line 35+1 lbl.asm
     dd 0x00000083+511
     dd 0x00000000
    %line 39+1 lbl.asm

     

    - NASM ELF 섹션

    : NASM에서 ELF 파일 포맷으로 빌드할 경우, 명시적으로 섹션을 명시하지 않으면 아래의 섹션들이 생성된다. 그리고 각 섹션들은 타입 및 속성을 갖는다. 아래의 표에서 2번째 컬럼이 이름이고, 3번째 컬럼부터를 섹션의 타입 및 속성이라고 한다. 이건 NASM이 자동으로 섹션이름을 보고 판단해서 적용하는 내용이다. 개발자가 명시적으로 바꾸지 않는 이상 아래의 내용들이 기본적으로 적용된다. 몇 가지만 알아보자. 

    section .text          progbits      alloc   exec    nowrite  align=16 
    section .rodata        progbits      alloc   noexec  nowrite  align=4 
    section .lrodata       progbits      alloc   noexec  nowrite  align=4 
    section .data          progbits      alloc   noexec  write    align=4 
    section .ldata         progbits      alloc   noexec  write    align=4 
    section .bss           nobits        alloc   noexec  write    align=4 
    section .lbss          nobits        alloc   noexec  write    align=4 
    section .tdata         progbits      alloc   noexec  write    align=4   tls 
    section .tbss          nobits        alloc   noexec  write    align=4   tls 
    section .comment       progbits      noalloc noexec  nowrite  align=1 
    section .preinit_array preinit_array alloc   noexec  nowrite  pointer 
    section .init_array    init_array    alloc   noexec  nowrite  pointer 
    section .fini_array    fini_array    alloc   noexec  nowrite  pointer 
    section .note          note          noalloc noexec  nowrite  align=4 
    section other          progbits      alloc   noexec  nowrite  align=1

    " [alloc|noalloc] - 해당 섹션이 프로그램이 메모리에 로드되어 실행될 때, 메모리에 배치될 것인지 말 것인지에 대한 여부를 나타낸다. 그래서 comment 및 note는 로드되지 않는 것을 알 수 있다.

    " [exec|noexec] - 해당 섹션이 프로그램의 실행 권한이 있는지 없는지 여부를 나타낸다.

    " [write|nowrite] - 프로그램이 동작중에 해당 섹션이 writeable이 가능한지 여부를 나타낸다.

    " [align|pointer] - 해당 섹션이 몇 바이트 정렬인지를 나타낸다. `pointer`는 파일 포맷이 elf32면 dword, elf64면 qword를 나타낸다.

    " [progbits|noprogbits] - 해당 섹션의 내용이이 오브젝트 파일에 포함되는지 여부를 나타낸다. 예를 들어, 코드나 데이터 섹션은 오브젝트 파일에 포함된다. 그러므로, `progbits` 속성을 갖는다. 그러나 BSS 영역은 오브젝트 파일에 포함되지 않으니 `noprogbits` 속성을 갖는다.

     

     

    - 테크닉

    : 파일 사이즈 구하기

    " 파일내에 최초 선언한 심볼을 통해서 구할 수 있는 방법이 있고, 섹션이 여러 개 일 경우, 링커 스크립트를 통해서 구할 수 있는 방법이 있다. 

     

    : 루프 돌리기

    " MBR 같은 파티션은 첫 섹터의 마지막 2바이트내 시그니처가 반드시 들어있어야 한다. 그런데, 어셈블리어로 패딩을 넣는 것이 생각보다 쉽지 않다. 루프를 이용한다면, `times`, `%rep` 등이 있고 , 정렬 주소를 이용하면 `align` 등을 이용할 수 있다.

     

    " 64비트 페이지 테이블 생성시, 아이텐티티 맵핑을 위해서 페이지 디렉토리 엔트리의 주소값을 1씩 증가시켜야 한다. 이때, C언어의 FOR문가 같은 기능이 필요한데 `%rep`를 사용하면 된다. `%rep 512`는 512번 루프를 돌린다. 그런데, 저 인자값으로 동적으로 값이 변하는 변수를 받을 수가 없다. 반드시 정적으로 고정된 값만 받을 수 있다.

    align 4096
    page_fhigh_pd: ; 1GB
    %assign i 0 
    %rep 512 
        dd 0x00000083+i
        dd 0x00000000                                                                                 
    %assign i i+1 
    %endrep

     " 결과는 전처리 옵션인 `-E`를 사용해서 확인이 가능하다. 

     

     

    : 메모리 주소에 직접 접근하기

    " 아래의 코드는 롱 모드 PML4를 세팅하는 코드다. 그런데, `[]`를 통해서 메모리에 직접 접근하는 것을 볼 수 있다. PML4를 0x100000에 선언하고 있다. 그리고 PDPT는 0x110000 위치에 있는것을 보인다.

     

    " 그리고 EDI, ECX, REP 조합으로 PML4 를 초기화는 것 같은데, 이 부분은 분석이 필요하다.

    ;;https://www.reddit.com/r/osdev/comments/mowl2z/failing_to_jump_to_higher_half_kernel/
    ;zero out memory starting at 0x100000
    	xor eax, eax
    	mov edi, 0x100000
    	mov ecx, 512*8
    	rep stosd
    
    
    	mov dword [0x100000], 0x110000 + 0b11 			;PML4 Entry #1
    	mov dword [0x110000], 0b10000011			;PDP Entry, 1 GiB identity mapped
    	mov dword [0x110000 + 8], 0b10000011			;Second PDP Entry
    
    	;enable PAE
    	mov eax, cr4
    	or eax, 1 << 5
    	mov cr4, eax
    
    	;move PML4 addr to cr3
    	mov eax, 0x100000
    	mov cr3, eax
    	
        ...
        ...

    '임베디드 SW > 어셈블리어' 카테고리의 다른 글

    x86 명령어  (0) 2023.08.12
    [어셈블리어] AArch64 어셈블리어  (0) 2023.08.03
Designed by Tistory.