ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [개발 도구] linker script
    Linux/development tool 2023. 8. 3. 02:30

    글의 참고

    - https://users.informatik.haw-hamburg.de/~krabat/FH-Labor/gnupro/5_GNUPro_Utilities/c_Using_LD/ldLinker_scripts.html#Input_section_description

    https://home.cs.colorado.edu/~main/cs1300/doc/gnu/ld_3.html

    - https://stackoverflow.com/questions/8458084/align-in-linker-scripts

    - https://stackoverflow.com/questions/9827157/what-does-keep-mean-in-a-linker-script

    - https://flint.cs.yale.edu/cs421/papers/x86-asm/asm.html

    - https://wiki.osdev.org/Linker_Scripts#KEEP 


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - 목적, 소개

    : 리눅스 레벨에서는 너무 쉽게 넘어갔던 부분이 이었는데, OS를 개발 할 때, 링커에 대한 내용을 모르면, `커널을 로딩할 위치, 스택 초기값 세팅, 커널 사이즈`등 여러 가지 구현 사항들이 어려워 진다. 그리고, 링커스크립트를 사용하서 파일의 엔트리 포인트 주소를 지정해 줄 수 있다. 이게 무슨말 이냐면, C 언어에서 원래 main() 함수가 엔트리 포인트인데, 이걸 링커스크립트를 통해서 바꿔줄 수 있다는 말이다.

     

     

    - 구조

    : 링커는 원래 아래 그림처럼 링커에게 입력으로 들어오는 여러 오브젝트 파일들의 섹션들을 합쳐서 하나의 실행 파일을 만드는게 링커의 일이다. 아래는 `foo1.o`와 `foo2.o` 라는 파일을 합쳐서 하나의 실행 파일로 만드는 것을 보여준다. 그런데 왜 하나의 파일로 만드는 것 일까?

    출처 - https://embed-avr.tistory.com/86


    - 출처 : https://embed-avr.tistory.com/86

     

    : 하나의 파일이 5만줄이라고 하자. 이 파일 하나만 여는데도, 렉이 걸릴 정도다. 그래서 5만 줄 짜리 파일을 500 줄 파일 100개(n0.c n1.c n2.c ... n99.c)로 분할했다. 그런데 문제가 있다. n1.c 파일의 n1() 함수를 n2.c의 n2() 함수에서 호출한다. 그런데, n2.c는 n1() 함수의 주소를 모른다. n1 함수의 주소를 알아야 호출이 가능하다. 이 때, 링커를 통해 n1.c n2.c를 하나의 파일로 합치면서, n1의 주소를 n2.c에게 알려준다. 

     

    : 그리고 또, 어셈블리 파일의 함수를 C 파일에서 어떻게 호출할까? 이것도 링커때문에 가능한 거다. 링커가 하나의 파일로 합치면서 각 파일에 존재하는 변수 및 함수들의 주소를 하나의 실행 파일로 만드는 과정에서 `심볼 주소 재배치`를 한다. 이 과정의 자세한 내용은 이 글에서는 설명하지 않는다. 이 글을 참고하자. 

     

    : 위의 그림에서 각 섹션이 연속적으로 RAM에 적재되었지만, 실제로는 그렇지 않을 수 있다. 섹션들은 미리 정의된 섹션들이 있고, 사용자가 정의 가능한 섹션들이 있다. 미리 정의된 섹션들은 다음과 같다.

    출처 - https://ourembeddeds.github.io/blog/2020/09/21/arm7m-startup-ldscript/

     

    : 세부적으로 나누면 .text, .rodata, .data, .bss, COMMON 섹션들이 있다. 이걸 다시 2가지로 나누어 볼 수 있다.

    1" 수정 가능 여부에 따라
    2" 초기화 여부에 따라

     

    : 대개 수정 가능 여부에 따라 나누면 .text, .rodata가 하나로 묶인다. 초기화 여부에 따라로 나누면 .rodata, .data가 하나로 묶이고, .bss , COMMON이 하나로 묶힐 수 있다. 

     

    : `SECTIONS` 을 통해서 흔히 `output sections`이라 부르는 섹션들을 특정 주소에 배치할 수 있다. 즉, 링크스크립트는 여러 오브젝트들을 받아서 하나의 실행 파일을 만들게 되는데, 그 실행 파일의 메모리 레이아웃을 어떻게 구성할 것인지에 대한 스크립트다.

     

    : 아래의 `.` 은 `현재 주소`를 의미한다. 흔히 location counter라고 부른다. 이 location count는 output section을 지나칠 때마다 그 섹션만큼 값이 증가한다. SECTIONS 안에서 location counter를 명시하지 않으면, `0`으로 시작한다. 즉, 현재 주소를 0이 된다는 것이다. 아래는 0부터 시작하지 않고, 0x10000 부터 시작한다. 

     

    : 그런데 아래의 같이 스크립트를 구성하면 실행 파일의 결과물은 결국 3개의 섹션만 존재하는 실행 파일이 된다. 즉, rodata(읽기 전용) 섹셔는 존재하지 않는다는 뜻이다. 

    SECTIONS
    {
    	. = 0x10000;
            .text = { *(.text)}
            . = 0x8000000;
      	.data : { *(.data) }
      	.bss : { *(.bss) }
      }

     

    : 위의 링크 스크립트를 통해서 output section인 `.text` 영역을 0x10000에 배치한다. 그리고 `.data` 영역은 0x8000000 에 배치한다. 위의 링크스크립트를 통해서 데이터 섹션과 BSS 섹션은 서로 연속적인 메모리 영역에 있게 된다. 즉, 데이터 섹션이 끝나면 BSS 섹션이 배치된다. 그 이유는 데이터 섹션과 BSS 섹션 사이에 location counter의 값을 바꾸지 않고, 그냥 바로 이어서 작성했기 때문이다. 이렇게 하면 location counter는 데이터 섹션이 끝나고, 곧 바로 BSS 섹션을 할당한다. 그렇면 BSS의 시작 주소는 어떻게 될까? BSS 섹션의 시작 주소는 `0x8000000 + 데이터 섹션 크기`가 된다.

     

    : 위에서 output section과 input section이 나오는데, 구조는 다음과 같다.

    .output_section : { *(.input_section) }

    : 위에서 `*`는 모든 파일 이름으로 대체된다. 즉, `*(.text)`에 의미는 모든 입력 파일들의 .text 섹션을 의미한다. 쉽게 말해서, 링크를 할 때 많은 오브젝트 파일들을 하나의 실행 파일로 만들어야 하는데, 그 때 입력 파일들의 text 영역을 실행 파일의 0x10000 에 배치하겠다는 뜻이다.

     

    : 그런데 입력 파일을 작성하는 방법은 한 가지가 더 있다. 

    *(.text .rdata)
    *(.text) *(.rdata)

    : 첫 번째는 순서없이 혼재된다. 즉, text 섹션과 rdata 섹션이 섞여서 배치된다는 뜻이다. 그러나 두 번째에서는, `.text` 섹션이 앞에 배치되고, 그 뒤를 `.rdata` 섹션이 배치된다.

     

    : 그리고 링커는 각 섹션의 주소에 대한 정렬의 업무를 맡는다. 0x10000과 0x8000000는 4로 정확하게 나누어 떨어진다. 즉, 4바이트 정렬이 되어 있다고 볼 수 있다. 그러나, BSS 섹션 같은 경우, 데이터 섹션의 크기에 따라 정렬이 될 수도 안 될 수도 있다. 정렬이 안되면, 링커는 자동으로 데이터 섹션과 BSS 섹션 사이에 아주 약간의 `갭`을 만들 수도 있다.

     

    : 몇 개의 링커 스크립트 예제를 통해 링크 스크립트에 대해 알아보자.

     

    - 예제 1

    : .text` 섹션안에 `.text.boot`가 존재하는데, 이건 커스텀 섹션이라고 보면 된다. ARM 진영에서는 관습처럼 `_start` 심볼을 선언되어 있는 파일의 텍스트 섹션을 `.text.boot` 로 선언한다. 별도로, `.text.boot`를 만든 이유는 텍스트 섹션에서 해당 코드를 제일 앞쪽에 두기 위해서다.

    ENTRY(_start)
     
    SECTIONS
    {
        /* Starts at LOADER_ADDR. */
        . = 0x80000;
        /* For AArch64, use . = 0x80000; */
        __start = .;
        __text_start = .;
        .text :
        {
            KEEP(*(.text.boot))
            *(.text)
        }
        . = ALIGN(4096); /* align to page size */
        __text_end = .;
     
        __rodata_start = .;
        .rodata :
        {
            *(.rodata)
        }
        . = ALIGN(4096); /* align to page size */
        __rodata_end = .;
     
        __data_start = .;
        .data :
        {
            *(.data)
        }
        . = ALIGN(4096); /* align to page size */
        __data_end = .;
     
        __bss_start = .;
        .bss :
        {
            bss = .;
            *(.bss)
        }
        . = ALIGN(4096); /* align to page size */
        __bss_end = .;
        __bss_size = __bss_end - __bss_start;
        __end = .;
    }

    : 만약, 위 링커 스크립트에 의해서 링킹되는 파일 이름이 start.S 라면, 위치상으로 따지면 아래 코드로 바꿔도 동일하게 동작한다.

     

    ENTRY(_start)
     
    SECTIONS
    {
        ...
        .text :
        {
            KEEP(start.o(.text))
            *(.text)
        }
        ...
    }

    : 위에서 ENTRY() 는 링커에게 실행 파일의 시작 지점을 알려준다. 즉, 링커를 통해 만든 실행 파일이 boot.elf 라면, 이걸 실행할 경우(`./boot.elf`) 저 심볼 지점부터 실행한다는 뜻이다.

     

     

    : 참고로, GNU 링커는 링커 스크립트에 엔트리 포인트가 작성되어 있지 않을 경우, 기본적으로 `_start` 심볼을 엔트리 포인트로 설정한다. 그래서 엔트리 포인트를 `_start`로 정했다면, 굳이 ENTRY(_start)를 작성하지 않아도 상관없다.

    // AArch64 mode
     
    // To keep this in the first portion of the binary.
    section ".text.boot"
     
    // Make _start global.
    global _start
     
    _start:
        // set stack before our code
        ldr     x5, =_start
        mov     sp, x5
     	
        ...

    : 위에 보면 `global _start` 라고 있는 것을 볼 수 있다. 그리고 boot.S에 `_start:`이 선언된 것을 볼 수 있다. 이건 `_start`라는 심볼을 선언한 뒤, 외부로 `_start` 심볼을 노출시키겠다는 뜻이다. `global`을 사용하지 않으면 외부에서 `_start` 심볼에 접근이 불가능하다.  

     

    : 그리고 `section ".text.boot" ` 부분은 이 boot.S 파일을 `.text.boot` 라는 섹션안에 선언한다는 뜻이다. 그래서 위의 링크스크립트에서 `.text` 섹션안에 `.text.boot`를 볼 수 있을 것이다. 그럼 이제 다시 링크 스크립트로 돌아가자. 

     

    : 첫 째줄의 `. = 0x80000` 는 이 링커 스크립트를 통해 만들어지는 실행 파일의 시작 주소를 0x80000로 하겠다는 뜻이다. 즉, 이 실행 파일을 0x80000에 배치하겠다는 뜻이다. 그리고 __start, __text_start 라는 심볼을 저 링크 스크립트안에서 선언하고 있다. 저 심볼들을 선언하는 이유는 각 섹션의 크기를 알기 위해서다. 그래서 보면 __text_start 와 __text_end가 쌍으로 선언된 것을 확인할 수 있다. 그리고 각 각 섹션, 즉, rodata, data 섹션등이 start, end 심볼로 쌍을 이룬다. 이 심볼들은 해당 링크 스크립트를 통해 만들어진 실행 파일에서 `extern` 키워드를 통해 접근이 가능하다. 

     

    : text 섹션안에 KEEP 키워드에 대해 예제를 통해 알아보자. 아래는 AT&T 문법으로 작성된 어셈블리어다.

    section .text
    global _start
    _start:
      
        mov $1, %eax
        // mov keep, %eax
        
        mov $4, %ebx
    
    .section .keep
        keep: .long 1
        
    .section .data
        temp: .long 3

    : 위의 코드에서 .keep 섹션에 keep 변수는 사용되지 않을 경우 가비지 컬렉션에 의해서 제거가 된다. 이렇게 되면, .keep 섹션 내에 사용되는 변수가 없기 때문에 .keep 섹션 자체가 가비지 컬렉터에 의해 사라질 수 있다. 즉, .keep 섹션은 실행 파일에 포함되지 않게된다. 여기서 아래와 같이 KEEP()으로 .keep 섹션을 묶으면, .keep 섹션이 사용되지 않더라도 제거히지 않고, 실행 파일에 포함시킨다. 즉, 다른 파일에서 이 keep 변수를 계속 사용할 수가 있게 된다. `.temp` 심볼은 사용되지도 않고 KEEP 키워드를 사용하고 있지도 않으므로, 최적화시에 제거되게 된다. `.data` 섹션에는 temp 심볼밖에 없었으므로, `.data` 섹션도 실행 파일에 포함되지 않는다. 

     

    ENTRY(_start)                                                                                                
    SECTIONS
    {
        . = 0x400000;
        .text :
        {
            *(.text)
            KEEP(*(.keep));
        }   
    }

    : KEEP()을 사용하는 경우는 대개 초기 부트 프로세스에서 적용된다. 그런데 인터럽트 벡터 테이블은 초기에 사용되지 않을 가능성이 높다. 왜냐면, 부팅 시점에 인터럽트를 비활성화 하기 때문이다. 그래서 링크 타임에 인터럽트 벡터 테이블에 대한 레퍼런스가 없는 것을 확인하고, 링커가 제거할 수 도 있다. 그러므로, 계속 남겨놓기 위해 KEEP() 명령어를 쓰는 것이다. 

     

    - 예제 2

    SECTIONS {
        outputa 0x10000 :
        {
        	all.o
        	foo.o (.input1)
        }
        
        outputb :
        {
        	foo.o (.input2)
        	foo1.o (.input1)
        }
        
        outputc :
        {
        	*(.input1)
        	*(.input2)
        }
    }

    : 위 예제에서는 세 개의 output sections 이 선언되어 있다. 첫 번째 `outputa 0x10000`의 의미는 outputa 섹션의 시작 주소는 0x10000 이라는 소리다. outputa 섹션안에 2개의 오브젝트 파일이 보인다. 첫 번째로 all.o이 보인다. 이건 0x10000(outputa)의 제일 앞쪽에 all.o의 모든 섹션을 배치하라는 소리다. 그리고 foo.o 오브젝트의 파일의 `.input1` 섹션이 all.o의 바로 뒤에 배치된다. all.o는 all.o의 모든 섹션이 배치되지만, foo.o(.input1)은 foo.o의 .input1 섹션만 배치된 다는 점이 특징이다.

     

    : outputb 섹션도 foo.o의 `.input2` 섹션이 outputb의 제일 앞쪽에 배치되고, 바로 뒤에 foo1.o의 `.input1` 섹션이 배치된다.

     

    : outputc 섹션에는 모든 파일들의 `.input1` 섹션과 `.input2` 섹션이 배치된다. outputa 와 outputb 섹션은 특정 파일의 특정 섹션을 배치하거나, 특정 파일 하나를 통째로 배치하는 내용이 었다면, outputc는 모든 입력 파일에서 특정 섹션을 뽑아내서 출력 섹션으로 배치하는 내용이다.

     

    : 참고로 outputa 섹션에서 각 섹션의 시작 주소를 설정할 수 있는 방법이 있다. 위의 예처럼, 출력 섹션 이름뒤에 주소를 적으면 해당 출력 섹션의 시작 주소를 설정할 수 있다.

    .text . : { *(.text) }
    .text : { *(.text) }
    .text ALIGN(0x10) : { *(.text) }

    : 위에서 첫 번째는 .text 섹션의 시작 주소를 현재 location counter(address counter) 의 값으로 설정한다. 두 번째 .text 출력 섹션은 입력 섹션의 가장 엄격한 정렬 주소를 반환받는다. 그리고 세 번째 .text 출력 섹션은 0x10 경계의 주소값을 반환받는다.

     

     

    - 섹션 배치

    " 섹션을 배치할 때, 특히 MBR 부트 섹터를 사용하는 경우에 텍스트 섹션과 데이터 섹션 배치에 주의해야 한다. https://github.com/yohda/Yohda-OS/tree/master/bootloader/x86/p16 링크에 있는 pbl.asm, sbl,asm 2개의 어셈블리어 파일에는 각각 텍스트 섹션만 명시가 되어 있다. 이 파일들을 `size` 유틸리티를 통해 확인해보면 다음과 같은 결과가 나온다. 

    $ size sbl.o 
       text	   data	    bss	    dec	    hex	filename
        512	      0	      0	    512	    200	sbl.o
    $ size pbl.o 
       text	   data	    bss	    dec	    hex	filename
        512	      0	      0	    512	    200	pbl.o

     

    " 결과를 보면 알 수 있겠지만, 데이터 섹션이 존재하지 않는다. 왜냐면, 해당 파일들에는 `SECTION TEXT` 지시어만 사용했기 때문이다. 여기서 pbl.asm 파일에 아래와 같이 밑에 `SECTION .data`를 추가해보자.

    ...
    ...
        pop bx
        pop ax
        
        pop bp
    
        ret 
    
    SECTION .data                                                                      
    
    vga_rows    :   dw 0
    SEC_BOOT_MSG:   db 'Ready for jumping secondary bootloader', 0   
    BOOT_MSG:       db 'YohdaOS Primary Boot Loader Start', 0 
    ACT_PART_MSG:   db 'There exist a active partition', 0
    
    times (446 - ($-$$)) db 0
    times 16 db 0
    times 16 db 0
    times 16 db 0
    ...
    ...

     

    " 빌드 후, `size` 명령어를 통해 결과를 보자. 왜, 데이터 섹션이 512B 일까? `$$` 때문이다. 앞에 기호는 현재 섹션을 기준으로 계산을 하기 때문에, 데이터 섹션도 512B가 나온 것이다. 전체 사이즈 또한 705B로 더 커졌다. 더 큰 문제는 따로 있다.

    $ size pbl.o 
       text	   data	    bss	    dec	    hex	filename
        193	    512	      0	    705	    2c1	pbl.o

     

    " 링커 스크립트 기준으로 보면, 데이터 섹션은 텍스트 섹션이 모두 배치되고 나서 나오는 영역이다. 그래서 `xxd` 명령어로 파일의 내용을 확인해보면 MBR 부트 시그니처 512B를 넘어 파일의 맨 끝에 가있는 것을 확인할 수 있다. 우리가 원했던 것은 0x1FE, 0x1FF에 부트 시그니처가 나와야 하는데, 대략 400바이트 뒤쪽에 부트 시그니처가 나오고 있다.

    00000400: 6d61 7279 2042 6f6f 7420 4c6f 6164 6572  mary Boot Loader
    00000410: 2053 7461 7274 0054 6865 7265 2065 7869   Start.There exi
    00000420: 7374 2061 2061 6374 6976 6520 7061 7274  st a active part
    00000430: 6974 696f 6e00 0000 0000 0000 0000 0000  ition...........
    00000440: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000450: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000460: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000470: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000480: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000490: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000004a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000004b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000004c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000004d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000004e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000004f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000500: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000510: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000520: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000530: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000540: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000550: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000560: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000570: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000580: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    00000590: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000005a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000005b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    000005c0: 0000 0000 0000 0000 0000 55aa            ..........U.

     

    " 그럼 `SECTION .data` 앞 쪽에 배치하면 어떨까?

    [BITS 16] 
    
    SECTION .data                                                               
        
    vga_rows    :   dw 0
    SEC_BOOT_MSG:   db 'Ready for jumping secondary bootloader', 0 
    BOOT_MSG:       db 'YohdaOS Primary Boot Loader Start', 0 
    ACT_PART_MSG:   db 'There exist a active partition', 0
    
    ; VGA 
    VGA_TEST_BASE equ 0xB800
    ...
    ...
    
    SECTION .text
    ...
    ...

     

    " 결과는 크게 달라지지 않는다. 결국 또 `$$` 때문에 문제가 발생하게 된다. 

    $ size pbl.o 
       text	   data	    bss	    dec	    hex	filename
        512	    106	      0	    618	    26a	pbl.o

     

     

    - 키워드

    : ALIGN

    " ALGIN(n) 형태로 쓰인다. 인자로 주어진 정렬된 주소를 반환한다.

     

    : AT

    " AT(addr) 형태로 쓰인다. 인자에는 해당 섹션을 로딩할 실제 물리 주소를 기입한다. 아래의 예시 코드를 보자.

    SECTIONS
      {
      .text 0x1000 : { *(.text) _etext = . ; }
      .mdata 0x2000 :
        AT ( ADDR (.text) + SIZEOF (.text) )
        { _data = . ; *(.data); _edata = . ;  }
      .bss 0x3000 :
        { _bstart = . ;  *(.bss) *(COMMON) ; _bend = . ;}
    }

     

    " 위의 링커 스크립트는 세 개의 섹션을 갖는다. 첫 번째 `.text` 섹션의 시작 주소는 0x1000 이다. `.mdata` 섹션은 `.text` 섹션 끝에 로드된다. 그런데, .`mdata` 섹션의 VMA은 0x2000이다. 그러나 AT 키워드를 통해 .`mdata` 섹션의 실제 로딩되는 물리 주소는 `.text` 섹션 바로 뒤에 딱 붙어서 로딩된다(AT(ADDR(.text) + SIZEOF(.text))). `.bss` 섹션은 시작 주소는 `0x3000`이다. `.data` 심볼은 0x2000의 값을 갖게 된다. 이 값은 물리 주소(LMA)가 아닌 가상 주소(VMA)다.

     

    " 링커 스크립트에서 LMA는 명시적인 키워드(`AT` , `> AT`)를 통해서 사용된다. 암묵적으로 작성된 변수 및 섹션들은 모두 VMA 가 할당된다. 물리 주소가 제대로 들어가 있는지에 대한 확인은 `readelf -e ${FILE_NAME}` 로 확인해 볼 수 있다. 아래 `PhysAddr`을 확인할 수 있을 것이다.

     

     

    : ADDR

    " ADDR(section name) 형태로 쓰인다. 인자로 들어간 섹션의 가상 주소를 반환한다.

     

     

    : PROVIDE

    " `PROVIDE` 키워드는 심볼을 정의하지는 않고, 참조용으로만 만들어주는 키워드다. 예를 들어, C 언어 파일에서 `etext` 함수가 존재하고, 아래에서 `PROVIDE(etext = .)` 가 아니고, `etext = .`라면 에러를 발생시킨다. 왜냐면, 2개의 심볼이 존재하지 않기 때문이다. 그럴 때, `PROVIDE(etext = .)`를 사용하면 링커는 `etext` 라는 심볼을 정의하지 않는다. 즉, C언어에서 심볼(변수 및 함수)들에 이름을 자유롭게 정의할 수 있게 해준다고 보면 된다. 그래서 `xv6`의 링커 스크립트에 선언되어 있는 모든 심볼들은 `PROVIDE`로 되어있다.  

    SECTIONS
    {
    	.text :
    	{
             *(.text)
             _etext = .;
             PROVIDE(etext = .);
    	}
    }
    For example, traditional linkers defined the symbol ‘etext’. However, ANSI C requires that the user be able to use ‘etext’ as a function name without encountering an error. The PROVIDE keyword may be used to define a symbol, such as ‘etext’, only if it is referenced but not defined. The syntax is PROVIDE(symbol = expression).

    In the previous-example, if the program defines ` _etext ', the linker will give a multiple definition error. If, on the other hand, the program defines ` etext ', the linker will silently use the definition in the program. If the program references ` etext ' but does not define it, the linker will use the definition in the linker script.
    ....

    - 참고 : https://sourceware.org/binutils/docs/ld/PROVIDE.html

     

     

    : /DISCARD/

    " `/DISCARD/` 섹션에 포함되는 모든 input sections 들은 output file 에 포함되지 않는다(`.debug_*` 접두사 붙은 섹션들은 기본적으로 elf 플래그인 SHF_ALLOC 가 포함되어 있지 않다. 그런데, 이 섹션들은 그래도, output file 에는 포함된다).

    The special output section name ‘/DISCARD/’ may be used to discard input sections. Any input sections which are assigned to an output section named ‘/DISCARD/’ are not included in the output file.

    - 참고 : https://sourceware.org/binutils/docs/ld/Output-Section-Discarding.html

     

     

     

     

    - .text vs .rodata

    " 2개의 섹션 모두 READ-ONLY 섹션이다. 그러나, `.text` 섹션은 실행이 가능한 READ-ONLY 섹션이고, `.rodata` 섹션은 실행은 불가능하고 단지 읽기만 가능한 섹션이다. 대게는 링커 스크립트는 2개의 섹션을 분리해서 작성한다.

    . = 0xC0100000;
    .text ALIGN(0x1000): AT(ADDR(.text) - 0xC0000000)
    {
    	main.o(.text)
    	*(.text)
    }
    
    .rodata ALIGN(0x1000) : AT(ADDR(.rodata) - 0xC0000000)
    {                                                                     
    	*(.rodata)
    }

     

    " 그런데, 꼭 분리해서 작성해야 할까? 2개를 같이 묶어서 `.text` 영역으로 볼 수는 없을까? CPU는 텍스트 섹션에 있는 모든 데이터는 명령어라고 인식한다. 그래서 해당 영역의 모든 데이터를 `실행` 한다. 그런데, `.text` 영역이 아래와 같이 `.rodata` 영역이랑 합쳐졌다고 생각해보자.

     

    . = 0xC0100000;
        .text ALIGN(0x1000): AT(ADDR(.text) - 0xC0000000)
        {
            main.o(.text)
            *(.text)
            *(.rodata)
        }

     

    " 위의 코드는 텍스트 영역의 사이즈도 커졌을 뿐만 아니라 CPU가 READ-ONLY 데이터까지 명령어로 인식해버린다. READ-ONLY기 때문에 에러를 발생시키지는 않지만 만약, `.rodata` 섹션이 아닌 `.data` 섹션이 었다면 얘기가 달라지니 그런 짓은 절대 하지 말기를 바란다.

     

     

    - 커스텀 섹션

    : x86을 기준으로 OS 개발 시, 커스텀 섹션을 만드는 경우가 많지는 않은 것 같다. 경우가 있다면, 상위 절반 커널을 만들기 위해서 준비 작업 섹션으로 만드는 경우가 있다. 그리고 ARM 같은 경우에는, 부트 코드를 텍스트 섹션에 제일 앞쪽에 배치하게 `.text.boot`라는 섹션을 별도로 만드는 경우가 있다. 결론적으로 많지는 않지만, 알고 있을 필요는 있다. 여기서는 NASM을 기준으로 커스텀 섹션을 만들어 본다. 아래 코드를 보자.

    ; lbl.asm
    section .text.mode64
    global _start64:

    _start64:
        ; Re-enable Paging
        mov rax, cr0 
        ;and rax, 1<<31
        mov cr0, rax    

        jmp $
    ; link.ld
    ENTRY(_start64)
    SECTIONS
    {
        . = 0x100000;
        .text.mode64 :
        {   
            lbl.o(.text.mode64)
        }
    }

    : 위와 같이 `.text.mode64`라는 섹션을 만들고 링커 스크립트에 작성했다. 근데, 저 섹션이 실행이 안된다. 왤까? 파일을 분석해보면 답을 알 수 있다. `readelf -a lbl.o` 을 통해서 나온 결과는 다음과 같다.

     

    ELF Header:
      ...
      Type:                              EXEC (Executable file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x100000
      Start of program headers:          64 (bytes into file)
       ...
      Number of section headers:         12
      Section header string table index: 11
    ...
    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
      [ 0]                   NULL             0000000000000000  00000000
           0000000000000000  0000000000000000           0     0     0
      [ 1] .text             PROGBITS         0000000000100000  00100000
           000000000000002b  0000000000000000  AX       0     0     16
      [ 2] .text.mode64      PROGBITS         000000000010002b  0010002b
           0000000000000008  0000000000000000   A       0     0     1
      [ 3] .data.mode64      PROGBITS         0000000000101000  00101000
           0000000000002000  0000000000000000   A       0     0     4096
     ...
     ...

    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
      L (link order), O (extra OS processing required), G (group), T (TLS),
      C (compressed), x (unknown), o (OS specific), E (exclude),
      l (large), p (processor specific)

    ...
    ...

    Symbol table '.symtab' contains 29 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         1: 0000000000100000     0 SECTION LOCAL  DEFAULT    1 
        ...
        ...
        25: 0000000000103000     0 NOTYPE  LOCAL  DEFAULT    3 page_pml5
        26: ffff800000000000     0 NOTYPE  GLOBAL DEFAULT  ABS HH_BASE
        27: 000000000010002b     0 NOTYPE  GLOBAL DEFAULT    2 _start64
        28: 0000000000100000     0 NOTYPE  GLOBAL DEFAULT    1 page_lmode_enable

    No version information found in this file.

    : `.text` 섹션에 플래그값으로 A,X가 설정되어 있다. 여기서 X가 중요하다. X는 실행이 가능한 섹션이라는 뜻이다. 즉, 우리가 만든 섹션을 실행 시키고 싶다면, 해당 섹션을 실행 가능한 섹션으로 만들어야 한다는 말이다. 그냥 만들기만 하면 링커는 해당 섹션을 메모리만 차지해놓는(`A`) `더미 섹션`으로 만들어 버린다.

     

    : 그리고 `_start64` 심볼의 위치가 0x10002b 로 나온다. 링커스크립트를 기준으로 하면 `_start64`이 0x100000에 나와야 하는데,  그 위치에 `page_lmode_enable`라는 심볼이 와있다. 엔트리 포인트 주소도 보면, 0x100000인데 이 주소는 `page_lmode_enable` 심볼이다. 원인은 링커가 실행 가능한 섹션(텍스트 섹션)을 실행 파일의 제일 앞쪽으로 배치하기 때문이다. 위 같은 경우, 실행 가능한 섹션이 `.text` 섹션만 존재하기 때문에 그런 것이다. 이제 아래와 같이 코드를 바꿔보자.

    ; lbl.asm
    section .text.mode64 exec
    global _start64:

    _start64:
        ; Re-enable Paging
        mov rax, cr0 
        ;and rax, 1<<31
        mov cr0, rax    

        jmp $
    ; link.ld
    SECTIONS
    {
        . = 0x100000;
        .text.mode64 :
        {   
            lbl.o(.text.mode64)
        }
    }

     

     

    ELF Header:
      ...
      Type:                              EXEC (Executable file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x100010
      Start of program headers:          64 (bytes into file)
       ...
      Number of section headers:         12
      Section header string table index: 11

    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
      [ 0]                   NULL             0000000000000000  00000000
           0000000000000000  0000000000000000           0     0     0
      [ 1] .text.mode64      PROGBITS         0000000000100000  00100000
           0000000000000008  0000000000000000  AX       0     0     1
      [ 2] .text             PROGBITS         0000000000100010  00100010
           000000000000002b  0000000000000000  AX       0     0     16
      [ 3] .data.mode64      PROGBITS         0000000000101000  00101000
           0000000000002000  0000000000000000   A       0     0     4096
      ...
      ...
      [10] .strtab           STRTAB           0000000000000000  00105528
           00000000000000bd  0000000000000000           0     0     1
      [11] .shstrtab         STRTAB           0000000000000000  001055e5
           0000000000000076  0000000000000000           0     0     1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
      L (link order), O (extra OS processing required), G (group), T (TLS),
      C (compressed), x (unknown), o (OS specific), E (exclude),
      l (large), p (processor specific)

    ...
    ...

    Symbol table '.symtab' contains 29 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         1: 0000000000100000     0 SECTION LOCAL  DEFAULT    1 
        ...
        ...
        26: ffff800000000000     0 NOTYPE  GLOBAL DEFAULT  ABS HH_BASE
        27: 0000000000100000     0 NOTYPE  GLOBAL DEFAULT    1 _start64
        28: 0000000000100010     0 NOTYPE  GLOBAL DEFAULT    2 page_lmode_enable

    : `_start64` 심볼의 위치를 확인하자. 0x100000인 것을 확인할 수 있다. `.text.mode64` 섹션에 `X` 플래그도 생긴 것을 확인할 수 있다. 그러나, 엔트리 포인트 주소가 아직 바뀌지 않았다. 왜 그럴까? GNU 링커는 엔트리 포인트를 지정해주지 않을 경우, 기본적으로 `_start` 심볼을 찾고 이 심볼의 주소로 엔트리 포인트를 설정하려고 한다. 그런데, `_start` 심볼이 없으면 `.text` 섹션의 제일 앞을 엔트리 포인트로 잡아버린다. `.text` 섹션 제일앞에 `page_lmode_enable` 심볼이 존재해서 위와 같이 0x100010이 엔트리 포인트가 된 것이다. 아래와 같이 수정하면 엔트리 포인트 값도 0x100000으로 바꿀 수 있다. 

     

    ; link.ld
    ENTRY(_start64)
    SECTIONS
    {
        . = 0x100000;
        .text.mode64 :
        {   
            lbl.o(.text.mode64)
        }
    }
    ELF Header:
      ...
      Type:                              EXEC (Executable file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x100000
      Start of program headers:          64 (bytes into file)
       ...
      Number of section headers:         12
      Section header string table index: 11
      ...

     

    - 링커 스크립트 변수

    : 링커 스크립트에 선언되는 변수들은 대게 섹션의 크기를 나타낸다. 아래 색깔별로 각 영역의 시작(start)과 끝(end)을 나타내는 변수를 선언하고 있다. 

    ENTRY(_start)
     
    SECTIONS
    {
        /* Starts at LOADER_ADDR. */
        . = 0x80000;
        /* For AArch64, use . = 0x80000; */
        __start = .;
        __text_start = .;
        .text :
        {
            KEEP(*(.text.boot))
            *(.text)
        }
        . = ALIGN(4096); /* align to page size */
        __text_end = .;
     
     ...
     ...
     
        __bss_start = .;
        .bss :
        {
            bss = .;
            *(.bss)
        }
        . = ALIGN(4096); /* align to page size */
        __bss_end = .;
        __bss_size = __bss_end - __bss_start;
        __end = .;
    }

    : 저 변수들은 실제 저안에서 사용된다기 보다는 소스 코드(C 파일 혹은 어셈블리어 파일)상에서 사용되면서 의미가 부여된다. 저 변수들을 소스 코드상에서 불러 오려면 어떻게 해야 할까? 아래의 코드를 보자.

    extern unsgined char __text_start; 
    uint8_t *text_start = &__text_start;
    
    extern unsgined char __text_start[]; 
    uint8_t *text_start = __text_start;

     

    : 대체로 링크 스크립트의 변수를 C 파일에서 사용하기 위해서는 위와 같이 불러와야 한다.

    Linker scripts symbol declarations, by contrast, create an entry in the symbol table but do not assign any memory to them. Thus they are an address without a value. So for example the linker script definition:

    foo = 1000;

    creates an entry in the symbol table called ‘foo’ which holds the address of memory location 1000, but nothing special is stored at address 1000. This means that you cannot access the value of a linker script defined symbol - it has no value - all you can do is access the address of a linker script defined symbol.

    Hence when you are using a linker script defined symbol in source code you should always take the address of the symbol, and never attempt to use its value. For example suppose you want to copy the contents of a section of memory called .ROM into a section called .FLASH and the linker script contains these declarations:
    start_of_ROM = .ROM;
    end_of_ROM = .ROM + sizeof (.ROM);
    start_of_FLASH = .FLASH;
    Then the C source code to perform the copy would be:
    extern char start_of_ROM, end_of_ROM, start_of_FLASH;
    memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);
    Note the use of the ‘&’ operators. These are correct. Alternatively the symbols can be treated as the names of vectors or arrays and then the code will again work as expected:
    extern char start_of_ROM[], end_of_ROM[], start_of_FLASH[];
    memcpy (start_of_FLASH, start_of_ROM, end_of_ROM - start_of_ROM);
    Note how using this method does not require the use of ‘&’ operators.

    : 공식 문서에서도 말하고 있지만, 링크 스크립트의 변수에는 주소만 들어있다. 그리고 그 주소에는 값이 포함되어 있지 않다. 그러니 해당 변수의 주소에 접근해서 값을 쓰지 말라고 언급하고 있다. 그 이유는 링커 스크립트에서 변수는 일반 프로그래밍 언어의 변수들과 목적이 다르기 때문이다. 프로그래밍 언어 변수들은 주로 특정 주소에 존재하는 데이터가 주목적이지만, 링크 스크립트의 변수들의 주목적은 주소 그 자체다. 그래서 위에서도 봤지만, 링커 스크립트의 변수는 주소만을 가지고 있기 때문에, 해당 변수에 접근하기 위해서 `&`를 연산자를 쓰는 것을 확인할 수 있다.

     

    : 그리고 C 파일에서 링커 스크립트 변수를 선언할 때, 2가지 방법을 사용하는데 배열 방식이 더 편리할 것이다. 왜냐면, `&`를 사용할 필요가 없기 때문이다. 그리고 링크 스크립트 변수들은 `char` 형으로 선언된다고 하니 참고하자.

    'Linux > development tool' 카테고리의 다른 글

    [개발 도구] QEMU  (0) 2023.08.07
    [개발 도구] GDB  (0) 2023.08.07
    [개발 도구] Shell Prompt 언어 설정  (0) 2023.08.03
    [LINUX][VIM] - Vim Session  (0) 2023.08.03
    [개발 도구] 디렉토리 내비게이션 명령어  (0) 2023.08.03
Designed by Tistory.