用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

推荐阅读更多精彩内容

  • 郭相麟 中国制造是基础,中国创造是根本,创造力可以演绎成创意经济,一切行业都是创意业! 制造业的创造在于决策者的开...
    郭相麟阅读 97评论 0 0
  • codepen 上的代码请 fork 后再修改。 环境基础 Chrome、FireFox等主流浏览器陆续支持 ES...
    脱非入欧阅读 903评论 0 0
  • 文 光头小和尚 上一章节 虽说大家是开着玩笑说了这些话,但敏感的陶小桃还是有些在于,在别人心里留下不好的印象,这...
    光头小和尚阅读 161评论 0 2