ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 재배치
    프로젝트/운영체제 만들기 2023. 6. 12. 14:14

    글의 참고

    - http://bravegnu.org/gnu-eprog/linker.html

    - https://people.cs.pitt.edu/~xianeizhang/notes/Linking.html


    글의 전제

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

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


    글의 내용

    " 심볼 리졸루션 단계가 끝나면, 각각의 심볼들이 서로 연결을 맞췄다는 뜻이 된다. 예를 들어, pbl.asm에서 사용하는 `_main` 심볼이 실제로는 sfl.asm에 있다는 것을 이 시점에서는 알게된다. 컴파일 및 어셈블 후에 생성되는 오브젝트 파일은 재배치 가능한 오브젝트 파일을 만든다. 이 재배치 가능한 오브젝트 파일은 각 섹션을 기준으로 주소가 0번지 부터 할당되는 파일을 의미한다. 예를 들어 아래의 pbl.asm 파일을 보자.

    SECTION .data
        arr:    db 0,1,2,3
    
    extern _main
    
    SECTION .text
    global _pbl_start
    
    _pbl_start:                                                                                          
        call _main
        jmp $               
    
    sec_start:
        mov bx, 0x7e00

     

    " 위의 내용을 어셈블하고 `nm` 명령어를 통해 pbl.o의 심볼 테이블을 확인해보면 아래와 같다.

     

    $ nasm -f elf -g3 -F dwarf pbl.asm -o pbl.o
    $ nm pbl.o 
    00000000 d arr
             U main
    00000000 T _pbl_start
    0000001f t sec_start

     

    " 원래라면 `_main` 심볼을 보고 에러를 발생시켜야 한다. 왜냐면, 내부에 어디에도 `_main` 심볼이 선언되어 있지 않기 때문이다. 그런데 어셈블러 및 컴파일러는 extern 키워드를 보면 다른 모듈에서 해당 심볼을 선언했다고 가정하고 일단 그냥 넘어간다. 그리고 실제 심볼 맵핑과정은 링커에게 떠 넘긴다. 근데 만약, 링커에서도 해당 심볼에 대한 선언 내용을 찾지 못하면 링커는 링킹 에러를 출력하고 해당 링킹 과정을 종료한다.

     

    " 첫 번째 컬럼은 주소를 의미한다. 흔히 컴파일러 및 어셈블러를 통해 나온 오브젝트 파일은 재배치가 가능한 오브젝트 파일이다. 이 파일들은 각 섹션의 주소를 0번지로 설정하여 심볼의 주소를 설정한다. 0번지로 놓는 이유는 나중에 링커를 통해 각 심볼들을 재배치할 때, 0을 기준으로 하는 것이 편하기 때문이다. 뒤에서 `섹션 병합`과 `섹션 배치` 그 이유를 다시 한 번 제대로 살펴본다.

     

    " 두 번째 컬럼인 심볼 타입이 중요하다. 심볼 타입은 소문자는 local, 대문자는 global을 의미한다. 그래서 `d`는 local 데이터 섹션을 의미한다. 즉, pbl.asm 파일내에서만 참조 가능한 심볼이라는 뜻이다. `U`는 undefined 심볼을 이라는 의미로, pbl.asm 내에서 extern으로 사용하는 심볼을 의미한다. 즉, main 심볼은 pbl.asm에서 선언하지는 않고, 외부 어딘가에서 선언했다는 의미를 나타낸다. 이걸 pbl.asm에서 사용하겠다는 뜻이다. `T` 텍스트를 의미하는데, 거의 함수라고 보면 좋을 듯 싶다. 위의 `_pbl_start` 심볼은 간단한 예시이기 때문에, 함수 형태를 띄지는 않았지만 대개 `T` 심볼 타입을 가지면 함수라고 보면 된다. 당연히 대문자인 이유는 `global _pbl_start`로 되었기 때문이다. `t`는 `global` 지시어로 선언되지 않은 텍스트 심볼을 의미한다. 여기서는 `sec_start` 심볼이 그러하다. 이 심볼은 외부에서 접근이 불가능 하다고 보면 된다. C언어의 static 함수와 같다고 보면 된다. `a` 라고 표시되는 것들은 상수값을 의미한다. 이 `a` 타입은 메모리 위치가 고정되어서 링커에 의해서도 변경되지 않은 절대적 위치(absolute)를 나타낸다. 예를 들어, 우리가 C에서 자주 사용하는 문자열 상수나, DEFINE 상수값들을 의미한다.

     

    " 링커의 재배치 과정은 2가지 과정을 통해 진행된다.

    1" 섹션 병합
    2" 섹션 배치

     

    " 그런데 위의 두 과정을 알기 전에 섹션이라는 것이 왜 생겼는지 부터 알아봐야 한다.

     

    - 섹션

    " 본질적으로 섹션을 나누는 것의 의미는 효율성 때문이다. CPU에게는 실행 명령어를 전달해야 한다. 즉, 코드를 전달해야 한다는 말이다. 데이터 선언같은 코드는 컴파일 시점에 끝내고, 실제 프로그램이 동작할 때는 코드를 실행해야 한다. 아래의 예시를 보자.

    ; pbl.asm
    SECTION .data
        arr:    db 0,1,2,3
    
    extern _main
    
    SECTION .text
    global _pbl_start
    
    _pbl_start:                                                                                     
        call _main
        jmp $               ; Nevere come here
    
    sec_start:
        mov bx, 0x7e00

     

    " 데이터 섹션과 텍스트(코드) 섹션이 각각 존재하는 파일이다. `objdump`를 통해 기계어를 확인해보자.

     

    $ objdump -D pbl.o 
    
    pbl.o:     file format elf32-i386
    
    
    Disassembly of section .data:
    
    00000000 <arr>:
       0:	00 01                	add    %al,(%ecx)
       2:	02 03                	add    (%ebx),%al
    
    Disassembly of section .text:
    
    00000000 <_pbl_start>:
       0:	e8 fc ff ff ff       	call   1 <_pbl_start+0x1>
       5:	eb fe                	jmp    5 <_pbl_start+0x5>
    
    00000007 <sec_start>:
       7:	66 bb 00 7e          	mov    $0x7e00,%bx

     

    " 데이터 섹션과 텍스트 섹션과 기계어가 많이 다르다. CPU는 데이터 섹션에 있는 기계어들은 런타임에 실행하지 않는다. 이미 컴파일 및 어셈블 시점에 데이터 섹션들은 모두 메모리 할당이 완료된다. 그래서 실제 프로그램이 로딩되어 실행될 때, 저 데이터들은 선언이 아니고, 참조만 된다. 왜냐면, 이미 컴파일 및 어셈블 시점에 데이터 섹션 영역의 심볼들은 메모리가 할당되기 때문이다. 

     

    " 그래서 만약 아래와 같이 텍스트 섹션 영역에 데이터를 선언하면 런타임시에 에러가 발생할 것이다.

    ; pbl.asm
    
    extern _main
    
    SECTION .text
    global _pbl_start
    
    _pbl_start:                                                                                     
        call _main
        arr:    db 0,1,2,3
        jmp $               ; Nevere come here
    
    sec_start:
        mov bx, 0x7e00

     

    " 데이터 섹션을 없애고, 오직 텍스트 섹션만 존재한다. 텍스트 섹션안에 변수를 선언하는 코드가 들어갔다. 기계어로 바꾸면 어떻게 해석될까?

     

    $ objdump -D pbl.o 
    
    pbl.o:     file format elf32-i386
    
    
    Disassembly of section .text:
    
    00000000 <_pbl_start>:
       0:	e8 fc ff ff ff       	call   1 <_pbl_start+0x1>
    
    00000005 <arr>:
       5:	00 01                	add    %al,(%ecx)
       7:	02 03                	add    (%ebx),%al
       9:	eb fe                	jmp    9 <arr+0x4>
    
    0000000b <sec_start>:
       b:	66 bb 00 7e          	mov    $0x7e00,%bx

     

    " 그리고 섹션을 나눈다는 것은 각 섹션에 대하여 해당 코드 및 데이터들을 정렬시키게 된다. 아래 그림을 보자.

    출처 -&nbsp;http://bravegnu.org/gnu-eprog/linker.html

     

    " 위에서 데이터 섹션은 2군데에 선언되어 있다. 텍스트 섹션도 마찬가지로 2군데에 선언되어 있다. 각 섹션을 나누고, 해당 섹션을 0번지로 가정하면 위에서 오른쪽 그림과 같이 잘 정렬된 모습과 같이 섹션이 나뉜 것을 볼 수 있다. 이제 CPU는 텍스트 섹션만 실행하면 된다. 참고로, BSS 섹션과 데이터 섹션을 나누는 이유는 이 글을 참고하자.

     

     

     

    - 섹션 병합

    " 아래의 2개의 파일이 있다.

    ; pbl.asm
    SECTION .data
        arr:    db 0,1,2,3

    extern _main

    SECTION .text
    global _pbl_start

    _pbl_start:                                                                                     
        call _main
        jmp $               ; Nevere come here

    sec_start:
        mov bx, 0x7e00
    ;sfl.asm
    SECTION .data
        brr:    db 3,2,1,0

    SECTION .text
    global _main

    _main: 
        mov ax, 0x10

        mov ds, ax
        mov es, ax
        mov fs, ax
        mov gs, ax

        jmp _two

    _two:
        mov ss, ax
        mov ebp, 0x7C00
        mov esp, 0x7C00

     

    " 2개를 어셈블하고 각 파일의 심볼 테이블은 다음과 같다.

     

    $ nm pbl.o
    00000000 d arr
                     U _main
    00000000 T _pbl_start
    00000007 t sec_start
    $ nm sfl.o 
    00000000 d brr
    00000000 T _main
    0000000e t _two

     

    " 이제 링킹을 통해 2개의 파일을 하나로 합쳐보자.

     

    $ i686-elf-gcc -ffreestanding -O2 -nostdlib -Ttext=0x00000000 pbl.o sfl.o -o bl.elf
    $ nm bl.elf 
    0000102c d arr
    00001030 d brr
    00001034 D __bss_start
    00001034 D _edata
    00001034 D _end
    00000010 T _main
    00000000 T _pbl_start
    00000007 t sec_start
             U _start
    0000001e t _two

     

    " 몇 가지 링커를 통해 ELF 포맷으로 만들면 몇 가지 디버깅 심볼이 포함된다. 예를 들면, `__bss_start`, `_edata`, `_end`, `_start` 심볼등이 추가된다. `_start` 심볼은 링커에게 실행 파일의 엔트리 포인트를 지정해주지 않을 경우, 기본으로 작성되는 심볼이다. 별도로 엔트리 포인트를 지정해주려면 `-e` 옵션을 사용하면 된다.

     

    " 그리고 데이터 섹션은 일반적으로 텍스트 섹션 뒤쪽에 배치된다. 텍스트 섹션과 읽기 전용 데이터 섹션은 제일 앞쪽에 배치된다. 그리고 여기서는 데이터 섹션이 0x1024 부터 할당되는 것으로 보이는데, 왜 `arr` 심볼은 0x0000102C에 할당되었을까? 앞에서 말한 것처럼 ELF 포맷은 데이터 섹션맨앞에 디버깅 관련 데이터 심볼을 선언해놓는다.

     

    " 위의 순서를 보면 알 수 있겠지만, pbl.o 파일이 앞쪽으로 배치되고 그 뒤로 sfl.o 파일이 배치된 것을 알 수 있을 것이다. 그래서 텍스트 섹션만 순서를 다시 바꿔서 보면 아래와 같다.

     

    $ nm bl.elf 
    00000000 T _pbl_start
    00000007 t sec_start
             U _start
    00000010 T _main
    0000001e t _two

     

    " 이렇게 각 파일의 동일한 이름을 갖는 섹션을(여기서는 텍스트 섹션) 하나의 섹션으로 합치는 것을 `섹션 배치`라고 한다. 이때 섹션 배치는 단순하게 파일을 앞과 뒤로 그냥 붙이는 것이다. 그래서 뒤쪽에 붙는 파일의 시작 주소는 앞의 파일 사이즈에 따라 달라진다. 

     

     

    - 섹션 배치

    " 섹션의 배치는 저렇게 합쳐진 파일을 어디에 배치할 것인가에 대한 내용이다. 예를 들어, 위에서는 텍스트 섹션을 `0x00000000` 에 배치했지만, 다른 곳에 배치할 수도 있다.

    $ i686-elf-gcc -ffreestanding -O2 -nostdlib -Ttext=0x2000 pbl.o sfl.o -o bl.elf
    $ nm bl.elf 
    0000302c d arr
    00003030 d brr
    00003034 D __bss_start
    00003034 D _edata
    00003034 D _end
    00002010 T _main
    00002000 T _pbl_start
    00002007 t sec_start
             U _start
    0000201e t _two

     

    " 텍스트 섹션의 시작주소를 `0x2000`으로 변경했다. `_pbl_start` 심볼의 위치가 0x2000으로 변경된 것을 확인할 수 있다. 여기서 중요한 건 섹션 배치는 결국 각 섹션들의 시작 주소를 변경하는 작업을 의미한다. 

     

    " 링커의 재배치를 아래의 그림으로 정리해보자.

    출처 -&nbsp;http://bravegnu.org/gnu-eprog/linker.html

    http://bravegnu.org/gnu-eprog/linker.html

     

     

    " a.s와 b.s 파일이 있다. 2개 모두 텍스트 섹션만 존재한다. 2개의 파일이 `병합` 과정을 거치면, a.s가 앞쪽으로 b.s가 뒤쪽으로 배치되는 것을 볼 수 있다. 그리고 `배치` 과정을 통해 재배치 가능한 목적 파일의 시작주소인 0x0000_0000을 0x2000_0000으로 바꿈으로써 실행이 가능한 목적  파일로 바꾸고(링킹) 있다.

    '프로젝트 > 운영체제 만들기' 카테고리의 다른 글

    [리눅스] - printf & printk  (0) 2023.06.19
    참고 자료  (0) 2023.06.14
    세그먼테이션  (0) 2023.06.07
    Higher Half Kernel  (0) 2023.06.06
    [x86] 인터럽트  (0) 2023.06.05
Designed by Tistory.