Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

20113
1478
166890


'리눅스 커널의 구조와 원리' 전체 목차(출간 완료) -----Table of Contents-----

@ 리눅스 개발자분들께...

제 블로그에 오셔서 댓글로 책이 언제 출간되는지 궁금해하시는 분이 계신 것 같은데요.

아래 링크에서 만나실 수 있습니다.  이 책은 리눅스 개발에 도움이 되는 유용한 내용을 담고 있으니, 많은 리눅스 시스템 개발자분들이 읽어 주셨으면 좋겠습니다.

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2

이 책의 주요 독자는 신입 개발자 혹은 졸업반 학생과 같은 리눅스 커널의 입문자이며 세부 예상 독자는 다음과 같습니다.

▣ 리눅스 보드로 졸업 과제를 준비 중인 졸업반 학생
▣ 연구실의 프로젝트를 리눅스 환경(리눅스 보드, 리눅스 배포판)에서 진행 중인 대학원생
▣ 리눅스 드라이버의 동작 원리를 더 깊게 알고 싶은 분
▣ 리눅스 커널을 실무에서 어떻게 디버깅하는지 알고 싶은 리눅스 시스템 개발자

# 책 디자인 스틸 컷은 다음과 같습니다. 
 

Thanks,
Austin Kim


디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

리눅스의 전망과 소개


라즈베리 파이 설정 

라즈베리 파이 설치하기
라즈베리 파이 기본 설정하기 
라즈비안 리눅스 커널 빌드


리눅스커널 디버깅


프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  

인터럽트 처리

인터럽트 소개  
   * 리눅스 커널에서의 인터럽트 처리 흐름    
인터럽트 컨텍스트  
인터럽트 핸들러는 언제 호출될까?  
인터럽트 핸들러는 어떻게 등록할까?  
인터럽트 디스크립터  
인터럽트 디버깅  


인터럽트 후반부 처리









6.9 Soft IRQ 서비스는 누가 언제 처리하나?




6.13 Soft IRQ 디버깅
6.13.1 ftrace Soft IRQ 이벤트 분석 방법
6.13.2 /proc/softirqs로 Soft IRQ 서비스 실행 횟수 확인

워크큐

워크큐 소개
워크큐 종류 알아보기
워크란  
워크를 워크큐에 어떻게 큐잉할까?
   워크를 큐잉할 때 호출하는 워크큐 커널 함수 분석   
워커 쓰레드란
워크큐 실습 및 디버깅
   ftrace로 워크큐 동작 확인   
   인터럽트 후반부로 워크큐 추가 실습 및 로그 분석 
   Trace32로 워크큐 자료 구조 디버깅하기 
딜레이 워크 소개  
   딜레이 워크는 누가 언제 호출할까?
라즈베리파이 딜레이 워크 실습 및 로그 확인  


디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2

커널 시간관리

커널 타이머 관리 주요 개념 소개
jiffies란
커널 타이머 제어
동적 타이머 초기화
동적 타이머 등록하기
동적 타이머는 누가 언제 실행하나?
라즈베리파이 커널 타이머 실습 및 로그 분석
   
커널 동기화

커널 동기화 기본 개념 소개
레이스 발생 동작 확인
커널 동기화 기법 소개
스핀락
뮤텍스란
커널 동기화 디버깅

프로세스 스케줄링

스케줄링 소개
프로세스 상태 관리
   어떤 함수가 프로세스 상태를 바꿀까?
스케줄러 클래스
런큐
CFS 스케줄러
   CFS 관련 세부 함수 분석  
선점 스케줄링(Preemptive Scheduling)   
프로세스는 어떻게 깨울까?
스케줄링 핵심 schedule() 함수 분석
컨택스트 스위칭
스케줄링 디버깅
   스케줄링 프로파일링
     CPU에 부하를 주는 테스트   
     CPU에 부하를 주지 않는 테스트 

시스템 콜

시스템 콜 주요 개념 소개
유저 공간에서 시스템 콜은 어떻게 발생할까
시스템 콜 핸들러는 어떤 동작을 할까? 
시스템 콜 실행 완료 후 무슨 일을 할까?
시스템 콜 관련 함수  
시스템 콜 디버깅  
   

시그널이란

시그널이란
시그널 설정은 어떻게 할까
시그널 생성 과정 함수 분석


가상 파일시스템

가상 파일시스템 소개
파일 객체
파일 객체 함수 오퍼레이션 동작
프로세스는 파일객체 자료구조를 어떻게 관리할까?
슈퍼블록 객체
아이노드 객체
덴트리 객체
가상 파일시스템 디버깅


커널 메모리 관리
 
가상 주소를 물리 주소로 어떻게 변환할까?   
메모리 존(Zone)에 대해서   
커널 메모리 할당은 어떻게 할까   
슬랩 메모리 할당자와 kmalloc 슬랩 캐시 분석   
커널 메모리 디버깅


[부록 A] GCC 지시어
   * inline    
   * noinline    
   * __noreturn   
   * unused   
[부록 B] 리눅스 커널 실력을 키우는 방법
[부록 C] 리눅스 커널 프로젝트에 기여하기  
C.1 리눅스 커널 오픈소스 프로젝트 소개 
   * 용어  
C.2 설정 방법 
C.3 패치 코드를 작성한 후 이메일로 보내기  
C.5 리눅스 커널 오픈소스 프로젝트로 얻는 지식 


[ARM] ARM Errata란 이제는 ARM의 시대다

프로젝트를 진행하다보면 소프트웨어적으로 도저히 그 원인을 분석할 수 있는 버그를 만날 때가 있습니다.
어떤 어셈블리 명령어를 실행한 다음에 갑자기 리셋이 되거나, 특정 CPU가 핫 플러그인 동작 후에 못 깨어나는 문제들이죠.

이럴 때 보통 하드웨어적으로 문제가 있는 지 의심을 하며, 보통 전원이 제대로 공급되는지, 혹은 메모리 비트 플립은 아닌 지 체크를 하죠.
그런데 문제가 해결이 안되면 종종 듣는 말이 있습니다. 그것은 ARM Errata입니다.

이번엔 ARM Errata가 무엇인지 소개합니다.

ARM Errata

ARM Errata는 용어 그대로 ARM 프로세서에 오류가 있을 때 이를 알리는 통지 번호를 의미합니다.
예를 들면, 'ARM Errata 1234567'과 같은 방식으로 전달되는데 보통 어셈블리 코드 포멧의 패치가 공유됩니다.

그렇다면 ARM Errata는 어떻게 전달이 될까요?
ARM 사에서 라이센싱을 한 업체에게 ARM Errata를 배포한다고 알려져 있습니다. 

반드시 수정해야 할 ARM Errata가 배포되면 리눅스 시스템 개발자나 커뮤니티에 공유가 되기도 합니다.

ARM Errata 패치 코드

이번에는 리눅스 커널 커뮤니티에 배포된 ARM Errata 패치 코드를 소개합니다. 

링크는 아래와 같은데요.
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v4.13&id=efbc74ace95338484f8d732037b99c7c77098fce
 
제목: ARM: 7345/1: errata: update workaround for A9 erratum #743622
author: Will Deacon <will.deacon@arm.com>

ARM 사의 개발자가 직접 리눅스 커널 소스 트리에 커밋을 생성한 것으로 보입니다. 
커미터를 보니 ARM-리눅스 커널 커뮤니티에서 가장 영향력이 있는 러셀 킹(Russell King)이 보이네요.

개발자 인터뷰

'A9 erratum #743622'과 관련된 개발자를 인터뷰 했는데요. 그 분께 다음과 같은 질문을 했습니다.

    * A9 erratum #743622를 반영하지 않았을 때 어떤 현상을 겪었나?

일단 한 달동안 개고생을 했는데, 어쩌구, 저쩌구...
메모리가 깨지고 갑자기 시스템이 리셋되는 현상을 겪었다고 합니다.

[리눅스커널] 커뮤니티: if 문에서 break를 쓰면 else를 쓸 필요가 없음

출처: 

리눅스 커널 커뮤니티에서 아주 유익한 패치를 확인했습니다. 
if 문 내에서 break 문을 사용하니 else를 사용할 필요가 없는 구문입니다.

diff --git a/drivers/gpu/drm/amd/display/dc/core/dc_link_dp.c b/drivers/gpu/drm/amd/display/dc/core/dc_link_dp.c
index a53e8fed56f3..cb731c1d30b1 100644
--- a/drivers/gpu/drm/amd/display/dc/core/dc_link_dp.c
+++ b/drivers/gpu/drm/amd/display/dc/core/dc_link_dp.c
@@ -3680,7 +3680,7 @@ static void set_crtc_test_pattern(struct dc_link *link,
  struct pipe_ctx *odm_pipe;
  enum controller_dp_color_space controller_color_space;
  int opp_cnt = 1;
- uint8_t count = 0;
+ int count;
 
  switch (test_pattern_color_space) {
  case DP_TEST_PATTERN_COLOR_SPACE_RGB:
@@ -3725,11 +3725,11 @@ static void set_crtc_test_pattern(struct dc_link *link,
  width,
  height);
  /* wait for dpg to blank pixel data with test pattern */
- for (count = 0; count < 1000; count++)
+ for (count = 0; count < 1000; count++) {
  if (opp->funcs->dpg_is_blanked(opp))
  break;
- else
- udelay(100);
+ udelay(100);
+ }
  }
  }
  break;

[ARM] ARM사의 라이센스 방식: 소프트 매크로/하드 매크로 이제는 ARM의 시대다

기존의 프로세서 설계 업체는 자체 프로세서를 제조해 배포합니다. 다른 업체들은 그 프로세서 설계 업체가 제작한 '프로세서 외부 인터페이스'를 이용해 자신의 시스템을 설계하는 방식으로 개발합니다. 설계되는 프로세서는 주변장치를 포함하기도 하고 프로세서들 사이에 기능을 추가하기도 합니다.

ARM사는 ARM 프로세서를 제조하지 않음

그런데 ARM사의 접근 방식은 다릅니다. ARM사는 ARM 프로세서를 직접 제조하지 않습니다.
ARM의 주요 임무는 프로세서를 만드는 것이 아니라 아키텍처를 제공합니다. 특정 사양의 CPU를 제조해서 고객에게 전달하는 것이 아니라 전략적 파트너가 되거나 고객 스스로가 자신의 칩에 ARM 프로세서를 탑재할 수 있는 솔루션을 제공하는 전략입니다.

자신의 지적 재산권을 다른 회사에게 라이센싱 할 뿐입니다. SoC의 업체인 인텔, 엔비디아, TI(텍사스 인스트루먼트) 그리고 삼성은 자신만의 ARM 프로세서를 제조합니다.
각각 SoC업체는 SoC 제품군에 따라 ARM 프로세서의 세부 스팩을 적용해 ARM 프로세서를 탑재합니다.

ARM 프로세서를 라이센싱하는 또 다른 예로 애플을 들 수 있습니다.

애플의 주력 제품은 아이폰과 아이패드입니다. 기존의 프로세서 아키텍처 솔루션은 너무 많거나 적은 주변 장치에 엑세스할 수 있는 인터페이스를 제공했습니다.
애플은 주변장치뿐만 아니라 속도와 소비 전력을 최적화할 수 있는 커스텀 캐시의 필요성을 느꼈습니다.  ARM 라이센스를 얻어 애플 주력 제품에 적용할만한 기능을 추가해 커스터마즈했고 그 결과 탄생한 것인 A4/A5 시리즈입니다.

정리하면 어느 IT 업체나 각자 SoC 스팩에 맞게 ARM 프로세서를 탑재할 수 있습니다. 

ARM 라이센스의 종류

ARM 라이센스는 2까지 형태로 분류될 수 있습니다.
SoC 업체들은 ARM 에서 제공하는 2가지 방식 라이센스를 적용해 자신의 칩에 ARM 프로세서를 탑재합니다.

● 소프트 매크로 

버스 시스템, 그래픽 로직과 같은 IP의 기능의 구현 방식을 설명하는 라이센스로, 포멧은  RTL (VHDL/Verilog)입니다. 
자유롭게 ARM 프로세서의 캐시와 같은 컴포넌트를 설계하려는 SoC 업체가 활용하는 라이센스입니다.

● 하드 매크로 

주어진 프로세스에 적합한 IP의 물리적 레이아웃에 대한 정보를 제공하는 라이센스입니다. 
커스터마이즈가 필요 없고 빠른 설계를 한 후 ARM 프로세서를 탑재하려는 SoC 업체가 사용하는 라이센스입니다.

[IT] 임베디드 BSP 코드몽키의 특징 임베디드 에세이

요즘 ARM 프로세서와 같은 기술적인 컨텐츠를 올리는데요.

이번 시간에는 조금 예민한 이야기를 에세이 형식으로 포스팅합니다. 주제는 임베디드 BSP 분야의 코드몽키의 특징입니다.
코드몽키가 뭔지 궁금하시다고요? 아래 동영상을 보면 알 수 있을 겁니다.

https://www.youtube.com/watch?v=ngZtrReINoY&t=2s

리눅스 개발자들과 교류를 하다가 가끔 술 한잔 할 때 가끔 코드몽키에 대해 스토리를 이야기합니다. 그 분들이 하는 말을 좀 듣고 나니 몇 가지 공통점이 있더라구요. 
몇 가지 공통점을 보여 요약을 해 봤거든요. (사실, 이 글을 블로그에 비공개로 업로드됐는데 공개로 올려도 될 것 같아 포스팅합니다.) 

제가 아는 친구는 중견 기업의 CTO 개발자로 일하고 있는데요. 
(그 친구가 말하는) 임베디드 코드몽키 때문에 회사 문을 닫을 뻔했다고 하는데요. 
개발 과정에서 종종 큰 사고를 쳐서 그 친구의 주요한 역할이 면접 때 임베디드 코드몽키를 가려내는 일이라고 하는군요. 

글을 조금만 읽으면 기분이 나뻐질 수 있는데요. 
"이 글을 쓴 너도 임베디드 코드몽키가 아니냐?"라고 반문을 할 지 모르겠습니다. 
이렇게 질문을 하면 '네, 저도 임베디드 코드몽키입니다'라고 대답하겠습니다. 그리고 '기분이 나쁜 분은 코드몽키일 가능성이 매우 낮습니다"라고 말씀드리고 싶어요.

참고로 이 글은 모두 제 생각은 아니고 시니어 급 리눅스 시스템 개발자들의 생각을 정리한 것입니다. 
이제부터 임베디드 분야의 코드몽키의 특징에 대해 몇 가지 정리해 보겠습니다.

배우려는 의지가 없다

다른 코드몽키와 마찬가지로, 임베디드 코드몽키의 특징은 스스로 배우려는 의지가 없다는 점을 들 수 있어요.
물론 개발을 하다보면 어쩔 수 없이 뭔가 배워야 할 때가 있습니다. 이 때는 임베디드 코드몽키는 자신에게 주어진 일을 할 정도의 지식만 배운다는 점입니다.
이것도 구글링을 통해 얻는 간단한 지식에 불과한 경우가 많죠. 

책을 읽지 않는 것은 물론 꼭 봐야할 스팩 문서도 쳐다보지 않습니다.

스팩에 맞게 공부해서 학점을 따는데 길들여 졌는지, 하는 일이 관심이 없는 지 모르겠지만,
아무튼 임베디드 코드몽키는 배우려는 의지가 거의 없습니다.

단순 반복적인 일을 편하게 느낀다 

개발을 하다보면 코드몽키성 업무는 누구나 마추치게 됩니다.
빌드 스크립트만 짜거나 특정 피쳐를 넣고 빼는 일, 기계적인 깃 머지 혹은 단순 브링업과 같은 일이죠.

임베디드 코드몽키들은 이런 반복적인 일을 좋아하고 편하다고 느낍니다.

고수 개발자들이 가장 혐오하는게 단순 반복적인 개발업무인데, 이런 임베디드 코드몽키들은 이런 코드몽키성 업무만 쫒아 다닙니다.

보통 신입 개발자들에겐 가장 쉬운 업무를 주는 경우가 많은데요. 어쩔 수 없이 코드몽키성 업무를 주는 상황이 많습니다.
그런데 5년차 이상의 임베디드 코드몽키 개발자들이 이런 업무에 스스로 뛰어들어 신입개발자들과 경쟁을 한다고 하네요.

메뉴얼 대로 시키는 일만 한다

대부분의 임베디드 코드몽키는 메뉴얼대로 하는 일을 좋아합니다.
대신 조금이라도 스스로 일을 하는 걸 싫어합니다. 절대적으로 시키는 일만 합니다.

다른 동료가 작성한 코드에 심각한 버그가 있던 말건 기계적으로 코드를 머지합니다.
하지만 메뉴얼을 벗어난 업무를 지시하면 분노합니다. 임베디드 코드몽키에게 메뉴얼화가 안된 새로운 일을 주면 대부분 싫어하기 때문이죠.
'이걸 왜 해야 해?'라고 질문을 던지며 갱스터 랩을 하면서 100가지 이상 이유를 들이댑니다. 

자신에게 주어진 일을 왜 해야 하는지, 결과물을 어떻게 나올 지 고민을 안하기 때문이라고 합니다.

노력을 할 필요가 없다고 생각한다 

임베디드 코드몽키는 노력을 해도 자신의 삶이 나아지지 않는다고 생각하는 경우가 많다고 합니다. 
의도적으로 개발자, 직장인의 한계를 말하는 경우도 있다고 하는군요. 예를 들면 다음과 같아요.

아무리 개발을 잘해봤자 재테크를 하는 것보다 돈을 벌지 못한다.
개발을 잘 하려고 공부를 해도 나이먹으면 치킨 집을 차릴 수 밖에 없다.

100% 틀린 말은 아니지만 구지 저런 소리는 자주 할 필요가 있는 지 좀 의문이 들긴 합니다. 

평범한 개발자를 유난히 높게 평가한다 

임베디드 코드몽키들은 지극히 평범한 그저 그런 개발자를 아주 뛰어난 개발자라고 칭송하는 경우가 많습니다. 
메뉴얼대로 시키는 일에 익숙해져 있기 때문에 조금이라도 일을 창의적으로 하는 개발자를 보면(다른 개발자가 보기엔 '개나 소나하는' 일인데) 
'우아, 정말 대단하다'라고 진심으로 박수를 보낸다고 합니다. 

저는 이런 상황을 겪진 않았지만 다른 시니어급 개발자들은 이런 상황을 종종 겪었나 봅니다.

코드몽키가 되는 건 사실 개개인의 선택이고 뭐라고 말할 필요는 없는 것 같습니다.
하지만 가끔 동료에게 피해를 주거나 부서에서 진행 중인 프로젝트에 엄청난 대형사고를 치는게 문제라고 합니다.


[펌][유튜브] 코드몽키의 미래 - 김포프님 임베디드 에세이

혹시 코드몽키란 말을 들어 본 적이 있나요?
코드몽키에 대해 잘 소개한 유튜브 동영상이 있어 소개합니다.

게임계의 꽃미남 엔지니어이자 개발자인 김포프님의 동영상입니다.

제목: 코드몽키의 미래

제 자신이 코드몽키인지 뒤돌아 보는 계기가 된 것 같은데요.
중요한 발언을 요약해 봤어요.

> (1:30)
> 너네는 이 직종은 무조건 괜찮아 라는 망상만 듣고 들어와서
> 이거면 끝이겠지라고 생각하겠지만, 정작 잘못 받 들여놓고 잘못 시작하면
> 10년 뒤에 돌이킬 수 없는 상황이 된다는..
> 그냥 평생 노예처럼 살아가야 된다라는 애기를 할 수 밖에 없어요.
> 주변에서 그런 사람도 많이 봤고, 제가 보는 미래도 그럴 수 밖에 없고...

코드 몽키가 되면 평생 노예와 같이 저임금, 끝없은 야근과 함께 개발해야 된다는 이야기 같습니다.
그렇게 될 수 밖에 없다고 예언을 하네요. 살벌하네요.

> (1:53)
> 그러나 결과적으로는 제가 말하는 엔지니어급, 아키텍터 급, 이런 사람들은
> 우리가 알고 있는 프로그래머가 누리는 사회적 지위하던가 금전적인 지위를 그대로 이어갈 것이라고 보지만
> 그게 안 되는 사람들, 코드몽키들의 미래는 아무도 쉽게 얘기하려 하지 않고, 
> 대충 알고 있어도 모른 척을 하려는게 있죠.

그래도 고급 개발자들은 나름대로 인정받고 잘 지낼 것 같다고 예측하네요. 

> (2:00) ~ (12:08)
>
> 요약: IT 산업 초창기에는 프로그래머의 수가 적어서 많은 연봉을 받으면서 개발을 했다.
> 하지만 IT 산업의 인프라가 늘면서 다수 하드웨어 개발자들이 SW 개발자로 유입되면서, SW 개발자의 수가 늘어났따.
> 인력도 늘어나고, 일자리도 늘어났다. 이제는 일반 사무직에 종사하는 분들도 엑셀로 프로그램을 하면서 코딩을 하는 세상이 됐다. 
> 이제 코딩 교육이 보편화되서 대부분 프로그래밍은 기본 기술로 여겨진다.

> (12:08)
> 그렇게 또 저렴한 인력이 생긴다는 거에요.
> 누구나 다 하니까 이게 비쌀리가 없잖아요.

음, 코드 몽키는 저렴한 인력이라고 말씀하시네요.

> (13:31)
> 제가 아는 아키텍트는 그런 이야기를 했거든요. 
> 너희들은 함수 대가리만 짜주면 코드만 짜서 오는 게 코드몽키 일이 될꺼라고..
> "그거보다 더 단순한게 코드몽키 일이 될 수 있겠다. 정형화될 수 있고, 정형화된 일을 많이 줄 수 있다면.
> 충분히 될 수 있겠구나."란 생각을 하는 거에요.

코드몽키는 단순, 반복적인 코딩, 정형화된 코딩을 하는 분들을 의미하는 것 같네요.

> (14:51)
> 그래 단순한 건 누구나 할 수 있어. 단순한 건 그 사람들한테 주면 돼

단순한 일을 코드몽키한테 주면 된다고 말씀하는 것 같네요.

> (15:13)
> 그만큼 코드 몽키가 많아지고 많아 질 수록, 학교에서의 정규 과정은 쉬워질 수 밖에 없죠.
> 일반적인, 좋지 않은 학교들의 ...
> 그런데 이제 문제는 그런 학교에서 가르치면서 "야 너희은 코드몽키가 될꺼야" 그런 얘기는 안한다구요.
> 결과적으로 그런 학교는 코드몽키를 만드는 학교고 그런 얘기는 안한다구요.
> 코드몽키가 많아져서 졸업생은 많아지고 질보다는 양으로 처리하는 학교가 되는 거고
> 그런 변화를 보고 있고...

이 부분을 보니, 
스타크래프트의 저그란 종족의 해처리에서 '미네랄이 적게 드는' 저글링이 나오는 모습이 떠오르는데요.

요즘 코드몽키를 육성하는 학교가 있나 보네요. (전 잘 모르겠지만) 

> (15:45) ~ (16:04)
> 그러면 더 훌륭한 학교가 있잖아요.
> 잘 가르쳐서 훌륭한 거든, 입학 점수가 훌륭한 거든, 뭐든 간에..
> ...
> 이거의(학교간의) 양극화도 더 커질 것 같아요.

학교간의 양극화도 심해질 것이란 예상입니다.

> (16:09)
> 굉장히 많은 학원과 굉장히 많은 학교가 생겨났지만,
> 그러면 그럴수록, 모든 사람이 이제 '아 나도 교육을 받았다는 만족감은 있고
> ...
> 삶의 질에서는 크게 차이가 없어지지 않나.
> 오히려 그 양극화가 커지지 않나 좋은 학교와 나쁜 학교 간에..
> ...
> 그래서 코드몽키도 그렇게 가고 있구나.

프로그래밍이라 직종이 더 대중화되면서 코드몽키가 더 많이 생긴다는 이야기를 하시는 것 같네요.

> (16:43)
> 결과적으로는 삶의 선택이거든요.

코드몽키나 되느냐, 되지 않느냐도 삶의 선택이라고 보는군요.
저도 이 생각에 동의합니다.

[ARM] ARM 프로세서의 특징을 활용한 최적화는 왜 중요할까? 이제는 ARM의 시대다

이번 포스팅에서는 'ARM 프로세서의 특징을 활용한 최적화는 왜 중요할까?'에 대해서 이야기하려고 합니다.
본론에 들어가기 앞서 일반적인 SW 개발자들이 최적화에 대해 어떻게 생각하는지 짚어 보겠습니다.   

생각보다 성능과 최적화는 중요하다

대부분 SW 개발자들은 주어진 스팩을 구현하기 위해 프로그램을 작성합니다.

화면을 꾸미는 프론트 엔드 개발자들은 화면이 제대로 구성됐는지, 메뉴나 폰트가 제대로 보이는 지 체크를 합니다. 네트워크 개발자들은 데이터 패킷이 제대로 전달이 됐는지 테스트를 할 것입니다.

프로그래머는 주로 주어진 스팩을 만족하면서 버그가 없도록 프로그래밍을 합니다. 하지만 프로그램이 스팩 내에서 올바르게 실행되도록 결함을 찾아 다듬는 것만으로는 충분하지 않을 수 있습니다.

고객이 사용하는 하드웨어 다비이스의 종류에 따라 프로그램이 느려질 수 있고, 가격을 후려치기 위해 하드웨어 팀에서 저사양 부품을 교체할 수 있으니까요. 혹은 예상치 않은 네트워크 시스템에 과부하가 걸려 웹 서버가 터지는 재앙을 맞이하기도 하죠.

그리고 프로젝트의 수주를 따기 위해 코드 처리량이나 화면 당 프레임 수를 기준으로 다른 업체와 경쟁을 하는 상황에 직면할 수 있습니다. 

생각보다 성능은 중요합니다. 성능은 어느 제품이던 성공과 실패의 척도가 될 수 있기 때문이죠.

최적화를 하지 말라는 충고

하지만, 많은 분들이 최적화를 논할 때 '최적화를 하지 말라'라고 경고합니다. 유명한 과학자(도널드 커누스)의 커멘트를 언급하면서 말이죠.

     * 작은 효율은 잊어버려라. 섣부른 최적화는 만약의 근원이다.
     출처: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.103.6084

월리엄 울프는 다음과 같이 논문에서 최적화에 대한 부정적인 의견을 펼쳤습니다.

     * 우리는 최적화나 효율성이란 이름으로 많은 죄악을 저질렀다.
     * 반드시 효율성을 달성하는 것도 아니면서 말이다.
출처:
Wulf, W. A. “A Case Against the GOTO.” Proceedings of the 25th National ACM Conference, August 1972: 791–97.

최적화에 대한 논의를 할 때 이런 논문 자료를 언급하는 개발자들은 생각보다 많다고 합니다. 제가 봤을 때 그 이유는 다음과 같다고 생각합니다.

    * 나약한 개발 습관에 대해 변명할 꺼리를 찾는다.
    * 더 빠른 코드를 구현하려는 노력을 조금도 하지 않으려는 게으름에 대한 핑계꺼리를 찾는다.

최적화를 하지 말라는 조언을 그대로 아무런 비판 없이 받아들이면 다음과 같은 결과를 초래합니다.

    * ARM 코어의 CPU 사이클이 낭비된다.
    * 사용자의 불편을 초래한다.
    * 프로젝트의 막바지엔 코드를 수정하는데 많은 시간이 소요된다.
    * 성능 문제가 발생해 고객사에게 패널티를 부과한다.

가장 심각한 케이스를 말하자면, 수주한 프로젝트의 성능 문제로 인해 아예 회사가 문을 받는 비극적인 상황을 맞이하기도 합니다.

최적화를 할 필요없다는 이유: 프로세서의 속도가 빠르다

SW 개발 세계에서 '최적화가 무의미하다'라는 소리를 종종 듣습니다. 그 이유는;

    * 코드의 실행 속도가 느리더라도 시간이 지날수록 프로세서가 빨라지기 때문에 성능 문제를 공짜로 해결할 수 있다는
      겁니다.

음, 과연 맞는 이야기일까요? 제 귀에는 펜스까지 120m까지 거리인 야구장에서 맨날 홈런을 두두려 맞는 투수가 '200m 짜리 펜스의 야구장이 있으면 홈런이 아니라 플라이로 잡힐 것이다'라는 소리로 들리네요.

이쯤되면 '이제 내가 나서서 이야기할 타이밍이군'이라고 생각하면서 임베디드 개발자가 나서기 시작합니다.
임베디드 디바이스에서는 부품 가격이나 메모리 공간이 한정돼 있기 때문에... 어쩌구...저쩌구...

임베디드 관점을 떠나 이런 사고 방식의 문제점은 바로 '프로세스가 빠를수록 낭비되는 명령어가 더 쌓이게 된다'라는 점입니다.
프로세서가 빠르다고 믿고 최적화에 대한 아무 고민없이 50% 정도 불필요한 코드(어셈블리 관점)가 실행이 된다고 가정합시다.

    * 불필요한 코드의 실행 속도가 아무리 빨라도 이를 삭제하는 것보다는 느릴 겁니다.

'밥을 먹으면 배부르다'와 같이 당연한 소리처럼 들리겠지만, 아예 불필요한 코드가 최적화로 제거되면 2배 빠르게 프로그램이 실행되니까요.

ARM 프로세서에서 최적화된 코딩을 해야 하는 결정적인 이유

ARM 프로세서에서 최적화된 코드를 작성해야 하는 가장 중요한 이유를 이야기하겠습니다. ARM 아키텍처나 ARM 코어의 동작 원리를 알면 그 이유를 알 수 있는데요. 결정적인 이유는 '메모리에 접근하는 비용은 ARM 프로세서의 다른 비용을 압도한다'라는 점입니다. 

또 다른 이유는 MCU나 SoC에 실장된 메인 메모리는 ARM 코어 내부에 있는 게이트와 레지스터보다 매우 느리다는 점입니다. 

이제는 하드웨어에서 물리학적인 관점으로 설명를 더 드리면요. 마이크로프로세서 칩에서 전자를 꺼내 상대적으로 광활한 구리 회로판 트레이스에 쏟아 넣고 그 트레이스를 몇 센티미터 아래에 있는 메모리 칩으로 이동시키는 일은, 마이크로프로세서 내부에서 트렌지스터를 분리하며 전자를 이동하는 작업보다 수천 배의 시간이 걸립니다.

결국, 메모리에 접근하는 비용은 프로세서의 다른 비용을 압도합니다.

'mov r0, r1' 와 같이 ARM 코어에 내장돼 있는 레지스터를 연산하는 일보다, 'ldr r1, [r0, #0x8]' 명령어와 같이 메모리 접근하는 동작이 더 많은 비용일 든다는 것입니다.

자, 여기서 'ldr r1, [r0, #0x8]' 명령어를 실행하면 ARM 코어에서 어떤 동작을 실행하는지 조금 더 짚어 봅시다.

'ldr r1, [r0, #0x8]' 명령어를 통해 접근한 데이터가 캐시에 있으면 그나마 다행일 겁니다. 운이 좋지 않게도 캐시에 데이터가 없으면 캐시 메모리 콘트롤러는 캐시 데이터 메모리 블록을 불러오고, 캐시 라인을 업데이트할 것입니다.

이 과정에서 AMBA와 같은 메모리 버스에 접근해 몇 가지 동작을 수행하게 됩니다. 여러 가지 복잡한 과정 중 Bus Transaction, Bus Transfer와 같은 일을 하게 됩니다.

여기서 제가 말하고 싶은 가장 중요한 포인트는 다음과 같습니다. 

    * ARM 코어와 같이 프로세서의 속도가 아무리 빨라져도 메인 메모리에 접근하는 인터페이스의 속도를 더 빠르게 만드는 
      경우는 거의 없다는 것입니다.

이해를 돕기 위해 자동차를 예를 들겠습니다. 자동차의 속도가 200km에서 400km로 늘어났다고 가정합시다. 어느 도로를 차가 달려도 건널목과 신호등들이 있기 마련일 것입니다. 자동차는 빨간 신호등을 보면 공회전을 하며 기다릴 수 밖에 없죠.

스피드를 개선하기 위해 코어의 갯수를 4개에서 8개로 늘려도 마찬가지입니다. ARM 코어끼리 메인 메모리에 접근하는 버스의 사이클을 얻기 위해 더 많은 경쟁(기다림)이 일어 날 꺼니까요. 

이런 성능의 한계를 메모리 장벽(Memory Barrier)라고 합니다.

정리

이번 포스팅에서는 '최적화가 왜 필요한지'에 대해 제 생각을 말씀드렸습니다.
앞으로는 ARM 프로세서의 특징을 활용한 C 코드 최적화 기법에 대해서 다루겠습니다.

---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

[IT] 개발자 역량 키우기 - 자기개발 후 피드백 받는 방법 임베디드 에세이

요즘엔 학창 시절이 잘 생각나지 않는다. 학교를 졸업한 지 15+년이 지나서 그런가?

희미한 기억 속의 학창 시절에는 뭔가 수업을 듣고 학점을 따는 방식으로 공부했던 것 같다. 학점을 잘 따기 위해 공부를 하는 과정에서 지식을 습득할 수 있었다. 물론, 1학기가 지나면 내가 얼마나 그 지식을 잘 알고 있는지 결과를 확인할 수 있었다. 학점이 나오기 때문이다. A~C 까지 학점을 받는 것이다. 

정확한 기억은 아니지만, 학창 시절 학점이 별로 좋지 않았다. 낮은 학점을 받고 나면 난 스스로 다음과 같이 말하며 위로했다.

    * 학점이 높은 사람이 반드시 그 지식을 잘 아는 사람이 아니다.

요즘 드는 생각은 '학점이 높은 사람이 좋은 학생이고 그 지식을 많이 알 가능성이 높다'이지만, 그 시절에 바퀴 벌레가 살충제를 맞고 꿈틀 거리 듯 힘들어 하는 게 나의 모습이었다.

졸업 후 정말 운이 좋게 취업을 해서 'SW 개발자'로 일을 한 다음, 학교의 전공 수업을 듣고 학점을 따는 방식으로 학습을 하지 않게 됐다. 배워야 할 지식이 있으면 그 때 그 때 맞게 배우는 방식을 선택할 수 밖에 없게 됐다. 

누구도 뭘 배우고 얼마나 알아야 하는지 알려주지 않는다. 또한 학습의 방향까지도 스스로 결정해야 한다.
무엇보다 배운 다음에 그 결과를 확인하기 어렵다. 차라리 학부 시절에 전공 수업을 듣고 학점을 받는 게 속 편할 지도 모르겠다는 생각도 든다. 그래서 다음과 같은 질문을 던지면서 몇 년 동안 고민을 했다. 

    * '내가 제대로 배운 내용을 알고 있는지, 그 수준은 얼마나 되는지에 대한 피드백'을 어떻게 확인할 수 있을까?

고민 끝에 대학교 학부 시절의 학점 수준은 아니지만 '공부한 내용에 대한 피드백'을 확인하는 몇 가지 알게 된 내용을 이야기해보고자 한다.

블로그의 방문자 수

배운 내용을 정리하고 이를 통해 실력을 키우고 싶은 개발자가 있다면 '반드시 블로그를 해보세요'라고 권하고 싶다.
분석한 소스나 로그를 글로 정리하는 과정에서 생각보다 많은 걸 배운다. 여기서 한 가지 주의해야 할 점이 있다.

    * 반드시 블로그에 올리는 글은 공개!로 해야 한다.

물론 일기나 개인적인 생각은 블로그에 비공개로 올려 자신만이 봐야 한다. 때로는 완성되지 않는 글이 있으면 비공개로 모드로 저장한 다음, 퇴고를 한 다음에 완성된 글을 블로그에 포스팅할 수 있다. 

   * 기술적인 내용을 정리해 블로그에 '비공개' 모드로 올리는 것은 그리 바람직하지 않는 것 같다. 

'비공개' 모드로 블로그에 글을 올리면 다른 개발자들이 글을 읽을 수 없다. 당연한 이야기지만 자신이 블로그에 올린 글은 네이버나 구글링에서 검색이 되지 않는다.

이게 왜 중요하냐면, 블로그에 글을 올리면 구글링을 통해 많은 개발자들이 블로그의 글을 읽을 수 있고, 이것이 통계 자료로 잡히기 때문이다. 블로그에 올린 내용이 개발자에게 도움이 되면 더 자주 블로그에 오거나, 블로그의 글을 읽는 시간이 늘어나기 마련이다. 

내가 하고 싶은 이야기는 블로그의 통계 자료가;

   * '내가 올린 글이 경쟁력이 있는 지' 혹은 '내가 배운 내용을 제대로 정리했는지' 

확인하는 척도가 되기 때문이다.

많은 개발자들이 구글링을 통해 개발 지식을 검색한다. 그런데 개발자들이 블로그에 방문하는 횟수와 블로그에 머물러 있는 시간을 구글 인공 지능 알고리즘이 계산해 이 정보를 바탕으로 검색 결과를 출력해준다는 것이다. 

그럼, rousalome.egloos.com 블로그의 검색 결과를 확인해볼까?

구글 웹마스터에 블로그를 등록하면 더 자세한 정보를 얻을 수 있는데 아래는 6월의 http://rousalome.egloos.com/ 블로그의 검색 실적이다.



위 출력 결과는 다음과 같은 사실을 말해준다.

    * 블로그에 접근한 총 클릭수: 블로그에 방문한 횟수(9.59천)
    * 총 노출 수:  키워드(리눅스나 관련 개발 지식)를 입력하면 구글 검색 엔진이 검색 결과를 보여주는 횟수 (7.37만)
    * 평균 게재순위:   키워드(리눅스나 관련 개발 지식)를 입력하면 구글 검색 엔진이 검색 결과를 보여주는 순위 (10.1)

이번엔 5월 통계 정보를 볼까?


6월보다 전반적으로 수치가 더 높다. 데이터는 다음과 같이 해석할 수 있겠다. 

    * 블로그의 6월 달에 검색 실적이 하락하고 있으니 더 분발해라!

책을 출간하기

블로그도 좋지만 지식을 체계적으로 정리한 다음에 책을 출간할 수 있다.
이 과정에서 학부에서 받는 학점보다 더 냉정하거나 냉혹한 피드백(결과)을 얻을 수 있다.

먼저 출간 제안 과정이다. 

IT 전문 출판사의 에디터들은 밥만 먹고 기술 서적을 읽는 분들이라,
개발자의 글을 조금만 읽으면 개발자의 내공을 바로 캐치한다고 한다. 개인적으로 아는 프리렌서 출판사 에디터 형님을 통해 알게 된 사실은, '대부분의 개발자들이 출판사에 출간 제안을 하면 대부분 거절 당한다고 한다'라는 것이다. 

운 좋게 출판사에서 출간 제안을 받아줬다고 가정하자. 그럼, 끝까지 원고를 마무리하는 비율은 얼마나 될까?
원고 집필이란 마라톤를 완주하는 확률은 3/50이라고 한다. 50명이 출간 계약을 하면 3명이 완주한다고 한다.
(이 데이터도 출판사 사장님께 들은 내용이다.)

이 모든 관문을 통과해서 책이 출간됐다고 가정하자. 그럼, 출판 시장의 아주 냉혹한 피드백을 받게 된다.
yes24에서 볼 수 있는 판매지수이다. 책이 내실이 있고 좋은 내용으로 구성돼 있으면 고객(개발자/학생)들이 구매한다.
하지만 반대의 경우 외면 받는다. 

책을 출간하는 과정으로 받는 피드백은 학부 과정에서 받는 학점보다 더 냉혹한 것 같다는 생각도 든다.

---
"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"

Thanks,
Austin Kim(austindh.kim@gmail.com)
---



[리눅스커널][ARM64] 시스템 콜: 시스템 콜 벡터(el0_sync) 처리 과정 11. 시스템 콜

'디버깅을 통해 배우는 리눅스 커널의 구조와 원리'란 책에서 시스템 콜이 처리되는 과정을,
ARM32 기반 시스템 기준으로 설명합니다.

여러 리눅스 개발자분들이 ARM32 대신 ARM64 기반에서 개발하시는 분들이 있어 이번 포스팅에서는 ARM64 아키텍처에서 시스템 콜이 발생하면 리눅스 커널에서 이를 처리되는 과정을 분석합니다.

ARM32 기반 리눅스 시스템에서는 유저 공간에서 커널 공간으로 모드를 스위칭하기 위해 슈퍼파이저 콜을 수행합니다.
이 때 'svc'명령어를 수행하는데요. ARM64 아키텍처에서도 마찬가지로 같은 명령어를 실행합니다.

ARM64 아키텍처 기준으로 EL0(유저 모드)에서 EL1(커널 모드)로 스위칭을 시작하는 분기점입니다. 그러면 커널 공간에서는 어떤 코드가 실행될까요?

    * 정답은 el0_sync 레이블입니다.

'ARM64 아키텍처 기반 리눅스 시스템에서 시스템 콜을 발생하면 커널 공간에서 el0_sync 레이블이 실행된다'라고 기억해두셨으면 좋겠습니다.

el0_sync 레이블 코드 분석

이어서 el0_sync 레이블의 어셈블리 코드를 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/entry.S
01 /*
02 * EL0 mode handlers.
03 */
04 .align 6
05 el0_sync:
06 kernel_entry 0
07 mrs x25, esr_el1 // read the syndrome register
08 lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class
09 cmp x24, #ESR_ELx_EC_SVC64 // SVC in 64-bit state
10 b.eq el0_svc

el0_sync 레이블이 실행되면 가장 먼저 처리되는 매크로 레이블은 kernel_entry입니다.
kernel_entry 레이블은 여러 가지 복잡한 처리를 하는데, 핵심은 '유저 공간(EL0)에 실행 중인 레지스터 세트를 커널 공간의 프로세스 스택 공간에 푸시한다'입니다. 시스템 콜을 유발해서 커널 공간(EL1)에서 여러 가지 처리를 한 후 유저 공간(EL0)으로 복귀하려면 유저 공간에서 실행했던 정보가 채워져 있는 레지스터를 다시 로딩해야 하기 때문입니다.

이어서 07~08번째 줄을 보겠습니다.

07 mrs x25, esr_el1 // read the syndrome register
08 lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class

07번째 줄입니다. 
익셉션이 발생한 세부 원인은 신드롬 레지스터에 저장하는데요, 이 esr_el1 레지스터를 읽어 x25에 저장하는 동작입니다.

이어서 x25에 있는 값을 #ESR_ELx_EC_SHIFT 값 만큼 왼쪽으로 비트 시프트한 다음 x24에 저장합니다.

이어서 09~10번째 줄입니다.

09 cmp x24, #ESR_ELx_EC_SVC64 // SVC in 64-bit state
10 b.eq el0_svc

x24와 ESR_ELx_EC_SVC64를 비교한 연산이 true이면 el0_svc 레이블로 브랜치를 하는 동작입니다.

entry.S가 컴파일된 후 vmlinux에서 본 어셈블리 코드는 다음과 같습니다.

   ZSX:FFFFFF8008082980|A90007E0  el0_sync: stp     x0,x1,[SP]
   ZSX:FFFFFF8008082984|A9010FE2            stp     x2,x3,[SP,#0x10]   ; x2,x3,[SP,#16]
   ZSX:FFFFFF8008082988|A90217E4            stp     x4,x5,[SP,#0x20]   ; x4,x5,[SP,#32]
   ZSX:FFFFFF800808298C|A9031FE6            stp     x6,x7,[SP,#0x30]   ; x6,x7,[SP,#48]
   ZSX:FFFFFF8008082990|A90427E8            stp     x8,x9,[SP,#0x40]   ; x8,x9,[SP,#64]
...
   ZSX:FFFFFF8008082A10|D5385219            mrs     x25,#0x3,#0x0,c5,c2,#0x0   ; x25, ESR_EL1
   ZSX:FFFFFF8008082A14|D35AFF38            lsr     x24,x25,#0x1A    ; x24,x25,#26
   ZSX:FFFFFF8008082A18|F100571F            cmp     x24,#0x15        ; x24,#21
   ZSX:FFFFFF8008082A1C|54002B20            b.eq    0xFFFFFF8008082F80   ; el0_svc

어셈블리 코드를 보니 #ESR_ELx_EC_SVC64 매크로의 정체는 0x15이네요.

el0_svc 레이블 분석

이어서 el0_svc 레이블의 코드를 분석합시다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/entry.S
/*
 * SVC handler.
 */
.align 6
el0_svc:
mov x0, sp
bl el0_svc_handler
b ret_to_user
ENDPROC(el0_svc)

소스를 보니 특별한 동작을 하지 않습니다. 스택 주소를 x0에 저장한 다음,
el0_svc_handler 함수를 호출합니다. 

자, 이 코드를 보니 한 가지 의문이 생깁니다.

    * 'bl el0_svc_handler' 명령어의 bl과 'b ret_to_user' b의 차이는 뭘까?

보통 bl 명령어는 C 코드 포멧의 함수를 호출할 때 사용하는데, 'bl 명령어' 다음 코드로 다시 되돌아 와야 하니 컴파일러에게 'el0_svc_handler() 함수를 호출하기 전에, x30 레지스터(32기준: r14(lr) 링크드 레지스터)에 복귀할 주소를 저장하라'라고 주문하는 것과 같습니다.

정리하면 el0_svc 레이블은 el0_svc_handler() 함수를 호출합니다.

el0_svc_handler() 함수 분석

이어서 el0_svc_handler() 함수의 코드를 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/syscall.c
asmlinkage void el0_svc_handler(struct pt_regs *regs)
{
sve_user_discard();
el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
}

el0_svc_common() 함수를 호출하는데, 여기서 이 함수에 전달하는 인자를 눈여겨볼 필요가 있습니다.

    * regs: 유저 공간에서 실행했던 레지스터 세트 정보를 가리키는 인자
    * regs->regs[8]: 시스템 콜 번호가 저장된 레지스터(음 x8 레지스터에 시스템 콜 번호가 저장됐군요)
    * __NR_syscalls: 시스템 콜 최대 번호
    * sys_call_table: 시스템 콜 테이블의 시작 주소

ARM64 아키텍처의 Calling Convention에 따라, 각각 인자는 다음 레지스터에 실려서 전달된다는 점 잊지마세요.

    * x0: regs
    * x1: regs->regs[8]
    * x2: __NR_syscalls
    * x3: sys_call_table

el0_svc_common() 함수 분석

el0_svc_common() 함수의 코드는 다음과 같습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/syscall.c
01static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
02    const syscall_fn_t syscall_table[])
03 {
04 unsigned long flags = current_thread_info()->flags;
05
06 regs->orig_x0 = regs->regs[0];
07 regs->syscallno = scno;
08
09 local_daif_restore(DAIF_PROCCTX);
10 user_exit();
11
12 if (has_syscall_work(flags)) {
13 /* set default errno for user-issued syscall(-1) */
14 if (scno == NO_SYSCALL)
15 regs->regs[0] = -ENOSYS;
16 scno = syscall_trace_enter(regs);
17 if (scno == NO_SYSCALL)
18 goto trace_exit;
19 }
20
21 invoke_syscall(regs, scno, sc_nr, syscall_table);

함수 코드 중 중요한 부분을 엄선해 분석해봅시다.

07번째 줄입니다.

07 regs->syscallno = scno;

2번째 인자인 scno을 'regs->syscallno'에 저장합니다.

일반적인 상황에서는 12번째 줄 if문이 동작하므로 if문 내의 다음 16번째 줄을 보겠습니다.

16 scno = syscall_trace_enter(regs);

ftrace의 sys_call 이벤트가 활성화됐을 때 실행하는 syscall_trace_enter() 함수를 호출합니다.

이어서 21번째 줄입니다.

21 invoke_syscall(regs, scno, sc_nr, syscall_table);

invoke_syscall() 함수를 호출합니다. 이 함수에 전달되는 인자는 이미 설명했으니 넘어 가겠습니다.

invoke_syscall() 함수 분석

invoke_syscall() 함수의 구현부는 다음과 같습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/syscall.c
01 static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
02    unsigned int sc_nr,
03    const syscall_fn_t syscall_table[])
04 {
05 long ret;
06
07 if (scno < sc_nr) {
08 syscall_fn_t syscall_fn;
09 syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
10 ret = __invoke_syscall(regs, syscall_fn);
11 } else {
12 ret = do_ni_syscall(regs, scno);
13 }
14
15 regs->regs[0] = ret;
16 }

일반적인 상황에서 08~10번째 줄이 실행되므로 이 기준으로 코드를 분석합시다.

먼저 09번째 줄을 보겠습니다.

09 syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];

syscall_table 심벌, 즉 시스템 콜 테이블에 접근해 시스템 콜 번호에 해당하는 시스템 콜 핸들러 함수를 syscall_fn 변수에 저장합니다.

이어서 10번째 줄을 분석합니다.

10 ret = __invoke_syscall(regs, syscall_fn);

__invoke_syscall() 함수를 호출합니다.

__invoke_syscall() 함수 분석

이제 시스템 콜 핸들러를 호출하는 순간까지 왔습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/syscall.c
static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
{
return syscall_fn(regs);
}

__invoke_syscall() 함수의 구현부는 딱 한 줄입니다.
2번째 인자로 전달된 시스템 콜 핸들러 함수를 호출하는 기능입니다.

다음 포스팅에서는 ARM64 비트 기반 리눅스 커널에서 시스템 콜이 수행될 때 디버깅 정보(ftrace, TRACE32)를 소개하겠습니다.





[IT] '리눅스 커널의 구조와 원리' 출간 후 스토리: 너무 감사합니다! 임베디드 에세이

'디버깅을 통해 배우는 리눅스 커널의 구조와 원리'라는 책이 출간된 후 가장 좋은 점은 그 동안 만나뵀지 못했던 교수님들, 리눅스 강사님들, 리눅스 고수 개발자분들과 교류를 할 수 있었다는 점입니다. 많은 분들이 집필에 대해 칭찬과 격려를 해주시고, 적극적으로 홍보해 주시기도 했습니다. 친한 선배님은 집 근처 도서관에 희망 도서를 신청하셨다고 알려주시기도 했습니다.

'문c 블로그'의 문영일 선배님께 책 출간 소식을 말씀드렸는데, '문c 블로그'를 통해 제 책을 홍보해주셨습니다. 
아래 링크를 참고하세요.


정말 감사드리고, 밥은 제가 사드려야 될 것 같습니다.

이 밖에도 여러 임베디드 리눅스 개발자와 취준생으로 부터 메일을 받거나 블로그의 댓글을 보게 됐습니다. 

이 중 아주 특이한 메일을 받았는데요. 책이 예약 판매가 될 때 주문을 해서 책을 받은 분인데요.
하루에 3시간 밖에 잠을 자지 않고, '디버깅을 통해 배우는 리눅스 커널의 구조와 원리' 책을 1달 반만에 다 읽었다고 하네요. 물론은 실습까지 다 마쳤다고 하네요.  

책의 실습까지 다 마치고 나니 실력이 늘어 자신감이 급상승해, 면접을 보고 무사히 통과를 했다는 스토리였습니다.

이거 1년 6개월 동안 쓴 원고인데 거의 50일만에 다 읽은 독자님도 있다니. 
얼마나 절실했으면 이렇게 까지 공부를 했을까란 생각이 들기도 하면서, 기분이 좋았습니다.

어떤 회사의 관리자분은 이 책을 '직무 교육 교재'로 선정했고 이런 책을 써줘서 고맙다고 연락을 주시기도 했습니다.
이 분께 세미나를 진행하는 요령과 책을 실무에 활용하는 방법에 대해 공유드렸고, 
가을에 원-포인트로 2시간 정도 제가 세미나를 할 계획이라고 말씀드리니 참 기뻐하셨습니다.  

모든 개발자님과 독자분께 감사드리고, 꾸준히 개발에 도움이 되는 컨텐츠를 발굴해 공유하도록 하겠습니다.

---
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)
---

[ARM64] GCC: 특정 함수를 최적화하지 않기 - optimize("O0") 옵션 이제는 ARM의 시대다

코드를 작성한 후 실행을 하다보면 가끔 자신이 작성한 함수의 심벌이 사라지거나, 가끔 예상을 벗어나는 동작을 할 때가 있습니다.
여러 가지 원인 중 하나가, 컴파일러가 최적화를 하면서 코드를 재배치를 하다가 발생합니다.

이번 포스팅에서는 GCC의 최적화 레벨을 각각의 함수에 지정하는 방법을 소개합니다.
결론부터 말씀을 드리면 함수의 선언부에 아래 키워드를 추가하면 됩니다.

    * __attribute__((optimize("O0")))

예제 코드와 함께 ARM-GCC 최적화에 대해 알아봅시다.

ARM-GCC가 최적화하는 코드 예시

먼저 샘플 코드를 소개합니다.
void add_cal_func(void)
{
unsigned int a, b, c;
a = 7;
b = 3;

c = a + b;
printk("c = %u \n", c);
}

이 함수의 의도는 지역 변수를 3개 선언한 다음에, a에는 7, b에는 3을 저장합니다.
이어서 a와 b를 더한 값을 c에 저장합니다.

위와 같이 코드를 작성한 후 컴파일을 한 후 어셈블리 코드를 보겠습니다.

01 ffffff800859e744 <add_cal_func_02>:
02
03    a = 7;
04    b = 3;
05
06    c = a + b;
07    printk("c = %u \n", c);
08 ffffff800859e744:   b0001e40    adrp    x0, ffffff8008967000 <raspberrypi_firmware_pll_divider_clk_ops+0x30>
09 ffffff800859e748:   52800141    mov w1, #0xa                    // #10
10 ffffff800859e74c:   9114c000    add x0, x0, #0x530
11 ffffff800859e750:   17ee57db    b   ffffff80081346bc <printk>

어셈블리 코드를 보니 C 코드와 달리 코드가 구성돼 있습니다.

a와 b로 선언한 지역 변수에 7과 3을 저장하는 코드는 사라졌고, 다음 09번째 줄과 같이
x1 레지스터에 0xa를 더합니다.

09 ffffff800859e748:   52800141    mov w1, #0xa                    // #10

이어지는 11번째 줄에서 printk의 2번째 인자로 x1 레지스터를 전달합니다.

11 ffffff800859e750:   17ee57db    b   ffffff80081346bc <printk>

이처럼 코드가 바뀐 이유는, ARM-GCC 컴파일러가 똑똑하게  03~06번째 줄을 실행하면 어짜피 결과가 10이니 이를 계산해 버린 것입니다.
이번에는 C로 의도한 코드와 ARM-GCC가 최적화 과정에 수정한 코드를 비교해서 보겠습니다.

< 의도한 코드 >
03    a = 7;
04    b = 3;
05
06    c = a + b;

< ARM-GCC 컴파일러가 수정한 코드 >
03    a = 10;

이렇게 ARM-GCC 컴파일러는 C 코드를 보고 최적화를 수행하는데, 컴파일 과정에서 최적화 레벨을 지정할 수 있습니다.
에를 든 코드는 최적화 레벨이 O2로 적용해 ARM-GCC가 컴파일을 한 것입니다.

위에서 든 예시는 ARM-GCC 컴파일러가 최적화를 해도 문제가 될 만한 부분은 없습니다.
ARM-GCC 컴파일러가 최적화를 해줘 코드 사이드가 줄어드니 고맙다고 말해야 겠죠.

최적화가 되면 안되는 함수 예시

그런데, ARM-GCC 컴파일러에 의해 최적화가 되면 안되는 상황이 있습니다.

디바이스를 초기화하는 다음 함수를 예로 듭시다.

unsigned int *device_init_mem;
void device_init(void)
{
unsigned int a, b, c;
device_init_mem = (unsigned int*)0xC000D000;
a = 7;
*device_init_mem  = a;

b = 3;
*device_init_mem  = b;

mdelay(10);
}

만약 ARM-GCC 컴파일러의 최적화 레벨이 O2로 적용됐다면, 아래 코드는 아예 삭제해버리고, 

a = 7;
*device_init_mem  = a;

다음 코드만 실행할 수도 있습니다.

b = 3;
*device_init_mem  = b;

혹은 아예 우리가 전혀 의도하지 않는 코드를 만들어 낼 수도 있죠.

특정 함수에만 최적화 레벨을 지정하기

이 상황에서 특정 함수만 ARM-GCC 최적화 레벨을 적용해 컴파일하는 방법이 있습니다.
다음과 같은 키워드를 함수에 선언하면 됩니다.

    * __attribute__((optimize("O0")))

이번에는 add_cal_func() 함수만 최적화 레벨을 적용하지 않고 컴파일을 해봅시다.

void __attribute__((optimize("O0"))) add_cal_func(void)
{
unsigned int a, b, c;
a = 7;
b = 3;

c = a + b;
printk("c = %u \n", c);
}

'__attribute__((optimize("O0")))' 키워드를 함수의 선언부에 추가한 후 컴파일한 결과, 어셈블리 코드는 다음과 같습니다.

01 void __attribute__((optimize("O0"))) add_cal_func(void)
02 {
03 ffffff800859e6ec:   f81e0ffe    str x30, [sp,#-32]!
04    unsigned int a, b, c;
05
06    a = 7;
07 ffffff800859e6f0:   528000e0    mov w0, #0x7                    // #7
08 ffffff800859e6f4:   b90017e0    str w0, [sp,#20]
09    b = 3;
10 ffffff800859e6f8:   52800060    mov w0, #0x3                    // #3
11 ffffff800859e6fc:   b9001be0    str w0, [sp,#24]
12
13    c = a + b;
14 ffffff800859e700:   b94017e1    ldr w1, [sp,#20]
15 ffffff800859e704:   b9401be0    ldr w0, [sp,#24]
16 ffffff800859e708:   0b000020    add w0, w1, w0
17 ffffff800859e70c:   b9001fe0    str w0, [sp,#28]
18    printk("c = %u \n", c);
19 ffffff800859e710:   b0001e40    adrp    x0, ffffff8008967000 <raspberrypi_firmware_pll_divider_clk_ops+0x30>
20 ffffff800859e714:   9114c000    add x0, x0, #0x530
21 ffffff800859e718:   b9401fe1    ldr w1, [sp,#28]
22 ffffff800859e71c:   97ee57e8    bl  ffffff80081346bc <printk>

어셈블리 명령어가 02 최적화 레벨을 적용할 때에 비해 굉장히 많이 늘어났습니다.
코드를 잠깐 보니, 클래식 피아니스트가 '악보에 있는 음표를 그대로 놓치지 않고 연주한다'라고 말하듯,
의도한 C 코드를 그대로 구현한 어셈블리 명령어가 보입니다.

어셈블리 명령어를 차근차근 보겠습니다.
먼저 06~08번째 줄입니다.

06    a = 7;
07 ffffff800859e6f0:   528000e0    mov w0, #0x7                    // #7
08 ffffff800859e6f4:   b90017e0    str w0, [sp,#20]

07번째 줄에서 x0 레지스터에 7을 저장하고, 08번째 줄은 스택 주소 +20 메모리에
x0 레지스터를 저장합니다.

이어서 09~11번째 줄을 분석합니다.

09    b = 3;
10 ffffff800859e6f8:   52800060    mov w0, #0x3                    // #3
11 ffffff800859e6fc:   b9001be0    str w0, [sp,#24]

10번째 줄에서 x0 레지스터에 3을 저장하고, 11번째 줄은 스택 주소 +24 메모리에
x0 레지스터를 저장합니다.

이 동작을 다음 그림으로 나타낼 수 있습니다.


스택 주소       값
----------------------
...
+ 0x20          7 (a) 
+ 0x24          3 (b)


다음으로 14~15번째 줄을 보겠습니다.

14 ffffff800859e700:   b94017e1    ldr w1, [sp,#20]
15 ffffff800859e704:   b9401be0    ldr w0, [sp,#24]

14번째 줄을 봅시다. 
'스택 주소 +20 메모리'에 저장된, a 변수가 저장한 값(7)을 x1 레지스터에 로딩합니다.
이 연산 결과 'x1 = 7'이 됩니다.

15번째 줄도 비슷한 명령어인데, 
스택 주소 +24 메모리'에 저장된, b 변수가 저장한 값(3)을 x0 레지스터에 로딩합니다.

마지막으로 16번째 줄을 분석하겠습니다.

13    c = a + b;
...
16 ffffff800859e708:   0b000020    add w0, w1, w0

x1(a = 7)과 x0(b = 3)을 더해서 x0에 저장합니다.
이 동작은 다음 수식으로 표현할 수 있습니다.

   * x0(c) = x0(a) + x1(b)

최적화 레벨 O1를 적용

이번에는 '__attribute__((optimize("O1")))' 키워드를 적용해 add_cal_func() 함수를 컴파일해보겠습니다.

void __attribute__((optimize("O1"))) add_cal_func(void)
{
unsigned int a, b, c;
a = 7;
b = 3;

c = a + b;
printk("c = %u \n", c);
}

컴파일을 한 후 디스어셈블리(어셈블리 코드 확인)를 한 결과는 다음과 같습니다.

void '__attribute__((optimize("O1")))' add_cal_func(void)
{
ffffff800859e728:   f81f0ffe    str x30, [sp,#-16]!

    a = 7;
    b = 3;

    c = a + b;
    printk("c = %u \n", c);
ffffff800859e72c:   52800141    mov w1, #0xa                    // #10
ffffff800859e730:   b0001e40    adrp    x0, ffffff8008967000 <raspberrypi_firmware_pll_divider_clk_ops+0x30>
ffffff800859e734:   9114c000    add x0, x0, #0x530
ffffff800859e738:   97ee57e1    bl  ffffff80081346bc <printk>

처음에 예시를 든 어셈블리 코드와 유사합니다.

정리

ARM 리눅스에서 최적화 옵션을 적용해 컴파일을 하면, 컴파일러가 너무 똑똑해서 원작자의 의도와 다르게 코드를 재배치하는 경우가 있습니다.
악보를 보고 그대로 연주를 해야 하는데, 컴파일러가 스스로 악보를 재해석을 하는 것이죠.

하지만, 코드 사이즈를 최적화하기 위해 O2 정도의 최적화 레벨을 적용하는 경우가 많습니다. 그런데 특정 함수는 최적화 레벨을 적용하면 안될 때가 있죠.

이런 상황에서 ARM-GCC 에서 제공하는 '__attribute__((optimize("O1")))' 키워드를 함수에 선언하시기 바랍니다.

---
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)
---

[리눅스커널] 커널 동기화를 배운 후 고찰 - 하드웨어만큼 소프트웨어가 중요하다 9. 커널 동기화(spinlock/mutex)

'CPU와 같은' 하드웨어 성능이 업그레이드가 되면 소프트웨어가 더 빨리 동작한다고 믿는 분들이 있습니다.
최대 200 킬로의 시속을 낼 수 있는 자동차의 엔진을 300킬로로 달리도록 바꾸는 것을 예로 들 수 있죠.

그런데 '디버깅을 통해 배우는 리눅스 커널의 구조와 원리'란 책의 9장을 읽고 나면 이런 편견을 버릴 수 있어요.
하드웨어 성능이 아무리 업그레이드되도 이를 뒷받침해주는 소프트웨어가 부실하면 성능 향상을 기대할 수 없다고 말하고 싶네요.

조금 더 구체적으로, 9장에서 다룬 스핀락과 뮤텍스를 예를 들어 제 생각을 뒷받침해 보겠습니다.

스핀락

어떤 프로세스가 스핀락을 획득하고 임계 영역을 실행 중에, 다른 프로세스가 임계 영역에 접근하면 스핀락을 획득할 수 없습니다.
다른 일을 못하고 계속 기다리죠. 이를 유익하게 Busy-Waiting이라고 하는데, CPU를 소모하는 동작만 수행합니다.

그래서 스핀락을 잠그는 코드의 구간은 되도록 빨리 실행되도록 구조를 잡아야 합니다.
또한 과도하게 스핀락으로 임계 영역을 보호하면 Busy-Waiting의 빈도가 늘어납니다.

뮤텍스

이번에는 뮤텍스에 대해 이야기를 해보겠습니다. 
어떤 프로세스가 뮤텍스를 획득하고 임계 영역을 실행 중에, 다른 프로세스가 임계 영역에 접근하면 바로 휴면 상태에 접근합니다.
커널은 이런 프로세스를 TASK_UNINTERRUPTIBLE 상태로 바꾼 다음 뮤텍스를 획득할 때 까지 인터럽트를 받지 않고 잠든 상태로 유지합니다. 그리고 임계 영역을 실행 중인 프로세스가 뮤텍스를 해제하면 커널은 잠든 상태로 기다리는 프로세스를 깨웁니다.

적절한 코드 구간에 뮤텍스를 걸어야 하지만 마찬가지로 뮤텍스를 잠근 코드 구간의 실행 시간이 너무 오래 걸리면,
다른 프로세스들이 연달아 뮤텍스를 획득하기 위해 휴면 상태에 빠지는 동작을 수행하게 됩니다. 

물론 뮤텍스를 기다리는 프로세스의 갯수가 2~3개 정도면 큰 문제가 되지 않지만 5개 이상이 되면 시스템의 성능이 떨어질 수 있습니다. 뮤텍스를 임계 영역에 너무 자주 걸어도 좋지 않습니다. 뮤텍스를 획득하기 위해 잠들었다가 깨어나는 동작이 추가될 수 있기 때문입니다.

과도한 mdelay

과도한 mdelay를 사용하면 시스템 성능을 떨어뜨릴 수 있습니다.
디바이스 드라이버의 코드를 보면 다음과 같이 mdelay() 함수를 활용한 루틴을 자주 볼 수 있습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/drivers/rapidio/rio.c
int
rio_mport_chk_dev_access(struct rio_mport *mport, u16 destid, u8 hopcount)
{
int i = 0;
u32 tmp;

while (rio_mport_read_config_32(mport, destid, hopcount,
RIO_DEV_ID_CAR, &tmp)) {
i++;
if (i == RIO_MAX_CHK_RETRY)
return -EIO;
mdelay(1);
}

return 0;
}

mdelay는 자동차의 시동을 킨 상태에서 공회전을 하는 동작과 유사해 불필요하게 mdelay를 실행하면 시스템 성능을 악화 시킬 수 있죠. 구지 mdelay를 써야 하면 밀리 초 이내로 사용하고 밀리 초 이상의 시간 동안 딜레이를 줘야 할 때는 usleep()/msleep() 함수를 사용하면 좋습니다. 마구 마구 mdelay() 함수를 사용하면 시스템 성능이 저하되고, 가끔 와치독으로 시스템이 돌아가시는 현상을 목격하게 됩니다.

정리

이번에는 스핀락, 뮤텍스 그리고 mdelay를 예로 들어 시스템 소프트웨어의 중요성을 설명드렸습니다.

CPU 성능이 높아졌다고 무조건 시스템 성능이 좋아질 수는 없습니다. 
자동차의 엔진을 300킬로 속도 까지 낼 수 있게 교체해도, 소프트웨어가 제대로 작동하지 않으면 200킬로 시속 밖에 낼 수 없습니다. 하드웨어를 받쳐주는 소프트웨어가 잘 돌아가야 겠죠.

[ARM] 최적화: 함수에 전달되는 아규먼트는 가급적 그대로 전달하세요 이제는 ARM의 시대다

이번 시간에는 함수의 인자 값을 전달할 때의 최적화 방법에 대해 이야기를 해보려 해요.
먼저, ARM 프로세서의 함수 호출 규약의 핵심을 말씀드리면; 

함수에 전달되는 인자는 R0 ~ R3 레지스터에 전달
함수가 반환하는 값은 R0 레지스터에 저장

그런데 코드를 작성하다보면 함수에서 전달되는 인자를 그대로 다른 함수에 전달하는 경우가 있습니다.
이 때 되도록 이면 인자의 순서를 그대로 유지한채 전달하면 조금 더 적은 명령어로 코드가 실행됩니다.

먼저 예시 코드를 보겠습니다.

01 unsigned int notrace noinline add_function(unsigned int x, unsigned int y, unsigned int z)
02 {
03 return (x + y + z);
04 }
05
06 void notrace  add_trace_function(unsigned int x, unsigned int y, unsigned int z)
07 {
08 unsigned int sum;
09
10 sum = add_function(y, x, z);
11 printk("add calculation: sum: %u, x = %u, y = %u, z=%u \n",
12                                      sum, x, y, z);
13
14 return sum;
15 }

각각 함수는 다음과 같은 기능을 수행합니다.

    * add_function() 함수: 입력 인자를 모두 더한 값을 반환하는 기능
    * add_trace_function() 함수:  add_function() 함수를 호출해 합계를 계산한 후 printk로 계산 결과를 출력

자, 여기서 소스를 조금 더 유심히 보겠습니다.

add_trace_function() 함수는 3개 인자(x, y, z)를 받아 add_function() 함수에 전달합니다.
그런데 다음과 같이 10번째 줄을 유심히 보면, x와 y를 바꿔서 인자로 전달합니다. 

10 sum = add_function(y, x, z);

이 동작을 수행할 때 어셈블리 코드를 확인하면 다음과 같습니다.

00 ffffff800859e73c <add_trace_function>:
01
02 void notrace  add_trace_function(unsigned int x, unsigned int y, unsigned int z)
03 {
04 ffffff800859e73c:   a9bd7bfd    stp x29, x30, [sp,#-48]!
05 ffffff800859e740:   910003fd    mov x29, sp
06 ffffff800859e744:   a90153f3    stp x19, x20, [sp,#16]
07 ffffff800859e748:   2a0003f3    mov w19, w0
08 ffffff800859e74c:   2a0103f4    mov w20, w1
09    unsigned int sum;
10
11 ffffff800859e750:   2a0103e0    mov w0, w1
12 ffffff800859e754:   2a1303e1    mov w1, w19
13 ffffff800859e758:   f90013f5    str x21, [sp,#32]
14 ffffff800859e75c:   2a0203f5    mov w21, w2
15    sum = add_function(y, x, z);
16 ffffff800859e760:   97fffff4    bl  ffffff800859e730 <add_function>

먼저, add_trace_function() 함수에 전달된 인자가 어느 레지스터에 저장되는지 확인합시다.

    * x0 : unsigned int x
    * x1 : unsigned int y
    * x2 : unsigned int z

ARM 아키텍처의 함수 호출 규약에 따라 각 인자는 x0, x1, x2 레지스터에 저장됩니다.
각 인자들이 add_function() 함수에 전달되는 과정을 어셈블리 명령어로 분석을 하겠습니다.

먼저 07~08 번째 줄을 봅시다.

07 ffffff800859e748:   2a0003f3    mov w19, w0
08 ffffff800859e74c:   2a0103f4    mov w20, w1

07번째 줄은 w0(unsigned int x)을 x19 레지스터에 저장하고, 
08번째 줄은 w1(unsigned int y)을 x20 레지스터에 저장합니다.

이어서 11~12번째 줄을 분석하겠습니다.

11 ffffff800859e750:   2a0103e0    mov w0, w1
12 ffffff800859e754:   2a1303e1    mov w1, w19 

11번째 줄은 w1(unsigned int y) 레지스터를 x0 레지스터에 저장하고,
12번째 줄은 w19(unsigned int y) 레지스터를 x1 레지스터에 저장합니다.

위 명령어는 x0와 x1 레지스터를 바꾸는 기능입니다.

    * x0 -> x1 : unsigned int x
    * x1 -> x0 : unsigned int y

이제, add_function() 함수를 호출하는 명령어를 보겠습니다.

14 ffffff800859e75c:   2a0203f5    mov w21, w2
15    sum = add_function(y, x, z);
16 ffffff800859e760:   97fffff4    bl  ffffff800859e730 <add_function>

14번째 줄은 x2(unsigned int z) 레지스터의 값을 x21 레지스터에 저장한 후,
16번째 줄과 같이 add_function() 함수를 호출합니다.

이번에는 add_trace_function() 함수의 인자를 그대로 add_function() 함수에 호출할 때 코드를 보겠습니다.

06 void notrace  add_trace_function(unsigned int x, unsigned int y, unsigned int z)
07 {
08 unsigned int sum;
09
10 sum = add_function(y, x, z);
11 printk("add calculation: sum: %u, x = %u, y = %u, z=%u \n",
12                                      sum, x, y, z);
13
14 return sum;
15 }

위 코드를 어셈블리 명령어를 포멧으로 보겠습니다.

00 void notrace  add_trace_function(unsigned int x, unsigned int y, unsigned int z)
01{
02 ffffff800859e73c:   a9bd7bfd    stp x29, x30, [sp,#-48]!
03 ffffff800859e740:   910003fd    mov x29, sp
04 ffffff800859e744:   a90153f3    stp x19, x20, [sp,#16]
05 ffffff800859e748:   f90013f5    str x21, [sp,#32]
06 ffffff800859e74c:   2a0103f4    mov w20, w1
07 ffffff800859e750:   2a0203f5    mov w21, w2
08 ffffff800859e754:   2a0003f3    mov w19, w0
09    unsigned int sum;
10
11    sum = add_function(x, y, z);
12 ffffff800859e758:   97fffff6    bl  ffffff800859e730 <add_function>
13

06~08번째 줄을 보면 x0~x2 레지스터를 x19~x21 레지스터에 저장합니다.
이어서 12번째 줄과 같이 add_function() 함수를 그대로 호출합니다.

만약, 인자를 그대로 전달했다면, 아래 명령어를 실행하지 않았을 것입니다.
 
11 ffffff800859e750:   2a0103e0    mov w0, w1
12 ffffff800859e754:   2a1303e1    mov w1, w19

[리눅스] insmod 명령어로 드라이버 설치 시 커널 내부 동작 디버깅해보기(ftrace) 리눅스 디바이스 드라이버

이전에 다음 글에서 insmod 명령어를 통해 모듈 타입 디바이스 드라이버가 설치될 때,
커널 내부 함수가 어떤 흐름으로 작동하는지 코드를 리뷰했습니다.

[리눅스] 드라이버: module_init 키워드로 지정한 함수가 호출되는 원리 - sys_finit_module()

이어서 이번 시간에는 ftrace를 통해 hello_module_init() 함수가 어떻게 호출되는지 살펴보겠습니다.


insmod 명령어로 디바이스 드라이버를 설치할 때의 전체 흐름

먼저 다음 그림은 insmod 명령어로 디바이스 드라이버를 설치할 때의 전체 흐름을 나타냅니다.


그림의 윗 부분에서 '유저 공간'으로 표기된 부분을 봅시다. insmod가 실행되어 시스템 콜이 발생되는 흐름을 확인할 수 있습니다. 이어서 그림 아랫 부분을 따라가 보면 커널 공간에서 다음과 같은 함수들이 호출된다는 사실을 알 수 있습니다.

    ● sys_finit_module() 함수
    ● load_module() 함수
    ● do_init_module() 함수
    ● do_one_initcall() 함수

위 함수 흐름을 보면 do_one_initcall() 함수에서 다음과 같이 module_init 키워드로 정의한 hello_module_init() 함수를 호출한다는 사실을 알 수 있습니다.

static int hello_module_init(void)
{
printk("Hello Module! \n");
return 0;
}
module_init(hello_module_init);

전체 흐름은 다음과 같이 정리할 수 있습니다.

    ● 'insmod' 명령어를 사용해 모듈식 디바이스 드라이버를 설치하면 된다. 
    ● 커널의 모듈 서스 시스템을 구성하는 do_one_initcall() 함수에서 hello_module_init() 함수를 호출한다.

모듈식 디바이스 드라이버를 설치하면 커널에서 어떤 흐름으로 함수가 호출되는지 알아봤으니, 
이어서 ftrace로 함수의 콜 스택을 확인하는 방법을 소개합니다.

ftrace 로그를 설정하는 방법

먼저 ftrace를 설정하는 명령어를 소개합니다. 
다음 코드를 같이 봅시다.

01 #!/bin/bash
02
03 echo 0 > /sys/kernel/debug/tracing/tracing_on
04 sleep 1
05 echo "tracing_off" 
06
07 echo 0 > /sys/kernel/debug/tracing/events/enable
08 sleep 1
09 echo "events disabled"
10
11 echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
12 sleep 1
13 echo "set_ftrace_filter init"
14
15 echo function > /sys/kernel/debug/tracing/current_tracer
16 sleep 1
17 echo "function tracer enabled"
18
19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable
20 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exec/enable
21 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exit/enable
22 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/enable
23
24 echo 1 > /sys/kernel/debug/tracing/events/signal/enable
25
26 echo printk > /sys/kernel/debug/tracing/set_ftrace_filter
27 sleep 1
28 echo "event enabled"
29
30 sleep 1
31 echo "set_ftrace_filter enabled"
32
33 echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
34 echo 1 > /sys/kernel/debug/tracing/options/sym-offset
35 echo "function stack trace enabled"
36
37 echo 1 > /sys/kernel/debug/tracing/tracing_on
38 echo "tracing_on"

위 명령어를 trace_module_init.sh 이름으로 저장합시다.

이제 명령어에서 중요한 부분을 소개합니다.
먼저 19~21번째 줄입니다.

19 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable
20 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exec/enable
21 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exit/enable

프로세스가 생성/실행/소멸되는 동작을 추적(트레이싱)하기 위한 ftrace 이벤트를 활성화하는 명령어입니다.

이어서 22번째 줄을 보겠습니다.

22 echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/enable

시스템 콜의 세부 동작을 추적하는 이벤트를 활성화하는 명령어입니다.

마지막으로, 26번째 줄을 보겠습니다.

26 echo printk > /sys/kernel/debug/tracing/set_ftrace_filter

set_ftrace_filter에 printk() 함수를 지정하는 명령어입니다.
갑자기 set_ftrace_filter에 printk() 함수를 지정하는 이유는 무엇일까요?

다음과 같이 디바이스 드라이버가 설치되는 과정에서 호출되는 hello_module_init() 함수를 보면 이유를 알 수 있습니다.

static int hello_module_init(void)
{
printk("Hello Module! \n");
return 0;
}

보시다시피 hello_module_init() 함수에서 printk() 함수를 호출해 커널 로그를 출력합니다.
set_ftrace_filter에 printk() 함수를 지정하면 hello_module_init() 함수의 콜 스택을 함께 볼 수 있습니다.

전체 실습 과정 소개

이제 전체 실습 과정을 소개합니다.
라즈베리 파이에서 터미널을 먼저 열겠습니다.

1> 가장 먼저 다음 명령어를 입력해 이번 포스팅에서 소개한 trace_module_init.sh 셸 스크립트를 실행합시다.
 
root@raspberrypi:/home/pi/work_0614# ./trace_module_init.sh

2> 이어서 'insmod hello_module.ko' 명령어를 입력해 모듈을 설치합시다.

root@raspberrypi:/home/pi/work_0614# insmod hello_module.ko

3> 마지막으로 터미널에서 다음 명령어를 입력해 ftrace 로그를 추출합니다.

echo 0 > /sys/kernel/debug/tracing/tracing_on
cp /sys/kernel/debug/tracing/trace . 
mv trace ftrace_log.c

이제 ftrace를 설정한 후 ftrace 로그를 추출했습니다.
이어서 ftrace 로그를 분석하겠습니다.

ftrace 메시지 분석하기

다음은 분석할 ftrace 메시지입니다.

01 insmod-2337 [001] .... 3058.441009: sched_process_exec: filename=/sbin/insmod pid=2337 old_pid=2337
02 ...
03 insmod-2337 [001] .... 3058.442694: sys_enter: NR 379 (3, 2cd30, 0, 0, d138b300, 2)
04 insmod-2337 [001] .... 3058.443179: printk+0x18/0x5c <-0x7f679020
05 insmod-2337 [001] .... 3058.443230: <stack trace>
06 => printk+0x1c/0x5c
07 => 0x7f679020
08 => do_one_initcall+0x50/0x214
09 => do_init_module+0x74/0x224
10 => load_module+0x1ee4/0x2610
11 => sys_finit_module+0xd8/0xe8
12 => __sys_trace_return+0x0/0x10
13 => 0x7ed70580
14 ...
15 insmod-2337  [001] ....  3058.443274: sys_exit: NR 379 = 0

먼저 01번째 줄입니다.

01 insmod-2337 [001] .... 3058.441009: sched_process_exec: filename=/sbin/insmod pid=2337 old_pid=2337

라즈베리 파이의 '/sbin/insmod' 파일이 실행되면서 insmod 프로세스가 실행되는 동작을 나타냅니다.
  
이어서 03번째 줄을 보겠습니다.

03 insmod-2337 [001] .... 3058.442694: sys_enter: NR 379 (3, 2cd30, 0, 0, d138b300, 2)

PID가 2337인 insmod 프로세스에서 379번 시스템 콜이 발생했다는 정보입니다.
여기서 379번 시스템 콜의 정체는 무엇일까요? 
라즈베리 파이에서 /usr/include/arm-linux-gnueabihf/asm/unistd.h 파일을 열어보면 그 정체를 알 수 있습니다.

/usr/include/arm-linux-gnueabihf/asm/unistd.h
# define __NR_timerfd_create (__NR_SYSCALL_BASE+350)
#define __NR_finit_module (__NR_SYSCALL_BASE+379)

379번 시스템 콜의 이름은 finit_module 이라는 사실을 알 수 있습니다.

이어서 04~13번째 줄을 분석하겠습니다.

04 insmod-2337 [001] .... 3058.443179: printk+0x18/0x5c <-0x7f679020
05 insmod-2337 [001] .... 3058.443230: <stack trace>
06 => printk+0x1c/0x5c
07 => 0x7f679020
08 => do_one_initcall+0x50/0x214
09 => do_init_module+0x74/0x224
10 => load_module+0x1ee4/0x2610
11 => sys_finit_module+0xd8/0xe8
12 => __sys_trace_return+0x0/0x10
13 => 0x7ed70580

finit_module 시스템 콜에 대한 시스템 콜 핸들러인 sys_finit_module() 함수가 호출된 후,
다음 목록의 커널 함수들이 호출된다는 정보를 확인할 수 있습니다.

    ● load_module()   
    ● do_init_module() 
    ● do_one_initcall()

07번째 줄에 0x7f679020이란 16진수가 보입니다.
07 => 0x7f679020

이는 이미 작성한 hello_module_init() 함수의 주소를 나타냅니다.

static int hello_module_init(void)
{
printk("Hello Module! \n");
return 0;
}

0x7f679020 주소의 정체 파악하기

자 그렇다면 0x7f679020 주소가 hello_module_init() 함수라는 사실은 어떻게 알 수 있을까요?
'cat /proc/kallsyms | grep hello' 명령어를 입력하면 심벌의 주소를 알 수 있습니다.

다음은 'cat /proc/kallsyms | grep hello' 명령어를 입력한 결과입니다.

root@raspberrypi:/home/pi/work_0614# cat /proc/kallsyms | grep hello
7f66b000 t $a [hello_module]
7f66b000 t hello_module_init [hello_module]

hello_module_init() 함수의 시작 주소가 7f66b000 임을 알 수 있습니다.
함수의 시작 주소가 7f66b000이고 hello_module_init() 함수에서 스택을 푸시하는 명령어를 빼면,
+0x20 오프셋 주소에서 printk() 함수를 호출할 것이라고 예측할 수 있습니다.

조금 더 명확히 이 정보를 확인하려면 objdump라는 바이너리 유틸리티를 사용해 hello_module.ko 파일에 포함된 hello_module_init() 함수를 확인 할 수 있습니다.
 
root@raspberrypi:/home/pi/work_0614# objdump -d hello_module.ko
hello_module.ko:     file format elf32-littlearm


Disassembly of section .text:

00000000 <hello_module_init>:
   0: e1a0c00d mov ip, sp
   4: e92dd800 push {fp, ip, lr, pc}
   8: e24cb004 sub fp, ip, #4
   c: e52de004 push {lr} ; (str lr, [sp, #-4]!)
  10: ebfffffe bl 0 <__gnu_mcount_nc>
  14: e3000000 movw r0, #0
  18: e3400000 movt r0, #0
  1c: ebfffffe bl 0 <printk>
  20: e3a00000 mov r0, #0

hello_module.ko 파일은 demangle 하기 전의 상태이므로 hello_module_init() 함수의 시작 주소는 00000000으로 표기됩니다.
그런데 printk를 호출하는 부분의 주소는 0x1c입니다. ARM 아키텍처는 파이프라인을 적용했으므로 실제 실행되는 주소보다 +0x4 바이트를 더해서 표기됩니다.

그래서 ftrace 메시지의 07번째 줄에 0x7f679020이란 16진수가 보이는 것입니다.



---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

[펌][유튜브] 초보 개발자, 이것만 안 해도 평균 이상 갑니다 임베디드 에세이

유튜브에 좋은 컨텐츠를 올리는 개발자들이 늘어났으면 좋겠다.
초보 개발자만 아니라 다른 개발자도 귀 담아 들으면 좋은 내용이 많은 것 같네.

출처: 초보 개발자, 이것만 안 해도 평균 이상 갑니다 (흔히 하는 실수 공개)

1. 최소한의 노력도 없이 구글링 하듯이 질문하기

    ● 고민을 하나도 하지 않고 자주 질문을 하면 다른 개발자에게 인터럽트를 유발하게 된다.
    ● 팀 동료의 생산성에 악영향을 끼친다.
    ● 동료 개발자에게 배려를 못한다는 느낌을 준다.

2.  반대로 너무 오래 침묵을 하기

    ● 질문을 하지 않고 혼자 끙끙 고민만 하다가 일정을 다 날려버리면 낭패다.
    ● 우선 하루나 반나절 동안 문제를 스스로 해결하기 위해 노력을 한 후, 스스로 해결이 불가능한지 
        판단을 내리는 게 중요하다.

3.  이해하기 전에 대답한다 

    ● 자신의 실력에 대한 과한 확신 때문에 해보지도 않고 성급하게 판단을 내린다.
    ● 큰 사고를 일으킬 수 있는 유형 중 하나다.
    ● 겸손한 자세를 갖는 게 중요하다.

4.  이해한 척 하기 

    ● 제대로 이해하기 못했는데 다시 물어보기 그래서 이해한 척을 한다.
    ● 예상하지 못한 낮은 성과물이 나올 가능성이 높다.

[6장] 인터럽트 후반부 처리: 오타 공지(468페이지) Question_Announcement

『디버깅을 통해 배우는 리눅스 커널의 구조와 원리』 책의 저자 김동현입니다.
『6장』 '인터럽트 후반부 처리'에서 오타가 확인돼 공지드립니다. 오타를 잡기 위해 최선을 다했으나 미비한 점이 보여 죄송합니다.

오타를 제보해주신 독자님께 정말 감사드리며, 이 내용을 포함해, 
오타나 오류로 확인된 부분은 2판과 출간될 이북(Ebook)에 꼭 반영토록 하겠습니다.

이제부터 오타에 대해 설명드리겠습니다.

468 페이지

468 페이지를 보면 다음과 같은 문장이 보입니다.

01~05번째 줄을 분석해 보면 or_softirq_pending() 매크로 함수의 실체는 다음과 같습니다.
irq_stat[cpu].__softirq_pending =| x;

위에서 보이는 '=|' 구문이 오타입니다. 
irq_stat[cpu].__softirq_pending 와 x 간 OR 비트 연산을 수행한 후, irq_stat[cpu].__softirq_pending에 저장하는 동작입니다.

(before)
irq_stat[cpu].__softirq_pending =| x;
->

(after)
irq_stat[cpu].__softirq_pending |= x;

같은 페이지에서 보이는 다음 문장도 마찬가지입니다.

이 조건에서 다음 연산을 실행하면 결과는 어떻게 될까요?
irq_stat[1].__softirq_pending =| x;

(before)
irq_stat[cpu].__softirq_pending =| x;
->

(after)
irq_stat[cpu].__softirq_pending |= x;

메모 형식으로 이 내용을 표현하면 다음과 같습니다.


같은 파에지에서 눈을 조금만 내려보면 아래 문장을 볼 수 있습니다.



468~469 페이지

468 페이지에서 irq_stat 변수를 확인할 수 있는데요.

(static irq_cpustat_t [4]) irq_stat = (
[0] = (
   (unsigned int) __softirq_pending = 0 = 0x1,
[1] = (
   (unsigned int) __softirq_pending = 0 = 0x3,
[2] = (
   (unsigned int) __softirq_pending = 0 = 0x0,
[3] = (
   (unsigned int) __softirq_pending = 0 = 0x6,

가운데 부분이 있는 0은 무시하시면 됩니다.
irq_stat 변수는 다음과 같이 표현할 수 있습니다.

(static irq_cpustat_t [4]) irq_stat = (
[0] = (
   (unsigned int) __softirq_pending = 0x1,
[1] = (
   (unsigned int) __softirq_pending = 0x3,
[2] = (
   (unsigned int) __softirq_pending = 0x0,
[3] = (
   (unsigned int) __softirq_pending = 0x6,

메모 형식으로 보여드리면 다음과 같습니다.

---
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

Table of contents(목차) - 이제는 ARM의 시대 이제는 ARM의 시대다

1장. ARM 프로세서 소개

1.1 ARM 프로세서는 왜 배워야 할까 
   ●   1.1.1 ARM 프로세서를 일반 SW 개발자도 배워야 하는 이유
   ●   1.1.2 ARM 프로세서를 잘 알면 얻게 되는 잇점

1.2 ARM 프로세서 학습하는 방법의 문제점 
   ●   1.2.1 ARM 프로세서는 왜 배우기 어려울까?
   ●   1.2.2 ARM 프로세서는 어떻게 공부해야 할까?

1.3 ARM 프로세서 소개  
   ●   1.3.1 ARM 프로세서의 역사
   ●   1.3.2 ARM 프로세서의 패밀리
   ●   1.3.3 ARM 프로세서의 전망

1.4 ARM 프로세서를 이루는 주요 개념  
   ●   1.4.1 전체 ARM 프로세서의 구조 
   ●   1.4.2 어셈블리 명령어
   ●   1.4.3 레지스터 세트와 ARM 모드  
   ●   1.4.4 익셉션
   ●   1.4.5 함수 호출 규약
   ●   1.4.6 MMU(Memory Managment Unit)
   ●   1.4.7 가상화 & 트러스트 존

1.5 ARM32와 ARM64 아키텍처의 차이점   
   ●   1.5.1 레지스터 세트 
   ●   1.5.2 함수 호출 규약

1.6 이 책에서 다루는 ARM 아키텍처    
   ●   1.6.1 리눅스와 Cortex-A57
   ●   1.6.2 Cortex-M3와의 차이점

2장. ARM 어셈블리 디버깅 툴 소개 

2.1 Virtual Box
  2.1.1 Virtual Box 설치하기
  2.1.2 Virtual Box 실행하기

2.2 GDB
  2.2.1 GDB 설치하기
  2.2.2 GDB 실행하기

2.3 Eclipse C/C++ 
  2.3.1 Eclipse C/C++ 설치하기
  2.3.1 Eclipse C/C++ 실행하기

2.4 QEMU 에뮬레이터 
  2.4.1 QEMU 에뮬레이터 설치하기
  2.4.1 QEMU 에뮬레이터 실행하기
  
3장. ARM 어셈블리 디버깅 방법 소개

3.1 컴파일 과정과 바이너리 유틸리티
   ●   3.1.1 컴파일 과정 소개
   ●   3.1.2 컴파일 관련 바이너리 유틸리티
   ●   3.1.3 오브젝트 파일 분석에 필요한 바이너리 유틸리티 
3.2 GDB
   ●   3.2.1 GDB 기본 사용 방법
   ●   3.2.2 GDB로 어셈블리 명령어 디버깅해보기
      ARM32/ARM64
   ●   3.2.3 QEMU 에뮬레이터에서 GDB 사용해보기
3.3 Eclipse C/C++ 툴  
   ●   3.3.1 Eclipse 실행하기 
   ●   3.3.2 이클립스로 어셈블리 명령어 디버깅해보기
3.4 TRACE32 (if necessary) 
   ●   3.4.1 TRACE32 실행하기 
   ●   3.4.2 TRACE32 어셈블리 명령어 디버깅해보기

4장. 어셈블리 명령어

4.1 어셈블리 명령어 소개  
   ● 4.1.1 어셈블리 명령어는 왜 배워야 할까 
         * 어셈블리 명령어는 ARM 프로세서의 언어
         * C의 모든 문법은 어셈블리 명령어로 해석 가능
         * 더 안정적이고 최적화된 코드 작성
         * 디버깅 실력의 기본기 
  ● 4.1.2 어셈블리 명령어를 배우기 어려운 이유
         * 어셈블리 코드만 따로 배움 
         * 사전식으로 명령어를 암기만 하려 함
  ● 4.1.3 어셈블리 명령어를 잘 배우는 방법 
         * 외우지 말고 C 코드와 분석하는 습관을 갖자
         * 디버깅을 자주 해보자
  ● 4.1.4 어셈블리 명령어를 배우기 위해 알아야 하는 지식  
         * 메모리와 함께 전체 시스템 구조 
         * 디버깅 방법(Eclipse/GDB/TRACE32)

4.2 ARM32 어셈블리 명령어    
   ● 4.2.1 어셈블리 명령어의 기본 문법 
          * 연산자/피연산자
          * Rd 기준 변수의 의미

   ● 4.2.2 데이터 저장과 관련된 명령어  
          * cpy/mov/ldr/str

   ● 4.2.3 연산관 관련된 어셈블리 명령어  
          * sub/add/lsr/rsl

   ● 4.2.4 함수 호출과 관련된 명령어
          * push/pop

4.3 ARM64 어셈블리 명령어    
   ● 4.2.1 어셈블리 명령어의 기본 문법 
          * 연산자/피연산자
          * Rd 기준 변수의 의미

   ● 4.2.2 함수 호출과 관련된 명령어
          * stp/ldp

5장. 함수 호출 규약(Calling Convention)

5.1 함수 호출 규약 소개
   ● 5.1.1 호출 규약을 배워야 하는 이유
         * 프로그래밍의 근본 원리 파악
         * 안정적이고 최적화된 코드 작성
         * 디버깅 실력의 기초 체력 증진
  ● 5.1.2 함수 호출 규약을 배우기 어려운 이유
         * 어셈블리 코드만 따로 배움 
         * 무조건 암기만 하려 함
  ● 5.1.3 함수 호출 규약을 배우는 방법 
         * C 코드와 어셈블리 명령어를 함께 분석
         * 콜 스택을 직접 복원해보는 실습
  ● 5.1.4 함수 호출 규약을 배우기 위해 알아야 하는 지식  
         * 프로세서의 스택 공간
         * 디버깅 방법

5.2 ARM32 함수 호출 규약
   ● 5.2.1 함수를 호출할 때 사용되는 레지스터
   ● 5.2.2 함수를 호출할 때 실행되는 명령어

5.3 ARM64 함수 호출 규약
   ● 5.3.1 함수를 호출할 때 사용되는 레지스터
   ● 5.3.2 함수를 호출할 때 실행되는 명령어

5.4 함수 호출 규약과 연관된 리눅스 커널 기능
   ● 5.4.1 인터럽트용(IRQ) 스택(ARM64)
   ● 5.4.2 스택 카나리: 스택 오염 디버깅
   ● 5.4.3 스택 공간에 레드 존 추가 

5.5 실전 케이스 스터디 이슈 소개
   ● 5.5.1 스택 오버플로우
   ● 5.5.2 스택 언더플로우

5.6 함수 호출 규약 디버깅 실습
   ● 5.6.1 ARM32 함수 호출 규약 실습
   ● 5.6.2 ARM64 함수 호출 규약 실습
   ● 5.6.3 TRACE32로 ARM32 콜 스택 복원하기
   ● 5.6.4 TRACE32로 ARM64 콜 스택 복원하기

6. ARM 모드와 레지스터

6.1 주요 개념 소개
   ●   6.1.1 ARM 모드는 왜 있는 걸까?
   ●   6.1.2 ARM 프로세서에서 레지스터란
   ●   6.1.3 ARM 모드 별로 레지스터가 있는 이유
   ●   6.1.4 ARM 모드를 배워야 하는 이유
* ARM 모드에 따라 인터럽트와 메모리에 접근하는 권한이 다름
* 하이퍼바이저나 트러스트 존과 같은 ARM의 기법을 이해하기 위해
* 시스템을 디자인하기 위해
6.2 ARM32 모드
   ●   6.2.1 ARM32 모드의 종류
   ●   6.2.2 ARM32 모드 별 세부 동작
              * User/Supdervisor/Secure 모드
   ●   6.2.3 ARM32 모드 관련 스페셜 레지스터 
              * cpsr, spsr 레지스터
   ●   6.2.4 ARM32 모드를 스위칭할 때 실행되는 어셈블리 명령어
              * svc/hvc

6.3 ARM64 모드
   ●   6.3.1 ARM64 익셉션 레벨이란
   ●   6.3.2 익셉션 레벨의 종류
   ●   6.3.3 익셉션 레벨 별 세부 동작
              * EL0/EL1/EL0
   ●   6.3.4 ARM64 익셉션 레벨 관련 스페셜 레지스터 
   ●   6.3.5 익셉션 레벨을 스위칭할 때 실행되는 어셈블리 명령어
              * svc/hvc

7장 익셉션

7.1 익셉션의 기본 개념
   ●   7.1.1 익셉션은 왜 발생할까
   ●   7.1.2 익셉션의 종류와 익셉션 벡터 
   ●   7.1.3 익셉션을 배워야 하는 이유
    * 운영체제의 깊이 있는 이해
    * 고급 디버깅을 위한 필수 지식
    * ARM Extention 기술(하이퍼바이저, 트러스트 존)의 기반 기법
   ●   7.1.4 익셉션을 배우는 방법
    * 리눅스의 ftrace
    * 어셈블리 명령어 분석
7.2 익셉션의 세부 동작
   ●   7.2.1 익셉션이란 누가 언제 발생할까
   ●   7.2.2 익셉션이 발생하면 처리하는 주요 동작  
7.3 리셋 익셉션의 처리 방식
   ●   7.3.1 ARM32
   ●   7.3.2 ARM64
7.4 인터럽트가 발생할 때 처리 방식
   ●   7.4.1 ARM32
   ●   7.4.2 ARM64
7.5 크래시가 발생할 때 익셉션
   ●   7.5.1 Data Abort
* ARM32
    * ARM64
   ●   7.5.2 Prefetch Abort
    * ARM32
    * ARM64   
   ●   7.5.3 Undefined 익셉션
    * ARM32
    * ARM64
7.6 ARM Extention 기능 관련 익셉션
   ●   7.6.1 Secure Monitor Call
    * ARM32
    * ARM64
   ●   7.6.2 Hypervisor Call
    * ARM32
    * ARM64
7.7 익셉션 디버깅
   ●   7.7.1 리눅스 ftrace로 인터럽트 익셉션 디버깅
   ●   7.7.2 리눅스 ftrace로 소프트웨어 인터럽트 디버깅
   ●   7.7.3 코어 덤프 디버깅

8. 메모리 관리 장치(MMU) 와 캐시

8.1 MMU를 구성하는 주요 개념
   ●   8.1.1 캐시
   ●   8.1.2 페이지 테이블
   ●   8.1.3 TLB(Translation Lookaside Buffer)
   ●   8.1.4 메모리 접근 권한
8.2 MMU를 제어하는 주요 명령어
   ●   8.2.1 CP15 레지스터란
   ●   8.2.2 MMU 설정 명령어
   ●   8.2.3 메모리 권한 설정
   ●   8.2.4 페이지 테이블 설정
8.3 메모리 계층 구조와 캐시 메모리
   ●   8.3.1 캐시 아키텍처  
   ●   8.3.2 캐시 정책 
   ●   8.3.3 CP15 레지스터로 캐시 설정
8.4 캐시의 주요 동작
   ●   8.4.1 캐시 플러시
   ●   8.4.2 캐시 클린
   ●   8.4.3 캐시 락다운
   ●   8.4.5 캐시와 소프트웨어 성능

9. 최적화된 C 프로그래밍

9.1 ARM 프로세서의 특징을 활용한 C 프로그램 최적화가 중요한 이유
   ●   9.1.1 임베디드 제품
       저렴한 가격/부족한 실장 공간/발열
   ●   9.1.2 클라우드 서버
       서버의 전기세/에어쿨러
   ●   9.1.3 방대한 SW 구조
9.2 적절한 데이터형 선언 
   ●   9.2.1 되도록 워드로 선언하자
        ARM32 실습
ARM64 실습 
   ●   9.2.2 비슷한 타입의 구조체 선언 
ARM32 실습
ARM64 실습    
9.3 함수에 전달되는 인자의 선언 
   ●   9.3.1 함수의 인자 갯수는 3개 이하로 선언하자 
  예제 코드 소개/ARM32 실습 
  예제 코드 소개/ARM64 실습    
   ●   9.3.2 인자의 갯수가 많으면 구조체의 포인터를 활용하자  
  예제 코드 소개/ARM32 실습 
  예제 코드 소개/ARM64 실습 
   ●   9.3.3 되도록 함수로 전달되는 인자의 순서를 지키자  
  예제 코드 소개/ARM32 실습 
  예제 코드 소개/ARM64 실습 
    ●   9.3.4 함수로 반환되는 인자는 다음 함수의 첫 인자로 전달하자  
  예제 코드 소개/ARM32 실습 
  예제 코드 소개/ARM64 실습
9.4 ARM 프로세서가 싫어하는 연산
    ●   9.4.1 나눗셈 연산을 ARM 프로세서는 어떻게 처리하나?
    ●   9.4.2 되도록 나눗셈 연산을 피하자 
    ●   9.4.3 되도록 2의 배수로 나눗셈을 연산하자
9.5 루프 최적화 
    ●   9.4.1 for 루프 대신에 do~while 문을 쓰세요 
    ●   9.4.2 되도록 증감 루프 카운터를 쓰세요

10장 리버싱

11장 ARM Extentsion

11.1 하이퍼바이저
    ●   11.1.1 하이저바이저란
    ●   11.1.2 ARM 아키텍처 관점으로 하이퍼바이저란
    ●   11.1.3 하이퍼바이저 관련 ARM 명령어
11.2 트러스트 존
    ●   11.1.1 트러스트 존이란
    ●   11.1.2 ARM 아키텍처 관점으로 하이퍼바이저란
    ●   11.1.3 트러스트 존 관련 ARM 명령어


12장 ARM 프로세서 관련 실전 이슈 케이스 스터티

12.1 스택 오염
    ●   12.1.1 스택 오버플로우
    ●   12.1.2 스택 언더플로우
    ●   12.1.3 ARM 명령어를 활용해 스팩 오염을 디버깅하기 
    ARM32 기반 디버깅 패치
    ARM64 기반 디버깅 패치

12.2 커널 크래시
    ●   12.1.1 Data Abort
    ●   12.1.2 Prefetch Abort
    ●   12.1.3 Undefined Abort 




[6장] 인터럽트 후반부 처리: 오타 공지(427페이지) Question_Announcement

『디버깅을 통해 배우는 리눅스 커널의 구조와 원리』 책의 저자 김동현입니다.
『6장』 '인터럽트 후반부 처리'에서 오타가 확인돼 공지드립니다.

오타를 제보해주신 독자님께 정말 감사드리며, 이 내용을 포함해, 
오타나 오류로 확인된 부분은 2판과 출간될 이북(Ebook)에 꼭 반영토록 하겠습니다.

수정된 irq_thread_trace.sh 파일은 아래 GitHub 링크에서 내려받아 확인하실 수 있습니다.


 
수정 내용

427 페이지의 가운데 부분에 다음과 같이 ftrace를 설정하는 명령어가 있습니다.

01 echo bcm2835_mbox_threaded_irq bcm2835_mbox_irq > /sys/kernel/debug/tracing/set_ftrace_filter
02 sleep 1
03 echo "set_ftrace_filter enabled"

01번째 줄을 보면 '/sys/kernel/debug/tracing/set_ftrace_filter' 파일에 다음 함수들을 지정하는데요.

    ● bcm2835_mbox_threaded_irq bcm2835_mbox_irq

bcm2835_mbox_threaded_irq bcm2835_mbox_irq 대신에,
bcm2835_mmc_thread_irq bcm2835_mmc_irq로 지정돼야 합니다.

여기서 소개된 수정 내역을 반영하지 않으면 ftrace에서,
bcm2835_mmc_thread_irq()/bcm2835_mmc_irq() 함수의 콜 스택을 볼 수 없습니다.

428 페이지에도 다음과 같은 설명이 보입니다.

" 이 명령어 중에서 중요한 부분을 선별해서 설명하겠습니다. 먼저 다음 부분은 bcm2835_mbox_threaded_
irq()와 bcm2835_mbox_irq() 함수를 set_ftrace_filter에 저장하는 명령어입니다.
echo bcm2835_mbox_threaded_irq bcm2835_mbox_irq > /sys/kernel/debug/tracing/set_ftrace_filter"  

마찬가지로 bcm2835_mbox_threaded_irq bcm2835_mbox_irq 대신에,
bcm2835_mmc_thread_irq bcm2835_mmc_irq로 지정돼야 합니다.

메모로 표기된 수정

이번에는 메모 형식으로 수정 내역을 소개합니다.

427 페이지

428 페이지

감사합니다.

[ARM] const static 키워드로 변수를 선언: 코드 크기 최적화 이제는 ARM의 시대다

코드를 작성하다보면 배열을 선언한 후 그 값을 상수와 같이 사용할 때가 있습니다.
한 가지 예를 들겠습니다. 다음 코드를 보면 rtw_cfg80211_default_mgmt_stypes 배열은static const  키워드로 선언돼 있습니다.

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.c
static const struct ieee80211_txrx_stypes
rtw_cfg80211_default_mgmt_stypes[NUM_NL80211_IFTYPES] = {
[NL80211_IFTYPE_ADHOC] = {
.tx = 0xffff,
.rx = BIT(IEEE80211_STYPE_ACTION >> 4)
},

rtw_cfg80211_default_mgmt_stypes 배열은 고정된 값을 저장하고 실행 중에 바뀌지 않습니다.

위 코드를 컴파일하고 코드 사이즈를 측정해볼까요? size란 바이너리 유틸리티를 활용해 오브젝트의 사이즈를 확인해봅시다.

root@raspberrypi:/home/pi/src/rpi_dev_code# ./aarch64-linux-gnu-size ./out/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.o
   text    data     bss     dec     hex filename
  26046    4888       0   30934    78d6 ./out/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.o


코드 수정해보기

이제 rtw_cfg80211_default_mgmt_stypes  변수에 const 키워드를 빼 보겠습니다.

diff --git a/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.c b/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.c
index 28807e9..a7319d9 100755
diff --git a/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.c b/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.c
index 28807e9..e641551 100755
--- a/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.c
+++ b/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.c
@@ -246,7 +246,7 @@ void rtw_spt_band_free(struct ieee80211_supported_band *spt_band)
 }

 #if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,37)) || defined(COMPAT_KERNEL_RELEASE)
-static const struct ieee80211_txrx_stypes
+static struct ieee80211_txrx_stypes
 rtw_cfg80211_default_mgmt_stypes[NUM_NL80211_IFTYPES] = {
        [NL80211_IFTYPE_ADHOC] = {
                .tx = 0xffff,

위와 같이 코드를 수정한 다음에 빌드를 한 후, size란 바이너리 유틸리티를 활용해 오브젝트의 사이즈를 확인해봅시다.

root@raspberrypi:/home/pi/src/rpi_dev_code# ./aarch64-linux-gnu-size ./out/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.o
   text    data     bss     dec     hex filename
  25998    4944       0   30942    78de ./out/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.o

결과가 달라졌습니다.

오브젝트 사이즈 크기 비교

이제 const 키워드가 있을 때와 없을 때 코드 사이즈를 비교해 봅시다.

(before)
   text    data     bss     dec     hex filename
  26046    4888       0   30934    78d6 ./out/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.o

(after)
   text    data     bss     dec     hex filename
  25998    4944       0   30942    78de ./out/drivers/net/wireless/realtek/rtl8192cu/os_dep/linux/ioctl_cfg80211.o

.data 섹션: +66 바이트
.dec  섹션:   +8 바이트
.text 섹션:   -48 바이트

오브젝트의 26바이트가 증가 됐습니다.

26바이트면 무시할만한 사이즈라 말할 수 있지만, 소형 임베디드 시스템에서는 1K라도 아끼는 최적화 코딩을 해야 합니다.
왜냐면 스택 사이즈도 적고, 파티션 사이즈도 최대한 작은 사이즈으로 구성하는 경우가 많이 때문이죠.

이런 코드가 모이면 전체 이미지의 1K, 5K 정도로 크기가 증가할 수 있으니, 이 팁을 잘 활용하면 좋을 것 같습니다. 


리눅스 커널 커뮤니티에서 유사한 패치가 보이니, 아래 내용도 참고하면 좋겠네요.

https://patchwork.kernel.org/patch/11133647/
From: Colin Ian King <colin.king@canonical.com>

Arrays pwr_info_offset and sprom_sizes can be make static const rather
than populating them on the stack. Shrinks object size by 236 bytes.

Before:
   text    data     bss     dec     hex filename
  11300    1320      64   12684    318c drivers/bcma/sprom.o

After:
   text    data     bss     dec     hex filename
  10904    1480      64   12448    30a0 drivers/bcma/sprom.o


diff --git a/drivers/bcma/sprom.c b/drivers/bcma/sprom.c
index 206edd3ba668..bd2c923a6586 100644
--- a/drivers/bcma/sprom.c
+++ b/drivers/bcma/sprom.c
@@ -222,7 +222,7 @@  static void bcma_sprom_extract_r8(struct bcma_bus *bus, const u16 *sprom)
 {
  u16 v, o;
  int i;
- u16 pwr_info_offset[] = {
+ static const u16 pwr_info_offset[] = {
  SSB_SROM8_PWR_INFO_CORE0, SSB_SROM8_PWR_INFO_CORE1,
  SSB_SROM8_PWR_INFO_CORE2, SSB_SROM8_PWR_INFO_CORE3
  };
@@ -578,9 +578,11 @@  int bcma_sprom_get(struct bcma_bus *bus)
 {
  u16 offset = BCMA_CC_SPROM;
  u16 *sprom;
- size_t sprom_sizes[] = { SSB_SPROMSIZE_WORDS_R4,
- SSB_SPROMSIZE_WORDS_R10,
- SSB_SPROMSIZE_WORDS_R11, };
+ static const size_t sprom_sizes[] = {
+ SSB_SPROMSIZE_WORDS_R4,
+ SSB_SPROMSIZE_WORDS_R10,
+ SSB_SPROMSIZE_WORDS_R11,
+ };
  int i, err = 0;
 
  if (!bus->drv_cc.core)

커밋 메시지를 보니 무려 236 바이트나 절약됐다고 하네요.

---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---


[4장] 프로세스: 오타 공지(293페이지) Question_Announcement

『디버깅을 통해 배우는 리눅스 커널의 구조와 원리』 책의 저자 김동현입니다.
『4장』 '커널 디버깅과 코드 학습'에서 오타가 확인돼 공지드립니다.

오타나 오류로 확인된 부분은 2판과 출간될 이북(Ebook)에 꼭 반영토록 하겠습니다.

293 페이지

293 페이지에서 있는 cpu_context_save 구조체를 보겠습니다.

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/arch/arm/include/asm/thread_info.h
struct cpu_context_save {
__u32 4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};

가장 첫 번째 필드가 '__u32 4'로 보이는데 이는 '__u32 r4'로 표기돼야 합니다.

아래는 실제 책의 내용에 메모를 추가한 화면입니다.

참고로 1부에서 cpu_context_save 구조체는 여러 페이지에서 확인할 수 있는데요.
모두 제대로 표기돼 있습니다.

---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

[3장] 커널 디버깅과 코드 학습: 오타 공지(84/110페이지) Question_Announcement

『디버깅을 통해 배우는 리눅스 커널의 구조와 원리』 책의 저자 김동현입니다.

『3장』 '커널 디버깅과 코드 학습'에서 오타가 확인돼 공지드립니다.
원고를 수정하는 과정에서 오타를 잡기 위해 최선을 다했으나, 약간의 미비한 점이 있는 점 양해 부탁드립니다.

해당 내용은 2판과 출간될 이북(Ebook)에 꼭 반영토록 하겠습니다. 

84 페이지

먼저 84 페이지에 보이는 아래 내용을 보겠습니다. (볼드체)

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/irqdesc.h
01 struct irq_desc {
02 struct irq_common_data irq_common_data;
03 struct irq_data irq_data;
04 unsigned int __percpu *kstat_irqs;
05 irq_flow_handler_t handle_irq;
...
06 struct irqaction *action; /* IRQ action list */

그런데 코드를 읽다 보니 인터럽트의 속성 정보는 위 05번째 줄과 같이 action 필드에 저장된다는 사실
을 알게 됐습니다.

그런데 '인터럽트의 속성 정보는 위 05번째 줄과 같이'에서 05번째는 06번째 줄입니다.

아래는 실제 책의 내용에 메모를 추가한 화면입니다.



110 페이지

이번에는 110 페이지에 있는 내용입니다. (볼드체)

■ h/s: h이면 인터럽트 컨텍스트, s이면 Soft IRQ 컨텍스트입니다.
■ 0~5: 프로세스의 thread_info 구조체의 preempt_count 필드값입니다.

'그림 3.3'에 명시된 preempt_count값의 범위는 '0~3'로 명시돼 있습니다.
'0~5'은 '0~3'로 수정돼야 합니다.

아래는 실제 책의 내용에 메모를 추가한 화면입니다.


---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

『2장』 질문: build_rpi_kernel.sh으로 라즈비안 커널 빌드가 되지 않습니다. Question_Announcement

『디버깅을 통해 배우는 리눅스 커널의 구조와 원리』 책의 저자 김동현입니다.

몇몇 독자분들이 실습을 하시면서 질문을 주셨는데요, 주의해야 할 점이 있어 포스팅을 올립니다.
먼저 질문을 소개합니다.

Q); build_rpi_kernel.sh으로 라즈비안 커널 빌드가 되지 않습니다.

A) build_rpi_kernel.sh 파일을 작성하신 후 오타가 있는지 한번 점검해보세요.
그런데 눈을 씻고 오타를 확인해도 찾기 어려운 부분이 있는데요.

한 가지 구문을 소개합니다. 
먼저 다음 빌드 스크립트 build_rpi_kernel.sh 파일을 봅시다. (54페이지)

01 #!/bin/bash
02
03 echo "configure build output path"
04
05 KERNEL_TOP_PATH="$( cd "$(dirname "$0")" ; pwd -P )"
06 OUTPUT="$KERNEL_TOP_PATH/out"
07 echo "$OUTPUT"
08
09 KERNEL=kernel7
10 BUILD_LOG="$KERNEL_TOP_PATH/rpi_build_log.txt"
11
12 echo "move kernel source"
13 cd linux
14
15 echo "make defconfig"
16 make O=$OUTPUT bcm2709_defconfig
17
18 echo "kernel build"
19 make O=$OUTPUT zImage modules dtbs -j4 2>&1 | tee $BUILD_LOG

16번째와 19번째 줄을 보면 'make O=$OUTPUT' 구문이 보이는데,
여기서 O는 숫자 0이 아니라 영어 알파벳 O입니다. 이 점을 주의했으면 좋겠습니다.

아래는 실제 책의 내용에 메모를 추가한 화면입니다.


이번에는 라즈비안을 설치하는 install_rpi_kernel_img.sh 셸 스크립트 파일을 보겠습니다. (59페이지)


역시 'make O=$OUTPUT' 구문이 보이는데,
여기서 O는 숫자 0이 아니라 영어 알파벳 O입니다. 이 점을 주의했으면 좋겠습니다.

정말 중요한 점(다시 리즈비안 커널을 빌드하세요)

여기서 정말 중요한 점을 말씀드리고 싶은데요.
만약 'make O=$OUTPUT' 대신 'make 0=$OUTPUT'(숫자 0)으로 라즈비안 커널을 빌드했으면, 반드시! out 폴더를 지우고 다시 빌드를 해야 한다는 점이에요. 만약 out 폴더를 지우지 않고 빌드를 하면 라즈베리 파이가 다시 부팅을 못할 수도 있거든요.

즉, 다음과 같이 'rm -rf out' 명령어를 입력해 out 디렉터리(/home/pi/rpi_kernel_src/linux/out) 만 지우시고,
root@raspberrypi:/home/pi/rpi_kernel_src# rm -rf out

책에서 명시한대로 './build_rpi_kernel.sh' 와 './install_rpi_kernel_img.sh' 명령어를 실행하시면 됩니다.
root@raspberrypi:/home/pi/rpi_kernel_src# ./build_rpi_kernel.sh

관련 빌드 스크립트는 아래 깃허브에도 있으니 참고하셨으면 좋겠네요.
https://github.com/wikibook/linux-kernel 

---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

『2장』 질문: 라즈베리 파이에서 디렉터리에 파일을 저장하거나 이동할 수 없습니다. Question_Announcement

『디버깅을 통해 배우는 리눅스 커널의 구조와 원리』 책의 저자 김동현입니다.

몇몇 독자분들이 실습을 하시면서 질문을 주셨는데요, 한 가지 공유드릴 사항이 있어 글을 남깁니다.
먼저 질문을 소개합니다.

질문

Q); 라즈비안에서 제공하는 파일 관리자를 통해 파일을 저장을 시도했는데, 저장이 되지 않는다.

'2.3.3 라즈비안 리눅스 커널 빌드' 절의 53 페이지에서 소개된 'build_rpi_kernel.sh' 라는 이름으로 저장했다. 
'rpi_kernel_src' 디렉터리에 저장하려 했는데 다음과 같은 에러 창이 보이면서 build_rpi_kernel.sh이 저장돼지 않는다.


Answer

A) 리눅스 터미널을 열고 다음과 같이 하시면 됩니다.

* root@raspberrypi:/home/pi/rpi_kernel_src 디렉터리로 이동합니다.
root@raspberrypi:/home/pi/rpi_kernel_src#

* 'touch build_rpi_kernel.sh' 명령어를 입력해 build_rpi_kernel.sh 파일을 생성합니다.
root@raspberrypi:/home/pi/rpi_kernel_src# touch build_rpi_kernel.sh

* 'chmod 777 build_rpi_kernel.sh' 명령어를 입력해 build_rpi_kernel.sh 파일에 읽기/쓰기/수정 권한 추가합니다.
root@raspberrypi:/home/pi/rpi_kernel_src# chmod 777 build_rpi_kernel.sh
root@raspberrypi:/home/pi/rpi_kernel_src# ls
build_rpi_kernel.sh linux

* 기니(Geany) 에디터로 '/home/pi/rpi_kernel_src/build_rpi_kernel.sh' 파일을 열고 수정 및 저장합니다.
* 혹시 build_rpi_kernel.sh 파일이 저장되지 않으면 리눅스 터미널에서 다시 'chmod 777 build_rpi_kernel.sh' 명령어를 입력합니다.

위와 같은 단계를 따라하면 파일을 저장할 수 있습니다.

결과 화면 


위와 같이 파일을 생성한 다음에, 라즈비안에서 제공하는 기니(Geany) 프로그램을 사용하면 파일을 제대로 저장할 수 있습니다.


---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

[IT] 낙타와 같은 SW 개발자가 되지 말자 임베디드 에세이

가끔 낙타와 같은 개발자가 눈에 보인다.
사막에서 주위 상황을 살피지 않고 모래에 머리를 박고 있는 낙타와 같이 개발을 하는 분들이다.

자신이 하고 있는 개발 업무가 회사 밖에서,

    ● 쓸모가 있는지,
    ● 경쟁력이 있는지,
    ● 활용이 가능한지,
    ● 어느 정도 난이도가 있는지,
    ● 개발자로써 역량을 키울 수 있는지,
    ● 다른 개발자가 인정할만한 일인지,

전혀 고민을 하지 않고 그냥 닥치는데로, 시키는데로만 일을 하는 개발자를 말한다.
이렇게 개발하는 개발자의 미래는 눈에 너무 선명하게 그려진다.

자신이 하고 있는 개발 업무가 회사 밖에서,

    ● 쓸모가 없고,
    ● 경쟁력이 없고,
    ● 활용이 불가능하고,
    ● 수준 낮은 일이고,
    ● 개발자로써 역량을 전혀 키울 수 없고,
    ● 다른 개발자가 '그게 개발 업무인지' 의문을 갖게 되는,

운명을 맞이할 가능성이 높게 된다. 

다른 SW 개발자들이 낙타와 같은 개발자가 되지 않길 빈다.

기존 ARM 프로세서/아키텍처 책의 한계점 & 개선 포인트 이제는 ARM의 시대다

기존 ARM 프로세서/아키텍처 책의 한계점 & 개선 포인트에 대해 이야기를 해보려 합니다.
이 포스팅은 저의 지극히 개인적인 의견이라는 점을 참고하고 읽으셨으면 좋겠습니다.

■ 임베디드 중심이다

ARM 책을 떠올리면 자연히 '임베디드'란 단어가 떠오릅니다. 특정 보드에서 Cortex-M3를 실습하는 구조로 구성돼 있어,
알고 싶지 않은 데이터 시트나 파형과 같은 내용이 보입니다.

임베디드 이외의 다른 개발자들은 ARM 책에서 이런 내용을 보면 자연히 거리감을 느낍니다.

=>
ARM은 임베디드 뿐만 아니라 다른 IT 기기의 영역으로 확장되고 있습니다.
일반 애플리케이션 개발자들도 ARM을 쉽게 배울 수 있도록 컨텐츠가 확장돼야 합니다.

■ 실습하기 불편하다

ARM 책이 임베디드 관점으로 집필돼 실습하기 어려운 구조로 구성돼 있습니다.
특정 보드를 구입해 이미지를 다운로드하고 보드에 전원을 연결하는 과정은 일반 개발자 입장에게 '참 귀찮다'라는 느낌을 줍니다.

=>
누구나 쉽게 따라서 ARM 프로세서를 실습할 수 있는 컨텐츠를 제공해야 합니다. 
데스크탑 PC에서 ARM 어셈블리 명령어를 실습할 수 있거나, 꼭 필요하다면 라즈베리 파이에서 쉽께 따라해보는 구성도 좋을 것 같습니다.

■ 사전 형식으로 어셈블리 명령어를 나열한다

실전 개발에서는 20개 이내의 ARM 어셈블리 명령어만 사용합니다. 
그런데 어셈블리 명령어를 사전식으로 나열해 책의 분량만 늘리는 경우가 많습니다.

=>
실전 개발에서 주로 사용하는 어셈블리 명령어를 쉽게 이해할 수 있는 방식으로 컨텐츠를 구성할 필요가 있습니다.

■ 어셈블리 명령어를 너무 어렵게 설명한다

ARM 어셈블리 명령어를 개념 위주로 접근해, 독자에게 '이걸 외워야 하나'란 생각이 들게 합니다. 
독자가 ARM 어셈블리 명령어를 암기하게 유도해 ARM 프로세서를 포기하게 만듭니다.

=>
ARM 어셈블리 명령어를 배우는 방법을 실습을 통해 소개하고, ARM 어셈블리 명령어에 친숙해는 컨텐츠를 만들어야 합니다. ARM 어셈블리 명령어는 C나 C++ 언어의 1/10의 노력만 투자해도 재미있게 배울 수 있습니다.

■ ARM 프로세서에 대해 배운 내용을 실전 개발에 어떻게 쓰이는지 알려주지 않는다

취준생들이 가장 답답해하고 불만을 품는 이유입니다. ARM 책들에서 ARM 프로세서가 실전 개발에서 어떻게 활용되는지 설명하지 못합니다.
누구나 ARM 프로세서는 잘 배우면 좋다라고 말합니다. 그런데 배운 내용이 실전 프로젝트에서 정확히 어떻게 쓰이는지, 구체적으로 알려주지 못합니다.

==>
케이스 스터디를 통해 ARM 프로세서가 실전 프로젝트에서 어떻게 활용되는지 알려 줄 필요가 있습니다.
ex) 함수 호출 규약을 알아야 스택 오염을 잘 디버깅할 수 있다.
      지역 변수로 배열을 너무 크게 잡으면 ARM 명령어가 실행될 때 

■ 대부분 32비트 기반 Cortex-M3 ARM 프로세서를 설명합니다 

RTOS에 펌웨어에 MCU로 탑재되는 Cortex-M3를 다룹니다.

대중적으로 사용되는 안드로이드나 라즈베리 파이에 탑재되는 
Cortex-A7, 64비트 ARM 아키텍처를 다룬 책이 없습니다.

그리고 요즘 떠오르는 하이퍼바이저, 트러스트 존와 같은 최신 ARM 아키텍처의 내용을 담고 있지 않습니다.

==>
Cortex-A7, 64비트 ARM 아키텍처를 다룬 책이 개발자에게 필요합니다.

■ ARM 아키텍처에서 너무 많은 내용을 다룹니다

리눅스 임베디드 시스템 개발자 입장에서도 '자주 쓰는 ARM 어셈블리 명령어', '함수 호출 규약', '인터럽트를 ARM 프로세서가 처리하는 방식',
'ARM 모드와 레지스터 세트'에 대해 정확히 배워도 개발하는데 지장이 없습니다.
그런데, 네온이나 플로팅 포인트, 그리고 세세한 MMU의 옵션까지 사전식으로 나열합니다.

==>
실전 개발에 꼭 필요한 내용을 엄선해 컨텐트를 정리할 필요가 있습니다.

■ [참고] 유익한 ARM 아키텍처 컨텐츠

* 임베디드 레시피
http://recipes.egloos.com/5000239

2) Microprocessor 아뜰리에 (Atelier) - ARM을 파헤쳐 보자
3) Software 데꾸바쮸 (Decoupage) - Software의 정체와 만들기
4) ARM 미장센 - ARM 제어의 구현

실전 임베디드 펌웨어 개발자가 쓴 블로그(책)으로 실전 개발에 유익한 내용을 담고 있습니다.
2)~4) 챕터는 ARM 프로세서에 대한 내용으로 읽어볼 만합니다. 
ARM11 기반으로 2009년 경에 작성된 오래된 자료란 점이 옥의 티입니다. 

* 문c 블로그
http://jake.dothome.co.kr/linux_5/

Exception -1- (ARM32 Vector)
Exception -2- (ARM32 Handler 1)
Exception -3- (ARM32 Handler 2)
Exception -4- (ARM32 VFP & FPE)
Exception -5- (Extable)
Memory Access Order Type & Shareable Region
TLB Cache (API)
http://jake.dothome.co.kr/inline-assembly/
http://jake.dothome.co.kr/cache4/

보석과 같은 자료들로 가득찬 블로그인데 ARM 프로세서에 대해선 위 내용들을 참고할만 합니다.

---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

[리눅스] 라즈베리파이: 커널 모듈(kernel module) 드라이버 설치 및 빌드해보기 리눅스 디바이스 드라이버

Overview

리눅스는 2가지 타입의 디바이스 드라이버를 지원합니다.

   * 모듈식
   * 빌트인 식

모듈 타입의 디바이스 드라이버는 리눅스 시스템이 부팅한 다음 드라이버를 설치하듯 시스템에 동적으로 적재됩니다. 빌트인 타입 디바이스 드라이버는 커널 이미지에 디바이스 드라이버 코드가 포함되는 방식이죠.

사실, 많은 리눅스 시스템에서는 모듈 타입 디바이스 드라이버를 적용합니다. 리눅스 서버와 같이 항상 켜져서 동작해야 하는 경우 동적으로 디바이스 드라이버를 적재할 수 있기 때문입니다.

이번 시간에는 라즈베리 파이에서 모듈 타입 디바이스 드라이버를 설치하는 방법을 소개합니다.

환경 설정

모듈 타입 디바이스 드라이버를 컴파일하려면 리눅스 시스템은 이를 빌드할 수 있는 환경이 구축돼 있어야 합니다.
그런데, 아래 포스팅에서 소개한 바와 같이 리눅스 커널 소스를 내려받아 라즈베리 파이에 빌드해 설치하면 자연스럽게 디바이스 드라이버를 모듈타입으로 빌드할 수 있습니다. 즉, Makefile와 디바이스 드라이버 소스 코드만 있으면 빌드를 할 수 있다는 이야기입니다.



이번에는 라즈베리 파이에서 이 내용을 확인해봅시다.

먼저 '/lib/modules/' 폴더로 이동합시다.

root@raspberrypi:/home/pi/work_0614# cd /lib/modules/
root@raspberrypi:/lib/modules# ls
4.14.98+  4.14.98-v7+  4.19.40-v7+

'/lib/modules' 폴더로 이동하니 위와 같이 3가지 폴더가 보이네요. 
가장 최근에 내려받은 리눅스 커널 소스 코드의 버전이 4.19.40-v7+이니 4.19.40-v7+ 디렉터리가 보입니다. 

4.19.40-v7+ 디렉토리로 이동하겠습니다.

root@raspberrypi:/lib/modules# cd 4.19.40-v7+/
root@raspberrypi:/lib/modules/4.19.40-v7+# ls
build        modules.alias.bin    modules.dep      modules.order    modules.symbols.bin
kernel        modules.builtin     modules.dep.bin  modules.softdep  source
modules.alias  modules.builtin.bin  modules.devname  modules.symbols

여러가지 파일들이 보이는데 이 중 핵심은 build 디렉터리입니다. 이 디렉터리가 있어야 디바이스 드라이버를 모듈 타입으로 빌드할 수 있습니다.

이제 모듈 타입 디바이스 드라이버를 설치할 수 있는 조건을 점검했으니 가장 간단한 리눅스 드라이버 모듈을 빌드해봅시다.

모듈식 리눅스 디바이스 드라이버 빌드해보기

먼저 디바이스 드라이버 코드를 소개합니다.
소스 코드는 다음과 같습니다.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("Dual BSD/GLP");

static int hello_module_init(void)
{
printk("Hello Module! \n");
return 0;
}

static void hello_module_exit(void)
{
printk("Good-bye Module! \n");
}

module_init(hello_module_init);
module_exit(hello_module_exit);

이번 포스팅은 리눅스 디바이스 드라이버를 라즈베리 파이에서 빌드해 설치하는 게 주제니, 소스 코드의 내용은 구지 설명하지 않겠습니다.

소스 코드를 hello_module.c 이름으로 저장합시다.

이어서 hello_module.c 소스 파일을 빌드할 수 있는 메이크 파일을 소개합니다.
코드의 내용은 다음과 같습니다.

obj-m += hello_module.o

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

위 명령어에서 '$(shell uname -r)' 구문은 현재 리눅스 시스템의 리눅스 커널 버전을 찾아서 치환해주는 기능입니다.

리눅스 시스템의 리눅스 커널 버전이 4.19.40-v7+이니, 
'make -C /lib/modules/$(shell uname -r)/build' 은 'make -C /lib/modules/4.19.40-v7+/build' 구문으로 치환됩니다.

hello_module.c 소스 파일과 Makefile를 'root@raspberrypi:/home/pi/work_0614' 디렉터리에 저장한 다음, make 명령어를 입력해 모듈을 빌드합니다.

root@raspberrypi:/home/pi/work_0614# make
make -C /lib/modules/4.19.40-v7+/build M=/home/pi/work_0614 modules
make[1]: Entering directory '/home/pi/kernel_src/out'
  CC [M]  /home/pi/work_0614/hello_module.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/pi/work_0614/hello_module.mod.o
  LD [M]  /home/pi/work_0614/hello_module.ko

제대로 빌드가 되서 hello_module.ko 파일이 생성됐음을 알 수 있습니다.

hello_module 드라이버 모듈 설치해보기

hello_module.c 파일을 컴파일 해서 hello_module.ko 파일을 생성했으니 이번에는 설치를 해볼 차례입니다.

터미널에서 'insmod hello_module.ko' 명령어를 입력해 보겠습니다.

root@raspberrypi:/home/pi/work_0614# insmod hello_module.ko

음, 에러 메시지가 보이지는 않으니 제대로 설치를 했는지 잘 모르겠습니다.
라즈베리 파이에 설치된 모듈 타입 디바이스 드라이버의 목록을 출력해주는 'lsmod' 명령어를 입력해봅시다.

01 root@raspberrypi:/home/pi/work_0614# lsmod
02 Module                  Size  Used by
03 hello_module           16384  0
04 fuse                  110592  3
05 rfcomm                 49152  4
06 bnep                   20480  2
07 hci_uart               40960  1

03번째 줄에서 hello_module와 같이 설치된 드라이버의 이름이 보이니 제대로 설치가 된 듯 합니다.

이번에는 커널 로그를 통해 'hello_module'이란 디바이스 드라이버가 제대로 설치됐는지 확인해봅시다.

root@raspberrypi:/home/pi/work_0614# tail -20 /var/log/kern.log 
Jun 14 17:03:15 raspberrypi kernel: [ 1668.847624] hello_module: loading out-of-tree module taints kernel.
Jun 14 17:03:15 raspberrypi kernel: [ 1668.847666] hello_module: module license 'Dual BSD/GLP' taints kernel.
Jun 14 17:03:15 raspberrypi kernel: [ 1668.847689] Disabling lock debugging due to kernel taint
Jun 14 17:03:15 raspberrypi kernel: [ 1668.848501] Hello Module! 

커널 로그에서 맨 마지막 부분에 'Hello Module!' 메시지가 보이니 hello_module.ko가 설치가 돼서 
hello_module_init() 함수가 호출됐음을 확인할 수 있습니다.

다음은 이번 포스팅 앞부분에서 소개한 hello_module_init() 함수의 구현부입니다.

static int hello_module_init(void)
{
printk("Hello Module! \n");
return 0;
}

이번에는 'rmmod hello_module.ko' 명령어를 입력해 hello_module.ko 드라이버를 라즈베리 파이에서 비적재(unload)해봅시다.

root@raspberrypi:/home/pi/work_0614# rmmod hello_module.ko

다시 커널 로그를 확인해 봅시다.

root@raspberrypi:/home/pi/work_0614# tail -20 /var/log/kern.log 
Jun 14 17:03:15 raspberrypi kernel: [ 1668.848501] Hello Module! 
Jun 14 17:03:36 raspberrypi kernel: [ 1689.798616] Good-bye Module! 

커널 로그의 'Good-bye Module!' 메시지가 출력되니 다음과 같이 hello_module_exit() 함수가 제대로 호출됐음을 확인할 수 있습니다.

static void hello_module_exit(void)
{
printk("Good-bye Module! \n");
}

정리

이번 포스팅에서는 라즈베리 파이에서 모듈 타입 디바이스 드라이버를 컴파일하고 로딩하는 방법을 소개했습니다.
키포인트는 모듈 타입 디바이스 드라이버를 컴파일하기 위해서는 라즈비안 커널 소스를 내려받고 빌드를 하면 된다는 점을 기억했으면 합니다.

그렇다며 이런 의문이 생길 것입니다. 

    * '라즈비안 소스를 내려받지 않고 리눅스 커널 모듈 드라이버를 설치할 수 있도록 어떻게 설정해야 할까?'

의문을 품기 위해서는 다음 링크에 있는 정보를 참고하면 됩니다.

https://stackoverflow.com/questions/20167411/how-to-compile-a-kernel-module-for-raspberry-pi
https://github.com/notro/rpi-source/wiki

위에서 소개한 링크대로 설치를 할 수도 있는데요. 시간이 훨씬 더 오래 걸리고 라즈베리 파이에서 더 많은 용량을 차지할 겁니다. 그 이유는 리눅스 커널 관련 파일과 헤더를 모두 내려받기 때문이죠. 또한 커널 모듈 드라이버 빌드와 상관없는라즈베리 파이와 관련된 유틸리티 파일을 다운로드도 합니다. 

차라리 라즈비안 커널 소스를 내려받아 빌드하는게 훨씬 시간을 절약할 것입니다.  

이번 시간에는 라즈베리 파이에서 디바이스 드라이버를 설치하는 방법을 소개했습니다. 이 내용 참고해서 많은 분들이 라즈베리 파이에서 디바이스 드라이버를 연습해봤으면 좋겠습니다.

---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---


[리눅스][유틸리티] 하위 디렉토리에 있는 특정 파일을 지우기 - find 활용 [Raspberry-pi] Tips

가끔 하위 디렉토리에 있는 특정 확장자의 파일을 지우고 싶을 때가 있습니다.
만약 *.png 파일을 지우고 싶으면 아래 명령어를 입력하면 됩니다.

$ rm -rf `find . -name *.png`

[리눅스][유틸리티] 서버간 파일 복사 - scp [Raspberry-pi] Tips

프로젝트를 진행하다보면 리눅스 서버에 있는 데이터를 바로 복사하고 싶을 때가 있습니다.
이때 scp 유틸리티를 쓰면 되는데요. 사용법은 다음과 같습니다.

[1] 먼저, 복사할 데이터가 있는 리눅스 서버에 접속합니다.

[2] 이어서 다음 명령어를 입력합니다. 

scp bald_candy.data bald.candy@12.345.67.89:~/bald_candy_data

여기서, 각 명령어의 내용은 다음과 같습니다.

복사할 대상의 리눅스 서버의 IP 주소: 12.345.67.89
복사할 대상의 리눅스 서버의 계정: bald.candy
복사하고 싶은 파일 이름: bald_candy.data

[3] 만약 폴더를 복사하려면 다음 명령어를 입력하면 됩니다.

scp -r bald_candy_folder bald.candy@12.345.67.89:~/bald_candy_data

(where)
복사할 대상의 리눅스 서버의 IP 주소: 12.345.67.89
복사할 대상의 리눅스 서버의 계정: bald.candy
복사하고 싶은 디렉토리 이름: bald_candy_folder

이번에 소개하는 scp 명령어를 잘 활용해서 집에 일찍 갑시다.

[리눅스] 드라이버: module_init 키워드로 지정한 함수가 호출되는 원리 - sys_finit_module() 리눅스 디바이스 드라이버

리눅스에서 실행 중인 디바이스 드라이버는 2가지 타입 중 하나입니다.

    ● 모듈 식 디바이스 드라이버
    ● 빌트인 식 디바이스 드라이버

모듈식 디바이스 드라이버가 설치 될 때 리눅스 내부에서 어떤 방식으로 동작하는지 살펴보겠습니다.

이 중에 모듈 형태의 디바이스 드라이버는 다음과 같은 명령어를 사용하면 리눅스에 설치할 수 있습니다.

가장 간단한 모듈식 디바이스 드라이버 코드 

먼저 가장 간단한 형태의 모듈식 디바이스 드라이버의 소스를 봅시다.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
  printk("<1> Hello world!\n");  
  return 0;
}
static int hello_exit(void)
{
  printk("<1> Good Bye \n");
}

module_init(hello_init);
module_exit(hello_exit);

모듈식 디바이스 드라이버를 설치하는 방법

위 소스를 빌드하고 난 후 device_driver.ko 이란 파일이 생성됐다고 가정합시다. 
리눅스 터미널을 열고 다음 명령어를 입력하면 device_driver.ko 파일이 설치됩니다.
 
$ insmod device_driver.ko 

정상적으로 모듈식 디바이스 드라이버가 설치되면 hello_init() 함수가 호출될 것입니다.

static int hello_init(void)
{
  printk("<1> Hello world!\n");  
  return 0;
}
module_init(hello_init);

그런데 공식처럼 외우고 있는 것은 바로 다음 문장입니다.

    'insmod device_driver.ko' 명령어를 입력하면 hello_init() 함수가 호출된다.

이번 시간에 조금 더 배워 볼 내용은 '리눅스 내부의 어느 함수에서 hello_init() 함수를 호출하는가'입니다.

sys_init_module() 함수의 선언부와 인자 확인하기

먼저 sys_init_module() 함수의 선언부를 보겠습니다.

https://elixir.bootlin.com/linux/v5.4.30/source/kernel/module.c
SYSCALL_DEFINE3(finit_module, int, fd,
         const char __user *, uargs, int, flags)

SYSCALL_DEFINE3 매크로로 선언된 함수는 컴파일 과정에서 다음과 같은 형태로 바뀝니다.

https://elixir.bootlin.com/linux/v5.4.30/source/include/linux/syscalls.h
asmlinkage long sys_finit_module(int fd, const char __user *uargs, int flags);

sys_finit_module() 함수에 전달되는 인자는 다음과 같이 정리할 수 있습니다.

   ■ fd: 파일 디스크립터
   ■ const char __user *uargs: insmod 명령어를 실행할 때 적용하는 아규먼트
   ■ int flags: insmod 명령어를 실행할 때 적용되는 플래그


sys_finit_module() 함수 분석

다음은 sys_finit_module() 함수의 구현부입니다.

https://elixir.bootlin.com/linux/v5.4.30/source/kernel/module.c
01 SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
02 {
03 struct load_info info = { };
04 loff_t size;
05 void *hdr;
06 int err;
07
08 err = may_init_module();
09 if (err)
10 return err;
11
12 pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags);
13
14 if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS
15       |MODULE_INIT_IGNORE_VERMAGIC))
16 return -EINVAL;
17
18 err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX,
19        READING_MODULE);
20 if (err)
21 return err;
22 info.hdr = hdr;
23 info.len = size;
24
25 return load_module(&info, uargs, flags);
26 }

먼저 12번째 줄을 봅시다.

12 pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags);

디바이스 드라이버 모듈이 설치된다는 동작을 커널 로그로 출력합니다.
pr_debug() 함수의 이름을 printk() 함수로 바꿔도 똑같이 동작합니다.

18~19번째 줄을 분석하겠습니다.

18 err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX,
19        READING_MODULE);

모듈의 헤더와 같은 정보를 읽는 동작입니다. 

마지막으로 25번째 줄을 보겠습니다.

25 return load_module(&info, uargs, flags);

load_module() 함수를 호출합니다.


load_module() 함수 분석

이어서 load_module() 함수를 분석합니다.

01 static int load_module(struct load_info *info, const char __user *uargs,
02                int flags)
03 {
04     struct module *mod;
05     long err = 0;
06     char *after_dashes;
07 
08     err = elf_header_check(info);
09     if (err)
10        goto free_copy;
11
12    err = setup_load_info(info, flags);
13    if (err)
14        goto free_copy;
...
15    /* Done! */
16    trace_module_load(mod);
17
18    return do_init_module(mod);
...
19 }

사실 load_module() 함수의 구현부는 수 많은 함수를 호출해 해석하기 어렵습니다.
그런데 사실, 가장 중요한 동작을 요약하면 다음과 같습니다.

   ■ 설치하는 디바이스 드라이버 모듈 파일인 *.ko 의 헤더 정보를 읽어 *.ko 파일에 유효한 정보가 담겨 있는 지 확인
   ■ do_init_module() 함수 호출

커널 입장에서 유저가 어떤 상태의 *.ko 파일로 insmod 명령어를 입력해 드라이버 모듈을 설치하는지 예상하기 어렵습니다.
가끔은 깨진 파일이나 *.o 파일을 *.ko 파일로 변환해 insmod 명령어로 설치할 수 있어, 이를 방지하기 위한 코드입니다.


먼저 01~10번째 줄을 보겠습니다.

08     err = elf_header_check(info);
09     if (err)
10        goto free_copy;

elf_header_check() 함수를 호출해 설치하려는 *.ko 파일의 헤더 정보를 체크합니다.
참고로, *.ko 파일을 포함해 컴파일된 *.o 을 ELF 파일이라고 부르니 elf_header_check() 함수를 호출합니다. 

이어서 18번째 줄을 보겠습니다.

18    return do_init_module(mod);

do_init_module() 함수를 호출합니다. 


do_init_module() 함수 분석

이어서 do_init_module() 함수를 분석합시다.

01 static noinline int do_init_module(struct module *mod)
02 {
03     int ret = 0;
04     struct mod_initfree *freeinit;
...
05     /* Start the module */
06     if (mod->init != NULL)
07         ret = do_one_initcall(mod->init);
08     if (ret < 0) {
09        goto fail_free_freeinit;
10    }

06~07번째 줄을 보겠습니다.

06     if (mod->init != NULL)
07         ret = do_one_initcall(mod->init);

06번째 줄에서 'mod->init'이 NULL이 아닌지 체크하고 07번째 줄에서 mod->init를 인자로 삼아 do_one_initcall() 함수를 호출합니다. 
여기서 'mod->init' 인자가 어떤 정보를 저장하는지 의문이 생깁니다.

'mod->init'는 예로 들었던 간단한 디바이스 드라이버 모듈 코드 기준으로 hello_init() 함수의 주소를 저장합니다. 

아래는 이번 페이지에 예시로 들었던 hello_init() 함수의 구현부입니다.

static int hello_init(void)
{
  printk("<1> Hello world!\n");  
  return 0;
}
module_init(hello_init);

do_one_initcall() 함수 분석

이어서 do_one_initcall() 함수의 구현부를 보겠습니다.

https://elixir.bootlin.com/linux/v5.4.30/source/init/main.c 
01 int __init_or_module do_one_initcall(initcall_t fn)
02 { 
03 int count = preempt_count();
04 char msgbuf[64];
05 int ret;
06
07 do_trace_initcall_start(fn);
08 ret = fn();
09 do_trace_initcall_finish(fn, ret);

do_one_initcall() 함수에서 가장 중요한 부분은 08번째 줄인데 module_init 키워드로 지정된 디바이스 드라이버를 초기화하는 함수를 호출합니다. 
fn은 아래 디바이스 드라이버 코드에서 hello_init() 함수의 주소를 저장합니다.

static int hello_init(void)
{
  printk("<1> Hello world!\n");  
  return 0;
}
module_init(hello_init);

do_init_module() 함수의 콜 스택 소개

이번에는 모듈 타입 디바이스 드라이버에서 module_init 키워드로 지정된 함수가 호출되는 콜 스택을 소개합니다.

아래 콜 스택을 같이 보겠습니다.

-000|NSR:0xBF03B114(asm)  
-001|do_one_initcall_debug(inline)
-001|do_one_initcall(fn = 0xBF03B000)
-002|do_init_module(inline)
-002|load_module(info = 0xDE895F48, ?, ?)
-003|sys_finit_module()
-004|ret_fast_syscall(asm)

이번 포스팅에서 소개한 함수 목록들이 보입니다. 

    ● sys_finit_module()
    ● load_module()
    ● do_one_initcall()


sys_finit_module() 함수 분석

참고로 sys_init_module() 함수의 구현부를 봅시다.

https://elixir.bootlin.com/linux/v5.4.30/source/kernel/module.c
01 SYSCALL_DEFINE3(init_module, void __user *, umod,
02         unsigned long, len, const char __user *, uargs)
03 {
04     int err;
05     struct load_info info = { };
06 
07     err = may_init_module();
08     if (err)
09         return err;
10
11    pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
12           umod, len, uargs);
13
14    err = copy_module_from_user(umod, len, &info);
15    if (err)
16        return err;
17
18    return load_module(&info, uargs, 0);
19}

먼저 11~12번째 줄을 봅시다.

11    pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
12           umod, len, uargs);

디바이스 드라이버 모듈이 설치된다는 동작을 커널 로그로 출력합니다.
pr_debug() 함수의 이름을 printk() 함수로 바꿔도 똑같이 동작합니다.

14~16번째 줄을 분석하겠습니다.

14    err = copy_module_from_user(umod, len, &info);
15    if (err)
16        return err;

모듈의 정보는 load_info 구조체 타입인 load_info 변수에 저장합니다.

마지막으로 18번째 줄을 보겠습니다.

18    return load_module(&info, uargs, 0);

load_module() 함수를 호출합니다.

정리 

이번에는 리눅스 시스템 전체 관점으로 '$ insmod device_driver.ko' 명령어를 실행할 때 흐름을 살펴보겠습니다.

허접하지만 다음 그림을 같이 볼까요?

유저 공간: insmod
               시스템 콜 실행  
-----------------------------------
커널 공간: 
               sys_finit_module()
           load_module()
               do_one_initcall() 

[1] 유저 공간: insmod 명령어를 실행한다. 
[2] 유저 공간: 시스템 콜을 발생한다.
[3] 커널 공간: 시스템 콜 핸들러인 sys_finit_module() 함수가 호출된다. 
[4] 커널 공간: do_one_initcall() 함수에서 module_init 키워드로 지정된 디바이스 드라이버를 초기화하는 함수를 호출한다.

이 정도면 hello_init() 함수가 호출되는 원리를 잊어 먹지 않을 것 같네요.

static int hello_init(void)
{
  printk("<1> Hello world!\n");  
  return 0;
}
module_init(hello_init);

다음 시간에는 hello_init() 함수가 호출되는 원리를 배워야 하는 이유에 대해서 이야기 하겠습니다.
뭔가를 배우려고 할 때 왜 배워야 하는지 알아야 머릿속에 더 오래 남거든요.

---
"이 포스팅이 유익하다고 생각되시면 공감 혹은 댓글로 응원해주시면 감사하겠습니다. 
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"

​Thanks,
Guillermo Austin Kim(austindh.kim@gmail.com)
---

[IT] 리눅스 시스템 개발자가 되려면 디바이스 드라이버를 배워야 하는 이유 리눅스 디바이스 드라이버

리눅스 세미나를 진행하다 보면 취준생들이 던지는 질문이 있습니다. 구체적인 질문을 소개하자면 다음과 같습니다.

    ■ 리눅스 시스템 프로그래밍까지 배우면 충분하지 않을까요?
    ■ 제가 아는 선배가 리눅스 디바이스 드라이버까지는 배울 필요가 없다는데요?

리눅스 시스템 SW 개발자 입장에서 질문에 대해 제 생각을 조금 이야기해보려 하는데요. 

실제 IT 개발에서 디바이스 드라이버를 배우면서 개발하라고 합니다.

취준생 분들은 미래의 신입 SW 개발자라고 볼 수 있어요. '리눅스 디바이스 드라이버'를 왜 알아야 되는지 말씀드리기 전에 먼저 '신입 리눅스 SW 개발자'들이 처음에 어떻게 개발 업무를 시작하는지 알 필요가 있어요.

처음에 회사에 출근을 하면 신입 SW 개발자들은 해야 할 업무를 배정 받습니다. 신입 SW 개발자 입장에서 학교에서 '날라 다녔다'고 자신을 평가해도 회사는 모든 신입 개발자를 똑같이 바라 봅니다. 예를 들면 처음 운영 체제를 설치한 컴퓨터처럼 말이죠.

그런데 어떤 회사에 취업을 하던 신입 개발자는 '자신이 하고 싶은 개발 업무'를 선택할 기회가 사실 상 거의 없습니다. 회사에서 시키는 개발 업무를 시작하는 거죠. 면담도 없이 부서에 배치 받는 것은 비일비재 하고요. 업무를 할당 받게 됩니다.

그런데 회사에서 만난 멘토 개발자나 고참 개발자가 신입 개발자에게 다음과 같이 이야기를 할 수 있습니다.  

    '박 연구원, 디바이스 드라이버 좀 해 봤니? 우리가 해야 할 일은 디바이스 드라이버를 포팅하는 일이거든.' 

자, 이 때 신입 개발자가 된 여러분은 어떻게 대답을 하실 꺼에요? 만약 디바이스 드라이버를 배운 적이 없다면 솔직히 다음과 같이 대답하는게 정답이겠죠.

    * '아, 제가 디바이스 드라이버는 만져본 적은 없습니다. 리눅스 시스템 프로그래밍까지 해 봤습니다.'

아마 이렇게 답할 수 있겠죠. 솔직하고 정확한 대답이라고 할 수 있는데요. 이렇게 대답을 하면 선배 개발자는 어떻게 반응을 할까요? 실전 개발에서 아마 다음과 같이 답할 겁니다.

    * '어, 그래? 디바이스 드라이버를 안 해 봤구나. 괜찮아, 같이 배우면서 하면 돼. 나도 처음엔 잘 몰랐어.

조금 체계가 잡힌 IT 업체에서 선배 개발자는 이렇게 답할 수 있겠네요.

    * '디바이스 드라이버 교육 프로그램이 있거던, 1주일 짜리 교육인데, 그거 듣고 와.'
      '교육 듣고 나서 같이 개발하면 되니 걱정하지마'

정리하면, 디바이스 드라이버를 배우면서 개발하자고 할 가능성이 99.9% 정도에요. '디바이스 드라이버를 한 적이 없으니 신입 개발자는 다른 일을 시켜야 겠네'라고 말하는 회사는 아마 거의 없을 겁니다.

만약, '전 디바이스 드라이버를 배운 적이 없으니 디바이스 드라이버 업무는 못하겠는데요'라고 대답하면 회사는 어떻게 반응할까요? 아마 좋게 바라보지는 않겠죠.

하드웨어 개발자도 디바이스 드라이버를 작성합니다.

종종 듣는 이야기인데요. 하드웨어 개발자에게 디바이스 드라이버를 같이 보는 경우도 많습니다. 하드웨어 개발자분들이 Schematic, 회로도, 배치도를 구성하면서 회로를 설계하는게 메인 업무입니다. 그래서 회사에서 업무를 나누는 기준이 다르지만 하드웨어 개발자에게 디바이스 드라이버 코드도 같이 보라는 경우도 많습니다. 이런 지시를 회사에서 받았는데 다음과 같이 반문을 하면 욕 먹을 가능성이 높습니다.

    * 저는 하드웨어 개발자인데요, 왜 제가 디바이스 드라이버 코드를 봐야 하죠?

만약 취준생 분들 중에 하드웨어 개발자가 되려는 분도 분명히 있을 것입니다. 그렇다면 실제 개발 현장에서 디바이스 드라이버 소스 코드를 들여다 볼 확률이 높습니다.

아예 다비이스 드라이버를 몰라도 바로 업무를 시킬 확률도 높아요.

이전 꼭지에서 들었던 예에서 선배 개발자는 '리눅스 디바이스 드라이버를 배운 적이 있는지' 물어 봤는데요. 사실 실전 개발에서 이런 질문도 하지 않고 일을 막 시킬 가능성도 높아요. 꼭 리눅스 디바이스 드라이버의 소스를 수정하지 않아도 다음과 같은 업무를 시킬 가능성이 높거든요.

    * 소스 코드를 분석해봐라.
    * 내가 작성한 패치 코드를 드라이버 코드에 적용하고 빌드해 이미지를 전달해죠.

이때 '전 디바이스 드라이버를 해 본 적이 없어요'라고 답하면 선배 개발자는 아마 다음과 같이 답할 확률도 높습니다.

    * 그래, 그래도 한번 해봐.

선배 개발자들은 3~5년차 정도 경력일 가능성이 높은데요. 안타까운 현실은 이 분들은 신입 개발자들이 어떤 처지인지, 뭘 모르는지 잘 헤아릴 수 없다는 것입니다. 그래서 일을 막 시키는 거죠.

취준생분들이 제대로 된 충고를 들을 수 있기를 간절히 바래요.

제 블로그에 올라오는 댓글이나 리눅스 세미나에서 취준생 분들이 주시는 질문을 들으면 제가 한 가지 느끼는 점이 있어요.

    * 제대로 취준생 분들에게 방향을 제시하는 개발자가 많지는 않구나.

취준생 분들이나 신입 개발자에게 이런 저런 충고를 하는 개발자들이 있는데, 이 분들은 2~3년 먼저 개발을 했을 뿐이지 취준생 분들이나 신입 개발자와 거의 비슷한 처지인 경우가 많거든요. 혹은 경쟁에 뒤처져 퇴물이 된 40대 꼰대 개발자인 경우도 있죠. 

더 안타까운 점은 취준생 분들이나 신입 개발자이 이런 분들에게 의지한다는 사실이에요. 이 분들이 해주는 조언을 그대로 받아 들이죠.

잘못된 충고를 듣고 따르는 것은 63빌딩에서 뛰어 내리면서 우산 대신 파라솔을 드는 것만큼 허망할 때가 많은 것 같아요. 잘못된 도움과 조언은 치명적인 결과를 초래할 수 있거든요.

몇 가지 예시를 들어볼까요?

    * 디버깅을 하는 방법은 그리 중요하지 않다.
    * 어셈블리 명령어는 배울 필요가 없다.
    * ARM 아키텍처의 구조는 왜 공부해야 하나? 
    * 리눅스 디바이스 드라이버는 구지 알 필요는 없다. 코드만 읽을 수 알면 된다.
    * 리눅스 커널도 알 필요는 없다. 니가 스케줄러 소스 코드를 고치니?

이렇게 소리를 들으면 '웃기고 자빠진다'란 생각이 들었으면 좋겠어요.

1 2 3 4 5 6 7 8 9 10 다음