Notice
Recent Posts
Recent Comments
Today
Total
04-30 00:02
Archives
관리 메뉴

Jeongchul Kim

리눅스 커널 모듈 프로그래밍 본문

Embedded Linux

리눅스 커널 모듈 프로그래밍

김 정출 2015. 12. 21. 19:25

11.1 펌-웨어, 커널 모듈, 디바이스 드라이버 비교 분석  

하드웨어를 제어하기 위한 소프트웨어를 흔히 들 “임베디드 소프트웨어(Embedded Software)”라 부른다.

그리고 그러한 임베디드 소프트웨어로는 운영체제 없이 제어하는 “펌웨어(Firmware)”와 이 책에서 다루는 임베디드 리눅스(Embedded Linux)에서 동작하는 “커널 모듈(Kernel Module)”, “디바이스 드라이버(Device Driver)”가 있다.


Firmware

Embedded Linux Kernel Module, Device Driver

main() 함수가 있다.

main() 함수가 없다.

독립적으로 실행될 수 있다.

User Application 요청이 있을 때만 실행될 수 있다.

개발자 마음대로 소스 작성이 가능하다.

Linux에서 요구하는 문법대로 소스를 작성해야 함.


Firmware에서는 main() 함수가 있는데 반해, Linux에서 동작하는 Kernel Module과 Device Driver에는 main()함수가 없다는 점


main() 함수 1개 = 실행 파일 1개 = 실행 프로세스 1개


하나의 실행 파일을 만들기 위해 여러 개의 소스 코드 파일이 필요할 수도 있으나, 이 경우에도 main() 함수는 반드시 1개, 컴파일 결과로 생성되는 실행 파일도 결국 1개가 된다.


실행된 실행 파일이 시스템 메모리에 올라가게 되면(Loading) C 언어 상의 함수들은 “TEXT” 영역에, 전역 변수들 중 초기화 되어 있는 전역 변수들은 “DATA” 영역, 초기화되지 않은 전역 변수들은 “BSS” 영역으로

마지막으로 지역 변수들은 “STACK” 영역으로 나뉘어 메모리에 올라가게 된다.

위의 그림에서 Non-RTOS에서는 각 태스크(Task)가 모두 프로세스(Process)로 되어 있는데 반해, RTOS에서는

쓰레드(Thread)로 되어 있다.


프로세스(Process) : 실행에 필요한 각 메모리 영역 들(TEXT, DATA, BSS, STACK) 독립적으로 할당된 소프트웨어의 실행 단위


쓰레드(Thread) : 실행에 필요한 각 메모리 영역 들을 다른 태스크와 공유해야 하는 소프트웨어의 실행 단위


쉽게 말해, 프로세스(Process)는 그 자체가 하나의 완전한 프로그램이지만, 쓰레드(Thread)는 다른 누군가의 도움을 받아야 하는 종속적인 프로그램이라는 뜻이다.



Embedded Linux에서는 Kernel Module과 Device Driver 모두 하드웨어를 제어할 수 있다.

차이점으로 Kernel Module은 커널 실행 메모리 영역에 언제든지 필요할 때마다 넣었다 빼었다 할 수 있지만,

Device Driver는 한 번 커널 메모리 영역에 들어가게 되면 뺄 수가 없다.


본래 Kernel Module 이든, Device Driver든 둘 다 커널에 종속적인 프로그램이다.


리눅스 커널의 기능을 하나 확장하려면 결과적으로 확장된 기능을 구현한 소스를 기존 리눅스 커널 소스에 포함하여 컴파일하고, 플래시 메모리에 구워 넣는(Flash Programming) 일련의 작업을 진행해야 한다. 이런 개발 과정은 여러 번의 디버깅을 거치므로, 여러 번 진행하기 때문에 번거롭다.


따라서, 리눅스 개발자들은 바뀌지 않는 리눅스 커널 부분은 그대로 두고, 기능적으로 확장되는 부분들만

현재 실행 중인 리눅스 커널 메모리 영역에 삽입(추가)해서 실행시켜 보고, 만약 원하는 대로 동작하지 않으면 다시 해당 영역들을 삭제 시킬 수 있는 방법을 고안해냈다.


이 방법이 다른 운영체제에서는 볼 수 없는 “Kernel Module” 기법이다. 커널 소스가 공개되어 있는 Linux에서만 가능한 방법이다.


Device Driver 개발 작업도 개발 단계에서는 Kernel Module 방식으로 손쉽게 실행 및 디버깅을 하고, 최종적으로 개발이 완료되면 Device Driver로 커널 소스에 추가하여 개발을 종료하는 방식을 채택하고 있다.
































11.2 Kernel 2.4 버전과 Kernel 2.6 버전 간 차이    



Linux Kernel 2.4

Linux Kernel 2.6

컴파일 결과물

*.o (ELF relocatable)

*.ko (ELF Relocatable)

Makefile 문법

User Application 용 문법

Kernel 소스용 문법

스케줄러 지원

Task Queue 방식

Work Queue 방식: 선점(Preemption) 지원

인터럽트 지원

IRQ 방식만 지원

IRQ, FIQ 방식 모두 지원(ARM) :

arch/arm/kernel/ 소스 참조

소스 코드 문법

int device24_open()

{

    printk();

    MOD_INC_USE_COUNT;

    return 0;

}

int device24_release()

{

    printk();

    MOD_DEC_USE_COUNT;

    return 0;

}

Struct file_operations device24_fops =

{

    open:    device24_open,

    read:     device24_read,

    write:    device24_write,
    ioctl:     device24_ioctl,

    release: device24_release,

}

int device26_open()

{

    printk();

    return 0;

}


int device26_release()

{

    printk();

    return 0;

}


Struct file_operations device26_fops =

{

    .open     = device24_open,

    .read      = device24_read,

    .write      = device24_write,
    .ioctl       = device24_ioctl,

    .release   =  device24_release,

}

기타


USB 프로토콜 지원 원할

ALSA 방식의 사운드 제어 지원


리눅스 커널 2.4 버전까지는 사실상 일반 User Application이나 Kernel Module(or Device Driver) 간에 컴파일 하는 방법 자체는 큰 차이가 없었다. 그러다 보니 컴파일할 때 사용되는 Makefile 역시 문법적으로 일반 User Application 과 차이가 없었고, 당연히 그 결과물인 오브젝트 파일 역시 “*.o” 파일로 차이가 없었다.


리눅스 커널 2.6 버전부터는 이를 개선하여 Kernel Module(or Device Driver)은 일반 User Application과 구분하여, Makefile 문법도 실제 커널 소스를 컴파일할 때 사용되는 커널 소스 컴파일용 Makefile 문법을 적용하였고, 그 결과도 “*.ko(kernel object)”의 확장자로 만들었다.


* Test Queue : 어떤 시점에서 시스템 내에 존재하는 모든 태스크 제어 블록대기 행렬.


* IRQ : Interrput Request : 일반적인 PC 시스템에는 16개의 IRQ(0 ~15번)를 지원합니다. (PCI interrupt와 ISA interrupt) IRQ라는 것을 좀 쉽게 설명하자면 PC 내부의 어떤 장치가 시스템에 자기가 system의 어떤 부분을 사용하기 위해 system을 콜을 하는 것입니다. 따라서 일반적으로 PC 시스템 내부의 장치는 각각 하나의 고유 IRQ를 갖습니다. 어떤 장치가 몇 번 IRQ를 사용해야 하는지는 규정이 되어 있기도 하고, 그렇지 않은 경우도 있습니다. 규정이 되어 있지 않은 장치들에 대해서는 보통 처음 컴에 전원을 넣고 부팅을 하기 전 과정에서 바이오스(BIOS)가 자동적으로 특정 IRQ로 세팅을 해주는 경우도 있고, 부팅 후 O/S가 비어있는 IRQ를 특정 장치에 할당해주는 경우도 있습니다. 주로 그 장치의 driver에 setting 이 되어있는 경우도 많죠.


*FIQ : Fest Interrupt Request




Kernel 2.4 version Makefile

Kernel 2.6 version Makefile

CC = arm-linux-gcc

KERNEL_INC = /root/2.4.X/include

CFLAGS = -DMODULE -D__KERNEL__

             -I $(KERNEL_INC)


DEV_SRC = driver_src

APP_SRC = user_app


all: $(DEV_SRC).o $(APP_SRC)


$(DEV_SRC).o : $(DEV_SRC)..c

         $(CC) $(CFLAGS) -c $ (DEV_SRC).c


$(APP_SRC).o : $(APP_SRC)..c

         $(CC) $(CFLAGS) -c $ (APP_SRC).c


clean :

         rm -f *.o $(APP_SRC)

CC           := arm-linux-gcc

KERNEL     := /root/2.6.X

obj_m       := driver_src.o


APP_SRC    := user_app


PWD         := $(shell pwd)


default: $(APP_SRC)

       $(MAKE) -C $(KERNEL) SUBDIRS=$(PWD) modules


$(APP_SRC) : $(APP_SRC).c

         $(CC) $(APP_SRC).c -o $(APP_SRC)


clean :

         rm -f *.ko *.mod.* .*.cmd .tmp* *.o

         rm -f $(APP_SRC)


커널 2.4 버전에서는 변수 값을 할당할 때 “=”을 사용하는데, 커널 2.6 버전에서는 “:=”을 사용한다는 점이다.

“:=” 표기법은 리눅스 커널 소스를 컴파일할 때 사용되는 리눅스 커널용 Makefile에서 사용하는 표기법이다.


∎ 리눅스 커널 2.4 버전의 Makefile

all: $(DEV_SRC).o $(APP_SRC) 부분을 보면 최종적으로 “driver_src.o”라는 Kernel Module(혹은 Device Driver)과 이를 이용한 User Application인 “user_app”을 얻기 위해 작성되었다.


한 가지 특이한 점은 컴파일할 때 사용되는 옵션 즉 CFLAGS는 컴파일할 때 가장 먼저 타겟 보드용 소스 내에 있는 헤더파일 들부터 찾으라는 -I 옵션이 보인다. Kernel Module(or Device Driver)에 사용되는 헤더 파일들은 커널 모듈이 실행될 커널 소스 내에 포함된 헤더파일들 이어야 한다.


마지막으로 -D 옵션은 소스 코드에서 “#define” 선언과 똑같은 처리를 한다. 즉 “-DMODULE”은 “#define MODULE”과 같으며,

“-D__KERNEL__은 “#define __KERNEL__”과 같다. 이 부분은 다른 일반 User Application의 오브젝트 파일과 구분되기 위해 선언해야 한다. ->  -DMODULE -D__KERNEL__


∎ 리눅스 커널 2.6 버전의 Makefile

커널 2.4 버전의 Kernel Module(혹은 Device Driver)은 소스 코드 파일이 있는 디렉토리 경로(PATH)를 기준으로 하여 커널 소스 디렉토리 경로를 참조하여 컴파일하지만, 커널 2.6 버전에서는 리눅스 커널 소스 디렉토리 경로를 기준으로 커널 모듈 소스 코드 파일이 있는 디렉토리를 서브(Sub) 디렉토리로 등록한 후 리눅스 커널 소스 디렉토리로 이동하여 make modules 명령어를 실행한다.

=> $(MAKE) -C $(KERNEL) SUBDIRS=$(PWD) modules


$(MAKE)는 make를 뜻하며, -C 옵션은 특정 디렉토리, 여기서는 $(KERNEL) 디렉토리, 즉 커널 소스가 있는 디렉토리로 이동하여(Change to Directory)하여 “make”를 실행하라는 뜻이다.

$(PWD) 부분은 $(shell pwd)로 되어 있는데, 현재 디렉토리의 절대 경로(PATH)를 출력하는 pwd(Present Working Directory) 명령어를 실행시킨 결과가 $(PWD) 변수에 저장되므로, 결과적으로  SUBDIRS=$(PWD) 부분은 현재 커널 모듈(혹은 디바이스 드라이버) 소스 코드 파일이 있는 디렉토리를 커널 소스 디렉토리의 서브 디렉토리로 등록하라는 뜻이다.

마지막에 있는 modules는 앞에 있는 make와 연결되어 make modules라는 명령어로 되어 커널 소스에 Kernel Module로 체크된 소스들을 컴파일하라는 명령어이다.

커널 소스를 컴파일하기 전에 반드시 실행시켜 주어야 하는 “make menuconfig”라는 명령어를 실행시켰을 때 보여지는 화면으로, 여기서는 타겟 보드용 커널 소스를 대상으로 하였다.

화면에 보면 <M>이라는 부분이 나오는데, 이 부분이 바로 해당 소스 코드 파일을 Kernel Module로 컴파일하라는 뜻이다.

<M> 해당 소스를 Kernel Module로 컴파일

[*] 해당 소스를 Device Driver로 컴파일


default: $(APP_SRC) 부분은 일반 Makefile에서 “all : “과 같은 부분이다. 즉 Makefile을 통해 최종적으로 생성되는 결과물을 정의하는 부분이다. 그런데 이 부분을 자세히 보니 User Application인 user_app만 작성되어 있다. 커널2.4 버전에서 처럼 커널 모듈 소스 이름을 별도의 변수를 선언하지 않고 obj-m := driver_src.o 표현.

커널 2.6 버전에서는 리눅스 커널 소스 디렉토리로 찾아가서 컴파일하기 때문에, 별다른 환경 변수를 선언하지 않아도 커널 소스 내

Kernel Module(or Device Driver)로 인식되어 컴파일이 된다.





11.3 “Hello” Kernel Module    

가장 기본적인 Kernel Module 소스이다.


File Name: hello_mod.c

1 /** hello_mod.c **/

2

3 #include <linux/module.h> <- 2개의 헤더는 Kernel Module(or Device Driver) 개발을 위해 반드시 포함

4 #include <linux/kernel.h>

5

6 int init_module(void)

7 {

8 printk(KERN_INFO “HELLO MODULE is loaded. \n”);

9 return 0;

10 }

11 void cleanup_module(void)

12 {

13 printk(“<6> HELLO MODULE is unloaded. \n”);

14 }

15 MODULE_LICENSE(“Dual BSD/GPL”);

<그림> Kernel Module 삽입(추가) 및 삭제 과정

리눅스 2.6 버전용 Kernel Module은 확장자가 “*.ko”이므로, 컴파일 하게 되면 hello_mod.ko 파일이 생성된다.

이렇게 생성된 hello_mod.ko 파일을 가지고 커널 메모리 영역에 삽입(추가)하거나, 삭제하기 위해서는 다음과 같은 명령어를 알아야 한다.


[root@  ]# insmod hello_mod.ko 커널 메모리 영역에 삽입(추가)

[root@  ]# lsmod 제대로 삽입(추가)이 되었는지 혹인

[root@  ]# rmmod hello_mod 커널 메모리 영역에서 삭제 ※ 삭제할 때 확장자를 반드시 생략


가장 중요한 부분은 init_module()과 cleanup_module()부분인데 Kernel Module의 큰 장점은 현재 실행 중인 커널 메모리 영역 내에 별도로 개발한 Kernel Module()의 오브젝트 파일을 언제든지 삽입(추가)하고 삭제가 가능하다는 점이다. insmod 명령어를 실행하면 호출되는 함수가 init_module() 함수이고 rmmod 명령어를 실행하면 호출되는 함수가 cleanup_module() 함수이다.


소스를 작성할 때 반드시 알아야 할 함수 printk()함수는 User Application에서 가장 손쉬운 디버깅 방법으로 사용된다. 문자열(String)을 출력하는 방법은 printf()와 동일하나, 그 목적이 Kernel Module을 리눅스 커널이 실행시킬 때 관련 정보들을 출력하기 위한 것이기 때문에, 출력 시킬 정보의 중요도(레벨)에 따라 메세지의 우선순위를 지정할 수 있다.


<linux/kernel.h> 선언 내용

우선 순위

#define

KERN_EMERG

<0>

우선 순위가 가장 높음, 시스템이 비정상적인 상황

#define

KERN_ALERT

<1>

반드시 바로 처리되어야 할 상황

#define

KERN_CRIT

<2>

심각(Critical)한 상황

#define

KERN_ERR

<3>

오류(Error)가 발생한 상황

#define

KERN_WARNING

<4>

경고(Warnng) 상황

#define

KERN_NOTICE

<5>

일반적이나 중요한 상황

#define

KERN_INFO

<6>

단순히 정보 전달 차원의 상황

#define

KERN_DEBUG

<7>

우선순위가 가장 낮음, 디버깅 레벨


printk 표기법으로

printk(KERN_INFO “정보 출력 문자열”); printk(“<6>” “정보 출력 문자열”);

printk(“<6> 정보 출력 문자열”);


printf() 함수를 실행시키면 그 결과(= 문자열)가 화면, 즉 터미널에 바로 출력이 된다.

즉 printf() 함수의 출력 대상은 표준 출력 디바이스(Standard Output)인 모니터라는 얘기다.


그러나, prink() 함수는 출력 대상이 약간 다르다. printf() 함수가 User Application이 실행될 때 문자열을 출력시키는 함수라면, printk()함수는 Kernel Module(or Device Driver)처럼 운영체제가 실행될 때 문자열을 출력시키는 함수이다. 따라서 출력 대상은 커널이 실행 중인 4GB 영역의 메모리이다.


여기서 차이점이 생기는데, User Application이 실행되는 메모리 영역이라 전혀 상관없으나, printk() 함수가 실행되는 커널 메모리 영역은 일반 User Mode 상태에서는 MMU 때문에 접근할 수 없다.


즉, printf()는 ARM 프로세서의 User Mode로 동작 되고, printk()는 Supervisor Mode로 동작 된다.

일반적으로 커널이 실행되는 도중에 커널 내부적으로 출력하는 메세지들은 모두 커널 메모리 영역에서 발생하기 때문에, 터미널 화면에서는 보이지 않는다. 그러나 시스템 관리적인 측면에서 해당 메세지들을 사용자(User)들이 참조해야 할 경우도 있으므로, 커널은 이러한 메시지들이 커널이 부팅 이후부터 전원이 꺼질 때까지 다음 디렉토리에 있는 파일에 계속적으로 추가하여 사용자들이 참조할 수 있도록 지원하고 있다.


/var/log/messages -> 커널의 로그 메세지를 기록하는 파일


따라서 printk() 함수는 문자열 출력이 바로 /var/log/messages/ 파일에 기록이 되므로, 열어서 확인


그런데 특이한 점은 타겟 보드와 시리얼 케이블로 연결하여 “minicom”과 같은 시리얼 통신 에뮬레이터 프로그램과 연결하여 실행시켰을 때에는 마치 printf() 함수처럼 그 결과가 바로 터미널 상에 보여진다는 점이다.

타겟 보드에 탑재된 Embedded Linux는 일반 PC와 달리 모니터가 없는 환경(타겟 보드)에서 실행되는 게 일반적이다. 따라서 “minicom”과 같은 시리얼 통신 에뮬레이터 프로그램으로 모든 상황을 알 수 있어야 한다.


컴파일하는 방법은 두 가지로 해본다. 먼저 PC 기반에서 컴파일하여 실행시켜 보고, 다음으로 Eddy DK 보드에 탑재하여 실행시켜 본다.


1. 일반 PC에서 컴파일 하는 법

Makefile이 필요하다.


File Name: Makefile

1 /** Makefile for PC **/

2

3 obj-m := hello_mod.o

4

5 CC := gcc

6

7 KDIR := /usr/src/kernels/2.6.18.-1.2798.fc6-i586

8

9 #PWD := ‘pwd’ <-

10 PWD := $(shell pwd) <- 둘 중 한 가지의 방법을 취하면 됨.

11

12 default :

13 $(MAKE) -C $(KDIR) SUBDIRS = $(PWD) modules

14

15 clean :

16 rm -rf *.ko

17 rm -rf *.mod.*

18 rm -rf .*.cmd

19 rm -rf .tmp*

20 rm -rf *.o


[root@Jungchul howto_mod_2.6]# pwd

/root/Jungchul/howto_mod_2.6

[root@Jungchul howto_mod_2.6]# make

........

[root@Jungchul howto_mod_2.6]#

[root@Jungchul howto_mod_2.6]# insmod hello_mod.ko

[root@Jungchul howto_mod_2.6]#

[root@Jungchul howto_mod_2.6]# lsmod | grep hello_mod

hello_mod 5632 0

[root@Jungchul howto_mod_2.6]#

[root@Jungchul howto_mod_2.6]# rmmod hello_mod

[root@Jungchul howto_mod_2.6]# lsmod | grep hello_mod

[root@Jungchul howto_mod_2.6]#


컴파일 되지 않는 이유


※ 커널 소스가 없다.

UBuntu 배포판이나 Fedora에서도 “개발” 관련 목록들을 선택해 주지 않으면 커널 소스가 없을 수 있으므로, 이럴 경우에는 PC에 맞는 커널 소스를 아무 디렉토리에나 다운받아 해당 디렉토리 경로로 Makefile을 수정해주면 된다. 커널 소스는 www.kernel.org 사이트에 접속하면 다운 가능하다.



※ 커널 소스는 있는데, 한 번도 컴파일 시켜 본 적은 없다.

우리가 테스트하려는 Kernel Module은 커널 소스를 컴파일하는 단계 상의 커널 소스 컴파일을 하고 난 이후에 추가적으로 진행하는 명령어이다. 따라서 커널 소스를 한 번이라도 컴파일 하지 않았다면,

커널 소스 디렉토리에서 make menuconfig 명령어를 실행시킨 후 별다른 설정 없이 변경된 내용을 저장하고 빠져나오면 된다.


더불어 insmod 명령어를 실행시킨 후 화면에 printk() 함수 결과가 출력 되지 않은 이유는 실행 환경이 일반 PC이기 때문이다.

확인을 하고 싶다면 /var/log/messages 파일 마지막 부분을 보면 함수 출력 내용이 보이는 것을 확인할 수 있다.




2. 타겟 보드에서 컴파일하는 방법

File Name: Makefile

1 /** Makefile for Target Board (Eddy DK) **/

2

3 obj-m := hello_mod.o

4

5 CC := arm-linux-gcc

6

7 KDIR := /root/MY_WORKS/lemonix_2.1.0.2

8

9 #PWD := ‘pwd’ <-

10 PWD := $(shell pwd) <- 둘 중 한 가지의 방법을 취하면 됨.

11

12 default :

13 $(MAKE) -C $(KDIR) SUBDIRS = $(PWD) modules

14

15 clean :

16 rm -rf *.ko

17 rm -rf *.mod.*

18 rm -rf .*.cmd

19 rm -rf .tmp*

20 rm -rf *.o



[EDDY]# insmod hello_mod.ko

HELLO MODULE is loaded.

[EDDY]#

[EDDY]# lsmod | grep hello_mod

hello_mod 1184 0

[EDDY]#

[EDDY]# rmmod hello_mod

HELLO MODULE is unloaded.

[EDDY]# lsmod | grep hello_mod

[EDDY]#







11.4 Kernel Module 예제들   

1. 심볼 중복 회피 : “Hello Kernel Module” 수정

Kernel Module과 Device Driver에서 가장 주의해야 할 심볼(Symbol) 처리


“insmod” 명령어 인자 값으로 확장자(*.ko)를 붙이는 이유는 하드 디스크에 만들어 놓은 Kernel Module(or Device Driver) 파일이 커널이 실행 중인 메모리에 추가(삽입)하려는 것이기 때문에 이상할 게 없다.

왜 삭제할 때는 확장자를 붙이지 않는 것인가??


이유는 심볼(Symbol)에 있다.


C 언어로 작성하는 소스 코드를 컴파일하게 되면 ELF 파일 타입의 태스크(Task)가 생성되며, 이 태스크는 여러 개의 섹션(Section)으로 구성되는데 그 중 대표적인 섹션은 밑 그림이다.

심볼 영역은 C 언어 소스에서 함수와 전역 변수 부분에 해당된다. 더불어 함수와 전역 변수들은 컴파일하는 시점에 이미 상대적인 메모리 주소(offset)와 할당 받아야 할 메모리 크기(size)가 정의된다.


결국 “insmod” 명령어로 K-M(D-D) 파일을 커널 메모리 영역에 삽입(추가) 한다는 얘기는 K-M(D-D) 내에 정의된 함수와 전역 변수들과 같은 심볼 정보들을 메모리 주소 정보와 함께 커널 메모리 영역에 추가(삽입) 한다는 뜻이다.

한 번 커널 메모리 영역에 추가되면 파일 형식이 아닌 메모리 형식이 되므로 “rmmod” 명령어로 추가 되어 있는 K-M(D-D)의 심볼들을 삭제한다는 뜻으로 심볼 이름을 인자 값으로 주는 것이다.

즉 “rmmod” 명령어의 인자 값은 파일 이름이 아니라 심볼의 이름인 것이다.


리눅스 커널은 현재 실행 중인 커널 메모리 영역에 포함된 모든 심볼 정보들을 하나의 파일로 관리하고 있는데, 그건 바로 “/proc/kallsyms” 파일이다. 즉 “insmod” 명령어로 K-M(D-D)을 추가하고 나서 파일을 “grep” 명령어로 검색해보면 선언한 함수와 전역 변수들을 살펴 볼 수 있다. 반대로, “rmmod” 명령어를 실행 후 “/proc/kallsyms” 파일을 확인해보면 삭제되어 있음을 확인할 수 있다.


[EDDY]# insmod hello_mod.ko

HELLO MODULE is loaded.

[EDDY]#

[EDDY]#cat /proc/kallsyms | grep hello_mod

00 00 00 00 a hellomod.c [hello_mod]

bf 01 40 00 t $a [hello_mod]

bf 01 40 18 t $d [hello_mod]

...

c0 03 b8 2c u printk [hello_mod]

[EDDY]#

[EDDY]# rmmod hello_mod

HELLO MODULE is unloaded.

[EDDY]# cat /proc/kallsysm | grep hello_mod

[EDDY]#


/proc/kallsyms 파일을 보면, bf014000 등은 모두 해당 심볼이 등록되어 있는 메모리 주소 값을 나타내며,

오른쪽에 보이는 것이 바로 심볼에 해당되는 함수와 전역 변수의 이름이다.


심볼(Symbol) = 메모리에 등록된 함수와 전역 변수의 이름


우리가 작성하는 User Application에서는 서로 다른 프로그램들이라도 같은 함수의 이름을 사용할 수 있었다.

왜냐하면 User Application 들은 서로 별개의 메모리 영역을 따로 할당 받기 때문에 중복되는 함수, 전역 변수 이름을 사용하더라도 상관 없다. 즉, 심볼이 중복되어 발생될 문제가 없다는 뜻이다.


그러나, Kernel은 다르다.

리눅스 커널 내부를 나타내는데, K-M(D-D)들 모두 커널 내부에서 쓰레드로 실행이 되며, 각 쓰레드 간에는 함수나 전역 변수의 이름들이 같은 메모리 영역을 공유하게 된다. 즉, 심볼이 중복될 경우에 어떤 심볼을 참조(Call)해야 하는지 문제가 생긴다.


심볼의 중복되는 문제를 예방하기 위해 심볼에 해당하는 함수, 전역 변수의 이름 앞에 K-M(D-D)의 이름을 접두사(Prefix)로 붙이면 간단하게 해결된다.

예를 들어 hello_mod.c 소스 코드 내에 사용하는 모든 함수나 전역 변수의 이름 앞에 hello_ 접두사를 붙이면 된다.


그러나 여기서 init_module() 함수와 cleanup_module() 함수는 문제가 된다.

두 함수는 현재 실행 중인 커널 메모리 영역에 새로 작성한 K-M(D-D)의 심볼들을 새롭게 삽입하거나 삭제하기 위한 인터페이스 부분이기 때문에, 리눅스 커널과 미리 약속된 규칙을 지켜야 한다.

init_module() 함수와 cleanup_module() 함수를 심볼의 중복을 피하기 위해 이름을 바꾸었다면, 약속된 규칙을 지킬 수 있게 지원해주는 함수 매크로를 사용해야 한다.


module_init(hello_init); -> init_module()로 변환

module_exit(hello_exit); -> cleanup_module()로 변환


File Name: hello_mod.c

1 /** hello_mod.c : modified **/

2 #include <linux/module.h>

3 #include <linux/kernel.h>

4 #include <linux/init.h> <- module_init() 함수 매크로 사용을 위한 헤더 파일

5

6 int hello_init(void)

7 {

8 printk(KERN_INFO “HELLO MODULE is loaded. \n”);

9 return 0;

10 }

11 void hello_exit(void)

12 {

13 printk(“<6> HELLO MODULE is unloaded. \n”);

14 }

15

16 module_init(hello_init);

17 module_exit(hello_exit);

18 MODULE_LICENSE(“Dual BSD/GPL”); <- license 정보를 알려주는 매크로이며, 쓸데없는 경고 메시지 출력 X


Kernel Module, Device Driver 소스 코드 작성시 3개의 헤더 파일은 무조건 선언해주고 암기하라

#include <linux/module.h> -> Kernel Module 관련 헤더 파일

#include <linux/kernel.h> -> printk() 및 Kernel 관련 헤더 파일

#include <linux/init.h> -> Kernel Module 초기화 관련 헤더 파일





2. 심볼 공유 예제

심볼을 공유하는 예제

File Name: sharedsyms_mod.c

1 /** sharedsyms_mod.c : Sharing Symbol Example **/

2 #include <linux/module.h>

3 #include <linux/kernel.h>

4 #include <linux/init.h>

5

6 int jungchul_sum(int arg1, int arg2) {

7 printk(“<6>JungChul Shared Sysbols Example\n”);

8 printk(“<6>-------------------------------------\n”);

9 return arg1+arg2;

10 }

11

12 int sharedsyms_init(void) {

13 printk(“<6>SHARED SYMBOLS MODULE is loaded.\n”);

14 return 0;

15 }

16 void sharedsyms_exit(void) {

17 printk(“<6>”SHARED SYMBOLS MODULE is unloaded.\n”);

18 }

19

20 EXPORT_SYMBOL(jungchul_sum); <- 외부에 해당 심볼을 공유하는 매크로

21

22 module_init(sharedsyms_init);

23 module_exit(sharedsyms_exit);

24

25 MODULE_LICENSE(“Dual BSD/GPL”);

File Name: callsyms_mod.c

1 /** callsyms_mod.c : Calling Symbol Example **/

2 #include <linux/module.h>

3 #include <linux/kernel.h>

4 #include <linux/init.h>

5

6 int callsms_init(void) {

7 int sum;

8

9 printk(“<6>CALL SYMBOLS MODULE is loaded.\n”);

10 printk(“<6>----------------------------------------\n”);

11 sum = jungchul_sum(10,20);

12 printk(“<6>Result : sum = %d\n”,sum);

13 return 0;

14 }

15

16 void callsyms_exit(void) {

17 printk(*”<6>CALL SYMBOLS MODULE is unloaded.\n”);

18 }

19

20 module_init(callsyms_init);

21 module_exit(callsyms_init);

22 MODULE_LICENSE(“Dual BSD/GPL”);


일반적인 C 문법 상으로 전체 코드 상에서 정의되지 않은 함수를 호출하는 것은 에러가 난다. 그러나 경고(warning)만 뜰 뿐 에러 나지는 않는다. 왜냐하면 커널 내부 메모리에서 심볼을 공유 했을 수도 있다고 생각하기 때문이다. 다만 실행할 때 순서를 어기면 문제가 된다.


Makefile

File Name: Makefile

1 /** Makefile for Target Board (Eddy DK) **/

2

3 obj-m := sharedsyms_mod.o callsyms_mod.o

4

5 CC := arm-linux-gcc

6

7 KDIR := /root/MY_WORKS/lemonix_2.1.0.2

8

9 #PWD := ‘pwd’ <-

10 PWD := $(shell pwd) <- 둘 중 한 가지의 방법을 취하면 됨.

11

12 default :

13 $(MAKE) -C $(KDIR) SUBDIRS = $(PWD) modules

14

15 clean :

16 rm -rf *.ko

17 rm -rf *.mod.*

18 rm -rf .*.cmd

19 rm -rf .tmp*

20 rm -rf *.o


컴파일해 주어야 할 소스 코드가 여러 개일 경우 하나의 공백을 두고 열거해 주면 된다.

경우에 따라 추가적으로 컴파일을 할 수도 있을 경우

obj-m += additional_mod.o 라고 써도 된다.



[EDDY]# insmod callsyms_mod.ko

callsyms_mod : Unknown symbol jungchul_sum

insmod: cannot insert ‘callsyms_mod.ko’ : Unknown symbol in module (-i) : No suchy

[EDDY]#

[EDDY]# insmod sharedsyms_mod.ko

SHARED SYMBOLS MODULE is loaded.

[EDDY]#

[EDDY]# insmod callsyms_mod.ko

CALL SYMBOLS MODULE is loaded.

---------------------------------------

JungChul shared Sysbols Example

---------------------------------------

Result : sum = 30

[EDDY]#

[EDDY]# lsmod | grep syms

callsyms_mod 1344 0

sharedsyms_mod 1568 1 callsyms_mod

[EDDY]#

[EDDY]# rmmod sharedsyms_mod

rmmod: sharedsyms_mod : Resource temporarily unavailable

[EDDY]# rmmod callsyms_mod

CALL SYMBOLS MODULE is unloaded.

[EDDY]# rmmod sharedsyms_mod

SHARED SYMBOLS MODULE is unloaded.







3. Startup Paramer 예제

Startup Parameter란 프로그램을 처음 시작할 때 줄 수 있는 인자 값을 말한다.

예를 들어 insmod sharedsyms_mod.ko 명령어를 보면 Startup Parameter는 sharedsyms_mod.ko 이다.


File Name: callsyms_mod.c

1 /** callsyms_mod.c : Calling Symbol Example **/

2 #include <linux/module.h>

3 #include <linux/kernel.h>

4 #include <linux/init.h>

5 #include <linux/moduleparam.h> Startup Parameter를 쓰기 위한 헤더 파일

6

7 static int age;

8 static char *name;

9

10 module_param(age, int, 0);

11 module_param(name, charp, 0);

12

13 int param_init(void) {

14 printk(KERN_INFO “PARAMETER MODULE is loaded.\n);

15 if( (age == 0) || (name == NULL)) {

16 printk(KERN_ERR “Please Input Startup Parameter!!\n”);

17 printk(KERN_ERR “\t usage>> insmod param_mod age=(int)xxx name=(char*)yyy \n”);

18 return -1;

19 }else {

20 printk(KERN_INFO “Your Paramter is ... \n”);

21 printk(KERN_INFO “\t age : %d\n”,age):

22 printk(KERN_INFO “\t name : %s\n”,name):

23 return 0;

24 }

25 }

26 void param_exit(void) {

27 printk(KERN_INFO “PARAMETER MODULE is unloaded.\n”);

28 }

29

30 module_init(param_init);

31 module_exit(param_exit);

32 MODULE_LICENSE(“Dual BSD/GPL”);


컴파일 하기 위해 필요한 Makefile은 “obj-m := param_mod.o”로만 수정해주면 된다.

module_param 함수 매크로

=> #define module_param(name, type, perm);

인수에 대한 설명으로 첫 번째는 변수 이름, 두 번째는 타입, 그리고 마지막은 Kernel Module 파일의 퍼미션(Permission)이다. 일반적으로 Normal 타입일 때 “0” 값을 입력한다.

타입에서 charp은 char*를 나타내는 값이다.


[EDDY]# insmod param_mod.ko

PARAMETER MODULE is loaded.

Please Input Startup Parameters!!

usage>> insmod param_mod age=(int)xxx name=(char*)yyy

insmod: cannot insert ‘param_mod.ko’ : Operation not permitted (-1) : ...

[EDDY]#

[EDDY]# insmod param_mod.ko age=24 name=”JungChul Kim”

PARAMETER MODULE is loaded.

Your Parameter is ...

age : 24

name : JungChul Kim

[EDDY]#

[EDDY]# lsmod | grep param

param_mod 1928 0

[EDDY]#

[EDDY]# rmmod param_mod

PARAMETER MODULE is unloaded.

[EDDY]#

[EDDY]# lsmod | grep param

[EDDY]#










Comments