捕獲NSLog日誌小記

南華coder發表於2019-04-19

既往不戀,縱情向前

一、NSLog概述

1、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之前)。

  • iOS10之後,蘋果使用新的統一日誌系統(Unified Logging System)來記錄日誌,全面取代ASL的方式,此種方式,是把日誌集中存放在記憶體和資料庫裡,並提供單一、高效和高效能的介面去獲取系統所有級別的訊息傳遞。

  • 新的統一日誌系統沒有ASL那樣的介面可以讓我們取出全部日誌。

2、NSLog日常使用
  • NSLog在除錯階段,日誌會輸出到到Xcode中,而在iOS真機上,它會輸出到系統的/var/log/syslog這個檔案中。

  • 在日常開發中,很多人喜歡使用NSLog來輸出除錯資訊,但是都知道NSLog是比較消耗效能呢,NSLog輸出的內容或次數多了之後,甚至會影響App的體驗。

  • 於是乎,比較常見的手段是,線上不使用NSLog,DEBUG下才真正使用NSLog。

#if DEBUG
#define MYLOG(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);
#else
#define MYLOG(fmt,...) {}
#endif
複製程式碼
3、常見的日記收集框架
  • 日誌收集主要用了兩個開源框架來實現:PLCrashReporterCocoaLumberjackPLCrashReporter主要用來崩潰日誌收集,CocoaLumberjack是用來收集非崩潰日誌。
  • CocoaLumberjack中實現了對NSLog日誌的捕獲。
4、捕獲NSLog日誌有三種方式
  • iOS 10以前可以通過ASL介面來獲取
  • 通過fishhook庫hook NSLog方法重定向NSLog函式
  • 使用dup2函式和STDERR控制程式碼重定向NSLog函式

二、獲取NSLog的日誌輸出(iOS 10前)

參考CocoaLumberjack中的DDASLLogCapture實現

1、流程介紹
  • 執行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控制檯的輸出,無侵入。

2、註冊程式間的系統通知
  • 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"
複製程式碼

三、NSLog重定向

1、介紹
  • 在iOS10之後,新的統一日誌系統(Unified Logging System)全面取代ASL,沒有ASL那樣的介面可以讓我們取出全部日誌,所以為了相容新的統一日誌系統,你就需要對NSLog日誌的輸出進行重定向。
  • NSLog 進行重定向,可以採用 Hook的方式。因為 NSLog 本身就是一個 C 函式,可以使用fishhook進行重定向。
  • fishhook是Facebook提供的一個動態修改連結Mach-O檔案的工具,能夠hook C函式。
2、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原理

3、利用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 函式

四、dup2重定向

1、介紹
  • NSLog最後重定向的控制程式碼是STDERR,NSLog輸出的日誌內容,最終都通過STDERR控制程式碼來記錄,而dup2函式式專門進行檔案重定向的;
  • 可以使用dup2重定向STDERR控制程式碼,將內容重定向指定的位置,如寫入檔案,上傳伺服器,顯示到View上。
2、核心程式碼
  • 實現重定向,需要通過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];
複製程式碼

相關文章