본문 바로가기

2013
임베디드월드

글: 라영호 | ratharn@naver.com / 2013-02-04


[연재 차례]

1. 안드로이드 시스템의 역사 및 동향
2. 안드로이드 시스템과 리눅스
3. 안드로이드 플랫폼의 이해
4. 안드로이드 바인더(Binder)의 이해
5. 안드로이드 서비스
6. 안드로이드 SurfaceFlinger와 프레임버퍼 드라이버
7. 안드로이드 User Interface와 ADK2012
8. Linux Sound Device와 안드로이드 사운드 시스템
9. 안드로이드 카메라 시스템
10. 안드로이드 카메라와 멀티미디어 프레임워크
11. 안드로이드 카메라와 멀티미디어 프레임워크 ②
12. 안드로이드 시스템 디버깅 및 기타


안드로이드 카메라와 멀티미디어 프레임워크, Android Camera and Multimedia Framework

안드로이드 시스템 개발자를 위한 안드로이드 시스템의 분석 및 이해 ⑫
안드로이드 시스템 디버깅 및 기타

지금까지 12회에 걸쳐 안드로이드 시스템에 대해 살펴봤다. 이번 호는 그 마지막으로써 디버깅 및 개발에 필요한 여러 가지 사항에 대해 살펴 보도록 하겠다. 처음 본 연재를 기획했을 때와 지금은 많은 상황이 변했다. 안드로이드 시스템을 채용한 스마트폰의 점유율의 변화가 가장 크겠다. 또한 시스템의 구조도 버전에 따라 점차 발전하고 있다.이러한 변화를 빠르게 따라가고자 본 연재를 시작했으나 시스템의 발전 속도는 그 예상을 넘었다. 이러한 발전은 그만큼 스마트 폰 시장이 급격히 변하고 있다는 증거이며, 그만큼 치열하게 변해야지만 살아 남는다는 것을 말해 주고 있기도 하다.이러한 발전에 조금이라도 도움이 되었기를 바라며 본 연재를 마무리하고자 한다. 마지막 주제는 안드로이드 시스템에 대한 디버깅 및 관련 내용에 대해 다루도록 하겠다.


드로이드 시스템의 개발과 디버깅
안드로이드 시스템의 개발은 어떠한 컴파일러 및 툴을 결정하는 것이 가장 먼저 시작하는 단계가 되겠고 여기에 부트로더,커널 버전,루트 파일 시스템 빌드를 통해 안드로이드 시스템을 구성하게 된다. 따라서 안드로이드 시스템을 구성하는 전체 구성은

▷ 부트 로더
▷ 커널
▷ 루트 파일 시스템(Root Filesystem)

총 3가지가 시스템을 구성하는 주요 요소다.


개발 및 디버깅의 주요 요소
안드로이드 시스템 개발에 있어 중요한 요소중의 하나는 어떠한 버전의 툴체인을 사용할 것인가이다. 컴파일러 버그가 최소화된 검증된 툴체인을 사용하여 개발을 하는 것이 중요한 요소중의 하나다. 부트로더/커널/미들웨어/응용 프로그램과의 호환성 검증이 되어야 한다.

JTAG기능을 사용하는 하드웨어 디버깅 장비를 사용할 것인지도 시스템 개발의 중요한 요소다. 대체적으로 현재 프로세서에 사용되는 JTAG 형태의 디버깅 장비는 무척 고가이기 때문에 여러 가지 요소를 고려하여 선택해야 한다. 부트로더는 어떤 식으로 구성하고 어떠한 형태의 부트로더를 이용할 것인가도 중요한 요소다. 부트로더의 많은 부분은 하드웨어에 가장 의존적인 코드로 구성되어 있기 때문에 이러한 부분을 어떻게 쉽게 구성하고 관리할 수 있는지가 중요한 개발의 요소다. 부트로더의 옵션에 따라 부팅 방법을 변경할 수 있는 방법을 제공하는지, 커널 디버깅에 유리한지, 개발에 얼마나 편한지, 네트웍를 사용할 것인지가 개발 및 시스템 디버깅에 중요한 요소다.

어떤 Kernel을 사용할 것인가?
보통 임베디드 시스템은 충분히 검증된 커널을 사용해야 한다.

여러 기능을 가진 최신 버전 보다 설계 명세서에 기술된 기능을 반영하는 커널을 선택하는 것이 좋다. 오래된 커널 버전을 쓰면 커뮤니티의 지원을 받을 수 없는 경우가 발생하기 때문에 버전에 대한 정보를 항상 업데이트 해야 한다. 불가피 할 경우 최신 커널을 위험을 감수하고 사용하기도 한다.

루트 파일 시스템에 포함할 것은 무엇인가? 결정해야 한다.
어떤 미들웨어와 라이브러리, 어떤 응용 프로그램을 사용할 것인지 사전에 설계되어야 한다. 소스/라이브러기가 충분히 검증 되었는지 사전에 검토해야 하고, 충분한 디버깅 환경이 제공이 되어야 한다. 또한 가능하면 사용예제와 사용 사례가 많이 있는 것이 시스템을 안정적으로 개발을 할 수 있다.


안드로이드 시스템 디버깅에 중요한 요소
안드로이드 시스템을 잘 디버깅 하기 위해서는 해당 시스템에 대해서 잘 알고 있어야 한다. 부트로더에 대한 이해가 제대로 되어야 하고, 커널에 대한 구조를 얼마나 이해하고 있는지가 디버깅의 효율을 높여주는 주요 요소가 될 수 있다. 루트 파일 시스템의 구성과 구성에 필요한 바이너리들이 커널과 어떻게 연결이 되는지에 대한 이해가 필요하다. 리눅스의 경우는 네트웍이 거의 필수인 경우가 많은데, Network에 대한 이해가 얼마나 필요한지, 임베디드 시스템의 경우 하드웨어에 대한 이해가 디버깅에 중요한 요소를 차지한다.


커널 디버깅의 개요
User space에서의 개발과 Kernel space개발과의 차이 중 가장 큰 것은 디버깅이 힘들다는 점이다.
커널 개발의 경우 커널에서 문제가 하나라도 발생하면 전체 시스템이 다운되게 된다. 커널 디버깅을 점점 잘하게 되는 것은 전적으로 경험과 운영체제에 대한 이해도에 달려있다.
어떠한 방법을 쓰더라도 커널 디버깅은 아주 복잡하고, 많은 지식을 필요로 하는 과정이다.

가장 중요한 점은 구조적/논리적으로 안정하게 코드가 작성되어야 한다는 점이다. 리눅스 커널 코드는 속도를 위해서 높은 수준으로 최적화 되어있기 때문에 디버깅에 어려움을 준다.

또한 컴파일러도 최적화 기술을 사용한다(Inline함수 등의 사용)는 점이다. 가상 메모리를 사용함으로써 물리 메모리와의 매핑 추적에 어려움이 생긴다(MMU세팅을 하는 부분에 대한 정해진 방식이 없기 때문에 코드의 추적에 어려움 생긴다). 사용자 영역 메모리와 커널 메모리가 분리되어 운영되기 때문에 커널에서 생긴 문제를 추적하는데 있어 어려움을 겪게 된다.
초기화 코드의 경우 하드웨어에 심하게 의존적이며, 디버깅에 활용할 수 있는 자원이 한정되어 있다(시리얼 콘솔 메시지가 거의 유일한 디버깅 방법이다. 아니면 JTAG 디버깅 장비를 사용해 디버깅을 해야 한다).


Kernel Debugging의 어려움
일단 버그가 필요하다. 잘 정의된(설명이 명확하게 될 수 있는)버그가 필요하다.재현이 가능한 버그가 있으면 가장 좋지만, 대부분의 버그는 정해진 방식대로 나타나지 않다. 버그가 있는 커널 버전이 필요하다. 즉, 버그가 수정된 버전과 비교할 수 있는 커널 버전이 필요(x86용 커널의 경우는 유용하다)하다. 임베디드 리눅스의 경우는 이런 경우가 드물기 때문에 커널 디버깅에 어려움이 많다. 즉, 동일한 증상이 리포트 되는 경우가 드물다. 반드시 버그를 재현할 수 있어야 한다. 버그가 재현이 안되고 랜덤한 방식으로 나올 경우는 구조적인 접근을 해야 한다. 하지만 대부분의 커널 디버깅의 어려움은 재현하기 힘든 버그일 경우가 많다.


Kernel에서의 버그
응용 프로그램에서의 버그만큼 커널에서도 다양한 버그가 발생한다. 명백한 변수 등의 오류에서부터 동기화오류(Global Variable에 대한 Locking 문제) 변수에 대한 reference count의 오류 등에 의한 데이터 스트럭쳐 해제에 관한 버그. NULL pointer 참조 오류와 같은 버그는 흔히 발생하는 오류다. 커널의 디버깅은 Multi-thread를 사용하는 대형 프로젝트 디버깅과 유사하다. 타이밍적인 문제와 Race Condition과 같은 커널만의 독특한 문제가 있다. 프로그래머가 구조적으로 잘 알고 있다면 커널에서의 버그를 줄일 수 있다.
커널 디버깅의 분류와 방법


Low Level Debugging
Kernel 콘솔이 동작할 때까지의 디버깅 영역이다. 콘솔이 동작하지 않을 경우 디버깅에 많은 어려움을 겪을 수 있다. JTAG 장비에 의존을 하거나, KGDB 등의 메커니즘에 의존을 해야 한다. Kernel의 low level debugging 옵션을 이용해서 디버깅이 가능하고(include/asm-arm/arch-s3c2410/debug-macro.S)부트로더의 기능에 의존하는 경우가 많아(초기화 루틴을 부트로더에서 세팅하여 사용한다) JTAG과 같은 장비를 써서 주로 디버깅하게 된다. SoC(System On Chip) Main Core에서의 디버깅이 많다. Architecture Startup-code, Memory/IRQ/Timer, Kernel console 영역이 주 디버깅 영역이다.


High Level Debugging
Kernel 콘솔이 동작하기 시작한 후의 디버깅 방법이다. 콘솔이 동작하기 시작하므로, printk 등을 사용할 수 있다. 마찬가지로 JTAG 장비를 이용 하거나, KGDB등의 메커니즘을 이용하여 디버깅 할 수 있다. SoC의 주변장치의 디버깅을 하는 경우에 사용한다. 루트 파일시스템과 응용 프로그램과의 상호작용 디버깅에 중점을 둔다. 루트 파일시스템까지 부팅이 된 이후는 Module을 이용한 디버깅이 가능하게 된다. TTY까지의 커널의 부팅의 하반부 디버깅, 주변 장치에 대한 디버깅, 응용 프로그램을 위한 커널 드라이버/Module 코딩한 부분에 대한 디버깅을 하게 된다.


KGDB
시리얼 포트를 통한 원격 커널 디버그에 관련된 패치이다. 2.6.25 커널서부터는 공식 커널에 포함되어 제공되는 기능이다. 2대의 컴퓨터 혹은 임베디드의 경우는 타깃과의 여러 개의 시리얼 연결이 필요하다. 강력한 gdb의 기능을 거의 이용 가능하다. 설정은 까다롭지만 완료가 될 경우는 강력한 디버깅 툴이 되게 된다. 하지만 해당 아키텍처에 알맞은 kgdb patch가 없을 경우 직접 작성해야 한다. 부트로더의 설정에 의존을 많이 한다. 부트로더의 디버깅에는 사용하지 못한다는 단점이 있다.


JTAG(Joint Test Action Group)
프로세서(SoC)의 디버깅 모드로 진입해서 디버깅할 수 있는 하드웨어 장비이다. 보드에 부트로더나 커널을 포팅할 경우 초기단계의 문제점을 파악하는데 가장 효율적인 개발 장비이다.
대부분 비주얼 통합환경을 제공하고 디버깅에의 높은 편이성을 제공하고 있다. 커널의 컴파일 옵션을 디버깅 장비에 맞춰줘야 한다. (-gdwarf-2 옵션)
장점으로는 실시간 디버깅이 가능하다는 점과 커널 재 컴파일 없이 디버깅 가능하다는 점이다.


Printk
커널 출력 함수이다. C library의 printf와 거의 동일한 동작을 하게 된다. Kernel 콘솔이 동작한 이후에는 가장 강력한 디버깅 방법으로 커널의 어느 곳에서든 호출이 가능하다. 인터럽트 혹은 프로세스 컨텍스트에서도 호출가능, lock이 걸려 있는 상황에서도 print가 가능하다. SMP(다중 멀티 프로세서)에서도 호출 가능하다. Loglevel에 따라 동작이 틀려진다. 통상 console log level 이상에서만 화면으로 출력된다. syslogd와 klogd를 사용할 경우 system log 파일로 출력이 된다. 커널이 부팅되고, root filesystem이 동작한 후의 커널 디버깅은 주로 printk에 의존을 한다.


printk의 문제점
커널 부팅 과정 중 콘솔이 초기화되기 이전에서는 사용 불가능 하다는 단점이 있다.
커널 부팅과정 중 setup_arch()부분에서의 디버깅에 어려움이 있다. 이런 문제 때문에 부트로더/JTAG 등의 도움을 받아야만 하는 경우가 생긴다. 콘솔로 많은 메시지를 출력하게 될 경우 디버깅에 더 어려움을 겪을 수 있다. 임베디드 시스템의 경우는 시리얼 콘솔을 캡쳐하면 되나, x86인 경우는 모니터에 대한 캡쳐가 어렵다. Runtime 시에 message를 출력하지 않도록 제어가 힘들다는 단점이 있고 새로운 메시지 출력을 하기 위해서는 커널 재컴파일이 필요하다는 단점이 있다.

printk log level

printk log level
#define KERN_EMERG “<0>” /* system is unusable */
#define KERN_ALERT “<1>”
/* action must be taken immediately */
#define KERN_CRIT “<2>” /* critical conditions */
#define KERN_ERR “<3>” /* error conditions */
#define KERN_WARNING “<4>” /* warning conditions */
#define KERN_NOTICE “<5>”
/* normal but significant condition */
#define KERN_INFO “<6>” /* informational */
#define KERN_DEBUG “<7>”
/* debug-level messages */


버그유발과 정보덤프
커널은 버그를 표시하고, 버그에 대한 assertion과 그 시점에서의 여러 정보를 덤프하기 위하여 다음과 같은 함수를 제공한다.

BUG ()/ BUG_ON()/ panic()

static int __init s3c2410ts_init(void)
{
BUG();
return platform_driver_register(&s3c2410ts_driver);
}
static int __init s3c2410ts_init(void)
{
int aa;
BUG_ON(aa);
return platform_driver_register(&s3c2410ts_driver);
}
static int __init s3c2410ts_init(void)
{
panic(“test”);
return platform_driver_register(&s3c2410ts_driver);
}


Oops
커널 동작 시 문제가 생겼을 경우 유저에게 알리는 방법이다. 사용자 영역이 잘못된 경우처럼 커널 자신이 문제를 고치거나 강제 종료 등의 일을 할 수 없다. 대신 oops 메시지를 내보내게 된다.
콘솔에 오류 메시지를 출력하고, 레지스터의 내용을 덤프, backtrace의 내용을 제공하게 된다.
2.4대의 커널에 비해 2.6대의 커널의 좋아진 점은 backtrace 정보를 symbol로 표현해준다.(컴파일러의 차이 때문)

Debugtest

static int *debugtest;
static int __init s3c2410ts_init(void)
{
debugtest[0] = 0x80000000;
return platform_driver_register(&s3c2410ts_driver);
}


리눅스 커널 구조의 이해

▷ 압축되어 있는 linux kernel entry point - arch/*/boot/compressed/head.S
   압축된 image를 압축 해지하고, 압축되지 않는 kernel의 entry point로 jump
▷ 압축되어있지 않은 linux kernel entry point - arch/*/kernel/head.S
   architecture와 관련 있는 hardware를 초기화를 진행하고 C 코드가 수행될 수 있도록 설정(stack 확보, bss 초기화)하게 된다.
   init/main.c에 있는 start_kernel() 함수를 호출 하게 된다.


start_kernel( )
리눅스 커널의 중요한 함수로써 architecture dependent 한 부분 초기화 과정에 중요한 역할을 하게 된다. Linux sub system 초기화를 진행하게 된다. kernel_thread(init, …)함수로 PID가 1인 init kernel thread 생성하게 된다. start_kernel() 함수 자체는 idle task로 전환하게 된다.


zImage 구성
리눅스 커널 이미지는 크기를 줄이기 위해 압축된 형태로 저장된 이미지 형태이다. vmlinux라는 커널 이미지를 압축한 형태가 zImage다.압축 해제 시 지정된 물리주소에 커널을 압축 해제 한다.

arch/arm/mach-s3c2410/Makefile.boot
ZRELADDR= zreladdr-y=0x30008000


그림 1. 압축된 리눅스 커널의 내부

그림 2. 리눅스 커널 로드 단계

[그림2]와 같이 커널이 시작되면서 페이지 테이블이 만들어진다. MMU가 설정되면 리눅스 커널은 가상주소를 기준으로 메모리 맵을 사용한다.
Trap init 단계에서 리눅스에서 사용되는 exception vector와 handler가 복사된다. [그림3]은 리눅스 커널 초기화에 따른 내부 초기화 절차 및 동작 단계 그림이다.


리눅스 커널 시작

▷ Bootloader로부터 architecture number를 인자로 받게 된다.
▷ Kernel Entry - arch/arm/kernel/vmlinux.lds 에 정의 대로 시작
▷ 커널이 시작되면 stext 부터 실행

ENTRY(stext)

71 __INIT
72 .type stext, %function
73ENTRY(stext)


ARM Linux kernel이 정상적으로 동작하기 위해 타깃 보드의 프로세서가 무엇인지 찾게 된다.
프로세서 정보가 없으면 커널 초기화가 중지되고 오류 메시지 출력 후 멈추게 된다. 프로세서 ID 값은 cp15의 레지스터 0에 명시되어 있다.

그림 3. 리눅스 커널 부팅에 따른 단계별 동작 내용


타깃 보드의 프로세서 찾기

76 mrc p15, 0, r9, c0, c0@ get processor id
77 bl__lookup_processor_type
@ r5=procinfo r9=cpuid
78 movsr10, r5
@ invalid processor (r5=0)?
79 beq __error_p @ yes, error ‘p’


▷ 머신 타입 검색 -ARM Linux kernel이 정상적으로 동작하기 위하여 머신 타입 정보도 올바르게 설정되어 있어야 한다. 해당하는 머신 타입 정보가 없으면 커널 초기화는 중단된다. 개발 대상 보드의 타입을 말한다. 리눅스에서 지원되는 모든 머신은 고유의 번호를 사용한다.


머신 타입 검색

80 bl__lookup_machine_type @ r5=machinfo
81 movsr8, r5@ invalid machine (r5=0)?
82 beq __error_a @ yes, error ‘a’




▷ __mmap_switched - __data_loc과 __data_start의 위치가 다를 경우 __data_loc에 위치한 데이터 세그먼트를 __data_start로 복사



__mmap_switched - __data_loc과 __data_start의 위치가 다를 경우

arch/arm/kernel/head-common.S
.type __switch_data, %object
__switch_data:
.long __mmap_switched
.long __data_loc @ r4
.long __data_start@ r5
.long __bss_start@ r6
.long _end @ r7
.long processor_id@ r4
.long __machine_arch_type @ r5
.long cr_alignment@ r6
.long init_thread_union + THREAD_START_SP
@ sp
/*
...
*r0= cp#15 control register
*r1= machine ID
*r9= processor ID
*/
.type __mmap_switched, %function
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5
@ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b

▷ __mmap_switched - BSS 영역 초기화 하게 된다. 아키텍처 버전(r9)과 부트로더에서 넘긴 머신 ID(r1)를 저장한다. 이후에 arch/arm/kernel/setup.c의 processor_id와 __machine_arch_type 변수를 통해 참조할 수 있다. 이후 start_kernel() 로 분기하게 된다.


arch/arm/kernel/head-common.S

arch/arm/kernel/head-common.S
mov fp, #0
@ Clear BSS (and zero fp)
1:cmp r6, r7
strcc fp, [r6],#4

bcc 1b
ldmia r3, {r4, r5, r6, sp}
str r9, [r4]
@ Save processor ID
str r1, [r5]
@ Save machine type
bic r4, r0, #CR_A
@ Clear ‘A’ bit
stmia r6, {r0, r4}
@ Save control register values
b start_kernel

▷ start_kernel() - 리눅스 커널 부팅의 거의 모든 과정이 start_kernel()에서 이루어 진다.
가장먼저 lock을 획득한다(lock_kernel(), Big Kernel Lock, Big Kernel Semaphore) init kernel thread 생성하게 된다.


init kernel thread터페이스 설정

init/main.c
asmlinkage void __init start_kernel(void)
{
lock_kernel();
boot_cpu_init();
page_address_init();
printk(KERN_NOTICE);
printk(linux_banner);
setup_arch(&command_line);

init kernel thread


(2) /dev/console 오픈 - 디버깅 메시지 출력을 위한 콘솔 오픈(stdin, stdout, Stderr)
(3) Kernel argument가 있으면 command를 실행 시키게 된다.
(4) /sbin/init, /etc/init, /bin/init, /bin/sh 순으로 프로세스 생성 시도. 하나라도 성공하면 PID 1번으로 init 프로세스 생성하게 된다.
(5) init 프로세스 생성이 실패하면 kernel panic 발생
(6) 일반적으로/sbin/init (System V init)이 실행된다. (7) System V init 과정을 진행하게 된다. /etc/inittab 파일의 내용에 따라 순서대로 실행한다.


Proc filesystem

커널의 현재 정보를 user space에서 확인할 수 있도록 하기 위해 만들어진 Pseudo filesystem이다.
커널정보의 확인 뿐 아니라 정보의 변경도 가능하도록 기능을 제공하고 있다. 2.6.x대에서는 sysfs로 device관련 부분을 넘겨주었으나, 현재도 device driver개발, 모니터링에 많이 사용하게 된다.
/proc 디렉토리에 마운트가 되며 하나의 file크기는 PAGE_SIZE(4kbytes)를 넘지 못한다. 디바이스 드라이버의 디버깅, 상태관찰, 제어에 유용하다.


User Space에서의 Debugging의 개요
디버그 작업이 편리하다. 커널과는 다르게 크래시 상황에서도 커널에서 시스템을 보호해 줄 수 있다. 특이한 경우(device driver제어)가 아닌 경우에는 시스템을 신경 안 쓰고 작업이 가능하다.
Software debugger를 사용할 수 있다는 장점이 있다. 기존에 존재하는 많은 라이브러리들을 이용할 수 있다. 예제가 많고, 문서화가 잘되어 있는 경우가 많다. 여러 가지 디버그 방법을 사용할 수 있다. 가장 기본적인 printf를 이용한 방법(단, 시간이 많이 걸린다는 단점이 있다.) GDB를 이용한 텍스트 기반의 source level debugger사용 가능하다. GDB와 연동한 GUI기반의 DDD(Data Display Debugger)나 xxgdb등을 사용할 수 있다.


GDB란?
GNU 소프트웨어 시스템을 위한 기본 디버거이다. 다양한 유닉스 기반의 시스템에서 동작하는 이식성 있는 디버거다. 임베디드 시스템을 디버깅할 때 사용되는 원격 모드를 지원한다. GDB 프로토콜을 알고 있는 원격지의 stub과 직렬 포트 혹은 TCP/IP를 통해 통신할 수 있다. 이 원격 디버깅 모드는 리눅스 커널에 사용되는 소스-레벨 디버거인 KGDB에서도 사용된다. GDB는 자체적인 GUI를 포함하고 있지 않으며 기본적으로 명령행 인터페이스를 사용 DDD, GDBk/Insight와 같은 몇 가지 프론트엔드들이 있으며, Emacs에서도 “GUD”모드를 지원한다. 이들은 통합 개발 환경에서 제공하는 것과 비슷한 디버깅 기능들을 제공 한다.


끝으로
지금까지 12개월에 거처 안드로이드 시스템과 개발에 관하여 살펴 봤다. 개발의 모든 부분을 살펴 볼 수 없어 아쉬웠다. 또한 일부 주제에 대해서는 심도 있게 접근하지 못한 것에 대해 아쉬움을 남긴다. 본 연재는 개발의 기초적인 것을 전달하는 것이 목적이었기 때문에 향후 심도 있는 부분에 대해서는 다양한 접근이 필요하다.





/필/자/소/개/

필자

라영호

국내 스마트폰의 초창기인 Cellvic에서부터 스마트폰을 개발하였고 윈도 모바일 및 다양한 임베디드 시스템을 개발하고 있다. 현재는 안드로이드 시스템 포팅 및 임베디드 시스템 개발, 컨설팅, 교육 등을 진행하는 회사를 운영하고 있다. 개인 블로그(www.embeddedce.com)를 통해 임베디드 시스템개발에 대한 다양한 생각과 방법론을 함께 생각해 보고자 노력 중이다.

※ 본 내용은 (주)테크월드(http://www.embeddedworld.co.kr)의 저작권 동의에 의해 공유되고 있습니다.
    Copyright ⓒ Techworld, Inc. 무단전재 및 재배포 금지

맨 위로
맨 위로