捕获NSLog日志小记

既往不恋,纵情向前ios

原文连接git

1、NSLog概述

一、NSLog是什么
  • NSLog是一个C函数,函数声明以下:
//Logs an error message to the Apple System Log facility.
FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;
复制代码
  • 根据苹果的文档介绍,NSLog的做用是输出信息到标准的Error控制台和 苹果的日志系统(ASL,Apple System Log)里面(iOS 10以前)。github

  • iOS10以后,苹果使用新的统一日志系统(Unified Logging System)来记录日志,全面取代ASL的方式,此种方式,是把日志集中存放在内存和数据库里,并提供单1、高效和高性能的接口去获取系统全部级别的消息传递。objective-c

  • 新的统一日志系统没有ASL那样的接口可让咱们取出所有日志。数据库

二、NSLog平常使用
  • NSLog在调试阶段,日志会输出到到Xcode中,而在iOS真机上,它会输出到系统的/var/log/syslog这个文件中。浏览器

  • 在平常开发中,不少人喜欢使用NSLog来输出调试信息,可是都知道NSLog是比较消耗性能呢,NSLog输出的内容或次数多了以后,甚至会影响App的体验。缓存

  • 因而乎,比较常见的手段是,线上不使用NSLog,DEBUG下才真正使用NSLog。bash

#if DEBUG
#define MYLOG(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);
#else
#define MYLOG(fmt,...) {}
#endif
复制代码
三、常见的日记收集框架
  • 日志收集主要用了两个开源框架来实现:PLCrashReporterCocoaLumberjackPLCrashReporter主要用来崩溃日志收集,CocoaLumberjack是用来收集非崩溃日志。
  • CocoaLumberjack中实现了对NSLog日志的捕获。
四、捕获NSLog日志有三种方式
  • iOS 10之前能够经过ASL接口来获取
  • 经过fishhook库hook NSLog方法重定向NSLog函数
  • 使用dup2函数和STDERR句柄重定向NSLog函数

2、获取NSLog的日志输出(iOS 10前)

参考CocoaLumberjack中的DDASLLogCapture实现服务器

一、流程介绍
  • 执行DDASLLogCapturestart方法,启动一个异步全局队列去捕获ASL存储的日志;
+ (void)start {
	//...
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
        [self captureAslLogs];
    });
}
复制代码
  • 当日志被保存到ASL的数据库时候,syslogd(系统里用于接收分发日志消息的日志守护进程)会发出一条通知。由于发过来的这一条通知可能有多条日志,须要先将几条日志进行合并。
+ (void)captureAslLogs {
    //....
}
复制代码
  • 将得到到的数据转成char 字符串类型,再转成NSString类型,最后封装成DDLogMessage对象,经过[DDLog log: message:] 方法将日志记录下来。
+ (void)aslMessageReceived:(aslmsg)msg {
    //...
}
复制代码

说明:以上方法不会影响Xcode控制台的输出,无侵入。app

二、注册进程间的系统通知
  • captureAslLogs中经过notify_register_dispatch来注册监听进程间的系统通知;
notify_register_dispatch(kNotifyASLDBUpdate, &notifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token)
        {
            //...
        });
复制代码
  • 其中宏kNotifyASLDBUpdate表示:日志被保存在ASL数据库发出的跨进程通知;
/*
 * ASL notifications
 * Sent by syslogd to advise clients that new log messages have been
 * added to the ASL database.
 */
#define kNotifyASLDBUpdate "com.apple.system.logger.message"
复制代码
  • 将日志保存到ASL数据库时还有不少通知,好比宏kNotifyVFSLowDiskSpace表示:系统磁盘空间不足,捕获到这个通知时,能够去清理缓存空间,避免缓存写入磁盘失败的状况。
#define kNotifyVFSLowDiskSpace "com.apple.system.lowdiskspace"
复制代码

3、NSLog重定向

一、介绍
  • 在iOS10以后,新的统一日志系统(Unified Logging System)全面取代ASL,没有ASL那样的接口可让咱们取出所有日志,因此为了兼容新的统一日志系统,你就须要对NSLog日志的输出进行重定向。
  • NSLog 进行重定向,能够采用 Hook的方式。由于 NSLog 自己就是一个 C 函数,可使用fishhook进行重定向。
  • fishhook是Facebook提供的一个动态修改连接Mach-O文件的工具,可以hook C函数。
二、fishhook原理
  • APP运行时,Mach-O文件被dyld(动态加载器)加载进内存

  • ASLR(地址空间布局随机化)让Mach-O被加载时内存地址随机分配

  • 苹果的PIC位置与代码独立技术,让Mach-O调用系统库函数时,先在Mach-O表中的_DATA段创建一个指针指向外部库函数,dyld加载MachO时知道外部库函数的调用地址,会动态的把_DATA段的指针指向外部库函数

  • fishhook可以替换NSLog等库函数,这事是由于Mach-O的符号表里有NSLog等,能够经过符号表找到NSLog字符串。

说明:具体原理参考iOS逆向工程 - fishhook原理

三、利用fishhook hook NSLog函数

实现代码以下:

//申明一个函数指针用于保存原NSLog的真实函数地址
static void (*orig_nslog)(NSString *format, ...);

//NSLog重定向
void redirect_nslog(NSString *format, ...) {
    
    //能够添加本身的处理,好比输出到本身的持久化存储系统中

    //继续执行原来的 NSLog
    va_list va;
    format = [NSString stringWithFormat:@"[hook success]%@",format];
    va_start(va, format);
    NSLogv(format, va);
    va_end(va);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
         struct rebinding nslog_rebinding = {"NSLog",redirect_nslog,(void*)&orig_nslog};
    	 rebind_symbols((struct rebinding[1]){nslog_rebinding}, 1);
         NSLog(@"%@, hello word!",@"ss");
    }
    return
}

//[hook success]ss, hello word!
复制代码
  • 利用fishhook对方法的符号地址进行了从新板顶,从而只要是NDSLog的调用就会转向redirect_nslog方法调用。

参考使用fishhook hook NSLog 函数

4、dup2重定向

一、介绍
  • NSLog最后重定向的句柄是STDERR,NSLog输出的日志内容,最终都经过STDERR句柄来记录,而dup2函数式专门进行文件重定向的;
  • 可使用dup2重定向STDERR句柄,将内容重定向指定的位置,如写入文件,上传服务器,显示到View上。
二、核心代码
  • 实现重定向,须要经过NSPipe建立一个管道,pipe有读端和写端,而后经过dup2将标准输入重定向到pipe的写端。再经过NSFileHandle监听pipe的读端,最后再处理读出的信息。
  • 以后经过printf或者NSLog写数据,都会写到pipe的写端,同时pipe会将这些数据直接传送到读端,最后经过NSFileHandle的监控函数取出这些数据。
- (void)redirectSTD:(int )fd {
    
    NSPipe * pipe = [NSPipe pipe] ;
    NSFileHandle *pipeReadHandle = [pipe fileHandleForReading] ;
    int pipeFileHandle = [[pipe fileHandleForWriting] fileDescriptor];
    dup2(pipeFileHandle, fd) ;
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(redirectNotificationHandle:)
                                                 name:NSFileHandleReadCompletionNotification
                                               object:pipeReadHandle] ;
    [pipeReadHandle readInBackgroundAndNotify];
}

- (void)redirectNotificationHandle:(NSNotification *)nf {
    NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
    NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ;
    //能够添加本身的处理,能够将内容显示到View,或者是存放到另外一个文件中等等
    //todo
    
    
    [[nf object] readInBackgroundAndNotify];
}

//使用
[self redirectSTD:STDERR_FILENO];
复制代码