Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>
단계별 자세한 분석

2.5. 단계별 자세한 분석

2.5.1. -Ttext 0x0의 의미

여기서 -Ttext 0x0에 대해 좀 알아보자.

.text

.global -start
-start:
test-val: .long test-data
	nop	
test-data:	.word 0xaa55

위와 같은 코드를 'gcc -E -traditional -o test.s test.S'로 컴파일 하면

# 1 "test.S"
.text

.global -start
-start:
test-val: .long test-data
	nop	
test-data:	.word 0xaa55

이렇게 되고 이를 다시 'as -o test.o test.s'로 어셈블하는데 -a를 사용해 중간 파일을 얻으면 다음과 같다.

GAS LISTING test.s 			page 1


   1              	# 1 "test.S"
   2              	.text
   3              	
   4              	.global -start
   5              	-start:
   6 0000 05000000 	test-val: .long test-data
   7 0004 90       		nop	
   8 0005 55AA     	test-data:	.word 0xaa55
   9 0007 90       	
AS LISTING test.s 			page 2


DEFINED SYMBOLS
              test.s:5      .text:00000000 -start
              test.s:6      .text:00000000 test-val
              test.s:8      .text:00000005 test-data

NO UNDEFINED SYMBOLS

test-val에 저장된 값은 test-data의 .text의 시작점에서 부터의 offset이다. 프로그램의 시작인 0에서 부터 5번째에 있단 소리다.

최종적으로 'ld -m elf-i386 -s --oformat binary test.o -o test.1'한 결과를 hex로 살펴보면 다음과 같다.

00000000	05 80 04 08 90 55 aa 90
0x90은 nop(no operation)을 의미한다.

그리고 'ld -m elf-i386 -Ttext 0x0 -s --oformat binary test.o -o test.2'한 결과는 다음과 같다.

00000000	05 00 00 00 90 55 aa 90

둘 사이의 차이점은 -Ttext 0x0가 있고 없고의 차이다. 바이너리 포맷의 경우 .text를 지정해 주지 않으면 시작 번지를 맘대로 정해버리므로 .text를 지정하지 않은 test.1에서는 시작이 0x09048000으로 설정되어 있는 것을 알 수 있다. 엉뚱한 곳의 값을 사용하도록 만들기 때문에 바이너리를 사용할 땐 제대로된 주소가 들어가도록 .text를 필요한 곳으로 지정해줄 필요가 있다.

또 .text의 시작을 0x02로 했을 때의 바이너리는 다음과 같다.

00000000	90 90 09 00 00 00 90 55 aa 90

위에서 보듯이 0xaa55는 offset이 7이지만 실제 지정된 것은 9로 .text가 2부터 시작하기 때문이다. 만약 이 바이너리를 메모리에 그대로 올려 놓는다면 제대로 된 값을 읽지 못할 수도 있다. 이 경우엔 .text가 2에서 시작하는 것을 염두에 두고 메모리에 적재해야 제대로 동작할 수 있다.

쉽게 하기 위해선 .text를 0에서 시작하게 하면 바이너리가 메모리의 어느 위치에 있던 상관없이 잘 동작할 수 있게 된다.

2.5.2. 분석

2.4절에서 살펴본 것과 같은 각 단계마다 자세한 내용을 살펴 본다. 이 절이 끝나면 이제 리눅스 커널이 어떻게 만들어지고 어떤 구조를 갖는지 완전히 알 수 있을 것이다.

1 ~ 12 단계는 vmlinux를 만들기 위한 한계이고 커널 설정을 어떻게 했는가에 따라 달라지므로 여기서는 다루지 않는다. 또 17, 18 단계도 bvmlinux를 만들기 위해 필요한 단계이므로 생략한다. 필요한 내용은 2.4절을 참조하거나 각자 log를 만들어 살펴보기 바란다.

  1. vmlinux

    (1)					
    ld -m elf-i386 -T /usr/src/linux-2.4.16/arch/i386/vmlinux.lds -e stext arch/i386/kernel/head.o arch/i386/kernel/init-task.o init/main.o init/version.o \
    	--start-group \
    	arch/i386/kernel/kernel.o arch/i386/mm/mm.o kernel/kernel.o mm/mm.o fs/fs.o ipc/ipc.o \
    	 drivers/acpi/acpi.o drivers/char/char.o drivers/block/block.o drivers/misc/misc.o drivers/net/net.o drivers/media/media.o drivers/char/agp/agp.o drivers/char/drm/drm.o drivers/ide/idedriver.o drivers/cdrom/driver.o drivers/sound/sounddrivers.o drivers/pci/driver.o drivers/pcmcia/pcmcia.o drivers/net/pcmcia/pcmcia-net.o drivers/pnp/pnp.o drivers/video/video.o drivers/md/mddev.o \
    	net/network.o \
    	/usr/src/linux-2.4.16/arch/i386/lib/lib.a /usr/src/linux-2.4.16/lib/lib.a /usr/src/linux-2.4.16/arch/i386/lib/lib.a \
    	--end-group \
    	-o vmlinux
    (2)					
    nm vmlinux | grep -v '\(compiled\)\|\(\.o$\)\|\( [aUw] \)\|\(\.\.ng$\)\|\(LASH[RL]DI\)' | sort > System.map
    (1)
    vmlinux는 커널 자체를 의미한다. 그래서 링크되는 오브젝트 들이 커널 설정에서 사용하겠다고 한 것들과 선택된 아키텍쳐에 관계된 것들이 뭉쳐져 하나의 파일을 만들어 낸다.

    사용된 옵션은 다음과 같다.

    • -m elf-i386

      어떤 포맷으로 출력물을 만들 것인지 지정

    • -T /usr/src/linux-2.4.16/arch/i386/vmlinux.lds

      링킹하는데 필요한 스크립트 파일을 지정한다. 이 파일에 대한 내용은 아래 vmlinux.lds를 참조하기 바란다.

    • -e stext

      프로그램의 시작점을 지정한다. 위 스크립트에 지정된 .stext를 사용한다.

    • --start-group ... --end-group

      ...에 지정된 오브젝트를 서로간에 참조한 변수나 함수가 에러나지 않을 때까지 계속해서 검색한다.

    • -o vmlinux

      출력물은 vmlinux로 지정

    링크에 사용된 스크립트(vmlinux.lds)는 아래와 같다.

    /* ld script to make i386 Linux kernel
     * Written by Martin Mares <mj@atrey.karlin.mff.cuni.cz>;
     */
    (1)
    OUTPUT-FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
    OUTPUT-ARCH(i386)
    ENTRY(-start)
    SECTIONS
    {
    (2)
      . = 0xC0000000 + 0x100000;
      -text = .;			/* Text and read-only data */
    (3)
      .text : {
    	*(.text)
    	*(.fixup)
    	*(.gnu.warning)
    	} = 0x9090
      .text.lock : { *(.text.lock) }	/* out-of-line lock text */
    
      -etext = .;			/* End of text section */
    
      .rodata : { *(.rodata) *(.rodata.*) }
      .kstrtab : { *(.kstrtab) }
    
      . = ALIGN(16);		/* Exception table */
      --start---ex-table = .;
      --ex-table : { *(--ex-table) }
      --stop---ex-table = .;
    
      --start---ksymtab = .;	/* Kernel symbol table */
      --ksymtab : { *(--ksymtab) }
      --stop---ksymtab = .;
    
    (4)
      .data : {			/* Data */
    	*(.data)
    	CONSTRUCTORS
    	}
    
      -edata = .;			/* End of data section */
    
    (5)
      . = ALIGN(8192);		/* init-task */
      .data.init-task : { *(.data.init-task) }
    
      . = ALIGN(4096);		/* Init code and data */
      --init-begin = .;
      .text.init : { *(.text.init) }
      .data.init : { *(.data.init) }
      . = ALIGN(16);
      --setup-start = .;
      .setup.init : { *(.setup.init) }
      --setup-end = .;
      --initcall-start = .;
      .initcall.init : { *(.initcall.init) }
      --initcall-end = .;
      . = ALIGN(4096);
      --init-end = .;
    
    (6)
      . = ALIGN(4096);
      .data.page-aligned : { *(.data.idt) }
    
      . = ALIGN(32);
      .data.cacheline-aligned : { *(.data.cacheline-aligned) }
    
      --bss-start = .;		/* BSS */
      .bss : {
    	*(.bss)
    	}
      -end = . ;
    
    (7)
      /* Sections to be discarded */
      /DISCARD/ : {
    	*(.text.exit)
    	*(.data.exit)
    	*(.exitcall.exit)
    	}
    
      /* Stabs debugging sections.  */
      .stab 0 : { *(.stab) }
      .stabstr 0 : { *(.stabstr) }
      .stab.excl 0 : { *(.stab.excl) }
      .stab.exclstr 0 : { *(.stab.exclstr) }
      .stab.index 0 : { *(.stab.index) }
      .stab.indexstr 0 : { *(.stab.indexstr) }
      .comment 0 : { *(.comment) }
    }
    (1)
    vmlinux의 포맷과 아키텍쳐를 지정하고 프로그램 시작점을 지정한다.
    (2)
    vmlinux의 시작 번지를 지정한다. 0x100000은 offset이고 앞의 0xc0000000은 gdt내에 들어갈 때 필요한 값으로 물리적으로는 0x100000 번지를 의미한다.

    여기서 부터는 다른 부분과 달리 gdt등이 설정된 상태인 프로텍티드 모드에서 동작 하므로 메모리 관련된 것을 실제 어드레스를 사용하면 안된다.

    (3)
    커널 코드가 위치할 곳이다. 0x9090은 빈공간에 채워넣기 할 때 0x9090을 사용하란 말이다.
    (4)
    특별히 지정되지 않은 모든 데이타는 여기에 위치한다. CONSTRUCTOR는 C++ constructor 정보를 여기에 기록하란 말이다.
    (5)
    arch/i386/kernel/init-task.c에 지정되어있고 프로세스 스택을 다루는 방식 때문에 8192 bytes 단위로 정렬되야한다.
    (6)
    arch/i386/traps.c에 정의되어 있고 Pentium F0 0F 버그를 피하기 위한 간단한 방법으로 페이지 정렬을 사용한다(페이지는 4096 bytes를 의미한다).
    (7)
    무시되고 사용되지 않는 섹션으로 vmlinux에 포함되지 않는다. 커널이 exit할 일은 없기 때문이다.

    (2)
    nm은 오브젝트 파일에서 심볼을 추줄해 주는 프로그램이다. 커널 이미지 파일에서 모든 심볼을 추출해내고 이 중에 필요한 부분만을 추려 System.map을 만든다. grep에 사용된 -v는 뒤에 나오는 경우를 제외한 것 들을 찾아준다. 커널 컴파일이 끝난 후 'nm vmlinux > test.map'만 한 결과와 System.map을 비교해 보면 grep에서 찾는 것들이 어떤 것인지 알 수 있을 것이다.

    System.map은 처음 부팅 때 메모리에 읽혀 올려지고 드라이버등이 커널 심볼을 찾을 때 사용한다.

  2. bbootsect

    gcc -E -D--KERNEL-- -I/usr/src/linux-2.4.16/include -D--BIG-KERNEL-- -traditional -DSVGA-MODE=NORMAL-VGA  bootsect.S -o bbootsect.s
    as -o bbootsect.o bbootsect.s
    ld -m elf-i386 -Ttext 0x0 -s --oformat binary bbootsect.o -o bbootsect
    bbootsect.s는 bootsect.S를 컴파일해 만들되 --BIG-KERNEL--을 정의해 만든다. bootsect.S를 살펴보면 이와 관련된 곳이 한 군데 있는 것을 발견할 수 있다.

    ld에 사용된 옵션은 다음과 같다.

    • -m elf-i386

      elf-i386를 에뮬레이션

    • -Ttext 0x0

      text 세그먼트의 시작을 0x0으로 지정

    • -s

      모든 디버깅 정보를 없앤다

    • -oformat binary

      bbootsect의 포맷은 바이너리

  3. bsetup

    gcc -E -D--KERNEL-- -I/usr/src/linux-2.4.16/include -D--BIG-KERNEL-- -D--ASSEMBLY-- -traditional -DSVGA-MODE=NORMAL-VGA  setup.S -o bsetup.s
    as -o bsetup.o bsetup.s
    ld -m elf-i386 -Ttext 0x0 -s --oformat binary -e begtext -o bsetup bsetup.o
    bbootsect와 같은 방법을 만든다.

  4. arch/i386/boot/compressed/piggy.o

    tmppiggy=-tmp-$$piggy; \
    rm -f $tmppiggy $tmppiggy.gz $tmppiggy.lnk; \
    objcopy -O binary -R .note -R .comment -S /usr/src/linux-2.4.16/vmlinux $tmppiggy; \
    gzip -f -9 < $tmppiggy > $tmppiggy.gz; \
    echo "SECTIONS { .data : { input-len = .; LONG(input-data-end - input-data) input-data = .; *(.data) input-data-end = .; }}" > $tmppiggy.lnk; \
    ld -m elf-i386 -r -o piggy.o -b binary $tmppiggy.gz -b elf32-i386 -T $tmppiggy.lnk; \
    rm -f $tmppiggy $tmppiggy.gz $tmppiggy.lnk
    piggy.o는 $(TOPDIR)/vmlinux를 압축해 만든다. 우선 vmlinux에서 심볼과 필요 없는 섹션을 없애고 바이너리 형태로 만든다음 gzip을 이용해 압축한다. 압축된 것을 다시 elf-i386 형태로 만들어 놓는다.

    ld에 사용된 옵션은 다음과 같다.

    • -m elf-i386

      elf-i386를 메뮬레이션

    • -b binary $tmppiggy.gz

      $tmppiggy.gz은 바이너리 형식

    • -b elf32-i386

      piggy.o는 elf32-i386 형식

    • -T $tmppiggy.lnk

      $tmppiggy.lnk를 사용해 링크한다.

    $tmppiggy.lnk의 내용은 다음과 같다.

    SECTIONS
    {
    	.data : {
    		input-len = .;
    		LONG(input-data-end - input-data)
    		input-data = .;
    		*(.data)
    		input-data-end = .;
    		}
    }

    압축된 vmlinux는 .data에 들어가게 되고 *(.data)로 표시된 곳에 들어가게 된다. 그 전후에 LONG(input-data-end - input-data)로 압축된 커널의 크기를 저장한다.

  5. arch/i386/boot/compressed/bvmlinux

    ld -m elf-i386 -Ttext 0x100000 -e startup-32 -o bvmlinux head.o misc.o piggy.o

    bvmlinux는 압축된 커널과 head.o, misc.o를 합쳐 만든다. head.o는 메모리 세팅이라고 보면되고 misc.o는 압축을 풀기 위한 코드가 들어있다. ld에 사용된 옵션은 $(TOPDIR)/vmlinux를 만들 때와 거의 흡사하다. 단 text의 시작 번지는 0x100000이다.

    부팅할 때 bvmlinux는 반드시 0x100000에 올려져서 실행되야한다. 그렇지 않으면 제대로 동작하지 않는다.

    압축된 커널의 크기를 piggy.o에 저장해 놓았기 때문에 메모리의 어느 위치에서 piggy.o가 끝나는지 알수 있다. 이뒤에 압축을 풀어 놓고 압축이 풀린 커널을 다시 0x100000으로 옮겨와 실행한다.

  6. build

    gcc -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -o tools/build tools/build.c -I/usr/src/linux-2.4.16/include

    build는 2.3.5절에서와 같이 동작하도록 만들어진다.

  7. bvmlinux.out

    objcopy -O binary -R .note -R .comment -S compressed/bvmlinux compressed/bvmlinux.out

    bvmlinux에서 필요 없는 것을 제외하고 바이너리로 만든다.

  8. bzImage

    tools/build -b bbootsect bsetup compressed/bvmlinux.out CURRENT > bzImage
    Root device is (3, 1)
    Boot sector 512 bytes.
    Setup is 4768 bytes.
    System is 899 kB

    build를 사용해 bzImage를 만든다. 지정된 루트 디바이스, 부트섹터 크기, setup의 크기 그리고 커널의 크기를 표시해 준다. build의 동작은 2.3.5절을 참조 바란다.