강좌
클라우드/리눅스에 관한 강좌입니다.
리눅스 분류

리눅스 커널과 디바이스 드라이버 프로그래밍

작성자 정보

  • 웹관리자 작성
  • 작성일

컨텐츠 정보

본문

리눅스 커널과 디바이스 드라이버 프로그래밍

 

강사소개 :  백창우

동국대학교에서 컴퓨터공학을 전공했고 RTOS 개발, 리눅스 커널, 디바이스 드라이버, 임베디드 시스템등과 관련된 이론 및 실무 경험을 보유하고 있다. 유닉스, 리눅스 프로그래밍 필수 유틸리티, TCP/IP 소켓프로그래밍 등의 단행본을 저술했다.

 

 

첫번째 강좌 : 리눅스와 리눅스 커널, 어떻게 다르지?

 

 

필자가 리눅스와 처음 인연을 맺게 된 때는 1997년이다. 당시 리눅스를 소개하는 대부분의 책들은 항상 리눅스는 핀란드 헬싱키 대학의 리눅스 토발즈가 만든 운영체제로서…” 와 같은 말로 시작하곤 했다. 요즘 나오는 책들에게도 가끔씩 이런한 문구를 볼 수 있는데 예전에 비해서 현저하게 줄어든 것을 확인할 수 있다. 왜냐하면 누구나 그 사실을 알고 있기 때문이다. 더 나아가 IT분야에서 성장하기 위해서라면 리눅스를 몰라서는 안되는 시대가 됐다. 리눅스에 대한 이해를 위해 리눅스 커널부터 살펴보자.

 

국내에 리눅스가 처음 소개됐던 10년 전에는 리눅스를 아는 사람보다 모르는 사람이 더 많았다고 리눅스를 OS로 사용한다는 사실만으로도 별종 취급 받기가 일쑤였다. 하지만 세상이 바뀌어서 리눅스는 이제 우리 IT 산업 전 분야에서 널리 활용되고 있다. 특히 서버, 임베디드분야에서 맹활약을 펼치고 있는데 2005년도 IDC 발표자료와 KIPA 발표 자료에 따르면 서버 시장에서 19.1%를 전체 임베디드 시장의 31%를 차지하고 있는 명실상부한 메이저 운영체제로 자리잡았다. 이러한 사실만 놓고 볼 때 IT분야에서 성장하기 위해서라면 이제는 더 이상 리눅스를 몰라서는 안된다는 사실을 확인할 수 있다. 필자는 이 코너를 통해 리눅스 커널(kernel)에 대해 소개하고 실질적으로 리눅스 커널은 어떠한 과정을 거쳐 부팅되는가를 소스 레벨에서 설명하며, 리눅스에서 디바이스 드라이버를 어떻게 만드는가에 대해서 설명하고자 한다.

 

* 리눅스란 무엇인가?

 

흔히들 레드햇(RedHat) 리눅스, 데비앙(Debian) 리눅스 , 젠투(Gentoo) 리눅스라고 말하지만 엄밀한 의미에서 리눅스는 리눅스 커널만을 의미한다. 우리가 소위 말하는 레드햇이니 데비앙, 젠투 리눅스는 배포판을 의미하지, 리눅스 자체를 의미하지 않는다. 리눅스 커널은 리누스 토발즈를 위시한 세계 각지의 개발자에 의해 오늘도 활발하게 개발되고 있다. 이렇게 개발되는 이 커널을 리눅스라 말한다.

이러한 커널에는 말 그대로 커널만 있기 때문에 이것만 가지고는 아무것도 할 수 없다. 이 커널을 가지고 각 개발 업체들이 컴파일러(gcc, g++), 편집기(vi, emacs ), 웹브라우저(모질라 등)과 같은 응용 어플리케이션을 추가해서 만든 것이 리눅스 배포판이다. 배포판은 배포판을 만드는 업체에서 붙이는 이름에 따라 레드햇, 데비앙, 젠투 등으로 나뉘어진다. 배포판이 다르다고 해서 리눅스 커널이 달라지는 것은 아니다. 배포판이 다름으로 인해 포함되는 응용 어플리케이션의 종류는 다르지만 리눅스 커널은 버전이 다른다는 것과 벤더에 따라 약간의 수정이 가해졌다는 것 빼고는 동일하게 사용되고 있다.

이 리눅스 커널은 현재 2.6.16 버전까지 개발됐는데 해가 바뀔 때마다 개발 속도를 더해가고 있다. 이는 리눅스 커널개발에 많은 사람들이 참여하는 것이 가장 큰 이유겠지만 리누스 토발즈가 이전에 근무하던 트랜스메타를 떠나 오픈소스 개발연구소(OSDL)에서 리눅스 kernel 개발에 전념하는 것도 이유가 될 수 있을 것이다.

 

* 리눅스 커널의 이해

 

리눅스 커널 버전은 linux-2.4.18 혹은 linux-2.6.16과 같은 형식을 띄고 있다. 이는 전통적인 유닉스의 버전 표기법이 약간 변형된 형태이다. 제일 앞에 있는 숫자는 메이저 버전이라고 해서 커널의 구조나 기능에 급격한 변화가 있을 때마다 바뀌는 버전이다. 두번째 오는 숫자는 마이너 버전이라고 해 메이저급처럼 큰 변화는 아니지만 내부 구조면에서 많은 변화가 있을 때 바뀌는 버전이다.

이 마이너 버전이 2.6.x 와 같이 짝수인 경우에는 안정된 커널 소스임을 의미하며 2.5.x 와 같이 홀수인 경우에는 개발 버전을 의미한다. 마지막으로 오는 숫자는 패치 버전이라고 해 기능, 구조적 변화는 없지만 버그등으로 인해 소스가 약간 수정됐을 때마다 바뀌는 버전이다. 그리고 간혹 2.6.16.9 와 같이 패치버전 다음에 숫자 또는 문자가 올 수 있는데 이는 개발자 개인이 붙이는 비공식 버전을 의미한다.

현재 리눅스 커널 버넌은 2.6.16 인데 이 버전정보만으로 우리는 리눅스 커널이 크게 두번째 바뀌었고 두번 바뀐 것중에서 3번째 안정 버전이고(홀수는 개발 버전) 패치는 16번 가해졌다는 것을 알 수 있다.

리눅스 커널의 구조를 그림으로 나타내면 아래와 같다.

 

image001.jpg

 

이 그림은 리눅스 커널의 구조를 잘 보여 준다. 사실 리눅스커널의 구조가 그림과 같이 명확하게 기능별로 나뉘어져 있지는 않다. 다른 OS들 특히 RTOS 에 비하면 리눅스 커널의 구조는 다소 경계가 모호한 편이다. 하지만 기능적으로는 이렇게 구분돼 있다고 보면 된다.

 

System Call Interface

 

시스템 콜 인터페이스는 user mode 프로세스인 응용 애플리케이션이 커널의 기능을 사용 가능하게 해준다. 유닉스 시스템의 대표적인 특징 중에 하나는 user mode kernel mode가 나뉘어져 있다는 것이다. 리눅스 역시 처음 시작은 유닉스를 모델로 만들어졌기 때문에 user mode kernel mode 로 나뉘어져 있다.

Kernel mode는 특권 mode로서 모든 주소 공간에 접근할 수 있고 모든 명령(instruction)을 수행할 수 있다. 그러나 user mode 에서는 user mod에 할당된 주소 공간만 접근할 수 있고 kernel mode의 주소 공간에 대해서는 접근 불가능하다. 또한 시스템에 치명적인 영향을 끼칠 수 있는 특권 명령(lgdt, lidt, cli, sti )들은 사용할 수가 없다.

이렇게 user mode kernel mode를 나누어 놓은 이유는 응용 애플리케이션의 비정상적인 혹은 악의적인 수행에 대해서 시스템을 보호하기 위한 것이다.

응용 애플리케이션은 user mode에서 동작하기 때문에 커널 영역에 있는 커널 함수 또는 커널 자료 구조에 대해 접근할 수 없다. 응용 애플리케이션이 커널 영역에 있는 커널 함수 또는 자료 구조를 사용하기 위해서는 반드시 시스템 콜을 사용해야 한다.

시스템 콜은 소프트웨어 인터럽트를 사용해 구현된다. 응용 애플리케이션은 소프트웨어 인터럽트 명령어(ex: int 0x80, swi)를 명시적으로 호출해서 시스템 콜을 호출한다.

 

Memory Management

 

메모리 자원은 시스템에 있어서 매우 귀중한 자원이다. 메모리를 얼마나 효율적으로 관리하느냐에 따라 시스템 성능이 결정된다. 메모리를 비효율적으로 관리한다면 메모리를 할당/해제하는데 많은 시간이 걸리고 단편화로 인해 남아 있는 공간을 제대로 활용하지 못하는 문제가 발생한다.

메모리 매니지먼트는 시스템에 있는 메모리 자원을 관리하는 부분으로서 리눅스는 메모리 관리 알고리듬으로 buddy system slab할당자를 사용한다.

 

Task Management

 

태스크 매니지먼트는 태스크를 관리하는 부분이다. 태스트의 생성,소멸,중단 등을 담당한다. 태스크를 스케줄링하고 우선순위를 집행하고 등의 일이 모두 태스크 매니지먼트에서 이루어지는 일이다.

 

IPC(Interprocess Communication)

 

IPC는 프로세스간  통신을 담당하는 부분이다. 프로세스 모델에서는 페이징(paging)기법을 사용해 가상 주소를 사용하는데 각 프로세스에는 각기 다른 4GB(아키텍처마다 다름. i386, arm ) 크기의 가상 주소 공간을 가지게 된다. 이렇게 되면 프로세스 A 0x100번지와 프로세스 B 0x100번지는 가상 주소는 같지만 물리 주소는 완전히 다른 곳을 가르키게 된다.

페이징기법을 사용해 프로세스의 주소 공간을 분리함으로 생기는 장점으로는 다른 프로세스의 비정상적 혹은 악의적 수행으로부터 보호받을 수 있고 요구 페이징/swapping 기법을 사용해 적은 메모리를 가지고도 4GB 메모리가 있는 것처럼 사용할 수 있으며 애플리케이션을 컴파일할 때 수행 위치에 대해서 고민하지 않아도 된다는 등 많은 장점이 있다.

반면 단점으로는 가상 주소를 물리 주소로 변화하는데 오버헤드가 발생하고 프로세스간 통신에 있어서 쉽지 않다는 문제가 발생하게 된다.

IPC는 프로세스간 통신 기능을 제공해 주는 부분으로서 리눅스에서 IPC signal, message queue, shared memory, semaphore 등을 제공하고 있다.

 

VFS(Virtual File System)

 

가상 파일 시스템은 각기 다른 파일 시스템과 디바이스 드라이브 등에 대해서 동일한 인터페이스를 제공하는 부분이다. 예를 들면 이렇다. ext2 파일 시스템에 있는 텍스트 파일에 데이터를 쓰던 vfat 파일 시스템에 있는 텍스트 파일에 파일을 쓰던 혹은 사운드 카드에 소리가 나게 데이터를 보내든 모두 write 시스템 콜을 사용한다.

이렇게 각기 다른 파일 시스템, 디바이스에 대해서 open, read, write, close 등과 같은 동일한 인터페이스 사용할 수 있게 해주는 부분이 바로 vfs이다.

리눅스가 이렇게 성장할 수 있게 된 이유중의 하나가 바로 이 VFS때문이다. VFS가 있음으로 인해 리눅스는 여러 파일 시스템을 사용할 수 있게 되었고 그럼으로 여러 운영체제와 함께 공존해서 사용하는데 장점이 있었다.

 

BSD Socket Interface

 

bind, connect, accept, send, recv 등으로 대변되는 BSD socket interface 를 제공하는 부분이다.

 

File System

 

ext2, ext3, vfat, jfs 등의 파일 시스템을 제공하는 부분이다.

 

Network Protocol Stack

 

ipv4, ipv6, atm, x25 와 같은 프로토콜 스택을 가지고 있는 부분이다.

 

Device Driver

 

하드디스크, 키보드, 마우스, CD롬등 디바이스 드라이버를 포함하고 있는 부분이다. 예전에 리눅스는 디바이스 드라이브 지원이 미미해 사용하기 힘든 운영체제였다. 특히나 X-windows 시스템을 띄우기 위해서는 그래픽 카드 디바이스 드라이브가 있어야 하는데 대부분 사람들의 그래픽 카드를 리눅스에서 지원해 주지 못했었다. 때문에 X-windows 화면을 본 사람들을 축복받은 사람이라고 부르던 시절이 있었다. 현재는 물론 지난날 추억일 뿐이다.

 

출처 : 공개 SW 리포트 1 60 ~ 63 페이지 발췌(2006 6) - 한국소프트웨어 진흥원 공개SW사업팀 발간

 


리눅스 커널과 디바이스 드라이버 프로그래밍

 

 

두번째강좌 : 리눅스 커널, 어떻게 움직이나?

 

 

리눅스를 보다 기술적으로 이해하기 위해 리눅스 커널에 대해 살펴 봤다. 리눅스커널이 어떤 구조로 되어 있는 지 어떤 기능을 제공하는 지는 리눅스 관련 서적에서 대부분 다루고 있으나 리눅스커널의 소스를 하나씩 분석, 짚어봄으로써 리눅스 커널을 보다 잘 이해할 수 있다. 커널은 매우 많은 파일과 방대한 자료 구조로 이뤄져 있으므로 방대한 소스를 분석하기 위한 도구의 종류와 사용법을 익혀야 할 필요가 있다. 그러면 리눅스 커널의 컴파일 과정을 알아 보고 커널분석을 위해 도움을 줄 수 있는 프로그램에는 어떤 것들이 있는 지 알아 보도록 하자.

 

지난호에는 리눅스 커널의 구조에 대해서 알아 봤다. 이러한 리눅스 커널의 구조는 기능적으로 리눅스 커널의 각 블록을 구분한 것으로서 실제로는 그림1과 같은 디렉토리 구조로 이뤄져 있다.

image002.jpg

[그림 1]

그림은 실제 리눅스 커널 2.6.13의 소스에 있는 디렉토리 구조를 나타낸 것으로 각 디렉토리에는 같은 종류의 소스파일들이 모여 있다.

이러한 소스 파일들은 사용자가 설정한 커널 옵션에 따라 선택적으로 컴파일, 링크돼 하나의 커널 이미지가 만들어 진다. 커널 옵션이란 make menuconfig 또는 make xconfig 명령을 통해 사용자가 설정하는 것으로서 커널의 기능을 추가/제거하고 커널의 특성을 결정지을 때 사용하는 것이다. 예를 들면 사용자가 make menuconfig 명령을 통해 vfat 파일 시스템을 커널에 추가했다면 CONFIG_VFAT_FS 커널 옵션이 활성화된다.

리눅스 커널은 다음과 같은 과정을 통해 컴파일된다.

 

먼저 커널소스의 top 디렉토리에서 make menuconfig 명령을 입력하면 (그림1)과 같은 과정이 일어 난다.

image003.jpg

 

(그림1) 2.4x 버전 대 리눅스 커널의 컴파일 과정을 나타낸 그림이다. 2.6.x 버전 커널 역시 위 과정과 크게 다르지 않기 때문에 2.4.x 버전 커널 컴파일 과정을 가지고 설명하고자 한다.

 

리눅스 커널 컴파일 과정

 

make menuconfig 명령을 입력하면 먼저 기존에 make menuconfig 시 생성한 심볼릭 링크 디렉토리인 include/asm 디렉토리를 지우고 컴파일하고자 하는 아키텍처의 헤더 파일이 들어 있는 디렉토리를 include/asm 디렉토리에 심볼릭 링크한다.

가령 컴파일하고자 하는 아키텍처가 i386이라면 include/asm-i386 디렉토리를 include/asm 디렉토리로 심볼릭 링크한다. 이는 커널 소스 내에서 아키텍처 종속적인 헤드 파일에 대해서 #include <asm/timer.h> 와 같은 형태로 참조하도록 돼 있기 때문이다.

커널 2.6에서는 조금 방식이 바뀌었는데 make zImage 또는 make bzImange 하는 순간에 include/asm-i386 디렉토리가 include/asm 디렉토리로 심볼릭 링크된다.

include/asm 심볼릭 링크를 만들었으면 scipts/lxdialog 디렉토리에 lxdialog 프로그램을 컴파일한다. make menuconfig 명령을 입력하면 푸른 화면의 메뉴가 나타남을 확인할 수 있을 것이다. 이는 ncurses 라이브러리를 사용하는 lxdialog 프로그램이 띄워 주는 것이다.

lxdialog make menuconfig 명령외에도 리눅스 응용애플리케이션의 화면 인터페이스를 위해 많이 사용되는 프로그램이다. 실제로 배포판에 따라 /usr/bin./lxdialog 또는 /usr/bin/dialog 프로그램이 존재함을 확인할 수 있다.

다음으로 scripts/Menuconfig 스크립트가 arch/i386/config.in 을 파싱해서 lxdialog의 인수로 넘겨주면 menuconfig 화면이 출력된다.

커널2.6에서는 이 방식이 약간 바뀌어 lxdialog 컴파일 이전에 scripts/kconfig/mconf 를 컴파일하게 되는데 scripts/kconfig/mconf arch/i386/Kconfig 를 파싱해서 lxdialog 의 인자로 넘겨 주도록 되어 있다.

2.4버전의 arch/i386/config.in 파일과 2.6버전의 arch/i386/Kconfig 파일은 커널 옵션의 종속 관계를 정리해 놓은 파일로서 여기에는 각 옵션에 대한 정의와 해당 옵션이 종속돼 있는 dhqtusemfd에 대해서 정리해 놓고 있다.

2.4 버전의 config.in 파일과 2.6버전의 Kconfig파일은 그 기능에서 동일하지만 문법적인 면에서 약간 차이가 있다. 2.4 버전의 config.in 파일은 문법이 다소 난잡하고 종속관계 기술이 힘들었던 반면 2.6 버전의 Kconfig 파일은 문법이 간결하며 종속관계 기술이 매우 쉽다.

makde menuconfig 명령을 내렸을 때 화면에 메뉴가 표시되는 것은 모두 config.in 또는 Kconfig 에서 메뉴를 정의하고 있기 때문이다. config.in 또는 Kconfi 파일을 바꾸게 되면 make menuconfig 화면을 바꿀 수 있게 된다.

사용자가 menuconfig 화면에서 커널 옵션을 설정하고 menuconfig 화면을 종료하면 include/linux/autoconf.h 파일과 .config 파일이 생성된다. include/linux/autoconf.h

파일은 C 헤더 파일로서 사용자가 선택한 커널 옵션이 C 매크로로 define 되어 있다. 나중에 autoconf.h 파일은 거의 대부분의 커널 소스에 include 되는데 #ifdef 문을 통해 커널 소스에서 선택적인 컴파일 또는 배열의 크기를 결정하는 등의 일에 사용된다.

.config 파일은 Makefile include 되는데 커널 옵션이 make 매크로로 정의되어 있다. .config 에 정의된 매크로를 참조해 make 는 컴파일해야 될 디렉토리 및 파일을 결정하고 링크 시 포함해야 될 object 에 대해서 결정하게 된다.

커널 옵션 설정이 끝났고 include/linux/autoconf.h 파일과 .config 파일이 생성됐으면 make dep 명령을 내리게 된다.

make dep 명령은 make 유틸리티와 관계된 내용으로서 각 소스 파일이 어떤 파일과 종속관계에 있는 지 파악해  .depend 라는 파일 종속 관계 정의 파일을 생성한다.  .depend 파일 역시 makefile include 돼 종속관계에 있는 파일이 변경시에 해당 소스 파일을 재 컴파일할 수 있게 한다. make dep 명령은 커널 2.4 까지 사용하였지만 커널 2.6 에서는 더 이상 사용되지 않고 있다.

다음으로 make zImage 또는 make bzImage 명령을 내려 커널을 컨파일하게 된다. 이 때 컴파일 되는 소스 파일은 사용자가 make menuconfig 를 통해 module dl 아니라 커널에 포함시킨 기능들에 대해서만 컴파일해서 링크하게 된다. 물론 커널 이미지에 포함할 지 모듈로 포함시킬 지는 make menuconfig 시에 생성된 .config 파일에 다 정의 되어 있다.

make zImage make bzImage 명령은 i386 아키텍처에서 커널 사이즈에 따라 로드되는 위치를 달리 할 때 사용되는 명령이다. 나중에 설명하겠지만 i386 아키텍처는 부팅 시 부트로드에 의해 0x90000 이후 번지에 사용되는데 0x10000 ~ 0x90000 번지까지 커널이 들어 간다면 make zImage 명령을 사용할 수 있다.

그러나 커널 사이즈가 커서 0x10000 ~ 0x90000 번지에 들어갈 수 없다면 make bzImage 명령을 사용해 컴파일해야 한다. make zImage 명령으로 컴파일하면 부팅시 커널을 0x10000 번지로 로드하고 make bzImage 명령으로 컴파일하게 되면 0x100000 위치에 로드하게 된다. i386 에서 bzImage zImage 의 차이는 부팅시 zImage 가 약간 빠르다는 것외에는 별반 차이가 없다.

 

리눅스 커널 소스를 분석해 보자

 

커널 이미지를 생성했으면 make modules 명령을 통해 사용자가 menuconfig module로 지정한 기능들에 대해서 컴파일하게 된다. 커널 module 은 파샬 링크된 elf object 로서 insmod 명령을 통해 커널 심볼 테이블을 참조해 최종 링크가 될 object 이다. 이 부분에 대해서는 나중에 자세히 다루도록 하겠다.

module 이 다 만들어졌으면 make modules_install 명령을 통해 module로 컴파일 된 object /lib/[커널버전] 디렉토리에 복사면 커널 컴파일 과정은 종료하게 된다.

지금까지 리눅스 커널의 대략적인 내용에 대해서 알아 봤다.

이제 리눅스 커널 부팅 과정에 대해서 알아 보자

리눅스 커널을 제대로 알기 위해서는 책만 보아서는 절대로 알 수 없다. 책을 보면 리눅스 커널이 대충 어떠한 기능을 제공하고 어떠한 구조로 돼 있다는 것을 알 수 있다. 하지만 그건 어디까지나 이론이다.

system call 을 예로 들자면 여러 많은 책들이 system call 에 대해서 설명하고 있는데 system call 의 이론은 책이 잘 설명하고 있지만 system call 의 구현에 대허서는 리눅스 커널 소스만큼 잘 설명해 놓은 것이 없다. 책을 보면 system call 을 이해할 수 있지만 system call 을 직접 구현할 수는 없다. 이론만 알고 구현을 못한다면 제대로 된 개발자 혹은 엔지니어라고 말할 수 있을까?

리눅스 커널 소스 분석 과정을 통해 리눅스 커널에 대한 막연한 두려움을 없애고 리눅스 커널의 부팅 과정을 이해하는데 도움을 얻을 수 있다. 필자의 견해로는 가장 중요한 것은 리눅스커널에 대한 막연한 두려움을 없애는 것이다. 리눅스 커널도 사람이 작성한 프로그램인 만큼 이해 못할 만큼 어려운 것이 아니다. 양이 많아서 그렇지 하나 하나 따져 보면 별것이 아니라는 사실을 확인할 수 있을 것이다.

불을 뿜어대는 무서운 용이 지키고 있는 공주를 구하기 위해 지혜로운 기사는 갑옷과 칼로 완전 무장을 하고 용과 맞서 싸운다. 커널 분석하는 것 역시 마찬가지이다.

커널은 매우 많은 파일과 방대한 자료 구조로 이뤄져 있어서 무턱대고 분석을 시도한다면 커널의 세계에서 길을 잃고 헤매고 만다. 초행길 여행을 떠날 때 적어도 길 안내를 해 줄 수 있는 지도와 나침반을 챙기는 것처럼 커널을 분석할 때도 방대한 양의 소스를 분석하기 위한 도구의 종류와 사용법에 대해서 간단하게 익혀야 할 필요가 있다. 그럼 간단하게 커널 분석을 위해 도움을 줄 수 있는 프로그램에는 어떤 것들이 있는 지 알아보도록 하겠다.

 

●  screen + vim + ctags + cscope

 

전통적인 unix/linux 환경에서 사용하는 소스 분석 프로그램이다. vim ctags cscop 를 연동해 소스 분석을 하는데 많은 편의를 제공한다. 예를 들면 ctags 를 사용하면 어떤 함수나 변수가 사용 되었을 때 그 함수나 변수가 정의된 곳으로 한번에 찾는 것이 가능하다. 그리고 cscope 는 어떤 변수나 함수가 사용된 모든 곳을 찾는데 편리하다.

각각의 유틸리티에 대해서 자세한 사용법을 알려 주면 좋겠지만 지면이 허락하지 않기 때문에 소개정도만 할까 한다. 해당 유틸리티는 unix/linux 개발 환경에서 필수적인 유틸리티이기 때문에 되도록 사용법을 익혀 두기 바란다.

 

●  lxr

 

커널 소스를 웹브라우저로 분석할 수 있게 해주는 도구이다. 독자들의 리눅스 서버에 직접 설치해서 사용해도 되고 설치하기가 귀찮으면 이미 설치돼 있는 lxr 을 사용하면 된다.

http://lxr.linux.no 에 가보면 이미 설치돼 있는 lxr 을 이용해 커널소스를 분석할 수 있다. vim + ctags + cscope 환경에 익숙하지 않은 독자들은 lxr 을 이용하기 바란다.

 

●  기타

 

기타 커널 소스를 분석하기 위한 여러 프로그램들이 많이 있다. 대표적인 프로그램들로써 source insite, source navigate 등이 있고 각자의 취향에 맞게 사용하면 된다. 필자가 생각하기에 전통적인 unix/linux 환경에서 소스 분석 프로그램으로는 vim + ctags + cscope 만한 것도 없다고 생각하는데 다소 배우기 까다로운 단점이 있다. 그렇지만 위에서 설명한 여러 도구들이 지원하는 모든 기능을 vim + ctags + cscope 에서 모두 제공하고 있고 또한 분석과 동시에 주석을 다는 것도 가능하기 때문에 vim + ctags + cscope 분석 환경을 추천한다.

다음호에는 리눅스 커널이 어떻게 부팅되는지 과정을 상세히 알아 보겠다.

 

 

출처 : 공개 SW 리포트 2 66 ~ 69 페이지 발췌(2006 8) - 한국소프트웨어 진흥원 공개SW사업팀 발간


리눅스 커널과 디바이스 드라이버 프로그래밍

 

 

세번째강좌 : 리눅스 커널의 부팅 과정 해부하기

 

 

리눅스 이해를 돕기위해 리눅스 커널이 어떤 구조로 돼 있는지 어떤 기능을 제공하는 지 살펴봤다. 지난 호에서는 리눅스 커널의 컴파일 과정을 알아 보고 커널 분석을 위해 도움을 줄 수 있는 프로그램에는 어떤 것들이 있는 지 알아 봤으며 이번 호에는 리눅스 커널의 부팅 과정에 대해 알아 보고자 한다. 리눅스 커널의 부팅 과정은 하드웨어 아키텍처에 따라 또 CPU 에 따라 각각 다른데, 커널을 직접 고쳐서 사용해 볼 수 있는 임베디드 기기용 CPU를 통해 부팅과정을 알아 보기로 한다.

 

리눅스뿐만 아니라 거의 모든 OS의 부팅 과정에서 하는 일을 단순화시키면 두가지 일밖에 없다. 하드웨어 초기화,  커널 자료 구조 초기화가 바로 부팅과정에서 일어나는 일이다. 이 중 하드웨어 초기화는 프로세서에 따라 달라지는 부분이라서 아키텍처 종속저깅ㄹ 수 밖에 없다. 이제 리눅스 커널의 부팅 과정을 하나씩 짚어 보자. 앞서 언급한 대로 어떤 하드웨어 아키텍처인가에 따라 하드웨어 초기화부분은 달라질 수 있다. 예를 들면 x86 과 같은 아키텍처는 부팅할 때 segmentation 관련 주소 레지스터를 초기화하는데 이러한 segmentation x86 아키텍처에만 있으며 ARM 과 같은 아키텍처에서는 존재하기 않기 때문에 관련 설정도 필요 없다. 이렇듯 하드웨어 초기화 부분이 아키텍처마다 다르기 때문에 우리는 부팅 코드 분석에 앞서 어떠한 아키텍처를 중심으로 분석을 할 것인지 결정을 내려야 한다.

x86 아키텍처를 분석하면 가장 많이 사용하는 아키텍처이기 때문에 마음에 와 닿는 바가 클 것이다. 그렇지만 x86 아키텍처는 서버와 데스트톱 시장에서 주류를 이루고 있어 커널을 직접적으로 고쳐서 사용하는 일이 드물다. 때문에 커널을 분석하는 것에만 의의가 있고 분석한 것을 이용해 보기는 힘든 아키텍처이다. 따라서 임베디드 시장에서 광범위하게 사용되고 있는 ARM 아키텍처의 리눅스 커널-2.6.13 버전으로 분석해 보기로 한다.

그리고 같은 ARM 아키텍처라고 해도 CPU 종류에 따라서 core 또는 하드웨어 IP(Intellectual Property, : timer, interrupt controller, memory controller )가 다르기 때문에 하드웨어를 초기화하는 코드도 달라진다. 때문에 하드웨어 아키텍처와 함께 CPU도 정해서 분석할 것인데 여기서는 임베디스 시스템에서 광범위하게 사용되고 있는 삼성 S3C2440 CPU 의 커널을 분석하고자 한다.

 

리눅스 커널 부팅 과정의 전체 구조

 

그림 1 ARM 아키텍처에서 리눅스 커널의 부팅 과정을 도식화한 것이다. 리눅스 커널의 부팅 과정을 간단하게 보면 그림 1과 같다고 말할 수 있다. 물론 각 부분은 보다 복잡하게 돼 있다.

image004.jpg

 

이 과정을 간단하게 설명하면 다음과 같다. 먼저 최초 전원이 인가되면 ARM 아키텍처에서는 bootloader 코드가 수행된다. bootloader 코드는 보통 0x0 번지에 위치한다.

bootloader는 최초 커널을 RAM에 로딩하기 위해 clock 을 초기화하고 RAM 컨트롤러를 초기화한다. 그리고 커널을 메인 메모리에 로딩하고 제어권을 커널에게 넘긴다.

그러면 커널의 제일앞에 있는 코드를 수행하게 되는데 커널의 제일 앞부분에는 arch/arm/boot/compressed/head.S  커널의 압축을 푸는 코드가 들어 있다. 커널은 보통 사이즈를 줄이기위해 압축돼 있는데 head.S 는 커널이 동작하는 위치에 압축을 풀게 된다.

그리고 제어권을 실제 커널의 head arch/arm/kernel/head.S 로 옮기게 된다. 실제 커널의 head arch/arm/kernel/head.S는 주로 하드웨어 초기화 및 BSS 초기화, XIP 적용 등을 담당하고 커널의 main 함수격인 start_kernel 로 제어를 넘기게 된다.

start_kernel 함수에서는 실로 여러가지 일들을 하게 돼 있다. 이 그림에서는 간단하게 세가지 일로 추려 놓았지만 실제로 커널이 부팅하면서 하는 거의 대부분의 일은 startr_kernel 함수에서 해 주게 돼 있다.

startr_kernel 함수의 막바지에는 init이라는 kernel thread를 생성하는데 init kernel thread 는 각종 디바이스 드라이브의 초기화 루틴을 호출해서 디바이스들을 초기화해주고 / 디렉토리를 마운트 해 주게 된다. 마지막으로 execv 시스템 콜을 사용해 /sbin/init 프로세스를 생성하게 된다.

/sbin/init 프로세는 /etc/rc.d 에 있는 각종 초기화 스크립트를 수행해 기본적으로 수행해야 하는 데몬 프로세스를 수행시켜주고 최종적으로 mingetty 라는 가상 터미널을 띄워주게 된다. mingetty 는 수행과 동시에 login 프로세스를 수행하게 되는데 그러면 우리에게 친숙한 loggin: 프롬프트를 보게 된다.

매우 복잡한 절차같지만 이는 부팅 과정을 최대한 단순하게 설명한 것이다. 그러니 실제로 소스를 분석할 때는 얼마나 복잡할까? 실제 소스를 분석하는 데는 책으로 써도 모자랄 만큼이다. 때문에 앞으로 소스를 가지고 설명할 때는 중요한 부분만을 중점적으로 설명하도록 하겠다. 그럼 이제부터 본격적으로 소스 분석을 해 보도록 하겠다.

 

bootloader code 수행

 

이전에 커널 부팅 과정 전체 구조에서 설명했듯이 ARM 아키텍처는 전원이 인가되는 순간 CPU에서 0x0 번지에 있는 code를 수행하기 시작한다. 보통 0x0 번지에는 NORflash 메모리가 매핑돼 있고 그 속에는 u-boot, blob 등과 같은 bootloader 코드가 들어가 있다.

제일 처음 수행되는 bootloader 의 역할은 최종적으로 flash 또는 ROM에 있는 커널의 이미지를 RAM에 로드하는 일이다. RAM 은 주로 가격이 낮은 DRAM 이 많이 사용되는데 DRAM 을 사용하기 위해서 DRAM controller 를 초기화해줘야 한다. 그러기 위해서는 먼저 시스템에서 사용되는 clock 을 초기화 하는 일이 선행돼야 한다.

결국 bootloader 에서 가장 중요하게 하는 일은 커널을 RAM load 하기 위해 시스템 clock을 초기화하고 DRAM controller 를 초기화해서 DRAM 을 사용 가능하게 하고 최종적으로 커널 이미지를 DRAM에 로드하는 일이다.

이것이 bootloader 가 수행하는 일반적인 일이다. 임베디드 시스템의 종류에 따라서 bootloader 에서 해줄 일이 천차만별이기 때문에 일반적인 경우만 짚고 넘어가겠다.

 

arch/arm/boot/compresse/head.S 수행

 

bootloader 에서 커널을 미리 약속된 DRAM 주소에 로드하고 나면 커널 이미지의 제일 처음에 있는 arch/arm/boot/compressed/head.S 의 코드가 수행된다.

그림 2 linux-2.6 ARM 아키텍처 커널의 구조를 나타낸 그림이다.

image005.jpg

아키텍처마다 또는 커널의 버전마다 생성되는 커널의 이미지 구조가 약간씩 다르다. arch/arm/boot/compressed/Makefile 을 분석해 보면 어떤 모양으로 커널 이미지가 만들어 지는 지 알 수 있다.

그림 2에서 head.o head.S 가 컴파일된 object로서 커널의 이미지가 압축돼서 만들어진 piggy.o 의 압축을 풀고 커널이 컴파일될 당시 Base 주소로 잡힌 위치로 커널을 옮겨 놓는 역할을 담당한다. piggy.o gzip 알고리듬으로 압축된 커널에 elf 헤더를 붙인 커널 이미지로서 커널의 본쳉 해당한다. 마지막으로 misc.o 는 압축을 풀기 위한 gzip 알고리듬을 가지고 있는 object이다.

그럼 이제 본격적으로 head.S 에 대해서 알아 보자 head.S PIC(PositionINdependent Code)로 작성돼 있다. 때문에 head.S 는 메모리상의 어떤 위치에 두더라도 정상적으로 수행 가능하게 돼 있다.

맨 앞에 보면 쓸데없는 define 문으로 어지럽게 돼 있을 것이다. 부팅시 그 부분은 건너뛰고 아래와 같이 start 라벨에서 시작하게 된다.

image006.jpg

위 소스에서 (.)으로 시작하는 것들은 모두 assembly에서 특수한 지시를 하기 위한 assembly directive 이다.  .rept ~  .endr   .rept .endr 사이에 있는 명령을 .rept 다음에 오는 횟수만큼 반복해서 쓰라는 의미이다.

즉 실제 이 코드는 mov r0, r0 8번 코딩해 놓은 것과 동일한 것이다. r0 r0에 넣는 것은 아무 의미 없는 일이고 nop 8번 수행한 것과 동일하다.

image007.jpg

다음으로 bootloader 에서 받아온 아키텍처 ID r7 에 저장하는 것과 Angel bootloader 로 부팅했을 시 처리하는 일이 있는데 그냥 넘어가도 좋다.

다음으로  r0 LC0 의 주소를 저장하고 r0의 주소를 기준으로 r1,r2,r3,r4,r5,r6,ip,sp 레지스터의 값을 각각 로드하는데 각각에는 LC0의 값에서 r1의 값을 빼는데 r0에는 LC0의 주소가 들어 있고 r0에도 LC0의 주소가 들어 있게 된다.

정상적으로 커널이 로드됐다면 r0에서 r1을 빼면 0이 되기 때문에 beq not_relocated 명령에 의해 not_reloacated 라벨로 branch 하게 된다.

image008.jpg

위에서 보는 것이 LC0 라벨 이하로 잡혀 있는 데이터 값들이다. 각각의 데이터는 4byte로 돼 있고 이전에 본 code에 의해 각 레지스터의 값으로 채워지게 된다.

not_relocated 에서는 제일 먼저 BSS를 초기화하게 된다. 여기서 BBS는 커널의 BSS가 아니라 head.o, piggy.o, misc.o BSS 를 의미한다.

image009.jpg

나중에 커널 압축이 풀리고 커널의 진짜 head  arch/arm/kernel/head.S 가 수행되면서 커널의  BSS가 초기화되도록 한다.

BSS를 초기화 했으면 cache enable 한다. cache_on 라벨에서는 call_cache_fn 라벨로 점프한다.

image010.jpg

그리고 r1 r2에 각각 프로세서의 ID값과 mask 값을 받아와서 아래에 있는 proc_types structure 배열에서 프로세서의 ID값과 mask 값이 일치하는 것을 찾아 cache_on 함수를 수행시켜 주는데 S3C2440 CPU ARMv4ㅆ에 해당되기 때문에 아래 부분을 수행하게 된다.

결국 _armv4_cache_on을 점프해서 _armv4_cache_on 라벨이하로 수행되는데 _setup_mmu 를 호출해 MMU를 셋업하고 _common_cache_on 호출해 cache enable 하고 마지막으로 I-cache, D-cache, TLB flash 해 주는 역할을 수행하도록 돼 있다.

 

image011.jpg

MMU를 셋업하는 부분은 page table 을 초기화하는 부분이 다소 복잡하기 때문에 생략하기로 하고 cache enable 하는 부분인 _common_cahce_on 의 코드는 아래와 같다.

image012.jpg

먼저 TTB 레지스터에 초기화한 page table 의 시작주소를 넣는다. 그리고 도메인 접근 제러를 로드하고, I-cache enable 한다. 이 과정이 끝나면 다시 이전의 _armv4_cache_on 라벨에서 bl_common_cache_on 명령어 다음으로 돌아 가서 I-cache, D-cache, TLB flash 시켜 주고 mov pc, r12 명령어에 의해 이전에 not_relocated 라벨의 bl cache_on 다음 명령으로 돌아가게 된다.

html>

관련자료

댓글 0
등록된 댓글이 없습니다.

공지사항


뉴스광장


  • 현재 회원수 :  60,035 명
  • 현재 강좌수 :  35,798 개
  • 현재 접속자 :  131 명