JTAG 디버거를 이용한 안드로이드 시스템 디버깅
상태바
JTAG 디버거를 이용한 안드로이드 시스템 디버깅
  • 김현경
  • 승인 2010.07.30 00:00
  • 댓글 0
이 기사를 공유합니다

글: 박종원 수석 / 고객지원팀 ㈜제이앤디테크 www.jndtech.com

최근 몇 년사이 모바일 시장의 최대 화두는 바로 ''스마트폰'' 일 것이다. 스마트폰 열풍을 일으킨 장본인이라 할 수 있는 애플의 아이폰과 이에 대항하기 위한 안드로이드 진영이 치열한 접전을 벌이고 있는 가운데, 심비안의 NOKIA, 블랙베리의 RIM 등 전통적인 스마트폰 시장의 강자들 역시 시장을 빼앗기지 않기 위해 총력을 기울이고 있는 양상이다.

스마튼폰은 기존의 피처폰에 비해서 사용자에게 더 많은 기능을 제공해야 하므로, 더 높은 프로세서 파워, 더 넓은 메모리 공간, 보다 뛰어난 멀티미디어 처리 능력 등 더욱 강력해진 하드웨어와 덩치는 더 커지고 복잡하지만 다양한 프로그램을 처리할 수 있는 운영체제를 사용한다. 최근의 모바일 제품들의 사양을 살펴보면 이게 과연 임베디드 시스템인가 고민하게 할 정도로 높아진 것이 사실이다.

이러한 스마트폰으로의 모바일 패러다임의 변화에 따라 스마트폰을 개발하기 위해서는 기존과는 다른 새로운 플랫폼과 그에 따른 개발환경을 사용해야 하는데, 하드웨어 성능이 높아진 만큼 S/W 개발환경도 복잡해져 더 많은 개발기간이 소요되는 요인이 되기도 한다.

최근 줄을 이어 출시되고 있는 안드로이드 기반 스마트폰만 하더라도, 리눅스 커널을 사용하는 안드로이드 플랫폼의 특성상 시뮬레이터로만 개발이 가능한 최상위 어플리케이션을 제외하면, 개발의 대부분이 리눅스 환경에서 이루어지게 된다.

또한 한 두 개의 바이너리로 만들어지던 기존 제품들과는 달리, 부트로더, 커널, 파일시스템 등으로 구성되는 시스템 구조와 안드로이드 플랫폼에 대한 이해가 필요한데, 리눅스 및 리눅스 환경을 이용한 개발에 익숙하지 않은 개발자들에게는 새로운 개발환경에 적응하는 것 자체가 작지 않은 스트레스가 될 수 있다.

더군다나 국내 휴대폰 제조사들은 최근까지 스마트폰에 대한 준비와 대응이 다소 미흡했다는 지적을 받고 있어 이를 만회하기 위해 스마트폰 출시에 더욱 박차를 가하고 있다. 그래서인지 요즘은 생소한 개발환경으로 인해 어려움을 호소하는 안드로이드 기반 스마트폰 개발자들을 자주 만나게 된다.

개발환경의 변화에 따라 개발을 돕기 위한 개발도구들도 발전해 가고 있는데, 개발도구들을 적절하게 잘 활용할 줄 안다면, 복잡한 스마트폰 개발이라 하더라도 보다 쉽게 개발을 할 수 있을 뿐만 아니라, 전체 개발 기간도 단축할 수 있는 효과가 있지 않을까 예상한다.
 
본 글에서는 JTAG 에뮬레이터를 이용해서 안드로이드 기반 시스템을 디버깅 하는 방법에 대해서 살펴봄으로써 안드로이드 기반 제품 개발을 하고 있는 개발자들이 공통적으로 느끼는 어려움을 해결해 나갈 수 있는 방향에 대해서 모색해 보도록 한다.

CodeViser / CVD 소개


그림 1. CodeViser

 

그림 2. CVD (CodeViser Debugger)

본 글에서 사용할 JTAG 에뮬레이터인 CodeViser는 국내 유일의 ARM 프로세서 코어 기반 임베디드 시스템 S/W 디버깅 툴 관련 원천 기술을 보유한 개발장비 전문 기업인 ㈜제이앤디테크의 제품으로서 에뮬레이터 H/W인 CodeViser와 전용 디버거 S/W인 CVD로 구성되어 있다.

10년 이상의 연구개발을 통해 현재 출시되어 있는 대부분의 ARM 기반 프로세서에 대한 디버깅을 지원하고 있고, 전용 디버거인 CVD를 통해서 간단하게 소스코드 레벨에서의 디버깅 및 다양한 시스템 정보를 확인할 수 있는 기능을 제공하고 있다. 모바일 시장의 트렌드에 발맞추어 멀티코어 디버깅 기능과 다양한 OS Awareness 기능을 제공하는 것이 특징이다.

환경소개 / 디버깅 개요

본 글에서는 멀티코어 디버깅의 안정성이 높아지고, 사용자 UI의 편의성이 크게 개선된 것으로 평가 받고 있는 CVD 2.3 버전과 삼성의 S5PC100(Cortex-A8)을 사용한 aESOP-C100 공동제작 보드를 활용하였으며, 사용한 안드로이드 버전은 2.1(커널 버전 2.6.29) 이다.

이상의 환경에서 안드로이드 커널, 어플리케이션, 라이브러리, 모듈 등 안드로이드 기반 제품개발에 필요한 다양한 컴포넌트들을 디버깅하는 방법에 대해서 살펴본다.

디버깅 작업이라고 하는 것은, 어떤 문제가 발생했을 때 그 문제가 발생하는 위치와 원인을 찾아내는 작업이라고 할 수 있다. 문제의 위치와 원인을 찾기 위해 프로그램을 단계적으로 실행하거나 특정 위치 또는 조건에서 시스템을 멈추도록 하여 시스템 상태와 관련한 정보들에 대해서 면밀하게 살펴보는 것이 기본적인 디버깅 방법이다.

이와 함께 안드로이드와 같이 다소 복잡한 시스템의 디버깅을 위해 디버거에서 제공하는 추가 기능이 있다면 이를 활용하여 디버깅 작업을 더욱 용이하게 수행할 수 있다.

Kernel 디버깅


그림 3. RTOS 지원 설정

CVD는 다양한 RTOS를 지원하는데, 안드로이드 디버깅을 하기 위해서는 [Config]-[RTOS Customize] 메뉴에서 RTOS 설정을 Linux로 설정해 주어야 한다. 설정이 완료되면 메인 메뉴에 Linux 메뉴가 표시되고, CVD가 Linux 디버깅을 위해 제공하는 기능들을 사용할 수 있게 된다.

안드로이드 플랫폼은 리눅스 커널을 사용하기 때문에 커널의 디버깅 방법은 일반적인 리눅스 커널을 디버깅 하는 방법과 같다. 커널 디버깅을 위해 CVD는 심볼 정보가 포함된 이미지 파일인 vmlinux를 사용한다. 이는 Kernel 컴파일 시에 생성되는 ELF 타입의 이미지이며, 리눅스 커널 소스의 최상위 디렉토리에 위치한다.

[Porgram]-[Load] 메뉴를 이용하여 vmlinux 파일을 로드한다. 이 때 옵션은 No-Code, Multi-image, Keep-state를 설정하도록 한다. 실제 타겟에서는 압축된 커널 이미지인 zImage를 이용해서 부팅을 하므로, vmlinux 이미지 파일에서 디버깅 심볼만 CVD로 로딩하고, 현재 타겟의 상태를 유지시키기 위한 옵션이다.


그림 4. 커널 이미지 선택 및 옵션 설정

vmlinux를 로딩하면 CVD에서 커널의 다양한 심볼을 검색, 확인할 수 있다. 만약 심볼 정보를 볼 수 없다면, 현재 생성된 이미지에 심볼 정보가 생성되지 않았거나 심볼정보 타입이 CVD가 인식할 수 있는 형태로 생성되지 않아 그런 것 이므로, 커널의 Makefile의 CFLAGS 부분에  g 와  gdwarf-2 옵션을 부여하여 새롭게 빌드 한 후 로드 한다.

커널 디버깅은 커널 압축 후 처음 실행되는 초기화 함수인 start_kernel()에 Breakpoint를 설정하거나 사용자가 디버깅을 하기 원하는 함수에 직접 Breakpoint를 설정하여 디버깅을 수행하는데, 이 때 Breakpoint의 타입은 코어에서 제공하는 H/W Breakpoint를 사용하도록 설정하도록 한다.

그림 5. 커널 심볼 정보 확인

start_kernel() 함수에 Breakpoint를 설정하고 타겟을 실행시켜 멈춘 모습이다. 이제부터는 Step을 눌러 단계별로 프로그램을 진행할 수도 있고, 각 단계별로 프로세서 레지스터, 메모리, 변수, 함수 Callstack 등 다양한 디버깅 정보를 확인할 수 있다.


그림 6. 커널 시작함수인 start_kernel()에 멈춘 모습


그림 7. 커널 CallStack 정보

init 프로세스 디버깅

init 프로세스는 최초의 프로세스이자 모든 프로세스의 최상위 부모 프로세스이다. 설정 파일을 읽어 들여 시스템을 초기화 하고 시그널에 따라 리부팅, 셧다운까지도 담당하는 매우 중요한 프로세스이다. init 프로세스는 커널 부팅 중에 커널에 의해 생성되어 이후의 부팅과정을 담당하므로, 부팅과정에 문제가 있다면 init 프로세스를 디버깅하여 문제를 찾아낼 수 있다.

디버깅 방법은 CVD가 제공하는 어플리케이션 디버깅 기능을 사용하여 편리하게 수행할 수 있는데, start_kernel() 디버깅 후 vmlinux 이미지가 올라와 있는 상태에서 [Linux]-[App Debugging] 메뉴를 실행한다.

init 프로세스 이미지를 선택 후 Debug Start를 누르면 CVD는 타겟을 실행시켜 커널이 부팅 중에 init 프로세스가 초기화 되고 실행되는 순간, init 이미지를 자동으로 로딩하고 메인에 PC를 멈춰 디버깅을 수행 할 수 있게 된다. init 프로세스에 대한 디버깅이 완료되면 로딩되어 있는 init 프로세스의 디버깅 심볼은 unload 시켜준다.


그림 8. Application 디버깅


그림 9. init 프로세스의 main 함수에 멈춘 모습


그림 10. init 프로세스의 디버깅 심볼 로딩 정보

Linux 정보

커널 이미지가 로드되어 있으면 현재 타겟의 상태에 따른 다양한 리눅스 커널 정보를 CVD 메뉴를 통해서 확인 가능하다. 제공되는 정보로는 Task List, Process List, Process Memory List, Module List, Library List, Mount Devices, /proc Filesystem 정보 등이 있다.


그림 11. Linux 메뉴 구성


그림 12. Task List 창


그림 13. Process List 창


그림 14. Process Memory List 창


그림 15. Library List 창

Application Debugging

어플리케이션 디버깅을 하기 위한 기본적인 방법은 직접 프로그램의 이미지를 로딩한 후 디버깅을 하려는 함수에 Breakpoint를 설정하여 디버깅을 진행하는 방법이다. 이 방법은 사용자가 직접 어플리케이션을 실행하는 경우 빠른 디버깅을 수행할 수 있는 장점이 있지만, 그 과정이 다소 번거롭게 느껴질 수도 있다.

CVD는 사용자 입장에서 어플리케이션 디버깅을 위한 과정을 단순화 시켜 보다 쉽게 디버깅을 수행할 수 있는 기능을 제공하는데, 앞서 init 프로세스 디버깅에서 사용했던 어플리케이션 디버깅 기능이 그것이다. 디버깅을 하려는 어플리케이션을 선택해 두면 커널이 해당 어플리케이션을 실행할 때, 이미지를 로딩하고 main 함수 부분에 자동으로 멈추어 주므로 매우 유용하게 디버깅 작업에 활용할 수 있다.

커널이 부팅할 때 실행하는 debuggerd 어플리케이션을 디버깅하는 과정을 예로 살펴보자. [Linux]-[App Debugging] 메뉴를 실행한 후 debuggerd 어플리케이션의 이미지를 선택해주고 Debug Start를 누르면 CVD는 타겟을 실행하면서 해당 어플리케이션이 실행될 때까지 스캔을 하여 다음과 같이 debuggerd의 main 함수부터 디버깅을 할 수 있도록 해 준다. [Symbol]-[List Program]을 통해 debuggerd의 심볼이 로딩되어 있는 것을 확인 할 수 있다.


그림 16. Application Debugging


그림 17. 선택한 Application의 main 함수에 멈춘 모습

Application 디버깅 기능을 활용하기 위해서는 반드시 리눅스 커널이 로딩되어 있어야 한다.

Library Debugging

라이브러리를 디버깅 하는 방법도 크게 두 가지 방법을 사용할 수 있는데, 현재 실행중인 프로세스가 사용중인 라이브러리의 디버깅 심볼을 직접 로딩하는 방법과 CVD가 제공하는 Library debugging 기능을 사용하는 방법이다.

우선 첫 번째 방법으로 debuggerd 어플리케이션이 호출하는 libc Shared 라이브러리의 디버깅을 진행해보겠다. Linux Task List를 통해 각각의 Task가 사용하는 라이브러리를 확인할 수 있으며, 라이브러리의 심볼을 로딩하여 디버깅 하는 것이 가능하다.

다음과 같이 앞의 어플리케이션 디버깅을 통해 실행된 debuggerd 어플리케이션이 사용하는 라이브러리 중 libc.so 라이브러리를 로딩한다. Symbol Browse 창을 이용하여 원하는 함수 또는 소스코드 위치에 Breakpoint를 설정한 후 어플리케이션을 실행하여 해당 라이브러리가 호출 될 때 디버깅을 수행할 수 있다.


그림 18. Application이 사용하는 라이브러리 목록 출력


그림 19. 디버깅할 라이브러리 디버깅 심볼 로딩


그림 20. 설정한 Breakpoint에 멈춘 모습

위와 같이 라이브러리의 디버깅 심볼을 직접 로딩하여 디버깅하는 방법은 특정 라이브러리를 사용하는 어플리케이션을 알고 있는 경우 빠르게 디버깅 할 수 있다. 하지만, 어떤 어플리케이션이 호출하는지에 대한 정보를 모르는 라이브러리를 디버깅해야 하는 경우 CVD가 제공하는 Library Debugging 기능을 이용하면 함수단위로 특정 라이브러리에 대한 디버깅이 가능하다.

CVD의 라이브러리 디버깅 기능을 이용하여 libutils 라이브러리의 sys_alloc 함수를 디버깅 해보도록 하자. [Linux]-[Library Debugging] 메뉴를 실행한 후 libutils.so 파일을 선택하고, 함수명에 sys_alloc을 지정한 후 Debug Start를 누르면 CVD는 타겟을 실행하면서 스캔을 수행하여 지정한 libutils 라이브러리의 sys_alloc 함수가 호출 될 때 해당 라이브러리의 심볼을 로딩하고 지정한 함수에서 멈추어 준다.

 
그림 21. 라이브러리 디버깅


그림 22. 선택한 라이브러리 함수에 멈춘 모습

CVD의 라이브러리 디버깅 기능을 활용하면 라이브러리 함수 호출과 관련된 정보를 알지 못하더라도 해당 라이브러리 함수를 디버깅 할 수 있는 유용한 기능이지만, 함수 호출 시까지 시스템을 스캔해야 하므로, 심볼을 직접 로딩하는 방식보다 다소 느리게 동작한다. 라이브러리 디버깅을 수행 하기 위해서도 역시 커널 이미지가 로딩되어 있어야 한다.

Module Debugging

모듈은 커널이 동작하고 있는 상태에서 동적으로 적재 / 제거 할 수 있는 프로그램으로서 주로 디바이스 드라이버를 작성할 때 사용된다. 모듈 역시 CVD가 제공하는 모듈 디버깅 기능을 활용하면 손쉽게 디버깅을 수행할 수 있다. 사용하는 방법은 앞에서 살펴본 어플리케이션이나 라이브러리와 거의 동일하며, 단지 차이점이 있다면 insmod 명령어를 이용해서 모듈이 적재될 때 해당 모듈의 디버깅 심볼이 로딩되고, 모듈의 초기화 함수에서 멈춘다는 점이다.

초기화 함수 이외의 모듈 함수들을 디버깅 하기 위해서는 각각의 함수에 Breakpoint를 설정하고, 모듈의 함수를 호출하는 어플리케이션을 실행하여 디버깅을 수행할 수 있다.

맺음말

지금까지 안드로이드 시스템의 다양한 디버깅 방법에 대해서 살펴보았다. 예전에는 임베디드 시스템 개발이라 하면 디버깅 환경을 별도로 갖추어야 할 만큼 복잡하지 않은 경우가 많았지만, 최근의 임베디드 시스템 개발은 그 규모나 복잡성이 나날이 커져가고 있어, 디버깅 툴은 더 이상 선택이 아닌 필수가 되어가고 있는 추세이다.

디버깅을 위한 디버거 제품 또는 솔루션의 종류는 무척 다양하고, 새로운 디버깅 툴에 대해 적응하는 것도 만만찮은 일일 수 있지만 어떤 것이든 개발자 본인이 익숙하게 다룰 수 있는 디버깅 툴이 있고, 그것을 이용해서 보다 빨리 문제를 해결 할 수 있는 자신만의 디버깅 스킬을 보유하고 있다면, 개발자의 경쟁력은 더욱 높아질 것이다.

 T I P ! !

Kernel Panic, Exception, Segmentation Fault 등이 발생했을 때 디버깅 방법

부팅 중 또는 시스템 동작 중 에러가 발생했을 때 진입하는 다음과 같은 커널의 루틴에 Breakpoint를 설정해 두면 특정 에러가 발생했을 때 해당 Breakpoint에 멈추게 되고 함수 CallStack을 참고하여 문제가 발생한 위치를 찾을 수 있다.
 
__do_fault (kernel_src/mm/memory.c)
__do_kernel_fault (kernel_src/arch/arm/mm/fault.c)
__bug (kernel_src/arch/arm/kernel/traps.c)
panic (kernel_src/kernel/panic.c)
die (kernel_src/arch/arm/kernel/traps.c)
do_bad_area (kernel_src/arch/arm/mm/fault.c)