The original link

Before reading this article, you need to read the articles Mach-O File Format and Function Call Stack.

In fact, there are many opportunities to deal with the thread call stack. When debugging with Xcode, you can see all the thread call stacks of the current program. Occasionally, when an application is published, we want to know what the user’s thread call stack is at that point in time, so that we can solve problems. For example, when a crash occurs or freezes, it is much easier for the developer to resolve the problem if the user’s thread calls the stack.

IOS gets the thread call stack

How does iOS get the thread call stack? The first thing that comes to mind is the system API:

Thread.callstackSymbols
Copy the code

Unfortunately, the API only gets the call stack of the current thread. As experienced developers know, a crash or stalling may not be caused by the current thread, but rather by an operation of another child thread. Therefore, simply fetching the call stack of the current thread is not sufficient.

Since the system API does not work, it is necessary to think of his method. Consider, what steps are required to get the thread call stack?

Get thread call stack step split

As mentioned above, the requirement is to get the call stack for all threads. So the first step, we need to get all the threads in the current program, there is no question about this step.

Once you get a thread, you need to get the call stack for that thread. According to the article “Function Call Stack”, as long as we get the function at the top of the thread, esp and EBP Pointers, we can get the call relation of all functions recursively, and then get the call stack of the thread.

Note that the start of the previous step is an address, the developer will not be able to solve the problem by just relying on an address. Instead, you should parse the address into a corresponding symbol, a corresponding string. In fact, the operation of an App depends on multiple image files, including the application executable, system dynamic libraries, binary files in the framework, and so on. For example, Foundation, ImageIOKit, libdispatch. Dylibv are all separate images. Therefore, you need to determine which mirror corresponds to the address obtained in the previous step.

After determining the image, read the image file (file format is Mach-O), or according to the address, and symbol table, string table match, determine the symbol name.

Ok, to summarize the steps above:

  1. Get all threads
  2. Get the top-level function of the corresponding thread, esp and EBP Pointers
  3. Locate the mirror based on the address
  4. Locate symbols by address, image file

Isn’t that easy? Let’s take a look at how each step actually works.

Get all threads

The Mach kernel provides an interface to get all threads as follows:

kern_return_t task_threads
(
	task_inspect_t target_task,
	thread_act_array_t *act_list,
	mach_msg_type_number_t *act_listCnt
);
Copy the code

The function does this:

All threads in the target_task task are stored in the act_list array, which contains act_listCnt threadsCopy the code

One parameter in the task_threads function is target_task, which represents the current task. The Mach kernel also provides an interface to get the current task, as follows:

Use mach_task_self() to get the current process tag target_taskCopy the code

At this point, all threads are available.

The complete code is as follows:

thread_act_array_t threads; mach_msg_type_number_t thread_count = 0; const task_t this_task = mach_task_self(); kern_return_t kr = task_threads(this_task, &threads, &thread_count); if(kr ! = KERN_SUCCESS) { return @"Fail to get information of all threads"; }Copy the code

All threads are stored in the Threads array.

Obtain esp and EBP Pointers

Fetching the top-level functions of a thread, as well as esp and EBP Pointers, still depended on the interfaces exposed by the Mach kernel. The function is:

Kern_return_t thread_get_state (thread_act_t target_act, Thread_state_flavor_t flavor, [ARM/x86]_THREAD_STATE64 thread_state_t old_state, Mach_msg_type_number_t *old_stateCnt;Copy the code

The target_act parameter is the single thread retrieved in the previous step.

Thread_state_t contains esp and EBP Pointers. Thread_state_t is defined as follows:

_STRUCT_X86_THREAD_STATE64 { __uint64_t __rax; __uint64_t __rbx; __uint64_t __rcx; __uint64_t __rdx; __uint64_t __rdi; __uint64_t __rsi; __uint64_t __rbp; // Frame pointer __uint64_t __rsp; // stack pointer __uint64_t __r8; __uint64_t __r9; __uint64_t __r10; __uint64_t __r11; __uint64_t __r12; __uint64_t __r13; __uint64_t __r14; __uint64_t __r15; __uint64_t __rip; // The current thread directive address __uint64_t __rflags; __uint64_t __cs; __uint64_t __fs; __uint64_t __gs; };Copy the code

In fact, the structure used to get esp and EBP Pointers is _STRUCT_MCONTEXT. _STRUCT_MCONTEXT has an attribute __ss that is of type thread_state_t.

Look at the definition of _STRUCT_MCONTEXT:

#if defined(__x86_64__)
    _STRUCT_MCONTEXT ctx;
    mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT;
    thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&ctx.__ss, &count);

    uint64_t pc = ctx.__ss.__rip;
    uint64_t sp = ctx.__ss.__rsp;
    uint64_t fp = ctx.__ss.__rbp;
#elif defined(__arm64__)
    _STRUCT_MCONTEXT ctx;
    mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT;
    thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&ctx.__ss, &count);

    uint64_t pc = ctx.__ss.__pc;
    uint64_t sp = ctx.__ss.__sp;
    uint64_t fp = ctx.__ss.__fp;
#endif
Copy the code

Once you have all the threads, for each thread, you can use the thread_get_state method to get all the information about the thread, which is populated in a structure of type _STRUCT_MCONTEXT. The _STRUCT_MCONTEXT structure stores the top stack pointer and frame pointer of the current thread. Using this information, you can obtain the call stack of all threads.

The complete code is as follows:

bool fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    return (kr == KERN_SUCCESS);
}
Copy the code

The stack frame information is saved to machineContext.

Corresponds to the image file

Image files have been mentioned above, so let’s go over them again.

An App relies on many system libraries to run smoothly, and these libraries, including the executable itself, are considered mirror files by the system. For example, Foundation, ImageIOKit, libdispatch. Dylibv are all separate images.

In fact, there are many mirrors in the crash logs generated by the system.

As shown in the above log, the images are Foundation, UIKitCore, IOKit, etc.

Each image file corresponds to an address range, and each image file is in Mach-O format.

The previous step is to get the address of the method according to the stack frame pointer. First, we need to determine which mirror file the method belongs to.

The logic is simple. The method address is in the address range of an image file. The question is, how many image files do I get, and what is the address range of each image file?

Dyld provides interfaces for obtaining the number, name, and address of mirrors. The interfaces are as follows:

uint64_t count = _dyld_image_count(); Const struct mach_header *header = _dyLD_GEt_image_header (index); //image mach-o header const char *name = _dyld_get_image_name(index); //image name uint64_t slide = _dyld_get_image_vmaddr_slide(index); //ALSR offset addressCopy the code

After obtaining the image, you can cycle through the address comparison. The complete code is as follows:

static uint32_t imageIndexContainingAddress(const uintptr_t address) { const uint32_t imageCount = _dyld_image_count(); const struct mach_header* header = 0; for(uint32_t iImg = 0; iImg < imageCount; iImg++) { header = _dyld_get_image_header(iImg); if(header ! = NULL) { // Look for a segment command with this address within its range. uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg); uintptr_t cmdPtr = firstCmdAfterHeader(header); if(cmdPtr == 0) { continue; } for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) { const struct load_command* loadCmd = (struct load_command*)cmdPtr; if(loadCmd->cmd == LC_SEGMENT) { const struct segment_command* segCmd = (struct segment_command*)cmdPtr; if(addressWSlide >= segCmd->vmaddr && addressWSlide < segCmd->vmaddr + segCmd->vmsize) { return iImg; } } else if(loadCmd->cmd == LC_SEGMENT_64) { const struct segment_command_64* segCmd = (struct segment_command_64*)cmdPtr; if(addressWSlide >= segCmd->vmaddr && addressWSlide < segCmd->vmaddr + segCmd->vmsize) { return iImg; } } cmdPtr += loadCmd->cmdsize; } } } return UINT_MAX; }Copy the code

The argument is the address of the function, and the return address is the index of the corresponding mirror.

Position mark

The image itself is also a mach-O file. After obtaining the image file, you can read the image file and obtain its symbol table information and string table information (including the number of symbols, symbol size, number of strings, and string size).

First, get symbol table address, string table address, code as follows:

Mach-o Header const struct mach_header* Header = _dyLD_GEt_image_header (index); Uint32_t iCmd = 0; // Uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) { const struct load_command* loadCmd = (struct load_command*)cmdPtr; if(loadCmd->cmd == LC_SYMTAB){ symtabCmd = loadCmd; } else if(loadCmd->cmd == LC_SEGMENT_64) { const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr; if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) { linkeditSegment = segmentCmd; // uintptr_t linkeditBase = (uintptr_t)slide + linkeditSegment->vmaddr  - linkeditSegment->fileoff; Const nlist_t *symbolTable = (nlist_t *)(linkeditBase + symtabCmd->symoff); Char *stringTab = (char *)(linkeditBase + symtabCmd->stroff); Uint32_t symNum = symtabCmd->nsyms;Copy the code

To traverse the symbol table, you need to first locate the symbol table position from load_commands. Symtab_command does not give us absolute location information, only a stroff and symoff, the string table offset and the symbol table offset, so we need to find out the actual memory address. We can get the absolute vmADDR and the offset fileoff from LC_SEGMENT(__LINKEDIT).

LC_SEGMENT(__LINKEDIT) and LC_SYMTAB can be combined to obtain the symbol table, string table position.

The data structures for symbol tables and string tables can be found in the article Mach-O File Formats.

Once you have the symbol table, you also need to locate the symbols. The search is to get the real memory addresses of signs and the function name, and through the function call stack access is inside the function executes instructions address, but the address and the real function deviation is not big, so can traverse the symbols of memory address with the function of the call stack symbols memory address closest compare with the best match of symbols, Is the symbol of the current call stack. The complete code is as follows:

const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx); const uintptr_t addressWithSlide = address - imageVMAddrSlide; // uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) { // If n_value is 0, the symbol refers to an external object. if(symbolTable[iSym].n_value ! = 0) { uintptr_t symbolBase = symbolTable[iSym].n_value; Uintptr_t currentDistance = addressWithslide-symbolbase; if((addressWithSlide >= symbolBase) && (currentDistance <= bestDistance)) { bestMatch = symbolTable + iSym; // Best matching symbol address bestDistance = currentDistance; }} if(bestMatch! = NULL) { info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide); if(bestMatch->n_desc == 16) { // This image has been stripped. The name is meaningless, and // almost certainly resolves to "_mh_execute_header" info->dli_sname = NULL; } else {info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx); if(*info->dli_sname == '_') { info->dli_sname++; }}}Copy the code

conclusion

That’s how you get the iOS thread call stack. In fact, the real problem is broken down, it is not difficult to obtain the call stack, most of the use is the system API, just need to follow the steps step by step.

The difficulty is how to get these steps. First, you need to understand the format of the Mach-O file and know that you can obtain information from the file, such as load commands, symbol tables, etc. The second is to understand the function call stack, function call stack is the basis of all thread call stacks, only understand the principle of the function call stack, to understand why to do this; Finally, it is important to know that there are relevant apis, whether Mach kernel or DYLD, these functions are rarely used in daily development, most exist in the system source code, which requires a certain understanding of the source code.