iOS中线程Call Stack的捕获和解析(二)

上接iOS中线程Call Stack的捕获和解析(一)

1. 部分参考资料

做这一块时也是查阅了很多链接和书籍,包括但不限于:

以及很多Google Search。

2. 相关API和数据结构

由于我们在上面回溯线程调用栈拿到的是一组地址,所以这里进行符号化的输入输出应该分别是地址和符号,接口设计类似如下:

- (NSString *)symbolicateAddress:(uintptr_t)addr;

不过在实际操作中,我们需要依赖于dyld相关方法和数据结构:

/*
 * Structure filled in by dladdr().
 */
typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

extern int dladdr(const void *, Dl_info *);
DESCRIPTION
     These routines provide additional introspection of dyld beyond that provided by dlopen() and dladdr()

     _dyld_image_count() returns the current number of images mapped in by dyld. Note that using this count
     to iterate all images is not thread safe, because another thread may be adding or removing images dur-ing during
     ing the iteration.

     _dyld_get_image_header() returns a pointer to the mach header of the image indexed by image_index.  If
     image_index is out of range, NULL is returned.

     _dyld_get_image_vmaddr_slide() returns the virtural memory address slide amount of the image indexed by
     image_index. If image_index is out of range zero is returned.

     _dyld_get_image_name() returns the name of the image indexed by image_index. The C-string continues to
     be owned by dyld and should not deleted.  If image_index is out of range NULL is returned.

又为了要判断此次解析是否成功,所以接口设计演变为:

bool jdy_symbolicateAddress(const uintptr_t addr, Dl_info *info)

Dl_info用来填充解析的结果。

3. 算法思路

对一个地址进行符号化解析说起来也是比较直接的,就是找到地址所属的内存镜像,然后定位该镜像中的符号表,最后从符号表中匹配目标地址的符号。

(图片来源于苹果官方文档)

以下思路是描述一个大致的方向,并没有涵盖具体的细节,比如基于ASLR的偏移量:

        // 基于ASLR的偏移量https://en.wikipedia.org/wiki/Address_space_layout_randomization

        /**

         * When the dynamic linker loads an image, 

         * the image must be mapped into the virtual address space of the process at an unoccupied address.

         * The dynamic linker accomplishes this by adding a value "the virtual memory slide amount" to the base address of the image.

         */

3.1 寻找包含地址的目标镜像

起初看到一个API还有点小惊喜,可惜iPhone上用不了:

extern bool _dyld_image_containing_address(const void* address)
__OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_3,__MAC_10_5,__IPHONE_NA,__IPHONE_NA);

所以得自己来判断。

怎么判断呢?

A segment defines a range of bytes in a Mach-O file and the addresses and memory protection attributes at which those bytes are mapped into virtual memory when the dynamic linker loads the application. As such, segments are always virtual memory page aligned. A segment contains zero or more sections.

通过遍历每个段,判断目标地址是否落在该段包含的范围内:

/*
 * The segment load command indicates that a part of this file is to be
 * mapped into the task's address space.  The size of this segment in memory,
 * vmsize, maybe equal to or larger than the amount to map from this file,
 * filesize.  The file is mapped starting at fileoff to the beginning of
 * the segment in memory, vmaddr.  The rest of the memory of the segment,
 * if any, is allocated zero fill on demand.  The segment's maximum virtual
 * memory protection and initial virtual memory protection are specified
 * by the maxprot and initprot fields.  If the segment has sections then the
 * section structures directly follow the segment command and their size is
 * reflected in cmdsize.
 */
struct segment_command { /* for 32-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT */
    uint32_t    cmdsize;    /* includes sizeof section structs */
    char        segname[16];    /* segment name */
    uint32_t    vmaddr;     /* memory address of this segment */
    uint32_t    vmsize;     /* memory size of this segment */
    uint32_t    fileoff;    /* file offset of this segment */
    uint32_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};

/**
 * @brief 判断某个segment_command是否包含addr这个地址,基于segment的虚拟地址和段大小来判断
 */
bool jdy_segmentContainsAddress(const struct load_command *cmdPtr, const uintptr_t addr) {
    if (cmdPtr->cmd == LC_SEGMENT) {
        struct segment_command *segPtr = (struct segment_command *)cmdPtr;
        if (addr >= segPtr->vmaddr && addr < (segPtr->vmaddr + segPtr->vmsize)) {
            return true;
        }

这样一来,我们就可以找到包含目标地址的镜像文件了。

3.2 定位目标镜像的符号表

由于符号的收集和符号表的创建贯穿着编译和链接阶段,这里就不展开了,而是只要确定除了代码段_TEXT和数据段DATA外,还有个_LINKEDIT段包含符号表:

The __LINKEDIT segment contains raw data used by the dynamic linker, such as symbol, string, and relocation table entries.

所以现在我们需要先定位到__LINKEDIT段,同样摘自苹果官方文档:

Segments and sections are normally accessed by name. Segments, by convention, are named using all uppercase letters preceded by two underscores (for example, _TEXT); sections should be named using all lowercase letters preceded by two underscores (for example, _text). This naming convention is standard, although not required for the tools to operate correctly.

我们通过遍历每个段,比较段名称是否和__LINKEDIT相同:

usr/include/mach-o/loader.h

#define SEG_LINKEDIT    "__LINKEDIT"

接着来找符号表:

/**

 * 摘自《The Mac Hacker's Handbook》:

 * The LC_SYMTAB load command describes where to find the string and symbol tables within the __LINKEDIT segment. The offsets given are file offsets, so you subtract the file offset of the __LINKEDIT segment to obtain the virtual memory offset of the string and symbol tables. Adding the virtual memory offset to the virtual-memory address where the __LINKEDIT segment is loaded will give you the in-memory location of the string and sym- bol tables.

 */

也就是说,我们需要结合__LINKEDIT segment_command(见上面结构描述)和LC_SYMTAB load_command(见下面结构描述)来定位符号表:

/*
 * The symtab_command contains the offsets and sizes of the link-edit 4.3BSD
 * "stab" style symbol table information as described in the header files
 * <nlist.h> and <stab.h>.
 */
struct symtab_command {
    uint32_t    cmd;        /* LC_SYMTAB */
    uint32_t    cmdsize;    /* sizeof(struct symtab_command) */
    uint32_t    symoff;     /* symbol table offset */
    uint32_t    nsyms;      /* number of symbol table entries */
    uint32_t    stroff;     /* string table offset */
    uint32_t    strsize;    /* string table size in bytes */
};

如上述引用描述,LC_SYMTAB和_LINKEDIT中的偏移量都是文件偏移量,所以要获得内存中符号表和字符串表的地址,我们先将LC_SYMTAB的symoff和stroff分别减去LINKEDIT的fileoff得到虚拟地址偏移量,然后再加上_LINKEDIT的vmoffset得到虚拟地址。当然,要得到最终的实际内存地址,还需要加上基于ASLR的偏移量。

3.3 在符号表中寻找和目标地址最匹配的符号

终于找到符号表了,写到这里有点小累,直接贴下代码:

/**
 * @brief 在指定的符号表中为地址匹配最合适的符号,这里的地址需要减去vmaddr_slide
 */
const JDY_SymbolTableEntry *jdy_findBestMatchSymbolForAddress(uintptr_t addr,
                                                              JDY_SymbolTableEntry *symbolTable,
                                                              uint32_t nsyms) {

    // 1. addr >= symbol.value; 因为addr是某个函数中的一条指令地址,它应该大于等于这个函数的入口地址,也就是对应符号的值;
    // 2. symbol.value is nearest to addr; 离指令地址addr更近的函数入口地址,才是更准确的匹配项;

    const JDY_SymbolTableEntry *nearestSymbol = NULL;
    uintptr_t currentDistance = UINT32_MAX;

    for (uint32_t symIndex = 0; symIndex < nsyms; symIndex++) {
        uintptr_t symbolValue = symbolTable[symIndex].n_value;
        if (symbolValue > 0) {
            uintptr_t symbolDistance = addr - symbolValue;
            if (symbolValue <= addr && symbolDistance <= currentDistance) {
                currentDistance = symbolDistance;
                nearestSymbol = symbolTable + symIndex;
            }
        }
    }

    return nearestSymbol;
}

/*
 * This is the symbol table entry structure for 64-bit architectures.
 */
struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

找到匹配的nlist结构后,我们可以通过.n_un.n_strx来定位字符串表中相应的符号名。

时间: 2024-09-19 09:28:21

iOS中线程Call Stack的捕获和解析(二)的相关文章

iOS中线程Call Stack的捕获和解析(一)

http://blog.csdn.net/jasonblog/article/details/49909209这里对上个月做的一个技术项目做部分技术小结,这篇文章描述的功能和我们在使用Xcode进行调试时点击暂停的效果类似. 一.获取任意一个线程的Call Stack 如果要获取当前线程的调用栈,可以直接使用现有API:[NSThread callStackSymbols]. 但是并没有相关API支持获取任意线程的调用栈,所以只能自己编码实现. 1. 基础结构 一个线程的调用栈是什么样的呢? 我

iOS中动态更新补丁策略JSPatch运用基础二

iOS中动态更新补丁策略JSPatch运用基础二 一.引言     上篇博客中介绍了iOS开发中JSPatch引擎进行动态热修复的一些基础功能,其中包括向Objective-C类中添加类方法与成员方法.添加临时成员变量,使用JavaScript调用原生的Objective-C属性和方法等.本篇博客将基于上一篇继续介绍Objective-C中的一些特殊数据类型在JavaScript文件中的使用方法,博客中大部分内容扩展自JSPatch开源git的wiki:https://github.com/ba

iOS中的NSTimer定时器的初步使用解析_IOS

创建一个定时器(NSTimer) - (void)viewDidLoad { [super viewDidLoad]; [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(actionTimer:) userInfo:nil repeats:YES]; } - (void)actionTimer:(NSTimer *)timer { } NSTimer默认运行在default mode下,default

iOS中的NSURLCache数据缓存类用法解析_IOS

 在IOS应用程序开发中,为了减少与服务端的交互次数,加快用户的响应速度,一般都会在IOS设备中加一个缓存的机制.使用缓存的目的是为了使用的应用程序能更快速的响应用户输入,是程序高效的运行.有时候我们需要将远程web服务器获取的数据缓存起来,减少对同一个url多次请求.下面将介绍如何在IOS设备中进行缓存.  内存缓存我们可以使用sdk中的NSURLCache类.NSURLRequest需要一个缓存参数来说明它请求的url何如缓存数据的,我们先看下它的CachePolicy类型.    1.NS

iOS网络编程入门:iOS中的Socket编程

使用Socket进行C/S结构编程,连接过程 服 务器端监听某个端口是否有连接请求.服务器端程序处于堵塞状态,直到客户端向服务器端发出连接请求,服务器端接受请求程序才能向下运行.一旦连接建立起 来,通过Socket可以获得输入输出流对象.借助于输入输出流对象就可以实现与客户端的通讯,最后不要忘记关闭Socket和释放一些资源(包括:关闭 输入输出流). 客户端流程是先指定要通讯的服务器IP地址.端口和采用的传输协议(TCP或UDP),向服务器发出连接请求,服务器有应答请求之后,就会建立连接.之后

iOS网络编程-iOS中Socket编程介绍

使用Socket进行C/S结构编程,连接过程   服务器端监听某个端口是否有连接请求.服务器端程序处于堵塞状态,直到客户端向服务器端发出连接请求,服务器端接受请求程序才能向下运行.一旦连接建立起来,通过Socket可以获得输入输出流对象.借助于输入输出流对象就可以实现与客户端的通讯,最后不要忘记关闭Socket和释放一些资源(包括:关闭输入输出流). 客户端流程是先指定要通讯的服务器IP地址.端口和采用的传输协议(TCP或UDP),向服务器发出连接请求,服务器有应答请求之后,就会建立连接.之后与

iOS 中的 21 种设计模式

iOS 中的 21 种设计模式 对象创建原型(Prototype) 使用原型实例指定创建对象的种类,并通过复制这个原型创建新的对象. 1 2 NSArray *array = [[NSArray alloc] initWithObjects:@1, nil]; NSArray *array2 = array.copy; array 就是原型了,array2 以 array 为原型,通过 copy 操作创建了 array2. 当创建的实例非常复杂且耗时,或者新实例和已存在的实例值相同,使用原型模式

在IOS中为什么使用多线程及多线程实现的三种方法_IOS

多线程是一个比较轻量级的方法来实现单个应用程序内多个代码执行路径. 在系统级别内,程序并排执行,程序分配到每个程序的执行时间是基于该程序的所需时间和其他程序的所需时间来决定的. 然而,在每个程序内部,存在一个或者多个执行线程,它同时或在一个几乎同时发生的方式里执行不同的任务. 概要提示: iPhone中的线程应用并不是无节制的,官方给出的资料显示,iPhone OS下的主线程的堆栈大小是1M,第二个线程开始就是512KB,并且该值不能通过编译器开关或线程API函数来更改,只有主线程有直接修改UI

全面解析iOS中同步请求、异步请求、GET请求、POST请求_IOS

先给大家分别介绍下iOS中同步请求.异步请求.GET请求.POST所代表的意思,然后在逐一通过实例给大家介绍. 1.同步请求可以从因特网请求数据,一旦发送同步请求,程序将停止用户交互,直至服务器返回数据完成,才可以进行下一步操作, 2.异步请求不会阻塞主线程,而会建立一个新的线程来操作,用户发出异步请求后,依然可以对UI进行操作,程序可以继续运行 3.GET请求,将参数直接写在访问路径上.操作简单,不过容易被外界看到,安全性不高,地址最多255字节: 4.POST请求,将参数放到body里面.P