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

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

真機(jī)測試中發(fā)現(xiàn)以下問題

  • NSLog沒有調(diào)用writev
  • print沒有調(diào)用fwrite

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

相關(guān)代碼如下:

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

// 由于真機(jī)再斷開數(shù)據(jù)線后會輸出到 /dev/null 中, 這里要手動將buff設(shè)置為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;
        // 將數(shù)據(jù)重新寫入到原始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;
    // 將數(shù)據(jù)重新寫入到原始fileno中
    write(ori_stderr_fileNo,utf8Str,strlen(utf8Str));
};

用這種方法也有一些問題

  1. 無法分割每一條數(shù)據(jù), 都是混到一起的

以下為原文

初衷

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

開搞

閑話不多說! UI方面沒什么可說的, 就是一個簡單的Window+UITextView, 重點是怎么把log信息獲取到?首先想到的就是像Runtime 一樣吧NSLog方法hook到, 然后google了一下發(fā)現(xiàn)個好東西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]));
  }
}

穩(wěn)了! 很符合預(yù)期嘛~首先用類似的方法嘗試hook NSLog

// orig_NSLog是原有方法被替換后 把原來的實現(xiàn)方法放到另一個地址中
// 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);
    }
}
...
// 初始化方法里進(jìn)行替換
rebind_symbols((struct rebinding[1]){{"NSLog", new_NSLog, (void *)&orig_NSLog}}, 1);

看一下運行效果

1.gif

DDLog

本來到這里就應(yīng)該結(jié)束了的, 不過看了一下自己項目里, 發(fā)現(xiàn)項目里用的是都是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);


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

- (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 是一個協(xié)議 遵守這個協(xié)議的一共有5個, 其中只有DDTTYLogger負(fù)責(zé)輸出到控制臺找到他實現(xiàn)的代理方法, 同樣是截取了一部分

- (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);
...
}

從這里可以看到他最終調(diào)了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);

再運行的時候 發(fā)現(xiàn) NSLog的底層調(diào)用也是調(diào)用了writev方法, 所以上邊hook的NSLog就可以先注釋掉了

看一下效果:

2.gif

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

Swift?

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

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

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

沒辦法了, 想了一上午, 突然想知道print方法內(nèi)部實現(xiàn)是什么樣的??

立馬開搞!, Swift已經(jīng)開源了正好看一下源碼.

按照這篇文章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調(diào)用了_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規(guī)格
    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版本優(yōu)化了 _Stdout 實現(xiàn)方式, 不再區(qū)分ascii與utf8, 統(tǒng)一都執(zhí)行utf8的方式調(diào)用 _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);
}

只是調(diào)了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); // 手機(jī)/ 模擬器走這里
#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
...
// 最后會調(diào)用這個
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就是傳進(jìn)來的字符
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也是傳進(jìn)來的字符
char cChar = (char)c;
    return orin___swbuf(c, p);
}
...
print("北京歡迎你aaaaasdfsdfg *^(*&R()8y23rkvwd")

這里是這樣的: 輸出的字符串有中文也有別的字符, 當(dāng)是中文時, 因為一個中文等于多個字符, 所以要把__swbuf連續(xù)幾次傳過來的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);
}

總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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