본문 바로가기

[Debugging] Tips

[안드로이드][리눅스커널] 시그널 - 유저 공간 abort(SIGABRT) 시 동작 시 흐름

유저 공간에서 zygote가 강제 종료되면서 부팅을 못하는 상황입니다. 커널 로그로 아래 메시지를 볼 수 있습니다.
아래 로그는 init 프로세스가 zygote에 SIGABRT(6) 시그널을 전달해서 zygote를 종료시키고 있습니다.
[   46.116831 / 01-02 01:20:24.859][0] init: Service 'zygote' (pid 1777) killed by signal 6
[   46.124107 / 01-02 01:20:24.869][1] init: Service 'zygote' (pid 1777) killing any children in process group
 
그럼 이 동작을 할 때 커널 관점으로 어떤 코드가 수행되는지 살펴보겠습니다.
 
zygote는 커널 공간에서 "main" 이란 쓰레드로 수행됩니다. 그래서 아래 코드를 눈여겨 보면 __send_signal()/send_sigqueue 함수에서 전달하는 struct task_struct *t 가 시그널 전달 받는 프로세스입니다. 이 프로세스의 comm이란 멤버가 프로세스 이름을 담고 있습니다.
 
참고로 아래와 같이 ftrace log를 설정하고, 강제로 zygote를 종료하는 테스트를 수행했습니다.
adb shell "echo 1 > /d/tracing/events/sched/sched_process_exit/enable"
adb shell "echo 1 > /d/tracing/events/sched/sched_process_fork/enable"
 
adb shell "echo 1 > /d/tracing/events/signal/enable"
 
kill -6 918, 즉 kill -6 [pid]로 자이고트에 SIGABRT 시그널을 전달하는 것입니다.
root      918   1     1111816 81936 poll_sched b2ae1ec4 S zygote
austin:/ # kill -6 918
 
그럼 다음과 같은 ftrace 로그를 볼 수 있습니다. main이란 프로세스가 sig 6 SIGABT를 맞고 결국 종료됩니다.
<...>-456   [005] d..2  1186.470605: signal_generate: sig=6 errno=0 code=-6 comm=main pid=918 grp=0 res=0
<...>-918   [005] d..2  1186.470786: signal_deliver: sig=6 errno=0 code=-6 sa_handler=0 sa_flags=14000000
<...>-6202  [007] d..2  1186.470881: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
<...>-6205  [005] d..2  1186.470895: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
<...>-6203  [007] d..2  1186.470919: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
<...>-6204  [005] d..2  1186.470969: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
<...>-6203  [007] ...1  1186.471086: sched_process_exit: comm=FinalizerDaemon pid=6203 prio=120
<...>-918   [005] ...1  1186.471092: sched_process_exit: comm=main pid=918 prio=120
<...>-6204  [005] ...1  1186.471780: sched_process_exit: comm=FinalizerWatchd pid=6204 prio=120
<...>-6202  [007] ...1  1186.471798: sched_process_exit: comm=ReferenceQueueD pid=6202 prio=120
 
그래서 프로세스 이름이 main일 때 강제 커널 패닉을 유발하는 코드를 작성했습니다.
diff --git a/kernel/signal.c b/kernel/signal.c
index 7408330..3157014 100644
--- a/kernel/signal.c
+++ b/kernel/signal.c
@@ -1031,6 +1031,12 @@ static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
        assert_spin_locked(&t->sighand->siglock);
 
        result = TRACE_SIGNAL_IGNORED;
+       if (!strcmp(t->comm, "main")) {
+               show_stack(NULL, NULL);
+               printk("[+][Pompeii] proc %s is ignored \n", t->comm);
+               BUG();
+       }
+
        if (!prepare_signal(sig, t,
                        from_ancestor_ns || (info == SEND_SIG_FORCED)))
                goto ret;
@@ -1120,6 +1126,7 @@ out_set:
        sigaddset(&pending->signal, sig);
        complete_signal(sig, t, group);
 ret:
+       printk("[+][Pompeii] proc %s sig: %d \n", t->comm, sig);
        trace_signal_generate(sig, info, t, group, result);
        return ret;
 }
@@ -1568,6 +1575,15 @@ int send_sigqueue(struct sigqueue *q, struct task_struct *t, int group)
        unsigned long flags;
        int ret, result;
 
+
+       if (!strcmp(t->comm, "main")) {
+               show_stack(NULL, NULL);
+               printk("[+][Pompeii] proc %s is ignored \n", t->comm);
+               show_stack(NULL, NULL);
+
+               BUG();
+       }
+
 
위 코드를 반영하고 타겟 디바이스에 다운로드를 했습니다. 역시 예상대로 부팅 후 20초 정도 시간이 지나자 커널 패닉이 발생했습니다.
 
유저 영역 콜스택까지 확인하니 아래 함수 흐름으로 __send_signal 함수가 호출됩니다.
참고로 심볼 정보가 포함된 libc.so 파일은 symbols\system\lib64에 위치한 libc.so로 로딩해야 한다는 점을 잊지 마세요.
-000|do_undefinstr(regs = 0xFFFFFFC001714630)
-001|__send_signal.constprop.27(sig = 1865235760, info = 0xFFFFFFC06F2D3E40, t = 0xFFFFFFC0000AE488, grou
-002|send_signal(inline)
-002|do_send_sig_info(sig = 6, info = 0xFFFFFFC06F2D3E40, p = 0xFFFFFFC06F2C8000, ?)
-003|do_send_specific(tgid = 748, ?, sig = 6, info = 0xFFFFFFC06F2D3E40)
-004|do_tkill(tgid = 748, pid = 748, sig = 6)
-005|SYSC_tgkill(inline)
-005|sys_tgkill(?, ?, ?)
-006|el0_svc_naked(asm)
 -->|exception
-007|tgkill(asm)
-008|pthread_kill(?, sig = 6)
-009|raise(sig = 748)
-010|abort()
-011|android_log_write_string8_len(?, ?, ?)
 
유저 공간에서 어떤 일이 있었는지 확인해보니 아래 흐름으로 abort가 발생했습니다.
abort -> raise -> pthread_kill -> tgkill
 
정리하면, 유저 영역에서 abort가 발생하면 커널 영역에 signal 6를 전달함을 알 수 있죠.
 
그런데 dump state 로그를 확인하면 아래 콜스택을 볼 수 있습니다.
/system/lib64/libc.so (tgkill+8)
/system/lib64/libc.so (pthread_kill+64)
/system/lib64/libc.so (raise+24)
/system/lib64/libc.so (abort+52)
/system/lib64/libart.so art::Runtime::Abort()                                                                          /system/lib64/libart.so art::LogMessage::~LogMessage()                                                                  /system/lib64/libart.so art::Thread::AssertNoPendingException()    
/system/lib64/libart.so art::ClassLinker::FindClass(art::Thread*, char const*, art::Handle<art::mirror::ClassLoader>)    /system/lib64/libart.so art::ClassLinker::FindArrayClass(art::Thread*, art::mirror::Class**)                                  /system/lib64/libart.so art::JNI::NewObjectArray(_JNIEnv*, int, _jclass*, _jobject*)
/system/lib64/libandroid_runtime.so                                                                                              /system/framework/arm64/boot-framework.oat (offset 0x1663000) (android.hardware.location.ContextHubService.nativeInitialize+124)                 
/system/framework/arm64/boot-framework.oat (offset 0x1663000) (android.hardware.location.ContextHubService.<init>+300)                            
/system/framework/oat/arm64/services.odex (offset 0xd1b000)
 
C++ 코드에서 클래스 메쏘드로 구현된 함수들은 심볼들이 이상하게 보입니다.
아래 폴더에 있는 바이너리 유틸리티 프로그램을 쓰면 원래 심볼로 보여줍니다. c++filt 옵션을 기억합시다. 
android/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-nm              ./aarch64-linux-androidkernel-nm libart.so | c++filt > sym_libart.c
 
참고로 dump state 원래 로그는 아래와 같습니다. 알 수 없는 _ZN3art7Runtime5AbortEv 심볼이 보이죠.
01-03 00:37:24.599  1721  1721 F DEBUG   : backtrace:
01-03 00:37:24.599  1721  1721 F DEBUG   :     #00 pc 000000000006b8c8  /system/lib64/libc.so (tgkill+8)
01-03 00:37:24.599  1721  1721 F DEBUG   :     #01 pc 0000000000068d4c  /system/lib64/libc.so (pthread_kill+64)
01-03 00:37:24.599  1721  1721 F DEBUG   :     #02 pc 00000000000242b8  /system/lib64/libc.so (raise+24)
01-03 00:37:24.599  1721  1721 F DEBUG   :     #03 pc 000000000001ccd4  /system/lib64/libc.so (abort+52)
01-03 00:37:24.599  1721  1721 F DEBUG   :     #04 pc 000000000042c5d0  /system/lib64/libart.so (_ZN3art7Runtime5AbortEv+352)
01-03 00:37:24.599  1721  1721 F DEBUG   :     #05 pc 00000000000e4b24  /system/lib64/libart.so (_ZN3art10LogMessageD2Ev+1204)
01-03 00:37:24.599  1721  1721 F DEBUG   :     #06 pc 000000000044eaf4  /system/lib64/libart.so (_ZNK3art6Thread24AssertNoPendingExceptionEv+836)
01-03 00:37:24.599  1721  1721 F DEBUG   :     #07 pc 0000000000123ec8  /system/lib64/libart.so (_ZN3art11ClassLinker9FindClassEPNS_6ThreadEPKcNS_6HandleINS_6mirror11ClassLoaderEEE+68)
01-03 00:37:24.599  1721  1721 F DEBUG   :     #08 pc 000000000011f170  /system/lib64/libart.so (_ZN3art11ClassLinker14FindArrayClassEPNS_6ThreadEPPNS_6mirror5ClassE+744)
01-03 00:37:24.600  1721  1721 F DEBUG   :     #09 pc 0000000000348c3c  /system/lib64/libart.so (_ZN3art3JNI14NewObjectArrayEP7_JNIEnviP7_jclassP8_jobject+652)
01-03 00:37:24.600  1721  1721 F DEBUG   :     #10 pc 0000000000142254  /system/lib64/libandroid_runtime.so
01-03 00:37:24.600  1721  1721 F DEBUG   :     #11 pc 0000000001baa5d0  /system/framework/arm64/boot-framework.oat (offset 0x1663000) (android.hardware.location.ContextHubService.nativeInitialize+124)
01-03 00:37:24.600  1721  1721 F DEBUG   :     #12 pc 0000000001ba9e80  /system/framework/arm64/boot-framework.oat (offset 0x1663000) (android.hardware.location.ContextHubService.<init>+300)
01-03 00:37:24.600  1721  1721 F DEBUG   :     #13 pc 0000000000dc8474  /system/framework/oat/arm64/services.odex (offset 0xd1b000)
 
정리하면 유저 공간에서 Abort가 떨어지면 abort() -> raise() -> pthread_kill() -> tgkill() 흐름으로 sys_tgkill란 시스템 콜이 호출되어 커널 공간에서는 __send_signal 함수가 호출됩니다.
 
위 동작은 모두 Aarch64 아키텍처 환경에서 설명을 드린 건데요. Aarch32 아키텍처는 약간 동작이 다릅니다.
-000|__do_user_fault(tsk = 0x46, addr = 0, fsr = 5, sig = 11, code = 0, regs = 0x0)
-001|__do_kernel_fault(inline)
-001|do_page_fault(addr = 3883189760, fsr = 5, regs = 0xE5BEFFB0)
-002|do_translation_fault(?, fsr = 3854499232, ?)
-003|do_DataAbort(addr = 0, fsr = 5, regs = 0xE5BEFFB0)
-004|__dabt_usr(asm)
 
arm32 비트 아키텍처에선 유저 영역에서 어보트가 떨어지면 __dabt_usr란 익셉션 벡터가 실행되고, (물론 유저 공간에서 수행 중인 레지스터를 스택에 푸시합니다.)
NSR:C0F733E0|E24DD048  __dabt_usr:       sub     r13,r13,#0x48    ; r13,r13,#72
NSR:C0F733E4|E98D1FFE                    stmib   r13,{r1-r12}
NSR:C0F733E8|EE117F10                    mrc     p15,0x0,r7,c1,c0,0x0   ; p15,0,r7,c1,c0,0 (system control)
NSR:C0F733EC|E51F80B4                    ldr     r8,0xC0F73340
NSR:C0F733F0|E8900038                    ldm     r0,{r3-r5}
NSR:C0F733F4|E28D003C                    add     r0,r13,#0x3C     ; r0,r13,#60
NSR:C0F733F8|E3E06000                    mvn     r6,#0x0          ; r6,#0
NSR:C0F733FC|E58D3000                    str     r3,[r13]
NSR:C0F73400|E5988000                    ldr     r8,[r8]
NSR:C0F73404|E8800070                    stm     r0,{r4-r6}
NSR:C0F73408|E9406000                    stmdb   r0,{r13-r14}^
NSR:C0F7340C|E1380007                    teq     r8,r7
NSR:C0F73410|1E018F10                    mcrne   p15,0x0,r8,c1,c0,0x0   ; p15,0,r8,c1,c0,0 (system control)
NSR:C0F73414|E3A0B000                    mov     r11,#0x0         ; r11,#0
NSR:C0F73418|EBC5BDA8                    bl      0xC00E2AC0       ; trace_hardirqs_off
NSR:C0F7341C|E1A0200D                    cpy     r2,r13
NSR:C0F73420|EBC2BC6E                    bl      0xC00225E0       ; v7_early_abort
 
v7_early_abort란 레이블에서 MMU의 fault register를 r1 레지스터로 읽습니다. 
NSR:C00225E0|EE151F10  v7_early_abort:     mrc     p15,0x0,r1,c5,c0,0x0   ; p15,0,r1,c5,c0,0 (data fault status)
NSR:C00225E4|EE160F10                      mrc     p15,0x0,r0,c6,c0,0x0   ; p15,0,r0,c6,c0,0 (data fault address)
NSR:C00225E8|EAFF97CF                      b       0xC000852C       ; do_DataAbort
 
위 코드에서 "mrc     p15,0x0,r1,c5,c0,0x0" 명령어를 잘 기억하세요.
 
결국 MMU fault register에서 읽은 값에 따라  fsr_info[5] 전역변수에 이미 정의된 do_translation_fault 함수를 호출하는군요.
  (static struct fsr_info [32]) fsr_info = (
    [0] = (
      (int (*)()) fn = 0xC001F58C = do_bad,
      (int) sig = 11,
      (int) code = 0,
      (char *) name = 0xC13F1AD6 = kallsyms_token_index+0x4FC6),
    [1] = (
      (int (*)()) fn = 0xC0021628 = do_alignment,
      (int) sig = 7,
      (int) code = 196609,
      (char *) name = 0xC13F1AE7 = kallsyms_token_index+0x4FD7),
    [2] = (
      (int (*)()) fn = 0xC00184B4 = hw_breakpoint_pending,
      (int) sig = 5,
      (int) code = 196612,
      (char *) name = 0xC13F0451 = kallsyms_token_index+0x3941),
    [3] = (
      (int (*)()) fn = 0xC001F58C = do_bad,
      (int) sig = 11,
      (int) code = 196609,
      (char *) name = 0xC13F17F3 = kallsyms_token_index+0x4CE3),
    [4] = (
      (int (*)()) fn = 0xC0F7528C = do_translation_fault,
      (int) sig = 11,
      (int) code = 196609,
      (char *) name = 0xC13F17D9 = kallsyms_token_index+0x4CC9),
    [5] = (
      (int (*)()) fn = 0xC0F7528C = do_translation_fault,
      (int) sig = 11,
      (int) code = 196609,
      (char *) name = 0xC13F193F = kallsyms_token_index+0x4E2F), 
 
    [6] = ((int (*)()) fn = 0xC001F58C = do_bad, (int) sig = 11, (int) code = 196609, (char *) name = 0xC13F17F3 = kallsyms_token_index+0x4CE3),
    [7] = ((int (*)()) fn = 0xC0F74E4C = do_page_fault, (int) sig = 11, (int) code = 196609, (char *) name = 0xC13F1970 = kallsyms_token_index+0x4E60),
 
# Reference: For more information on 'Linux Kernel';
 
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
 
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2