最详细的BSBacktraceLogger解析

2021-05-27  本文已影响0人  客三消

简述

BSBacktraceLogger是一个轻量级的线程函数堆栈导出工具.

简单用法:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"%@",[BSBacktraceLogger bs_backtraceOfMainThread]);
    });
    [self foo];
}

- (void)foo {
    [self bar];
}

- (void)bar {
    while (true) {
        ;
    }
}

日志

2021-05-27 20:02:33.483299+0800 BacktraceLoggerTest[41034:5171140] Backtrace of Thread 259:
BacktraceLoggerTest             0x1069c64fc -[ViewController bar] + 12
BacktraceLoggerTest             0x1069c64e4 -[ViewController foo] + 36
BacktraceLoggerTest             0x1069c6590 -[ViewController viewDidLoad] + 128
UIKitCore                       0x7fff23f806a9 -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 88
UIKitCore                       0x7fff23f8504c -[UIViewController loadViewIfRequired] + 1084
UIKitCore                       0x7fff23f85436 -[UIViewController view] + 27
UIKitCore                       0x7fff246ffc73 -[UIWindow addRootViewControllerViewIfPossible] + 313
UIKitCore                       0x7fff246ff362 -[UIWindow _updateLayerOrderingAndSetLayerHidden:actionBlock:] + 219
UIKitCore                       0x7fff24700325 -[UIWindow _setHidden:forced:] + 362
UIKitCore                       0x7fff247133a6 -[UIWindow _mainQueue_makeKeyAndVisible] + 42
UIKitCore                       0x7fff24951c05 -[UIWindowScene _makeKeyAndVisibleIfNeeded] + 202
UIKitCore                       0x7fff23b0e80c +[UIScene _sceneForFBSScene:create:withSession:connectionOptions:] + 1671
UIKitCore                       0x7fff246c2df9 -[UIApplication _connectUISceneFromFBSScene:transitionContext:] + 1114
UIKitCore                       0x7fff246c3128 -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 289
UIKitCore                       0x7fff241a4ab4 -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 358
FrontBoardServices              0x7fff25a1b40b -[FBSScene _callOutQueue_agent_didCreateWithTransitionContext:completion:] + 398
FrontBoardServices              0x7fff25a43e55 __94-[FBSWorkspaceScenesClient createWithSceneID:groupID:parameters:transitionContext:completion:]_block_invoke.176 + 102
FrontBoardServices              0x7fff25a28f12 -[FBSWorkspace _calloutQueue_executeCalloutFromSource:withBlock:] + 209
FrontBoardServices              0x7fff25a43b28 __94-[FBSWorkspaceScenesClient createWithSceneID:groupID:parameters:transitionContext:completion:]_block_invoke + 352
libdispatch.dylib               0x106c4174e _dispatch_client_callout + 8
libdispatch.dylib               0x106c44656 _dispatch_block_invoke_direct + 295
FrontBoardServices              0x7fff25a695d0 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 30
FrontBoardServices              0x7fff25a692b6 -[FBSSerialQueue _targetQueue_performNextIfPossible] + 433
FrontBoardServices              0x7fff25a6977b -[FBSSerialQueue _performNextFromRunLoopSource] + 22
CoreFoundation                  0x7fff20390ede __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
CoreFoundation                  0x7fff20390dd6 __CFRunLoopDoSource0 + 180
CoreFoundation                  0x7fff20390300 __CFRunLoopDoSources0 + 340
CoreFoundation                  0x7fff2038a9f7 __CFRunLoopRun + 875
CoreFoundation                  0x7fff2038a1a7 CFRunLoopRunSpecific + 567
GraphicsServices                0x7fff2b874d85 GSEventRunModal + 139
UIKitCore                       0x7fff246c14df -[UIApplication _run] + 912
UIKitCore                       0x7fff246c639c UIApplicationMain + 101
BacktraceLoggerTest             0x1069c6912 main + 114
libdyld.dylib                   0x7fff2025abbd start + 1

源码阅读

⚠️ 本文只讨论 + (NSString *)bs_backtraceOfMainThread.
⚠️ 本文以注释形式解析代码.本代码可直接覆盖GitHub原库.m文件.边断点调试边阅读注释.
名词:

/*
    // 符号数据结构
    struct BS_NLIST {
    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 符号所在的 section index
    uint16_t n_desc;       // see <mach-o/stab.h>
    uint64_t n_value;      // value of this symbol (or stab offset) 符号的地址值
    };
 */
 
// 栈帧抽象结构体,只关注有用的两个指针
typedef struct BSStackFrameEntry{
    const struct BSStackFrameEntry *const previous; // FP: *FP 上一个函数起始
    const uintptr_t return_address;                 // LR: *(FP + 8) 此函数结束后返回的上一个函数的下一条指令的地址
} BSStackFrameEntry;

static mach_port_t main_thread_id;

@implementation BSBacktraceLogger

+ (void)load {
    main_thread_id = mach_thread_self();
}

#pragma -mark Implementation of interface

+ (NSString *)bs_backtraceOfNSThread:(NSThread *)thread {
    // NSThread 转 Mach thread.
    return _bs_backtraceOfThread(bs_machThreadFromNSThread(thread));
}

+ (NSString *)bs_backtraceOfCurrentThread {
    return [self bs_backtraceOfNSThread:[NSThread currentThread]];
}

+ (NSString *)bs_backtraceOfMainThread {
    // 传入NSThread主线程
    return [self bs_backtraceOfNSThread:[NSThread mainThread]];
}

+ (NSString *)bs_backtraceOfAllThread {
    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";
    }
    
    NSMutableString *resultString = [NSMutableString stringWithFormat:@"Call Backtrace of %u threads:\n", thread_count];
    for(int i = 0; i < thread_count; i++) {
        [resultString appendString:_bs_backtraceOfThread(threads[i])];
    }
    return [resultString copy];
}

#pragma -mark Get call backtrace of a mach_thread

/// 导出堆栈符号字符串
NSString *_bs_backtraceOfThread(thread_t thread) {
    uintptr_t backtraceBuffer[50];  // 待符号化的堆栈地址数组
    int i = 0;
    NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];
    
    /*
     * _STRUCT_MCONTEXT64
     * {
     *   _STRUCT_X86_EXCEPTION_STATE64   __es;
     *   _STRUCT_X86_THREAD_STATE64      __ss;
     *   _STRUCT_X86_FLOAT_STATE64       __fs;
     * };
     
     * _STRUCT_ARM_THREAD_STATE64
     * {
     *   __uint64_t __x[29];    // General purpose registers x0-x28
     *   __uint64_t __fp;       // Frame pointer x29
     *   __uint64_t __lr;       // Link register x30
     *   __uint64_t __sp;       // Stack pointer x31
     *   __uint64_t __pc;       // Program counter
     *   __uint32_t __cpsr;     // Current program status register
     *   __uint32_t __pad;      // Same size for 32-bit or 64-bit clients
     * };
     */
    
    _STRUCT_MCONTEXT machineContext;
    
    // 通过thread初始化machineContext,里面有__ss, __ss里面有LR、FP、SP等寄存器.
    if(!bs_fillThreadStateIntoMachineContext(thread, &machineContext)) {
        return [NSString stringWithFormat:@"Fail to get information about thread: %u", thread];
    }
    
    // PC寄存器.即将要执行的下一条指令.
    // ❓为什么要先加入PC? 因为在符号化的时候可以找到当前函数地址.如果只有LR,当前函数没有指针指向了.
    const uintptr_t instructionAddress = bs_mach_instructionAddress(&machineContext);
    backtraceBuffer[i] = instructionAddress;
    ++i;
    
    // LR寄存器,函数返回地址.用于递归符号化堆栈
    uintptr_t linkRegister = bs_mach_linkRegister(&machineContext);
    if (linkRegister) {
        backtraceBuffer[i] = linkRegister;
        i++;
    }
    
    if(instructionAddress == 0) {
        return @"Fail to get instruction address";
    }
    
    BSStackFrameEntry frame = {0};  // 初始化一个空的栈帧模型
    const uintptr_t framePtr = bs_mach_framePointer(&machineContext); // 拿FP,栈帧指针. 指向函数起始地址, *FP保存上一个函数起始地址. 用于递归函数调用栈. machineContext->__ss.__fp
    
    // 用FP初始化首个栈帧模型BSStackFrameEntry.bs_mach_copyMem() 从framePtr和framePtr + 8的位置拿值,分别初始化 previous、returnAddress
    if(framePtr == 0 || bs_mach_copyMem((void *)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS) {
        return @"Fail to get frame pointer";
    }
    
    /*  例子:
        long * ptt = (long *)framePtr;              // 0d6163429232 -> 0x16f5e7770
        NSLog(@"%lx", *ptt);                        // *0x16f5e7770(FP) = 0x16f5e8c20 = frame.previous

        long * returnAdd = (long *)(framePtr + 8);  // 0d6163429240 -> 0x16f5e7778
        NSLog(@"%lx", *returnAdd);                  // *0x16f5e7778(FP + 8 = LR) = 0x1dd76d224 = frame.returnAddress
     */
    
    // i = 2
    for(; i < 50; i++) {
        backtraceBuffer[i] = frame.return_address;
        
        // 调用bs_mach_copyMem(), 递归: FP = *FP, LR = *(FP + 8)
        if(backtraceBuffer[i] == 0 || frame.previous == 0 || bs_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
            break;
        }
    }
    
    int backtraceLength = i;
    Dl_info symbolicated[backtraceLength];
    bs_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0); // 对当前的堆栈进行符号化
    for (int i = 0; i < backtraceLength; ++i) {
        [resultString appendFormat:@"%@", bs_logBacktraceEntry(i, backtraceBuffer[i], &symbolicated[i])];
    }
    [resultString appendFormat:@"\n"];
    return [resultString copy];
}

#pragma -mark Convert NSThread to Mach thread
// NSThread 与 Mach thread 对应
thread_t bs_machThreadFromNSThread(NSThread *nsthread) {
    char name[256];
    mach_msg_type_number_t count;
    thread_act_array_t list;
    task_threads(mach_task_self(), &list, &count);  // 将 mach_task_self() 进程中的所有线程枚举保存在list中
    
    // 因为NSThread中 name 和 Mach thread 是一个.所以用 name 匹配
    // 给NSThread设置一个名字,然后去pthread中查找同名的线程.
    // ⚠️ 注意:可能有名称重复的问题,所以用时间戳作为新名字.匹配完毕之后名字记得还原
    NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
    NSString *originName = [nsthread name];
    [nsthread setName:[NSString stringWithFormat:@"%f", currentTimestamp]];
    
    if ([nsthread isMainThread]) {
        return (thread_t)main_thread_id;
    }
    
    for (int i = 0; i < count; ++i) {
        pthread_t pt = pthread_from_mach_thread_np(list[i]);
        if ([nsthread isMainThread]) {
            if (list[i] == main_thread_id) {
                return list[i];
            }
        }
        if (pt) {
            name[0] = '\0';
            pthread_getname_np(pt, name, sizeof name);
            if (!strcmp(name, [nsthread name].UTF8String)) {
                [nsthread setName:originName];
                return list[i];
            }
        }
    }
    
    [nsthread setName:originName];
    return mach_thread_self();
}

#pragma -mark GenerateBacbsrackEnrty
/// 组装符号字符串
NSString* bs_logBacktraceEntry(const int entryNum,
                               const uintptr_t address,
                               const Dl_info* const dlInfo) {
    char faddrBuff[20];
    char saddrBuff[20];
    
    const char* fname = bs_lastPathEntry(dlInfo->dli_fname);
    if(fname == NULL) {
        sprintf(faddrBuff, POINTER_FMT, (uintptr_t)dlInfo->dli_fbase);
        fname = faddrBuff;
    }
    
    uintptr_t offset = address - (uintptr_t)dlInfo->dli_saddr;
    const char* sname = dlInfo->dli_sname;
    if(sname == NULL) {
        sprintf(saddrBuff, POINTER_SHORT_FMT, (uintptr_t)dlInfo->dli_fbase);
        sname = saddrBuff;
        offset = address - (uintptr_t)dlInfo->dli_fbase;
    }
    return [NSString stringWithFormat:@"%-30s  0x%08" PRIxPTR " %s + %lu\n" ,fname, (uintptr_t)address, sname, offset];
}

const char* bs_lastPathEntry(const char* const path) {
    if(path == NULL) {
        return NULL;
    }
    
    char* lastFile = strrchr(path, '/');
    return lastFile == NULL ? path : lastFile + 1;
}

#pragma -mark HandleMachineContext
bool bs_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    // 获取线程所有信息,写入machineContext中, ss: Stack Segment 堆栈段寄存器.内涵FP、LR、SP、PC...等寄存器
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    return (kr == KERN_SUCCESS);
}

uintptr_t bs_mach_framePointer(mcontext_t const machineContext){
    return machineContext->__ss.BS_FRAME_POINTER;
}

uintptr_t bs_mach_stackPointer(mcontext_t const machineContext){
    return machineContext->__ss.BS_STACK_POINTER;
}

uintptr_t bs_mach_instructionAddress(mcontext_t const machineContext){
    return machineContext->__ss.BS_INSTRUCTION_ADDRESS;
}

uintptr_t bs_mach_linkRegister(mcontext_t const machineContext){
#if defined(__i386__) || defined(__x86_64__)
    return 0;
#else
    return machineContext->__ss.__lr;
#endif
}

/// 拷贝FP到结构体
/// @param src FP
/// @param dst BSStackFrameEntry
/// @param numBytes BSStackFrameEntry长度
kern_return_t bs_mach_copyMem(const void *const src, void *const dst, const size_t numBytes){
    vm_size_t bytesCopied = 0;
    return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
}

#pragma -mark Symbolicate

/// 地址转符号字符串
/// @param backtraceBuffer 栈数据数组
/// @param symbolsBuffer 空数组
/// @param numEntries 栈数据长度
/// @param skippedEntries = 0
void bs_symbolicate(const uintptr_t* const backtraceBuffer,
                    Dl_info* const symbolsBuffer,
                    const int numEntries,
                    const int skippedEntries){
    int i = 0;
    
    // 第一个存储的是PC寄存器
    if(!skippedEntries && i < numEntries) {
        bs_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
        i++;
    }
    
    // 后面存储的都是LR
    for(; i < numEntries; i++) {
        bs_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]);
    }
}

/// 找到LR指针最近的符号, 放到 info 中
bool bs_dladdr(const uintptr_t address, Dl_info* const info) {
    info->dli_fname = NULL;
    info->dli_fbase = NULL;
    info->dli_sname = NULL;
    info->dli_saddr = NULL;
    
    const uint32_t idx = bs_imageIndexContainingAddress(address);                       // 获取address所在的image序号
    if(idx == UINT_MAX) {
        return false;
    }
    const struct mach_header* header = _dyld_get_image_header(idx);                     // 获取映像的mach-o头部信息结构体指针, header对象存储load command个数及大小
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);    // slide.
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;                      // LR虚拟内存地址
    const uintptr_t segmentBase = bs_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;   // vmaddr - fileoff + slide
    if(segmentBase == 0) {
        return false;
    }
    
    info->dli_fname = _dyld_get_image_name(idx);
    info->dli_fbase = (void*)header;
    
    // Find symbol tables and get whichever symbol is closest to the address.
    const BS_NLIST* bestMatch = NULL;                  // 符号结构体
    uintptr_t bestDistance = ULONG_MAX;
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header); // Load Commands
    if(cmdPtr == 0) {
        return false;
    }
    
    // 遍历Load Commands,找到 LC_SYMTAB 段
    for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        if(loadCmd->cmd == LC_SYMTAB) {
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;    // LC_SYMTAB,内含符号表位于可执行文件的偏移以及字符串表位于可执行文件的偏移
            const BS_NLIST* symbolTable = (BS_NLIST*)(segmentBase + symtabCmd->symoff); // 符号表内存真实地址, symoff 为符号表在 Mach-O 文件中的偏移
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;              // 字符串表内存真实地址, stroff 为字符串表在 Mach-O 文件中的偏移
            
            // 遍历所有符号,找到与LR最近的那个. symtabCmd->nsyms指示了符号表的条目
            for(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;                           // n_value 符号的虚拟内存地址值
                    uintptr_t currentDistance = addressWithSlide - symbolBase;                  // LR与符号的距离
                    if((addressWithSlide >= symbolBase) && (currentDistance <= bestDistance)) { // 函数地址值在本符号之后 且 距离小于之前的最近距离
                        bestMatch = symbolTable + iSym;                                         // 最匹配的符号 = 当前符号表结构体 + n个偏移
                        bestDistance = currentDistance;                                         // 最近距离 = 当前距离
                    }
                }
            }
            if(bestMatch != NULL) {
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);                       // 符号真实地址 = n_value + slide
                info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);    // 字符真实地址 = 字符串表地址 + 最接近的符号中的字符串表(数组)索引值 n_strx(输出从此处到下一个null)
                if(*info->dli_sname == '_') {
                    info->dli_sname++;
                }
                // This happens if all symbols have been stripped.
                if(info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                    info->dli_sname = NULL;
                }
                break;
            }
        }
        cmdPtr += loadCmd->cmdsize;
    }
    return true;
}

/// 找到Header下面第一个Command
uintptr_t bs_firstCmdAfterHeader(const struct mach_header* const header) {
    switch(header->magic) {
        case MH_MAGIC:
        case MH_CIGAM:
            return (uintptr_t)(header + 1); // 向下偏移1个mach_header长度.
        case MH_MAGIC_64:
        case MH_CIGAM_64:
            return (uintptr_t)(((struct mach_header_64*)header) + 1); // 向下偏移1个mach_header_64长度.mach_header_64比mach_header多4个字节.
        default:
            return 0;  // Header is corrupt
    }
}

// 遍历loadCommands,确认adress是否落在当前image的某个segment中. address: LR地址
uint32_t bs_imageIndexContainingAddress(const uintptr_t address) {
    const uint32_t imageCount = _dyld_image_count(); // 返回当前进程中加载的映像的数量
    const struct mach_header* header = 0;
    
    // 遍历image
    for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
        header = _dyld_get_image_header(iImg); // 得到image_header
        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);  // 得到减去ASLR之后的地址.在mach-o中真实地址
            uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);                                  // 得到Header下面第一个loadCommands的地址
            if(cmdPtr == 0) {
                continue;
            }
            
            // ncmds: loadcommands的数量.
            for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
                const struct load_command* loadCmd = (struct load_command*)cmdPtr;
                if(loadCmd->cmd == LC_SEGMENT) { // command 类型为 LC_SEGMENT, 使用结构体segment_command
                    const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
                    if(addressWSlide >= segCmd->vmaddr &&
                       addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        return iImg; // 如果LR的地址落在这个模块里,则返回映像索引号
                    }
                }
                else if(loadCmd->cmd == LC_SEGMENT_64) { // command 类型为 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; // 如果LR的地址落在这个模块里,则返回映像索引号
                    }
                }
                cmdPtr += loadCmd->cmdsize; // command地址是连续的,移动到下一个command位置
            }
        }
    }
    return UINT_MAX;
}

/*
 *  sym_vmaddr(符号表虚拟地址) - vmaddr(LINKEDIT虚拟地址) = symoff(符号表文件地址) - fileoff(LINKEDIT文件地址)
 *  sym_vmaddr = vmaddr - fileoff + symoff
 *  因为 符号表内存真实地址 = sym_vmaddr + slide
 *  所以 符号表真实内存地址 = vmaddr - fileoff + symoff + slide
 *  此函数只为计算 vmaddr - fileoff
 */
uintptr_t bs_segmentBaseOfImageIndex(const uint32_t idx) {
    const struct mach_header* header = _dyld_get_image_header(idx);
    
    // Look for a segment command and return the file image address.
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    if(cmdPtr == 0) {
        return 0;
    }
    for(uint32_t i = 0;i < header->ncmds; i++) {
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        if(loadCmd->cmd == LC_SEGMENT) {
            const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr;
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                return segmentCmd->vmaddr - segmentCmd->fileoff;
            }
        }
        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) { // __LINKEDIT是链接信息段,可以通过__LINKEDIT进行符号地址计算
                return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff);
            }
        }
        cmdPtr += loadCmd->cmdsize;
    }
    return 0;
}

@end

上一篇 下一篇

猜你喜欢

热点阅读