用fishhook hook輸出方法(NSLog, print)

更新2021/2/26(感謝@lgq_9b65的提醒, 由于我一直沒用真機測試, 才搞出這個烏龍.)

真機測試中發現以下問題

  • NSLog沒有調用writev
  • print沒有調用fwrite

由于暫時沒有找到真機底層調用方法, 所以刪除了fishhook, 使用dup2 + pipe來重定向輸出

相關代碼如下:

let stdoutPipe = [[NSPipe alloc] init];
let stderrPipe = [[NSPipe alloc] init];

// 由于真機再斷開數據線后會輸出到 /dev/null 中, 這里要手動將buff設置為unbuffered
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);

// 保留原始的fileno, 用于之后重新輸出到控制臺
int ori_stdout_fileNo = dup(STDOUT_FILENO);
int ori_stderr_fileNo = dup(STDERR_FILENO);

dup2(stdoutPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO);
dup2(stderrPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO);


stdoutPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle * _Nonnull handle) {
    NSData *data = handle.availableData;
    NSString *str = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
    [[logInWindowManager share] addPrintWithMessage:str];
    const char * utf8Str = str.UTF8String;
        // 將數據重新寫入到原始fileno中
    write(ori_stdout_fileNo,utf8Str,strlen(utf8Str));
};
        
stderrPipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle * _Nonnull handle) {
    NSData *data = handle.availableData;
    NSString *str = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
    [[logInWindowManager share] addPrintWithMessage:str];
    const char * utf8Str = str.UTF8String;
    // 將數據重新寫入到原始fileno中
    write(ori_stderr_fileNo,utf8Str,strlen(utf8Str));
};

用這種方法也有一些問題

  1. 無法分割每一條數據, 都是混到一起的

以下為原文

初衷

一直以來做項目都是手機連電腦, 然后在控制臺查看log信息, 中午吃飯突然想拿出手機看下項目, 但是在食堂沒有電腦, 沒法看log, 所以心血來潮, 想把log信息顯示在window上.

開搞

閑話不多說! UI方面沒什么可說的, 就是一個簡單的Window+UITextView, 重點是怎么把log信息獲取到?首先想到的就是像Runtime 一樣吧NSLog方法hook到, 然后google了一下發現個好東西fishhook, 下邊是他的用法:

#import <dlfcn.h>

#import <UIKit/UIKit.h>

#import "AppDelegate.h"
#import "fishhook.h"
 
static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);
 
int my_close(int fd) {
  printf("Calling real close(%d)\n", fd);
  return orig_close(fd);
}
 
int my_open(const char *path, int oflag, ...) {
  va_list ap = {0};
  mode_t mode = 0;
 
  if ((oflag & O_CREAT) != 0) {
    // mode only applies to O_CREAT
    va_start(ap, oflag);
    mode = va_arg(ap, int);
    va_end(ap);
    printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
    return orig_open(path, oflag, mode);
  } else {
    printf("Calling real open('%s', %d)\n", path, oflag);
    return orig_open(path, oflag, mode);
  }
}
 
int main(int argc, char * argv[])
{
  @autoreleasepool {
    rebind_symbols((struct rebinding[2]){{"close", my_close, (void *)&orig_close}, {"open", my_open, (void *)&orig_open}}, 2);
 
    // Open our own binary and print out first 4 bytes (which is the same
    // for all Mach-O binaries on a given architecture)
    int fd = open(argv[0], O_RDONLY);
    uint32_t magic_number = 0;
    read(fd, &magic_number, 4);
    printf("Mach-O Magic Number: %x \n", magic_number);
    close(fd);
 
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

穩了! 很符合預期嘛~首先用類似的方法嘗試hook NSLog

// orig_NSLog是原有方法被替換后 把原來的實現方法放到另一個地址中
// new_NSLog就是替換后的方法了
static void (*orig_NSLog)(NSString *format, ...);
void(new_NSLog)(NSString *format, ...) {
    va_list args;
    if(format) {
        va_start(args, format);
        NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
        [[logInWindowManager share] addPrintWithMessage:message needReturn:true];
        orig_NSLog(@"%@", message);
        va_end(args);
    }
}
...
// 初始化方法里進行替換
rebind_symbols((struct rebinding[1]){{"NSLog", new_NSLog, (void *)&orig_NSLog}}, 1);

看一下運行效果

1.gif

DDLog

本來到這里就應該結束了的, 不過看了一下自己項目里, 發現項目里用的是都是DDLog, 這就尷尬了.所以咱們來看一下他的代碼.所有的宏定義都匯聚到下面這個方法上:

/**
 * Logging Primitive.
 *
 * This method is used by the macros or logging functions.
 * It is suggested you stick with the macros as they're easier to use.
 *
 *  @param asynchronous YES if the logging is done async, NO if you want to force sync
 *  @param level        the log level
 *  @param flag         the log flag
 *  @param context      the context (if any is defined)
 *  @param file         the current file
 *  @param function     the current function
 *  @param line         the current code line
 *  @param tag          potential tag
 *  @param format       the log format
 */
+ (void)log:(BOOL)asynchronous
      level:(DDLogLevel)level
       flag:(DDLogFlag)flag
    context:(NSInteger)context
       file:(const char *)file
   function:(const char *)function
       line:(NSUInteger)line
        tag:(id)tag
     format:(NSString *)format, ... NS_FORMAT_FUNCTION(9,10);


經過一系列的找, 找到下面的方法(截取了一部分)

- (void)lt_log:(DDLogMessage *)logMessage {
...
    if (_numProcessors > 1) {
        for (DDLoggerNode *loggerNode in self._loggers) {
            if (!(logMessage->_flag & loggerNode->_level)) {
                continue;
            }
            dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool {
                [loggerNode->_logger logMessage:logMessage];
            } });
        }
        dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER);
    } else {        
        for (DDLoggerNode *loggerNode in self._loggers) {
            if (!(logMessage->_flag & loggerNode->_level)) {
                continue;
            }
            dispatch_sync(loggerNode->_loggerQueue, ^{ @autoreleasepool {
                [loggerNode->_logger logMessage:logMessage];
            } });
        }
    }
...
}

loggerNode->_logger 是一個協議 遵守這個協議的一共有5個, 其中只有DDTTYLogger負責輸出到控制臺找到他實現的代理方法, 同樣是截取了一部分

- (void)logMessage:(DDLogMessage *)logMessage {
...
            int iovec_len = (_automaticallyAppendNewlineForCustomFormatters) ? 5 : 4;
            struct iovec v[iovec_len];

            if (colorProfile) {
                v[0].iov_base = colorProfile->fgCode;
                v[0].iov_len = colorProfile->fgCodeLen;

                v[1].iov_base = colorProfile->bgCode;
                v[1].iov_len = colorProfile->bgCodeLen;

                v[iovec_len - 1].iov_base = colorProfile->resetCode;
                v[iovec_len - 1].iov_len = colorProfile->resetCodeLen;
            } else {
                v[0].iov_base = "";
                v[0].iov_len = 0;

                v[1].iov_base = "";
                v[1].iov_len = 0;

                v[iovec_len - 1].iov_base = "";
                v[iovec_len - 1].iov_len = 0;
            }

            v[2].iov_base = (char *)msg;
            v[2].iov_len = msgLen;

            if (iovec_len == 5) {
                v[3].iov_base = "\n";
                v[3].iov_len = (msg[msgLen] == '\n') ? 0 : 1;
            }

            writev(STDERR_FILENO, v, iovec_len);
...
}

從這里可以看到他最終調了writev這個方法那么接下來同樣的方法hook他

static ssize_t (*orig_writev)(int a, const struct iovec * v, int v_len);
ssize_t new_writev(int a, const struct iovec *v, int v_len) {
    NSMutableString *string = [NSMutableString string];
    for (int i = 0; i < v_len; i++) {
        char *c = (char *)v[i].iov_base;
        [string appendString:[NSString stringWithCString:c encoding:NSUTF8StringEncoding]];
    }
    ssize_t result = orig_writev(a, v, v_len);
    dispatch_async(dispatch_get_main_queue(), ^{
        [[logInWindowManager share] addPrintWithMessage:string needReturn:false];
    });
    return result;
}
...
rebind_symbols((struct rebinding[1]){{"writev", new_writev, (void *)&orig_writev}}, 1);

再運行的時候 發現 NSLog的底層調用也是調用了writev方法, 所以上邊hook的NSLog就可以先注釋掉了

看一下效果:

2.gif

這回附加的信息也都出來了, 完美!!

Swift?

原文是以Swift3為例子, 后續添加了一些Swift5的更新

這回到這里該結束了吧....又來需求了... 項目里還有一些swift文件怎么辦?本來想像hookC方法那樣hook print 結果swift獲取不到函數指針google上找到一篇文章: Function hooking in Swift, 按照文章的說明, clone下來rd_route滿心歡喜的寫demo測試一下, 結果......

[圖片上傳失敗...(image-2f691c-1610858461950)]

沒辦法了, 想了一上午, 突然想知道print方法內部實現是什么樣的??

立馬開搞!, Swift已經開源了正好看一下源碼.

按照這篇文章How to Read the Swift Standard Library Source步驟, 編譯完成打開源碼看一下, 首先找到print方法:

@inline(never)
@_semantics("stdlib_binary_only")
public func print(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n"
) {
  if let hook = _playgroundPrintHook {
    var output = _TeeStream(left: "", right: _Stdout())
    _print(
      items, separator: separator, terminator: terminator, to: &output)
    hook(output.left)
  }
  else {
    var output = _Stdout()
    _print(
      items, separator: separator, terminator: terminator, to: &output)
  }
}

print調用了_print, 再看一下_print:

@_versioned
@inline(never)
@_semantics("stdlib_binary_only")
internal func _print<Target : TextOutputStream>(
  _ items: [Any],
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
) {
  var prefix = ""
  output._lock()
  defer { output._unlock() }
  for item in items {
    output.write(prefix)
    _print_unlocked(item, &output)
    prefix = separator
  }
  output.write(terminator)
}

接著_print_unlocked:

@_versioned
@inline(never)
@_semantics("optimize.sil.specialize.generic.never")
@_semantics("stdlib_binary_only")
internal func _print_unlocked<T, TargetStream : TextOutputStream>(
  _ value: T, _ target: inout TargetStream
) {
  // Optional has no representation suitable for display; therefore,
  // values of optional type should be printed as a debug
  // string. Check for Optional first, before checking protocol
  // conformance below, because an Optional value is convertible to a
  // protocol if its wrapped type conforms to that protocol.
  if _isOptional(type(of: value)) {
    let debugPrintable = value as! CustomDebugStringConvertible
    debugPrintable.debugDescription.write(to: &target)
    return
  }
  if case let streamableObject as TextOutputStreamable = value {
    streamableObject.write(to: &target)
    return
  }

  if case let printableObject as CustomStringConvertible = value {
    printableObject.description.write(to: &target)
    return
  }

  if case let debugPrintableObject as CustomDebugStringConvertible = value {
    debugPrintableObject.debugDescription.write(to: &target)
    return
  }

  let mirror = Mirror(reflecting: value)
  _adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
}
...
internal struct _Stdout : TextOutputStream {
  mutating func _lock() {
    _swift_stdlib_flockfile_stdout()
  }

  mutating func _unlock() {
    _swift_stdlib_funlockfile_stdout()
  }

  mutating func write(_ string: String) {
    if string.isEmpty { return }
// 非中文輸出走這里
// 如果符合ascii規格
    if let asciiBuffer = string._core.asciiBuffer {
      defer { _fixLifetime(string) }

      _swift_stdlib_fwrite_stdout(
        UnsafePointer(asciiBuffer.baseAddress!),
        asciiBuffer.count,
        1)
      return
    }
// 中文輸出走這里
// 不符合ascii 一個一個輸出
    for c in string.utf8 {
      _swift_stdlib_putchar_unlocked(Int32(c))
    }
  }
}

// ----- 更新Swift 5.0 -----
internal struct _Stdout: TextOutputStream {
  internal init() {}

  internal mutating func _lock() {
    _swift_stdlib_flockfile_stdout()
  }

  internal mutating func _unlock() {
    _swift_stdlib_funlockfile_stdout()
  }

  internal mutating func write(_ string: String) {
    if string.isEmpty { return }

    var string = string
    _ = string.withUTF8 { utf8 in
      _swift_stdlib_fwrite_stdout(utf8.baseAddress!, 1, utf8.count)
    }
  }
}

先看一些非中文的情況 _swift_stdlib_fwrite_stdout

Swift5.x版本優化了 _Stdout 實現方式, 不再區分ascii與utf8, 統一都執行utf8的方式調用 _swift_stdlib_fwrite_stdout 方法

SWIFT_RUNTIME_STDLIB_INTERFACE
__swift_size_t swift::_swift_stdlib_fwrite_stdout(const void *ptr,
                                                  __swift_size_t size,
                                                  __swift_size_t nitems) {
  return fwrite(ptr, size, nitems, stdout);
}

只是調了fwrite, 那么咱么只需要hook這個方法就行了.

static size_t (*orig_fwrite)(const void * __restrict, size_t, size_t, FILE * __restrict);
size_t new_fwrite(const void * __restrict ptr, size_t size, size_t nitems, FILE * __restrict stream) {
    
    char *str = (char *)ptr;
    __block NSString *s = [NSString stringWithCString:str encoding:NSUTF8StringEncoding];
    [[logInWindowManager share] addPrintWithMessage:s needReturn:false];
    return orig_fwrite(ptr, size, nitems, stream);
}

下面都是Swift3.x的處理, 可以忽略了.

上邊是非中文的情況, 下面看一下中文的情況

SWIFT_RUNTIME_STDLIB_INTERFACE
int swift::_swift_stdlib_putchar_unlocked(int c) {
#if defined(_WIN32)
  return _putc_nolock(c, stdout);
#else
  return putchar_unlocked(c); // 手機/ 模擬器走這里
#endif
}
...
#define putchar_unlocked(x) putc_unlocked(x, stdout)
...
#define putc_unlocked(x, fp)    __sputc(x, fp)
...
#if defined(__GNUC__) && defined(__STDC__)
__header_always_inline int __sputc(int _c, FILE *_p) {
    if (--_p->_w >= 0 || (_p->_w >= _p->_lbfsize && (char)_c != '\n'))
        return (*_p->_p++ = _c);
    else
        return (__swbuf(_c, _p));
}
#else
...
// 最后會調用這個
int __swbuf(int, FILE *);

hook掉__swbuffwrite分別看一下hook到的是什么樣的


static size_t (*orig_fwrite)(const void * __restrict __ptr, size_t __size, size_t __nitems, FILE * __restrict __stream);
size_t new_fwrite(const void * __restrict __ptr, size_t __size, size_t __nitems, FILE * __restrict __stream) {
// 這里的_ptr就是傳進來的字符
char *chars = (char*)_ptr;
    return orig_fwrite(__ptr, __size, __nitems, __stream);
}
static int  (*orin___swbuf)(int, FILE *);
int new___swbuf(int c, FILE *p) {
// 這里的c也是傳進來的字符
char cChar = (char)c;
    return orin___swbuf(c, p);
}
...
print("北京歡迎你aaaaasdfsdfg *^(*&R()8y23rkvwd")

這里是這樣的: 輸出的字符串有中文也有別的字符, 當是中文時, 因為一個中文等于多個字符, 所以要把__swbuf連續幾次傳過來的c合成成一個中文再配合fwrite的非中文合到一起再輸出

下邊是我想到的辦法, 如果有更好的辦法請告訴我, 謝謝!

static char *__chineseChar = {0};
static int __buffIdx = 0;
static NSString *__syncToken = @"token";
static size_t (*orig_fwrite)(const void * __restrict __ptr, size_t __size, size_t __nitems, FILE * __restrict __stream);
size_t new_fwrite(const void * __restrict __ptr, size_t __size, size_t __nitems, FILE * __restrict __stream) {
    
    char *str = (char *)__ptr;
    __block NSString *s = [NSString stringWithCString:str encoding:NSUTF8StringEncoding];
    dispatch_async(dispatch_get_main_queue(), ^{
        @synchronized (__syncToken) {
            if (str[0] == '\n' && __chineseChar[0] != '\0') {
                s = [[NSString stringWithCString:__chineseChar encoding:NSUTF8StringEncoding] stringByAppendingString:s];
                __buffIdx = 0;
                __chineseChar = calloc(1, sizeof(char));
            }
        }
        [[logInWindowManager share] addPrintWithMessage:s needReturn:false];
    });
    return orig_fwrite(__ptr, __size, __nitems, __stream);
}

static int (*orin___swbuf)(int, FILE *);
int new___swbuf(int c, FILE *p) {
    @synchronized (__syncToken) {
        __chineseChar = realloc(__chineseChar, sizeof(char) * (__buffIdx + 2));
        __chineseChar[__buffIdx] = (char)c;
        __chineseChar[__buffIdx + 1] = '\0';
        __buffIdx++;
    }
    return orin___swbuf(c, p);
}

總結

代碼都不是很難懂, 主要是分享一下我解決問題的過程.源碼在我的Github

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容

  • 郭相麟 中國制造是基礎,中國創造是根本,創造力可以演繹成創意經濟,一切行業都是創意業! 制造業的創造在于決策者的開...
    郭相麟閱讀 344評論 0 0
  • codepen 上的代碼請 fork 后再修改。 環境基礎 Chrome、FireFox等主流瀏覽器陸續支持 ES...
    脫非入歐閱讀 1,259評論 0 0
  • 文 光頭小和尚 上一章節 雖說大家是開著玩笑說了這些話,但敏感的陶小桃還是有些在于,在別人心里留下不好的印象,這...
    桃_夭閱讀 382評論 0 2