本文主要是介绍NE 源码流程集锦(MTK Android R),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
深入分析Android native exception框架
MTK NE异常流程图
Ptrace
模块组成
Tombstoned
libdebuggerd_handler
crash_dump
进程关系
Pipe 管道
进程通讯关系
debuggerd
Android P上Java Crash、Native Crash的异常处理流程学习
Linux 信号
深入分析Android native exception框架
参考MTK网站
https://online.mediatek.com/QuickStart/fc6dd989-890d-444d-b4bf-13a93219702d
参考博客
https://blog.csdn.net/hl09083253cy/article/details/78927104?utm_source=blogxgwz0
https://www.jianshu.com/p/110ea9bd2e3f
https://www2.lauterbach.com/pdf/rtos_linux_stop.pdf
MTK NE异常流程图(Q 之前)
异常发送后,会执行到Arm_notify_die ,用户模式则执行force_sig_info 发送对应信号给用户进程,svc 模式就直接die(),重启手机。
Ptrace
https://www.cnblogs.com/tangr206/articles/3094358.html
模块组成
NE常见类型
如空指针,非法指针,程序跑飞,内存踩坏,段地址错误等
这类问题会被MMU 捕获,MMU 就会发送abort 信号给到cpu,这个时候cpu 根据信号类型执行对应的向量表函数,同时也由用户态切换到内核态,内存处理完会调用__send_signal()发送信号,debuggerd_init()里注册的函数debugger_signal_handler()会接收到信号
一般native 应用都会动态链接一些库如libc.so/libutils.so,这些库动态加载是通过linker 完成的。Kernel 将native 应用、linker 加载到应用进程空间,先跑linker,再跑应用。
linker执行期间还做了一件事:注册信号,具体的函数调用流程如下:
__linker_init() -> __linker_init_post_relocation() -> debuggerd_init()
system\core\debuggerd\tombstoned
Tombstoned :
"tombstoned/intercept_manager.cpp",
"tombstoned/tombstoned.cpp"
libdebuggerd、libevent
"tombstoned/tombstoned.rc
Debuggerd:
Debuggerd.cpp
libdebuggerd_client
crash_dump
crash_dump.cpp
libtombstoned_client
libdebuggerd_client
client/debuggerd_client.cpp
libdebuggerd_handler (need by linker)
handler/debuggerd_handler.cpp
libtombstoned_client
tombstoned/tombstoned_client.cpp
libdebuggerd
"libdebuggerd/backtrace.cpp",
"libdebuggerd/gwp_asan.cpp",
"libdebuggerd/open_files_list.cpp",
"libdebuggerd/tombstone.cpp",
Tombstoned :
"tombstoned/intercept_manager.cpp",
"tombstoned/tombstoned.cpp"
libdebuggerd、libevent
"tombstoned/tombstoned.rc
bionic/libc/platform/bionic/reserved_signals.h
#define BIONIC_SIGNAL_DEBUGGER (__SIGRTMIN + 3)
Tombstoned
tombstoned.cpp
注册tombstone 自己的信号处理函数
tombstone 建立监听回调处理函数,当客户端发起connect ,socket accept 就会触发对应事件,执行回调。intercept_socket 用于debuggerd 向 tombstoned 请求输出 tombstone,其中backtrace 通过debuggerd 输出。
intercept_socket 与crash_socket 交互
debuggerd -b pid //native backtrace
intercept_socket 与java_socket 交互
debuggerd -j pid //java backtrace
crash_socket 用于发生NE 时,内核发送信号,程序收到信息进入其注册的异常处理信号函数,在这里面最后会通过crash_socket连接到tombstoned 打印tombstone
这里tombstoned 自己发生异常,则直接执行_exit(1)
Libevent之evconnlistener
https://blog.csdn.net/u010710458/article/details/80067676
https://blog.csdn.net/bestone0213/article/details/46729247
bind()将端口跟socket 关联起来,listen()则成为服务端进入监听,accept()则当客户端有connect 则响应。
accept()接到连接则执行客户端注册的回调。
第一个参数是event_base,也就底层在监听套接字上有新的 TCP 连接
第二个参数是accept()时执行回调,第三个参数是传递给回调的参数
回调函数中,event_new 创建一个新的event加入监听
第三个参数是事件触发类型
第四个参数是事件触发回调函数
第二个参数、第三个参数、第四个参数都是传递给crash_request_cb 的
从socket 中读取请求数据,判断dump 类型及pid
- 判读如果是java dump 就,通过for_anrs 创建CrashQuere,其它类型通过for_tombstone创建CrashQueue,这里指定了日志保存目录,最大日志数,及最大并行处理数。
- anr 保存/data/anr ,最大日志64,最大并行4,tombstone 保存/data/tombstone,最大日志32,最大并行1。
- 如果当前正在处理dump 达到最大并行数,则将当前请求加入CrashQueue队尾,否则执行dump。
- 执行dump
libdebuggerd_handler
handler/debuggerd_handler.cpp
bionic/linker/linker_main.cpp
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。
struct sigaction结构体介绍
struct sigaction {
void (*sa_handler)(int);//信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
sa_handler 是一个函数指针,其含义与 signal 函数中的信号处理函数类似。或者设置为SIG_IGN忽略信号。
sa_sigaction 则是另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。
sa_flags 成员的值包含了 SA_SIGINFO 标志时,系统将使用 sa_sigaction 函数作为信号处理函数,否则使用 sa_handler 作为信号处理函数。在某些系统中,成员 sa_handler 与 sa_sigaction 被放在联合体中,因此使用时不要同时设置。
sa_mask 成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。
sa_flags中包含了许多标志位,一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)
https://blog.csdn.net/q1007729991/article/details/53893743
Ptrace
https://www.cnblogs.com/tangr206/articles/3094358.html
debuggerd_signal_handler
创建子线程
clone返回创建进程的进程ID,出错的话返回-1;
CLONE_PARENT 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
CLONE_PTRACE 若父进程被trace,子进程也被trace
CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
CLONE_VM 子进程与父进程运行于相同的内存空间
CLONE_PID 子进程在创建时PID与父进程一致
CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
CLONE_THREAD :将子进程加入父进程线程组,否则设置新的线程组
CLONE_SIGHAND:信号处理函数跟父进程一样
如果设置了CLONE_PARENT_SETTID,内核会将子进程的线程ID写入ptid所指向的位置。如果设置了CLONE_CHILD_SETTID,那么clone()会将子线程的线程ID写入指针ctid所指向的位置。如果设置了CLONE_CHILD_CLEARTID,则会在子进程终止时将ctid所指向的内存清零。
等待子线程开始和结束
debuggerd_dispatch_pseudothread
int dup(int oldfd);等效fcntl(oldfd, F_DUPFD, 0)
Dup 用于复制oldfd 所执行的文件描述符,若成功则返回尚未使用的最小的文件描述符。新文件描述符跟oldfd 指向同一文件;
int dup2(int oldfd, int newfd);等效close(oldfd);fcntl(oldfd, F_DUPFD, newfd);
使用newfd 文件描述符指定oldfd ;若newfd 已经存在,则关闭newfd 指向文件;若newfd 跟oldfd 相等,则返回newfd,不关闭newfd 指向文件
创建读写管道
子线程中设置输出输入文件描述符;
main_tid 是发生crash 的进程,pseudothread_tid是clone 创建的线程,通过exccle 执行crash_dump。
AEE产生流程图:
crash_dump
crash_dump.cpp
libtombstoned_client
将信号处理函数设置为默认,信号屏蔽掩码设置为空(这样阻塞时不会丢弃信号),设置sigpipe 处理函数
让进程摆脱原会话的控制
让进程摆脱原进程组的控制
让进程摆脱原控制终端的控制
setsid函数的进程成为新的会话的领头进程
创建子线程,父进程通过pipe 读子进程,进入等待;
解析execle 传递的参数,g_target_thread 是发生异常进程,pseudothread_tid是clone 出来的线程。
获取进程名,/proc/pid/cmdline,线程名,/proc/pid/comm,/proc/self/comm
获取进程的文件句柄,/proc/pid/fd 下文件句柄
获取进程组中的线程
ptrace()系统调用函数提供了一个进程(the “tracer”)监察和控制另一个进程(the “tracee”)的方法,并且可以检查和改变“tracee”进程的内存和寄存器里的数据,它可以用来实现断点调试和系统调用跟踪。
其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED,而父进程通过waitpid(wstatus)(或者其它wait系统调用)被通知收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。当被跟踪后,每当系统调用信号量传来,甚至信号量会被忽略时,tracee会暂停,被跟踪的程序在进入或者退出某次系统调用的时候都会触发一个SIGTRAP信号。
PTRACE_O_TRACECLONE:被跟踪进程在下一次调用clone()时将其停止,并自动跟踪新产生的进程。这样wait_for_vm_process 可以通过PTRACE_GETEVENTMSG获取clone 产生的新进程。
新产生的进程开始执行时就已设置SIGSTOP信号,新产生的进程刚执行就收到SIGSTOP信号,wait_for_vm_process 会等待新的进程停止信号SIGSTOP,检测是否是SIGSTOP信号,并ptrace(PTRACE_CONT, child, 0, 0)让clone 的子进程继续运行;
这时pseudothread_tid进程调用create_vm_process,会调用clone 创建子进程,被跟踪,子进程执行就发送SIGSTOP停止,这时wait_for_clone 获取到子进程pid,并让子进程继续执行,子进程执行时会再次调用clone 创建孙进程,从而获取到孙进程pid,重复上面流程。
crash_dump 通过socket connect 到tombstoned_client,tombstone_client 与socket 服务端tombstoned 通讯。
这里g_output_fd 就是这次socket 请求端(crash_dump 中fork 的子进程)发送数据文件句柄,engrave_tombstone 将dump的信息通过g_output_fd 发送给tombstoned。
连接tombstoned后,crash_dump端会通过g_output_fd将要写入的日志内容发送给tombstoned,tombstoned最终存在文件中。
这里tombstone_path 是/proc/%d/task/%d/fd/%d 文件句柄对应的链接,即g_output_fd文件句柄对应链接,这里通知aee_aed。aee_aed 就是libaed.so 中crash_mini_dump_notify 方法来获取mini dump信息。
aee_aed每次会创建同名子进程,子进程创建aee_dumpstate。
aee_aed 抓取相应日志与打包,aee_dumpstate获取/proc/$pid 下文件。
根据/proc/sys/kernel/core_pattern 启动aee_core_forwarder 获取coredump,跟aee_aed 一起打包为db.
init->aee_aed64->【(aee_aed64与父进程同名->aee_dumpstate)】
init->aee_aedv64->aee_aedv64->aee_dumpstatev
2->aee_core_forwarder coredump
通知system_server。
进程关系
- 在debuggerd_signal_handler 中是发生crash 的线程,gettid 为进程id,getpid 是组进程id,不同,是父子关系。
1、debuggerd_signal_handler 中clone进程pseudothread
clone(debuggerd_dispatch_pseudothread, pseudothread_stack,
CLONE_THREAD | CLONE_SIGHAND | CLONE_VM | CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID,
getpid 与debuggerd_signal_handler 中一样,gettid 不同于进程debuggerd_signal_handler 中;但是打印的log,pid:tid 跟发生问题debuggerd_signal_handler 中一样。
2、create_vm_process 中
clone(nullptr, nullptr, CLONE_FILES, nullptr)
- pseudothread中__fork()进程,在子进程中执行crash_dump
execle(CRASH_DUMP_PATH
在父进程中(__fork()进程)getid跟getpid一样,getppid跟debuggerd_signal_handler 中getpid 一样,在crash_dump中跟父进程中(__fork()进程)一样
- crash_dump 调用setSid 前
调用setSid后gettid、getpid、getppid 一样
fork 创建子进程,在子进程中 gettid跟getpid 一样,getppid 为crash_dump
- debuggerd_signal_handler 中create_vm_process与crash_dump 通过fork 创建的子进程中wait_for_vm_process(pseudothread_tid)
Pipe 管道
管道也是unix ipc的最老形式,管道有两种限制
数据自己读不能自己写
它们是半双工的。数据只能在一个方向上流动。
数据一旦被读走,便不在管道中存在,不可反复读取
它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道由pipe函数创建而成pipe(pipe_fd)经由参数pipe_fd返回两个文件描述符,pipe_fd[0]为读而打开,pipe_fd[1]为写而打开。pipe_fd[1]的输出是pipe_fd[0]的输入。
函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。
父进程写,子进程读流程:
父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
管道读写4中情况
如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
下一篇
https://blog.csdn.net/lei7143/article/details/118244656
这篇关于NE 源码流程集锦(MTK Android R)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!