Kyle's Notes 2014-11-11T03:32:43+00:00 justfavme@gmail.com 如何获取系统调用的调用栈 2014-11-07T00:00:00+00:00 Kyle http://kyleliu.github.io/hook-system-call-and-log-callstack 如何获取系统调用的调用栈

问题描述

机顶盒在进行老化测试的时候发现,放置一段长时间后,盒子会因为分配不到文件描述符(file descriptor)而重启,可以肯定有fd的泄漏问题。通过lsof发现泄漏的是一个pipe(包含两个fd),但是查找发现系统中使用pipe的地方很多,从review源代码的角度无从下手,只能用一些非常规的方式了。

要定位这个bug,需要解决两个问题:一是要知道是谁调用的pipe,也就是调用pipe的调用栈;二是要拦截pipe的系统调用,添加我们自己的调试代码。

下面就这两个问题详细说明一下。

如何获取调用栈

我们系统是一个深度定制的android系统,使用android系统的好处是有很多现有的功能可以使用,其中就包括调用栈的获取。android提供了一个CallStack的类(见system/core/include/utils/CallStack.h)来收集和打印调用栈,使用方式如下:

#include <utils/CallStack.h>
...
void foo(void) {
...
    android::CallStack stack;
    stack.update();
    stack.log("XXX");
...
}

由于并不是每一次调用都需要打印出来,也可以将其保存在一个buffer中,使用如下方法:

String8 toString(const char* prefix = 0) const;

对于泄漏的pipe调用来说,我们期望做到的是只打印那些还没有释放的pipe调用,如何做到呢?可以将每次pipe调用的调用栈保存在一个hashmap中,以创建的fd为key保存,当fd关闭的时候再从hashmap中移除,这样内存中永远只保留有尚未关闭的pipe调用栈。hashmap可以使用android提供的system/core/include/cutils/hashmap.h来实现。

如何拦截系统调用

系统调用pipe()的地方很多很多,再没有地方添加代码显然是不现实的。

linux(android)的动态库loader有一种机制,可以通过指定环境变量LD_PRELOAD来预先加载一个动态库,这个动态库中导出的符号可以覆盖其它动态链接库的相同符号,也就是说我们可以写一个自己的动态链接库,用此动态链接库中的函数来覆盖其它动态链接库中的同名函数。

我们的pipe()函数可以这样来实现:

#define _GNU_SOURCE

#include <stdio.h>
#include <dlfcn.h>
#include <utils/CallStack.h>

extern "C"{

int pipe(int fds[2]) {
    // 打印调用栈
    android::CallStack stack;
    stack.update();
    stack.log("Pipe");

    // 最后调用原始的pipe(...)接口
    int (*original_pipe)(int*);
    original_pipe = dlsym(RTLD_NEXT, "pipe");
    return (*original_pipe)(fds);
}

} // extern "C"

注意_GNU_SOURCE必须定义,不然找不到RTLD_NEXT宏;RTLD_NEXT表示在后来加载的动态链接库中查找符号;链接时候需要加入-ldl参数,因为我们用到了dlsym。

编译链接成功后,得到一个动态库,假设保存在目标板的/system/lib/libdbg.so,使用时可以在init.rc中需要调试的进程前加上LD_PRELOAD=/system/lib/libdbg.so,如:

service zygote /system/bin/app_process
    setenv LD_PRELOAD /system/lib/libdbg.so
    ...

当然,由于zygote服务进程负责派生所有其他应用进程,因此所有这些进程对pipe的调用都会打印出来,会有很多,因此需要我们在pipe的跟踪函数中过滤那些我们不需要的进程,可以通过进程号或者进程名称等手段过滤。

]]>
bootloader实现规范 2014-08-07T00:00:00+00:00 Kyle http://kyleliu.github.io/bootloader-spec BCB信息格式
  • command[32] - 指令,可以为:
    • boot-recovery - 引导进入recovery系统
    • boot-fastboot - 引导进入fastboot模式
    • boot-main - 无条件引导进入主系统,oneshot
    • update-hboot - 更新bootloader firmware
  • status[32] - 状态
    • 如果为空(内容全部为0或者全部为0xFF)则表示状态正常
    • 如果不为空则设置为出错的信息
  • recovery[1024] - 传递给recovery系统的参数,每行一个参数,第一行必须是recovery,其它如下:
    • --send_intent=anystring 要求recovery将anystring写入/cache/recovery/intent文件
    • --update_package=path 指定升级包的路径
    • --wipe_data 重新格式化/data分区和/cache分区
    • --wipe_cache 格式化/cache分区
    • --set_encrypted_filesystem=on|off 是否支持加密文件系统
    • --just_exit 退出recovery而不做任何事情

bootloader引导过程

  1. 上电启动,进行必要的初始化
  2. 读取misc分区的信息,将头32个字节存入bcb.command
  3. 如果bcb.command内容为boot-recovery,则引导进入recovery
  4. 如果bcb.command内容为boot-main,则强制引导进入主系统,并将misc分区清空
  5. 如果bcb.command内容为boot-fastboot,则进入fastboot状态,等待客户端接入
  6. 如果bcb.command内容为update-hboot,则读取misc分区的数据,将其覆盖掉bootloader,成功或者失败后将command内容写入boot-recovery,并将错误信息写入bcb.status,引导进入recovery系统
  7. 如果command内容为空或者其它无效信息,则检查U盘,如存在recovery.command文件,并且其内容第一行为recovery,则将其内容写入bcb.recovery域,引导进入recovery系统,否则引导进入主系统

引导参数(bootargs)的处理

  1. androidboot.serialno保存于环境变量
  2. 在引导系统之前,必须将这个环境变量取出来附加在bootargs环境变量之后
  3. 这样在主系统或者recovery里面,就可以通过读取属性ro.serialno来获得本设备的设备号

从USB升级

  1. 从USB升级有两种方式,一是升级过程完全由bootloader实现,不同的bootloader可以有不同的实现方式,这里不做讨论,二是bootloader只是检测是否需要USB升级,然后引导进入recovery进行具体的升级动作,这个是我们要规范的方式
  2. U盘的根目录下必须存在recovery.command文本文件,并且其内容必须和bcb.recovery规定的内容一致
  3. 对于U盘的检查必须在BCB的检查之后,以确保BCB的高优先级
  4. 升级完成后,recovery系统会写入boot-main到bcb.command,表示强制bootloader引导至主系统,同时擦除bcb信息
  5. 这样USB升级完成后第一次重启系统不会再检查U盘中的文件,给用户足够的时间拔出U盘
]]>
以Fpanel为例说明如何为Webdroid系统扩充功能 2014-08-07T00:00:00+00:00 Kyle http://kyleliu.github.io/add-fpanel-function-for-webdroid 以Fpanel为例说明如何为Webdroid系统扩充功能
  • 写在前面的话

    最近项目中刚好有添加一个前面板功能的要求,就是要求Webapp应用能够控制前面板地显示,别看这是一个小功能需求,但麻雀虽小五脏俱全,这个功能地实现从最底层地Kernel驱动到最顶层地应用接口都有涉及,从这个实现中刚好可以了解一个系统级别的功能是如何添加进Webdroid的,借此机会跟大家分享一下。相信了解这个过程以后,不仅可以将这个功能完整地Merge到其它系统,而且写一个新地功能也再是一个困难的事情。

    为了继续后面的描述,我假定我们的代码根目录为$(WEBDROID)。

  • HAL层接口定义

    HAL是硬件抽象层(Hardware Abstract Layer)的简称,最先是Android系统为了规避GPL的版权问题,在应用层定义的一套硬件访问接口规范,使得很多硬件厂商可以在此基础上开发驱动而不用开放源代码。这一层实际上是一个接口定义规则,它并不包括具体有哪些接口,这就为扩展系统功能提供了无尽可能。

    基本上每一个设备对应一个头文件,同时一般来说都会提供一个默认的实现。以fpanel设备为例,其接口头文件存放在$(WEBDROID)/hardware/libhardware/include/hardware下,名称为fpanle.h,同时我们也为它准备了一个默认实现,代码见$(WEBDROID)/hardware/libhardware/modules/fpanel目录,这个默认实现只是加了一些打印,实际不做任何事情。

    涉及的文件:

    $(WEBDROID)/hardware/libhardware/include/hardware/fpanel.h $(WEBDROID)/hardware/libhardware/modules/fpanel/fpanel.c $(WEBDROID)/hardware/libhardware/modules/fpanel/Android.mk

  • HAL层接口的实现

    有了接口和默认实现还不够,每一个平台都要去实现这套接口,以海思系统为例,这个实现涉及到的文件有:

    $(WEBDROID)/hardware/hisilicon/godbox/fpanel/fpanel_godbox.c $(WEBDROID)/hardware/hisilicon/godbox/fpanel/fpanel_test.c $(WEBDROID)/hardware/hisilicon/godbox/fpanel/Android.mk $(WEBDROID)/hardware/hisilicon/godbox/Android.mk

    其中特别要注意最后一个Android.mk,因为fpanel为新添加的目录,它不是默认就包含在编译系统之内的,我们必须在这个.mk中添加fpanel目录才能正常编译。

  • 添加系统服务

    Webdroid是一个多进程系统,一般来说每个webapp就是一个进程,要访问像fpanel这样的稀有硬件资源,如果每个进程都直接调用硬件接口的话,可能会产生冲突,最好的办法是找一个中间代理人来管理,所有请求都经过代理人进行序列化操作,是众多的请求按照一定的顺序执行,这个代理人就是Service,就前面板来说就是FpanelService。

    在Framework框架中,我们一般会用到一种模式,就是Manager<->Service模式,XXXManager一般是由使用者(应用或者其它代码)调用的接口,每个进程都会产生一个Manager实例,而XXXService是跑在SystemServer进程中的,它只可能产生一个实例。因此就前面板来说,我们会有FpanelManager和FpanelService。

    服务定义好以后,应用如何去获得和调用这个服务呢?这就要带出Context这个东西了,Context顾名思义就是上下文的意思,对于程序来说就是一个运行环境,可以通过它的接口获取任何需要的功能,Context.getSystemService()接口是用来获取指定的服务的,因此只要有Context存在我们就能获得Service。同样也意味着,如果我们想提供Fpanel服务,我们就必须修改Context的实现。

    同时,由于我们的Service是采用java实现的,而我们的HAL层代码是C/C++的,因此我们必须使用JNI的方式使Java代码和C/C++代码能够互通。

    综上所述,这里我们涉及到的文件有:

    $(WEBDROID)/frameworks/base/Android.mk $(WEBDROID)/frameworks/base/core/java/android/app/ContextImpl.java $(WEBDROID)/frameworks/base/core/java/android/content/Context.java $(WEBDROID)/frameworks/base/core/java/android/dvb/FpanelManager.java $(WEBDROID)/frameworks/base/core/java/android/dvb/IFpanelService.aidl $(WEBDROID)/frameworks/base/services/java/com/android/server/SystemServer.java $(WEBDROID)/frameworks/base/services/java/com/android/server/dvb/FpanelService.java $(WEBDROID)/frameworks/base/services/jni/com_android_server_FpanelService.cpp $(WEBDROID)/frameworks/base/services/jni/Android.mk $(WEBDROID)/frameworks/base/services/jni/onload.cpp

  • 添加JS接口

    服务做好以后,Java应用就可以直接使用了,但我们是webapp,因此还有一步工作要做,就是为webapp提供javascript接口。由于我们的应用框架采用的是cordova,引入接口的方式已经大大简化,在此不做深入展开。列出此步骤涉及到的文件:

    $(WEBDROID)/frameworks/base/core/java/android/app/webapp/dvb/FpanelCore.java $(WEBDROID)/frameworks/base/core/res/assets/webapp/dvb/Fpanel.js $(WEBDROID)/frameworks/base/core/res/assets/webapp/webapp_plugins.json $(WEBDROID)/frameworks/base/core/res/res/xml/webapp_plugins.xml

  • 应用层面的调用方式

  • 其它方面的修改

    由于涉及到硬件操作,所以有些硬件配置需要做修改。这里以hisilicon为例,修改的文件如下:

    $(WEBDROID)/device/hisilicon/hisi3716m/BoardConfig.mk $(WEBDROID)/device/hisilicon/hisi3716m/configs/init.godbox.rc $(WEBDROID)/device/hisilicon/hisi3716m/hisi3716m.mk

    主要功能是在系统启动的时候自动加载hi_keyled.ko驱动,并将HAL实现模块加入编译系统的默认编译列表。

]]>
Recovery相关问题 2014-08-07T00:00:00+00:00 Kyle http://kyleliu.github.io/about-recovery 进入RECOVERY的方式

恢复出厂设置

  1. 主系统下用户选择“恢复出厂设置”
  2. 主系统写入命令“--wipe_data”到文件/cache/recovery/command
  3. 主系统重新启动,并设置bootloader control block (BCB)为启动到recovery系统,如何设置后面会说到
  4. bootloader检查BCB参数,如果是boot-recovery,则引导recovery系统
  5. recovery将检查/cache/recovery/command文件,并擦除userdata/cache分区数据
  6. 完成动作后擦除BCB信息
  7. 重启进入主系统

OTA升级

  1. 主系统接收升级服务器推送包,存入/cache/xxx.zip
  2. 主系统写入命令“--update_package=/cache/xxx.zip”到文件/cache/recovery/command
  3. 主系统重启,设置BCB参数为boot-recovery
  4. bootloader检查BCB,如果是boot-recovery则进入recovery
  5. 尝试安装升级包
  6. 升级成功后擦除BCB
  7. 如果升级失败,提示用户重启
  8. 检查是否更新固件(bootloader)
    1. recovery将boot-recovery和--wipe_cache写入BCB
    2. recovery将固件映像写裸入cache分区
    3. recovery将update-radio/hboot和--wipe_cache写入BCB
    4. 重启进入bootloader
    5. bootloader尝试将/cache的内容烧入到固件分区
    6. bootloader将boot-recovery和--wipe_cache写入BCB
    7. 进入recovery格式化/cache分区
    8. recovery擦除BCB
  9. 重启系统

USB升级

  1. 插入U盘,重启系统
  2. bootloader检查U盘根目录是否有文件recovery.command
  3. 有则读取文件内容,将内容写入BCB
  4. 引导进入recovery
  5. 其它同OTA升级

BCB(bootloader control block)格式说明

  1. 总共分为三个部分:command/status/recovery,其中command占用32字节,status占用32字节,recovery占用1024字节
  2. BCB保存在misc分区里,对于nand来它占用3个页面大小,但实际数据存在于第2个页面上,读取和写入时要特别注意保持一致,mmc则直接按顺序存储
  3. command支持的取值有boot-recovery(进入recovery)、boot-hboot(刷bootloader)、boot-radio(刷电话固件)
  4. status由bootloader填入,主要表示刷固件的结果
  5. recovery数据是传递给recovery程序的参数,每一行表示一个参数,第一行参数必须为recovery,其它参数有
    • --send_intent=anystring - 要求recovery将anystring存入/cache/recovery/intent文件
    • --update_package=path - 指定OTA包的位置
    • --wipe_data - 重新格式化/data和/cache分区
    • --wipe_cache - 只格式化/cache分区
    • --set_encrypted_filesystem=on|off - 文件系统是否加密
    • --just_exit - 不做任何其它事情

bootloader的实现

每个平台都有自己的bootloader,基本上都是基于u-boot的某个版本的深度定制版本,理论上我们可以使用u-boot来统一bootloader的实现,但考虑到有的平台私货太多,支持又差,统一起来有很大的压力,所以暂时还是采用各家的私有bootloader,但是为了达到平台的表现一致性,特提出bootloader的一般性功能规范:

  • 必须支持android boot image格式的镜像;
  • 必须读取BCB信息以确定引导哪个系统;
  • 必须支持stbid的保存并将其作为androidboot.serialno的值传递给kernel;
  • 支持fastboot协议(这个目前还都没有实现);
  • 支持U盘升级;
  • 支持我们的烧写工具;
]]>
从头再来 2014-08-06T00:00:00+00:00 Kyle http://kyleliu.github.io/start 一切从头来过

记录点点滴滴

]]>