综合开发

NSLog 和 print 源码阅读和捕获方案

微信扫一扫,分享到朋友圈

NSLog 和 print 源码阅读和捕获方案

NSLog 作为 iOS开发常用的调试和日志打印方法,大家都是很熟悉了, 开源社区也为我们贡献了很多非常优秀的日志框架,比如OC中大名鼎鼎的 CocoaLumberjack ,有兴趣的同学可以移步 github.com/CocoaLumber… 在Swift语言下我们还有另外一种选择,那就是 print

如果要自己做日志监控的话,就需要就需要自己重定向 NSLogprint 方法了

网络上大概有如下几种方法

  • asl读取日志(iOS10以后已经弃用) NSLog默认的输出到了系统的 /var/log/syslog这个文件,asl框架能从syslog中读取日志,此种方法对系统无侵入,可惜iOS10后已经获取不到日志
  • 采用dup2的重定向方式 NSLog最后重定向的句柄是STDERR,NSLog输出的日志内容,最终都通过STDERR句柄来记录,使用dup2重定向STDERR句柄,可以将内容重定向指定的位置,但是重定向之后
  • 采用fishhook方式 采用facebook的开源框架 fishhook 来动态替换 NSLogprint 方法

本文选择基于fishhook的方式捕获 NSLog 和 print 方法 福利: github.com/madaoCN/Sup… 已实现轻量级的日志打印,使用fishhook hook了 NSLog 和 print 方法

前期准备

  • fishhook原理 网上有很多写的很好的文章,这里就不献丑啦,大家自行百度/谷歌哈

  • 编译 swift 源码 swift已经开源了,我们可以阅读源码来一窥究竟 项目地址 github.com/apple/swift 编译也很简单,就是比较耗时

brew install cmake ninja
mkdir swift-source
cd swift-source
git clone https://github.com/apple/swift.git
./swift/utils/update-checkout --clone
cd swift
utils/build-script --release-debuginfo
复制代码

最终的编译文件可能会比较大,我的大概是 44.18GB, 编译前请预留足够的空间

搞定了的话,黑喂狗~

NSLog源码阅读

NSLog的源码位置在 你的编译工程目录/Swift-Build/swift-corelibs-foundation

/* Output from NSLogv is serialized, in that only one thread in a process can be doing
* the writing/logging described above at a time. All attempts at writing/logging a
* message complete before the next thread can begin its attempts. The format specification
* allowed by these functions is that which is understood by NSString's formatting capabilities.
* CFLog1() uses writev/fprintf to write to stderr. Both these functions ensure atomic writes.
*/
public func NSLogv(_ format: String, _ args: CVaListPointer) {
let message = NSString(format: format, arguments: args)
CFLog1(CFLogLevel(kCFLogLevelWarning), message._cfObject)
}
public func NSLog(_ format: String, _ args: CVarArg...) {
withVaList(args) {
NSLogv(format, $0)
}
}
复制代码

我们可以看到 NSLog 调用

NSLog ------> NSLogv ------> CFLog1
复制代码

结合源码中的注释 CFLog1() uses writev/fprintf to write to stderr 基本可以猜到 NSLog 最终会调用 writev 和 fprintf 方法,接下来我们顺腾摸瓜看下 CFLog1 的逻辑

void CFLog1(CFLogLevel lev, CFStringRef message) {
#if TARGET_OS_ANDROID
if (message == NULL) message = CFSTR("NULL");
...
CFStringEncoding encoding = kCFStringEncodingUTF8;
CFIndex maxLength = CFStringGetMaximumSizeForEncoding(CFStringGetLength(message), encoding) + 1;
...
if (maxLength == 1) {
// was crashing with zero length strings
// https://bugs.swift.org/browse/SR-2666
strcpy(buffer, " "); // log empty string
}
else
CFStringGetCString(message, buffer, maxLength, encoding);
__android_log_print(priority, tag, "%s", buffer);
// ======= 注意这里 =======
fprintf(stderr, "%sn", buffer);
if (buffer != &stack_buffer[0]) free(buffer);
#else
// ======= 注意这里 =======
CFLog(lev, CFSTR("%@"), message);
#endif
}
复制代码

可以看到如果是安卓环境下,会调用 fprintf , 否则会调用 CFLog 方法

NSLog
↓
NSLogv
↓
CFLog1
↓
CFLog
复制代码

现在调用的顺序是这样滴,接下来往下走, 我们看看 CFLog

void CFLog(int32_t lev, CFStringRef format, ...) {
va_list args;
va_start(args, format);
_CFLogvEx3(NULL, NULL, NULL, NULL, lev, format, args, __builtin_return_address(0));
va_end(args);
}
// 调用了_CFLogvEx3
CF_EXPORT void _CFLogvEx3(CFLogFunc logit, CFStringRef (*copyDescFunc)(void *, const void *), CFStringRef (*contextDescFunc)(void *, const void *, const void *, bool, bool *), CFDictionaryRef formatOptions, int32_t lev, CFStringRef format, va_list args, void *addr) {
_CFLogvEx2Predicate(logit, copyDescFunc, contextDescFunc, formatOptions, lev, format, args, _cf_logging_style_legacy);
}
// 调用了_CFLogvEx2Predicate
static void _CFLogvEx2Predicate(CFLogFunc logit, CFStringRef (*copyDescFunc)(void *, const void *), CFStringRef (*contextDescFunc)(void *, const void *, const void *, bool, bool *), CFDictionaryRef formatOptions, int32_t lev, CFStringRef format, va_list args, _cf_logging_style loggingStyle) {
#if TARGET_OS_MAC
uintptr_t val = (uintptr_t)_CFGetTSD(__CFTSDKeyIsInCFLog);
if (3 < val) return; // allow up to 4 nested invocations
_CFSetTSD(__CFTSDKeyIsInCFLog, (void *)(val + 1), NULL);
#endif
CFStringRef str = format ? _CFStringCreateWithFormatAndArgumentsAux2(kCFAllocatorSystemDefault, copyDescFunc, contextDescFunc, formatOptions, (CFStringRef)format, args) : 0;
CFIndex blen = str ? CFStringGetMaximumSizeForEncoding(CFStringGetLength(str), kCFStringEncodingUTF8) + 1 : 0;
char *buf = str ? (char *)malloc(blen) : 0;
if (str && buf) {
Boolean converted = CFStringGetCString(str, buf, blen, kCFStringEncodingUTF8);
size_t len = strlen(buf);
// silently ignore 0-length or really large messages, and levels outside the valid range
if (converted && !(len <= 0 || (1 << 24) < len) && !(lev < ASL_LEVEL_EMERG || ASL_LEVEL_DEBUG < lev)) {
if (logit) {
logit(lev, buf, len, 1);
}
else if (loggingStyle == _cf_logging_style_os_log) {
// ======= 注意这里 =======
__CFLogCString(lev, buf, len, 1);
}
else if (loggingStyle == _cf_logging_style_legacy) {
// ======= 注意这里 =======
__CFLogCStringLegacy(lev, buf, len, 1);
}
}
}
if (buf) free(buf);
if (str) CFRelease(str);
#if TARGET_OS_MAC
_CFSetTSD(__CFTSDKeyIsInCFLog, (void *)val, NULL);
#endif
}
复制代码

会调用到 __CFLogCString__CFLogCStringLegacy 这两个方法,那么现在调用的流程是这样

NSLog
↓
NSLogv
↓
CFLog1
↓
CFLog
↓
_CFLogvEx3
↓
_CFLogvEx2Predicate
|
/ 
/     
/         
__CFLogCString   __CFLogCStringLegacy
复制代码

继续阅读源码 __CFLogCString__CFLogCStringLegacy 这两个方法最终都调用了 _logToStderr 方法

static void _logToStderr(char *banner, const char *message, size_t length) {
#if TARGET_OS_MAC
struct iovec v[3];
v[0].iov_base = banner;
v[0].iov_len = banner ? strlen(banner) : 0;
v[1].iov_base = (char *)message;
v[1].iov_len = length;
v[2].iov_base = "n";
v[2].iov_len = (message[length - 1] != 'n') ? 1 : 0;
int nv = (v[0].iov_base ? 1 : 0) + 1 + (v[2].iov_len ? 1 : 0);
static CFLock_t lock = CFLockInit;
__CFLock(&lock);
// ======= 注意这里 =======
writev(STDERR_FILENO, v[0].iov_base ? v : v + 1, nv);
__CFUnlock(&lock);
#elif TARGET_OS_WIN32
size_t bannerLen = strlen(banner);
size_t bufLen = bannerLen + length + 1;
char *buf = (char *)malloc(sizeof(char) * bufLen);
if (banner) {
// Copy the banner into the debug string
memmove_s(buf, bufLen, banner, bannerLen);
// Copy the message into the debug string
strcpy_s(buf + bannerLen, bufLen - bannerLen, message);
} else {
strcpy_s(buf, bufLen, message);
}
buf[bufLen - 1] = '';
fprintf_s(stderr, "%sn", buf);
// This Win32 API call only prints when a debugger is active
// OutputDebugStringA(buf);
free(buf);
#else
size_t bannerLen = strlen(banner);
size_t bufLen = bannerLen + length + 1;
char *buf = (char *)malloc(sizeof(char) * bufLen);
if (banner) {
// Copy the banner into the debug string
memmove(buf, banner, bannerLen);
// Copy the message into the debug string
strncpy(buf + bannerLen, message, bufLen - bannerLen);
} else {
strncpy(buf, message, bufLen);
}
buf[bufLen - 1] = '';
// ======= 注意这里 =======
fprintf(stderr, "%sn", buf);
free(buf);
#endif
}
复制代码

可见 NSLog 最终都调用了 writevfprintf 方法

NSLog
↓
NSLogv
↓
CFLog1
↓
CFLog
↓
_CFLogvEx3
↓
_CFLogvEx2Predicate
|
/ 
/     
/         
__CFLogCString   __CFLogCStringLegacy
         /
     /
 /
_logToStderr
↓
writev / fprintf
复制代码

结果与之前的注释一致,那么我们只需要使用 fishhook 对 writev / fprintf 方法进行hook就能达到我们的目的了,那么我们继续看看 print 函数的源码

print函数源码阅读

print的源码位置在类似 你的编译工程目录/Swift-Build/build/Xcode-RelWithDebInfoAssert/swift-macosx-x86_64 具体名字和编译参数和机器有关

我们很容易就找到了源码

// ============ print方法1
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方法2
public func print<Target: TextOutputStream>(
_ items: Any...,
separator: String = " ",
terminator: String = "n",
to output: inout Target
) {
// ======= 注意这里 =======
_print(items, separator: separator, terminator: terminator, to: &output)
}
复制代码

可见 print 方法会调用 _print 方法

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 源码
@usableFromInline
@_semantics("optimize.sil.specialize.generic.never")
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.
// Note: _isOptional doesn't work here when T == Any, hence we
// use a more elaborate formulation:
if _openExistential(type(of: value as Any), do: _isOptional) {
let debugPrintable = value as! CustomDebugStringConvertible
debugPrintable.debugDescription.write(to: ⌖)
return
}
if case let streamableObject as TextOutputStreamable = value {
streamableObject.write(to: ⌖)
return
}
if case let printableObject as CustomStringConvertible = value {
printableObject.description.write(to: ⌖)
return
}
if case let debugPrintableObject as CustomDebugStringConvertible = value {
debugPrintableObject.debugDescription.write(to: ⌖)
return
}
let mirror = Mirror(reflecting: value)
_adHocPrint_unlocked(value, mirror, ⌖, isDebugPrint: false)
}
复制代码

可见调用流程如下

print ------> _print ------> _print_unlocked
复制代码

这里的

TextOutputStreamable
CustomDebugStringConvertible
CustomStringConvertible
////////////////////   CustomStringConvertible
public protocol CustomStringConvertible {
var description: String { get }
}
////////////////////   CustomDebugStringConvertible
public protocol CustomDebugStringConvertible {
var debugDescription: String { get }
}
////////////////////   TextOutputStreamable
public protocol TextOutputStreamable {
/// Writes a textual representation of this instance into the given output
/// stream.
func write<Target: TextOutputStream>(to target: inout Target)
}
复制代码

等都是协议, 将 Target 传入并调用 Targetwrite 方法

我们回过头来看下函数名 internal func _print_unlocked<T, TargetStream: TextOutputStream>( _ value: T, _ target: inout TargetStream )

target 是遵循 TextOutputStream 协议的对象,也就是我们之前看到的 _Stdout () 函数

////////////// TextOutputStream 协议
public protocol TextOutputStream {
mutating func _lock()
mutating func _unlock()
/// Appends the given string to the stream.
mutating func write(_ string: String)
mutating func _writeASCII(_ buffer: UnsafeBufferPointer<UInt8>)
}
////////////// _Stdout
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 源代码
SWIFT_RUNTIME_STDLIB_INTERNAL
__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);
}
复制代码

我们可以看到

_Stdout -> _swift_stdlib_fwrite_stdout -> fwrite
复制代码

结合之前的调用方法, 最终也调用了 fwrite 方法

print -> _print -> _print_unlocked ->  (print items) -> (write/description.write/debugDescription.write)-> Stdout -> _swift_stdlib_fwrite_stdout -> fwrite
复制代码

绕了这么大的一圈,我们得出的结论是, print 函数最终调用了 fwrite

中场休息~~~~

最终,如果我们要日志监控的话,只需要hook如下三个方法

NSLog 调用  writev / fprintf
print 调用  fwrite
复制代码

Hook代码

  • 首先我们hook writev , 函数原型
static ssize_t writev(int a, const struct iovec * v, int v_len);
/// struct iovec 类型
struct iovec {
void *   iov_base;      /* [XSI] Base address of I/O memory region */
size_t   iov_len;       /* [XSI] Size of region iov_base points to */
};
复制代码

具体hook代码

//--------------------------------------------------------------------------
// MARK: hook NSLog
//--------------------------------------------------------------------------
// origin writev IMP
static ssize_t (*orig_writev)(int a, const struct iovec * v, int v_len);
// swizzle method
ssize_t asl_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]];
}
////////// do something  这里可以捕获到日志 string
// invoke origin mehtod
ssize_t result = orig_writev(a, v, v_len);
return result;
}
复制代码
  • 然后是 fprintf 函数,这里因为 fprintf 是可变参数,具体可变参数相关使用可见博主的另外一篇博客 va_list 可变参数概览

这里先使用 NSString 的 @selector(initWithFormat : arguments) 方法生成要输出的字符串,直接调用 origin_fprintf 将自行生成的字符串作为参数就行了,免去再次传递可变参数至 origin_fprintf

//--------------------------------------------------------------------------
// MARK: hook fprintf
//--------------------------------------------------------------------------
// origin fprintf IMP
static int     (*origin_fprintf)(FILE * __restrict, const char * __restrict, ...);
// swizzle method
int     asl_fprintf(FILE * __restrict file, const char * __restrict format, ...)
{
/*
typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list[1];
*/
va_list args;
va_start(args, format);
NSString *formatter = [NSString stringWithUTF8String:format];
NSString *string = [[NSString alloc] initWithFormat:formatter arguments:args];
////////// do something  这里可以捕获到日志
// invoke orign fprintf
int result = origin_fprintf(file, [string UTF8String]);
va_end(args);
return result;
}
复制代码
  • 然后是 fprintf 方法

调用例如 fprintf ("test"); 方法时候 asl_fwrite 会调用两次,参数一次是 test ,另一次是 n ,所以先将字符串放入 __messageBuffer ,等收到 n 时,再将 __messageBuffer 中转成字符串一次性读取

//--------------------------------------------------------------------------
// MARK: hook print for swift
//--------------------------------------------------------------------------
// origin fwrite IMP
static size_t (*orig_fwrite)(const void * __restrict, size_t, size_t, FILE * __restrict);
static char *__messageBuffer = {0};
static int __buffIdx = 0;
void reset_buffer()
{
__messageBuffer = calloc(1, sizeof(char));
__messageBuffer[0] = '';
__buffIdx = 0;
}
// swizzle method
size_t asl_fwrite(const void * __restrict ptr, size_t size, size_t nitems, FILE * __restrict stream) {
if (__messageBuffer == NULL) {
// initial Buffer
reset_buffer();
}
char *str = (char *)ptr;
NSString *s = [NSString stringWithCString:str encoding:NSUTF8StringEncoding];
if (__messageBuffer != NULL) {
if (str[0] == 'n' && __messageBuffer[0] != '') {
s = [[NSString stringWithCString:__messageBuffer encoding:NSUTF8StringEncoding] stringByAppendingString:s];
// reset buffIdx
reset_buffer();
////////// do something  这里可以捕获到日志
}
else {
// append buffer
__messageBuffer = realloc(__messageBuffer, sizeof(char) * (__buffIdx + nitems + 1));
for (size_t i = __buffIdx; i < nitems; i++) {
__messageBuffer[i] = str[i];
__buffIdx ++;
}
__messageBuffer[__buffIdx + 1] = '';
__buffIdx ++;
}
}
return orig_fwrite(ptr, size, nitems, stream);
}
复制代码

最后就是 hook 的代码,没啥好说的

//--------------------------------------------------------------------------
// MARK: fishhook调用
//--------------------------------------------------------------------------
// hook writev
rebind_symbols((struct rebinding[1]){{
"writev",
asl_writev,
(void*)&orig_writev
}}, 1);
// hook fwrite
rebind_symbols((struct rebinding[1]){{
"fwrite",
asl_fwrite,
(void *)&orig_fwrite}}, 1);
// hook fprintf
rebind_symbols((struct rebinding[1]){{
"fprintf",
asl_fprintf,
(void *)&origin_fprintf}}, 1);
复制代码

接下来我们看下成果

具体的代码,请见 github.com/madaoCN/Sup… 功能还在完善中,将间断更新

苹果宣布Mac用户福利:2大杀手级软件免费90天!

上一篇

Canvas常用API

下一篇

你也可能喜欢

评论已经被关闭。

插入图片

热门栏目

NSLog 和 print 源码阅读和捕获方案

长按储存图像,分享给朋友