iOS獲取任意線程調(diào)用堆棧信息

場(chǎng)景需求

線上app運(yùn)行過(guò)程中有內(nèi)存突變、卡頓效五、cpu飆升地消、crash等情況,需要獲取發(fā)生這些情況時(shí)的所有堆棧信息畏妖,以此來(lái)輔助定位問(wèn)題

1. callStackSymbols

只能獲取當(dāng)前堆棧信息脉执,不能獲取指定其他線程的信息,所以不滿足要求

 [NSThread callStackSymbols];
 
 0   LXDAppFluecyMonitor                 0x0000000102a30699 -[ViewController tableView:didSelectRowAtIndexPath:] + 89,
1   UIKitCore                           0x0000000116721902 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:deselectPrevious:] + 1962,
2   UIKitCore                           0x000000011672113d -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 94,
3   UIKitCore                           0x0000000116721bcb -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 341,
4   UIKitCore                           0x0000000116a322d5 -[_UIAfterCACommitBlock run] + 54,
5   UIKitCore                           0x0000000116a327cd -[_UIAfterCACommitQueue flush] + 190,
6   libdispatch.dylib                   0x000000010c7d6816 _dispatch_call_block_and_release + 12,
7   libdispatch.dylib                   0x000000010c7d7a5b _dispatch_client_callout + 8,
8   libdispatch.dylib                   0x000000010c7e6325 _dispatch_main_queue_drain + 1169,
9   libdispatch.dylib                   0x000000010c7e5e86 _dispatch_main_queue_callback_4CF + 31,
10  CoreFoundation                      0x000000010b5d6261 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9,

2. Mach Thread

思路

  1. 通過(guò)內(nèi)核API獲取所有線程列表
  2. 遍歷每個(gè)pthread_t戒劫,獲取線程上下文信息_STRUCT_MCONTEXT
  3. 通過(guò)context獲得棧幀指針半夷,然后不斷調(diào)用previous獲得當(dāng)前線程的所有調(diào)用堆棧
  4. 通過(guò)棧幀指針獲得函數(shù)調(diào)用地址
  5. 通過(guò)_dyld_image相關(guān)API遍歷所有image鏡像
  6. 找到load commands的LC_SEGMENT(__TEXT)中包含函數(shù)地址的鏡像
  7. 獲取ASLR,然后找到函數(shù)地址在符號(hào)表中對(duì)應(yīng)的位置
  8. 然后去字符表中查找函數(shù)名字

獲取堆棧函數(shù)調(diào)用地址

  1. 所有線程:調(diào)用內(nèi)核API函數(shù)task_threads獲取指定task線程列表迅细,即list

    thread_act_array_t list;
    mach_msg_type_number_t count;
    task_threads(mach_task_self(), &list, &count);
    
  2. 指定線程:調(diào)用API函數(shù)pthread_from_mach_thread_np獲得對(duì)應(yīng)線程pthread_t巫橄,非UI線程比較name

    for (int idx = 0; idx < count; idx++) {
        pthread_t pt = pthread_from_mach_thread_np(list[idx]);
        if ([nsthread isMainThread] && list[idx] == main_thread_id) { return list[idx]; }
        if (pt) {
            name[0] = '\0';
            pthread_getname_np(pt, name, sizeof(name));
            if (!strcmp(name, [nsthread name].UTF8String)) {
                [nsthread setName: originName];
                return list[idx];
            }
        }
    }
    
  3. 線程信息:調(diào)用thread_get_state獲得指定線程上下問(wèn)信息_STRUCT_MCONTEXT。thread_get_stateAPI兩個(gè)參數(shù)隨著cpu架構(gòu)不同而改變茵典。_STRUCT_MCONTEXT結(jié)構(gòu)存儲(chǔ)當(dāng)前線程棧頂指針(stack pointer)和最頂部的棧幀指針(frame pointer)湘换,從而獲得整個(gè)線程的調(diào)用棧。
    thread_get_state傳入thread,_STRUCT_MCONTEXT->__ss(寄存器指針結(jié)構(gòu)體)彩倚,以及cpu相關(guān)常量(target_act筹我,old_stateCnt),來(lái)實(shí)現(xiàn)_STRUCT_MCONTEXT賦值

    bool lxd_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT * machineContext) {
        mach_msg_type_number_t state_count = LXD_THREAD_STATE_COUNT;
        kern_return_t kr = thread_get_state(thread, LXD_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
        return (kr == KERN_SUCCESS);
    }
    
  4. 棧幀結(jié)構(gòu)體賦值vm_read_overwrite

    1. 棧幀結(jié)構(gòu)體
      typedef struct StackFrameEntry{
          const struct StackFrameEntry *const previous;  //前一個(gè)棧幀地址
          const uintptr_t return_address;  //棧幀的函數(shù)返回地址
      } StackFrameEntry;
      
    2. 通過(guò)上一步獲取的machineContext獲取第一個(gè)棧幀指針
       lxd_mach_copyMem((void *)machineContext->__ss.LXD_FRAME_POINTER, &frame, sizeof(frame))
      
      //參數(shù)src:棧幀指針
      //參數(shù)dst:StackFrameEntry實(shí)例指針
      //參數(shù)numBytes:StackFrameEntry結(jié)構(gòu)體大小
      kern_return_t lxd_mach_copyMem(const void * src, const void * dst, const size_t numBytes) {
          vm_size_t bytesCopied = 0;
          //   調(diào)用api函數(shù)帆离,根據(jù)棧幀指針獲取該棧幀對(duì)應(yīng)的函數(shù)地址
          return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
      }
      
      打印frame
      Printing description of frame:
      (LXDStackFrameEntry) frame = {
        previous = 0x0000000109f6cb68
        return_address = 11598032417672659023
      }
      
    3. 通過(guò)frame.previous獲取前一個(gè)棧幀地址蔬蕊,不斷遍歷,獲得當(dāng)前線程所有函數(shù)調(diào)用的地址
      //循環(huán)遍歷,停止條件MAX_FRAME_NUMBER棧幀個(gè)數(shù)
      for (; idx < MAX_FRAME_NUMBER; idx++) {
          //棧幀函數(shù)賦值
          backtraceBuffer[idx] = frame.return_address;
          if (backtraceBuffer[idx] == FAILED_UINT_PTR_ADDRESS ||
              frame.previous == NULL ||
              //根據(jù)當(dāng)前的棧幀的previous哥谷,獲取前一個(gè)棧幀地址
              lxd_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
              break;
          }
      }
      

獲得堆棧調(diào)用函數(shù)名

關(guān)于Mach-O的相關(guān)知識(shí)可以看這篇文章:https://www.coderzhou.com/2019/06/05/fishhook/#Mach-O
源碼參考:https://github.com/bestswifter/BSBacktraceLogger

  1. 創(chuàng)建一個(gè)和上面backtraceBuffer長(zhǎng)度一樣的Dl_info數(shù)組
    Dl_info symbolicated[backtraceLength];
    
  2. 逐個(gè)遍歷backtraceBuffer岸夯,獲取對(duì)應(yīng)的符號(hào)信息添加到symbolicated中
  3. 找到棧幀地址對(duì)應(yīng)的image鏡像
    • 遍歷鏡像,通過(guò)_dyld_get_image_vmaddr_slide獲取ASLR偏移地址呼巷,計(jì)算出調(diào)用函數(shù)棧幀地址在mach-O文件中的地址
    • 遍歷mach-o的load commands找到LC_SEGMENT
    • 計(jì)算調(diào)用函數(shù)在mach-o中的地址是否包含在LC_SEGMENT段中
    • 返回鏡像idx
    uint32_t lxd_imageIndexContainingAddress(const uintptr_t address) {
        const uint32_t imageCount = _dyld_image_count();
        const struct mach_header * header = FAILED_UINT_PTR_ADDRESS;
        
        for (uint32_t iImg = 0; iImg < imageCount; iImg++) {
            header = _dyld_get_image_header(iImg);
            if (header != NULL) {
    //           ASLR: _dyld_get_image_vmaddr_slide獲取偏移slide
                uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
                uintptr_t cmdPtr = lxd_firstCmdAfterHeader(header);
                if (cmdPtr == FAILED_UINT_PTR_ADDRESS) { 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) {
                            
                            char *image_name = (char *)_dyld_get_image_name(iImg);
                            const struct mach_header *mh = _dyld_get_image_header(iImg);
                            intptr_t vmaddr_slide = _dyld_get_image_vmaddr_slide(iImg);
                         
                            printf("Image name %s at address 0x%llx and ASLR slide 0x%lx.\n",
                                   image_name, (mach_vm_address_t)mh, vmaddr_slide);
                            return iImg;
                        }
                    }
                    cmdPtr += loadCmd->cmdsize;
                }
            }
        }
        return UINT_MAX;
    }
    
    image.png

    用MachOView查看囱修,和上面獲取的數(shù)據(jù)是一致的


    image.png

    打印出segCmd的虛擬內(nèi)存結(jié)束的地址,判斷函數(shù)虛擬內(nèi)存地址是否在當(dāng)前段中


    image.png
  4. 找到對(duì)應(yīng)鏡像中l(wèi)oad commands的起始段地址王悍,這里正好是代碼段__TEXT
    uintptr_t lxd_segmentBaseOfImageIndex(const uint32_t idx) {
        const struct mach_header * header = _dyld_get_image_header(idx);
        
        uintptr_t cmdPtr = lxd_firstCmdAfterHeader(header);
        if (cmdPtr == FAILED_UINT_PTR_ADDRESS) { return FAILED_UINT_PTR_ADDRESS; }
        for (uint32_t idx = 0; idx < header->ncmds; idx++) {
            const struct load_command * loadCmd = (struct load_command *)cmdPtr;
            if (loadCmd->cmd == LC_SEGMENT) {
                const struct segment_command * segCmd = (struct segment_command *)cmdPtr;
                if (strcmp(segCmd->segname, SEG_LINKEDIT) == 0) {
                    return segCmd->vmaddr - segCmd->fileoff;
                }
            } else if (loadCmd->cmd == LC_SEGMENT_64) {
                const struct segment_command_64 * segCmd = (struct segment_command_64 *)cmdPtr;
                if (strcmp(segCmd->segname, SEG_LINKEDIT) == 0) {
                    return segCmd->vmaddr - segCmd->fileoff;
                }
            }
            cmdPtr += loadCmd->cmdsize;
        }
        return FAILED_UINT_PTR_ADDRESS;
    }
    
  5. 遍歷load commands破镰,找到LC_SYMTAB,里面包含了符號(hào)表和字符串表的偏移信息
    struct symtab_command {
        uint32_t    cmd;        /* LC_SYMTAB */
        uint32_t    cmdsize;    /* sizeof(struct symtab_command) */
        uint32_t    symoff;     /* 表示符號(hào)表的偏移 */
        uint32_t    nsyms;      /* 符號(hào)表?xiàng)l目的個(gè)數(shù) */
        uint32_t    stroff;     /* 字符串表在文件中的偏移 */
        uint32_t    strsize;    /* 字符串表的大小 */
    };
    
    image.png
  6. 遍歷符號(hào)表压储,找到函數(shù)地址對(duì)應(yīng)的符號(hào)表?xiàng)l目所在的地址
    符號(hào)表單條目結(jié)構(gòu)體
    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) */
    };
    
  7. 通過(guò)上一步獲取的符號(hào)表數(shù)據(jù)鲜漩,獲得函數(shù)符號(hào)在字符串表中的偏移量,然后獲得對(duì)應(yīng)的字符串
    if (loadCmd->cmd == LC_SYMTAB) {
        //LC_SYMTAB 是符號(hào)表和字符串表的偏移信息
        const struct symtab_command * symtabCmd = (struct symtab_command *)cmdPtr;
        //符號(hào)表在內(nèi)存中的地址(包含偏移) symoff符號(hào)表的偏移
        const LXD_NLIST * symbolTable = (LXD_NLIST *)(segmentBase + symtabCmd->symoff);
        //字符串表在內(nèi)存中的地址(包含偏移) stroff字符串表在文件中的偏移
        const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
        //nsyms符號(hào)表?xiàng)l目的個(gè)數(shù)
        for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
            if (symbolTable[iSym].n_value == FAILED_UINT_PTR_ADDRESS) { continue; }
            //符號(hào)表每一項(xiàng)開始地址
            uintptr_t symbolBase = symbolTable[iSym].n_value;
            //函數(shù)地址在符號(hào)表的偏移
            uintptr_t currentDistance = addressWithSlide - symbolBase;
            if ( (addressWithSlide >= symbolBase && currentDistance <= bestDistance) ) {
                bestMatch = symbolTable + iSym;
                bestDistance = currentDistance;
            }
        }
        if (bestMatch != NULL) {
            info->dli_saddr = (void *)(bestMatch->n_value + imageVMAddressSlide);
            //n_un.n_strx 表示符號(hào)名在字符串表中的偏移量集惋,用于表示函數(shù)名
            info->dli_sname = (char *)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
            NSLog(@"%s",info->dli_sname);
            if (*info->dli_sname == '_') {
                info->dli_sname++;
            }
            if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                info->dli_sname = NULL;
            }
            break;
        }
    }
    
    函數(shù)調(diào)用地址在符號(hào)表中對(duì)應(yīng)的位置0x00000001000048a0
    image.png

    MachOView中查看
    image.png

    ASLR地址是0x0000000004f31000
    函數(shù)調(diào)用字符在字符串表中的地址0x0000000104f40940
    去掉偏移量的地址:0x0000000104f40940 - 0x0000000004f31000 = 0x000000010000F940
    image.png

    在MachOView中查看
    image.png

    打印信息
    image.png

參考文章
http://www.reibang.com/p/df5b08330afd
http://www.reibang.com/p/8b78bbbcaf89
https://blog.csdn.net/jasonblog/article/details/49909209
https://elliotsomething.github.io/2017/06/28/thread%E5%AD%A6%E4%B9%A0/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末孕似,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子刮刑,更是在濱河造成了極大的恐慌喉祭,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雷绢,死亡現(xiàn)場(chǎng)離奇詭異泛烙,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)翘紊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門蔽氨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人帆疟,你說(shuō)我怎么就攤上這事鹉究。” “怎么了踪宠?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵自赔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我殴蓬,道長(zhǎng)匿级,這世上最難降的妖魔是什么蟋滴? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮痘绎,結(jié)果婚禮上津函,老公的妹妹穿的比我還像新娘。我一直安慰自己孤页,他們只是感情好尔苦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著行施,像睡著了一般允坚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蛾号,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天稠项,我揣著相機(jī)與錄音,去河邊找鬼鲜结。 笑死搓蚪,一個(gè)胖子當(dāng)著我的面吹牛业筏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播露乏,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼炒嘲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼完慧!你這毒婦竟也來(lái)了砰嘁?” 一聲冷哼從身側(cè)響起享言,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纫事,沒(méi)想到半個(gè)月后勘畔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丽惶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年咖杂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚊夫。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖懦尝,靈堂內(nèi)的尸體忽然破棺而出知纷,到底是詐尸還是另有隱情,我是刑警寧澤陵霉,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布琅轧,位于F島的核電站,受9級(jí)特大地震影響踊挠,放射性物質(zhì)發(fā)生泄漏乍桂。R本人自食惡果不足惜冲杀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望睹酌。 院中可真熱鬧权谁,春花似錦、人聲如沸憋沿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)辐啄。三九已至采章,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間壶辜,已是汗流浹背悯舟。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留砸民,地道東北人抵怎。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像阱洪,于是被迫代替她去往敵國(guó)和親便贵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容