
在介绍《全球通史》的一期我们提到作者以公元 1500 年为界,1500 年以后是地理大发现,全球走向统一的时期,其标志事件可以 1492 年哥伦布发现新大陆开始。那个年代之所以称为“地理大发现”,是因为此乃西方(或人类世界)第一次产生对全球的认知,西方从此不再认为“地中海”(Mediterranean Sea)是地球的中心,航海家们从世界各地发现千奇百怪的物种,带回大量的财富,也给当地文明带来毁灭性的灾难。这种全球地理大发现很大程度上归功于当时航海技术的飞速发展,以及欧洲政治上鼓励航海探险开辟新航线的政策。哥伦布在西班牙皇室的赞助下发现新大陆,达伽马在葡萄牙皇室的支持下绕过非洲开辟新航线。这些都对后来的经济全球化有极为深远的影响。
1497 年达伽马的船队绕过好望角,抵达印度洋,随后在 1499 年返回里斯本。自此葡萄牙利用新航线优势开始在印度洋的扩张,征服者中就有赫赫有名的“东方凯撒”、“海上雄狮”、“葡萄牙战神”——阿方索·德·阿尔布克尔克(Afonso de Albuquerque)。1515 年,阿方索晚年时期曾派遣使者带着礼物去跟“Cambay”的统治者苏丹商讨在第乌(Diu)上兴建堡垒一事。虽然事情告吹但是苏丹回了许多礼物,其中就包括一只巨大的犀牛。
阿方索也不知道该怎么处理这只大型动物,索性把它转赠给葡萄牙皇室。1515 年的欧洲,从来没有人见到过活的犀牛,另外要在海上运送一头将近一吨重的大型动物,在当时也是件极大的技术挑战。最终凭借葡萄牙先进的航海技术,这只犀牛漂洋过海,顺利运达里斯本,成为 1515 年欧洲一大新闻。当时有个叫 Valentim Fernandes 的印刷商在里斯本见到过这头犀牛,于是写了封信给德国纽伦堡的商会,描述了这头犀牛的外观。后来又有不知名的作者写了第二封信,附带一张犀牛的手绘草稿。当时我们知道 16 世纪的欧洲正处文艺复兴时期,当时身处纽伦堡的丢勒因为跟当地的印刷商颇有来往,所以他看到了这两封信。随后丢勒就创作了流芳百世的《丢勒的犀牛》木版画。他虽然没有亲眼见过犀牛,但仍凭文字描述和草稿画出了这幅极具表现力的作品。尽管有些细节并不符合现实,比如丢勒画的犀牛头顶有一个小角,腿上也覆盖了许多鳞片,但这并不妨碍丢勒的犀牛成为当时最流行的木版画。
如今英国伦敦的大英博物馆就收藏了几张丢勒的木版画。所以今天我们要介绍的这本书正是来自大英博物馆和 BBC 广播四台(Radio 4)联合制作的《大英博物馆世界简史》,英文原名为 A History of the World in 100 Objects。英文书名非常简单直白,就是由大英博物馆选取 100 件具有代表性的藏品,时间跨度从 200 万年前人类起源到今天。简体的译名只取了前半部分,我觉得不太好,因为我个人被这本书吸引的点其实是后半部分:精选 100 件馆藏。不过繁体的译名就更跳跃了——《看得到的世界史: 99樣物品的故事 你對未來會有1個答案》,连 100 这个数字都被拆成了两半。如果说简体译名是信息缺失,繁体译名就是整个搞错了。因为这本书其实是 BBC Radio 4 跟大英博物馆合作项目的产物,据称筹备了 4 年之久,首播是在 2010 年 1 月 18 日,每周播出 5 期。所以本书的大部分文稿实际上是为了广播节目而写的,目的是让听众可以在“看不见文物”的情况下也能听到有趣的历史,而不是“看得到的世界史”。
以文物的视角串联世界史是一件很特别的事情,大英博物馆始于 1753 年,距今 200 多年历史,馆藏十分丰富,像是来自帕台农神庙的浮雕,来自埃及的木乃伊,法老拉美西斯二世头像,以及来自复活节岛的摩埃石像等等。这世界上能够有底气拿出 100 件横贯人类史文物的博物馆没有几个,能把这件事情讲述得生动有趣,平易近人,则更是难得。
鉴于该项目本是为了广播节目而制作,文稿纂写自然要用平实易懂的语言。当时的馆长 Neil MacGregor——本书的主要作者——在制作这档节目时还担心民众能否仅通过聆听就能理解历史,结果节目刚一播出就大受好评。现在读者朋友们还可以在 BBC 的官网上下载该节目收听。
因为每周播出 5 集,所以 100 件文物被分为 20 个单元,每个单元 5 件。本书按照时间线来组织,除了第一节,馆长私心选择了“木乃伊”作为第一件藏品来讲述之外,其他的单元均按时间线排列。但是与此前介绍的《世界简史》不同,本书并没有把大家所熟知的(也许是西方所熟知的)历史大事件拿出来讲,比如像是原子弹爆炸,像是哥伦布发现新大陆之类的事件,本书并未细讲。相反,作者挑选了一些相对而言比较冷门的藏品,比如文首介绍的《丢勒的犀牛》。1515 年在世界史上可能并不算一个重大的转折点,但是促成丢勒画出犀牛木版画的前提条件却正好是世界史的转折期:地理大发现时期。如前文所述,如果不是奥斯曼土耳其帝国阻断了东西方贸易航线,那么就不会有那么多人去寻找新航线。如果不是达伽马在皇室的赞助下发现了绕过非洲好望角的新航线,就不会有后来的阿方索在印度洋的征服。如果不是西方航海技术的发展,这一切都不会发生,将近一吨的犀牛也不可能漂洋过海来到里斯本,丢勒也就不会画下这头想象中的犀牛。
所以文物本身可能只是个静态的物件,但它却能给后人讲述很多动人的故事,甚至当时制作它的人也无法预知它将在后世成为历史的见证。对于没有文字且已经消失在历史上的文明,这点显得极为重要。
在有文字的文明里,我们还可以通过文字资料来解读历史,但是像南美洲大陆很多文明尚未发展出文字就已经因为各种各样的原因消失了。比如阿兹克特帝国因为西班牙入侵者而毁灭,有自身分裂的原因,也有入侵者带来的枪炮与病菌。本书选择的《泰诺仪式用椅》就讲述了一个已经消失的美洲文明的故事。这个世界上没有留下文字就消失的文明还有很多,像古埃及文明的象形文字、美索不达米亚的楔形文字这些后人还可以通过新出土的文物来试图破解,但是想了解没有文字的文明就要困难得多。这种时候学者们“往往需要通过想象,穿过后世的重重解读去寻找真相,而这些做出解读的人常有着与失落族群不同的思维方式,本组语言中也没有能表达他们思想的现成词汇。”如果连学者都难以通过文物破解古代文明的谜题,作为外行的普通参观者就更难解其中奥妙了。

面对由绿松石拼接成的阿兹特克文物“双头蛇”,外行人可能只会说“好看”二字。而了解阿兹特克的专家就知道,蛇在他们的文化中是重生和复活的象征。阿兹特克有个叫做奎兹特克的羽蛇神,这件双头蛇文物很可能就是神的象征。
博物馆的展览有一个重要作用是教育民众,但是仅仅通过展品和旁边(可能)附带的简述其实是远远不够的。能够通过广播节目或书籍了解文物背后的故事是一件很棒的事情。不过因为本书的选品稍微有点另辟蹊径,所以可能在很大程度上绕过了读者朋友们的历史常识,虽然这并不影响我们阅读本作,但是如果能先了解“主流”世界史大事件,再读一次本作,则风味更佳。
近来俗事缠身,工作繁忙不分昼夜,本书每篇一件文物的结构很适合时间不充裕的我在碎片时间阅读,当然也每每在临睡前沉迷其中不愿放下。读罢本作,我想再看看那条陈放在黑色背景展箱里熠熠生辉的双头蛇。
2020.06.03/下午
于 T.i.T 创意园

新闻的获取渠道与接收方式多种多样,既可以主动订阅和筛选,也可以被动接收推送或训练推荐算法。
本期节目我们分享了各自获取新闻资讯的渠道和方法,也讨论了不同方式的特点和各自的看法,其中不乏一些好的方法供大家参考,也欢迎大家一起交流获取新闻资讯的经验和心得。
节目中我们提到的渠道和方式都会列在 Show Notes “相关信息”一栏中,如果在某些平台看不到 Show Notes 的朋友可移步我们官网阅读。
推荐使用泛用型播客客户端搜索“枫言枫语”来订阅收听本节目。我们也在国内的荔枝FM和喜马拉雅有同步音源。

今天同事提交了一个 Bug Fix,把基于 CFAbsoluteTimeGetCurrent() 计算的 Time Stamp 改成了基于 [[NSDate date] timeIntervalSince1970] 计算,原因是 CFAbsoluteTimeGetCurrent() 是从 2001 年 1 月 1 日 0 点开始计算的。
/* absolute time is the time interval since the reference date */
/* the reference date (epoch) is 00:00:00 1 January 2001. */
CFAbsoluteTime CFAbsoluteTimeGetCurrent(void);
那么问题来了,为什么 CF 的时间戳不是从大家熟悉的 1970-01-01 00:00:00-00 开始,而是从 2001 开始呢?一开始我想,是不是很早以前那个 epoch 32 位 Int 用完的问题,但是那个是 2038 年问题,跟 2001 年接近的是 Y2K 问题,也跟这个无关。
所以我们直接看 CF 的源码看看有没有线索。
CFAbsoluteTime CFAbsoluteTimeGetCurrent(void) {
CFAbsoluteTime ret;
struct timeval tv;
gettimeofday(&tv, NULL);
ret = (CFTimeInterval)tv.tv_sec - kCFAbsoluteTimeIntervalSince1970;
ret += (1.0E-6 * (CFTimeInterval)tv.tv_usec);
return ret;
}
很简单就是走内核接口取了下时间,然后 - kCFAbsoluteTimeIntervalSince1970,这个数字就是 1970 和 2001 的差距,以秒为单位。
const CFTimeInterval kCFAbsoluteTimeIntervalSince1970 = 978307200.0L;
所以这个函数的实现就是简单地减去了这么多秒,也没有留下注释。
最后在 Stack Overflow 的这个问题 Why are dates calculated from January 1st, 1970? 发现原来历史上有一直存在多种不同的 Epochs。只是因为 1970 是 Unix 用的,也是 POSIX 标准所以比较多人知道而已。
维基百科的 Epoch 词条里列举了 15 种 Epochs:
| Epoch date | Notable uses | Rationale for selection |
|---|---|---|
| 0 January 1 BC | MATLAB | |
| 1 January AD 1 | Microsoft .NET, Go, REXX, Rata Die | Common Era, ISO 2014, RFC 3339 |
| 14 October 1582 | SPSS | |
| 15 October 1582 | UUID version 1 | The date of the Gregorian reform to the Christian calendar. |
| 1 January 1601 | NTFS, COBOL, Win32/Win64 (NT time epoch) | 1601 was the first year of the 400-year Gregorian calendar cycle at the time Windows NT was made. |
| 31 December 1840 | MUMPS programming language | 1841 was a non-leap year several years before the birth year of the oldest living US citizen when the language was designed. |
| 17 November 1858 | VMS, United States Naval Observatory, DVB SI 16-bit day stamps, other astronomy-related computations | 17 November 1858, 00:00:00 UT is the zero of the Modified Julian Day (MJD) equivalent to Julian day 2400000.5 |
| 30 December 1899 | Microsoft COM DATE, Object Pascal, LibreOffice Calc, Google Sheets | Technical internal value used by Microsoft Excel; for compatibility with Lotus 1-2-3. |
| 31 December 1899 | Dyalog APL, Microsoft C/C++ 7.0 | Chosen so that (date mod 7) would produce 0=Sunday, 1=Monday, 2=Tuesday, 3=Wednesday, 4=Thursday, 5=Friday, and 6=Saturday. Microsoft’s last version of non-Visual C/C++ used this, but was subsequently reverted. |
| 0 January 1900 | Microsoft Excel,Lotus 1-2-3 | While logically 0 January 1900 is equivalent to 31 December 1899, these systems do not allow users to specify the latter date. Since 1900 is incorrectly treated as a leap year in these systems, 0 January 1900 actually corresponds to the historical date of 30 December 1899. |
| 1 January 1900 | Network Time Protocol, IBM CICS, Mathematica, RISC OS, VME, Common Lisp, Michigan Terminal System | |
| 1 January 1904 | LabVIEW, Apple Inc.'s classic Mac OS, JMP Scripting Language, Palm OS, MP4, Microsoft Excel (optionally), IGOR Pro | 1904 is the first leap year of the 20th century. |
| 1 January 1960 | SAS System | |
| 31 December 1967 | Pick OS and variants (jBASE, Universe, Unidata, Revelation, Reality) | Chosen so that (date mod 7) would produce 0=Sunday, 1=Monday, 2=Tuesday, 3=Wednesday, 4=Thursday, 5=Friday, and 6=Saturday. |
| 1 January 1970 | Unix Epoch aka POSIX time, used by Unix and Unix-like systems (Linux, macOS), and programming languages: most C/C++ implementations, Java, JavaScript, Perl, PHP, Python, Ruby, Tcl, ActionScript. Also used by Precision Time Protocol. | |
| 1 January 1978 | AmigaOS. The Commodore Amiga hardware systems were introduced between 1985 and 1994. Latest OS version 4.1 (December 2016). AROS, MorphOS. | |
| 1 January 1980 | IBM BIOS INT 1Ah, DOS, OS/2, FAT12, FAT16, FAT32, exFAT filesystems | The IBM PC with its BIOS as well as 86-DOS, MS-DOS and PC DOS with their FAT12 file system were developed and introduced between 1980 and 1981. |
| 6 January 1980 | Qualcomm BREW, GPS, ATSC 32-bit time stamps | GPS counts weeks (a week is defined to start on Sunday) and 6 January is the first Sunday of 1980. |
| 1 January 2000 | AppleSingle, AppleDouble, PostgreSQL, ZigBee UTCTime | |
| 1 January 2001 | Apple's Cocoa framework | 2001 is the year of the release of Mac OS X 10.0 (but NSDate for Apple's EOF 1.0 was developed in 1994). |
最接近现在的时间是苹果的 1 January 2001。因为乔帮主回归苹果后发布的 OS X 就是在 2001 年(有点像纪念 iPhone 发布时间,所有官方宣发的 iPhone 锁屏界面都停留在 9:41)。
所以 CF 接口的时间戳都从 2001 年开始,CoreData 也是。从上表可以看到,主流的编程语言如 C/C++, Java, JavaScript, Perl, PHP, Python, Ruby, Tcl, ActionScript 都是用的 1970 的 Unix Epoch,也许是因为这个所以给大家一种全世界都用 1970 的错觉。
这种“约定但不俗成”的时间潜规则让我想起几年前为了给 Finder 下载中文件加一个下载中的进度条,用了 Progress API。但是怎么设置都不生效,后来在 StackOverflow 的帮助下发现需要把正在下载的文件加一个特殊日期时间戳(NSFileCreationDate): 1984-01-24 08:00:00 +0000 才能生效。
这个时间,就是第一台 Macintosh 发布的时间。

酷壳有个经典文章: 一个fork的面试题 挺有趣的,不仅涉及 fork() 函数,还有一个缓冲区继承的技术点。
#include <stdio.h> #include <sys/types.h> #include <unistd.h>int main(void) { int i; for(i=0; i<2; i++){ fork(); printf("-"); } sleep(1); sleep(1);
return 0;
}
简单解释一下,上述代码只考虑 fork() 的话应该输出 6 个 "-"。因为有两层循环,i = 0 和 i = 1。
i = 0 时 fork() 出一个子进程,此时有两个进程,print 两次。i = 1 时这两个进程又各自 fork() 出两个进程,一共四个,print四次,所以一共六次。
但是这里还涉及 printf() 的缓冲设计,因为子进程在被 fork() 时会继承父进程的所有信息,包括缓冲区,所以有两个子进程在被 fork() 那一刻,拿到了父进程缓冲的 "-" 字符,加上自己的 print,总共会多出来两个 "-"。
此前在macOS 内核之一个 App 如何运行起来有介绍到被 fork() 的子进程会拿到所有的 vmap 之类的指针,所以理论上父进程所持有的内存就会自动被子进程继承,所以父进程buffered 数据子进程就可以接着往下走。
原文也提到我们可以加上换行符 "\n" 或调用 fflush() 来强行清空缓冲区。
我好奇的问题在于,这个 printf() 的缓冲是怎么设计的?他的源码是怎么写的?
stdout 的缓冲设计我们 printf() 实际上是往 stdout 标准输出写入数据。原文解释缓冲设计时还提到另外一个例子:
#include <stdio.h> #include <unistd.h> int main() {while(1) { fprintf(stdout,"hello-std-out"); fprintf(stderr,"hello-std-err"); sleep(1); } return 0;
}
上述代码在 macOS 上不会输出 stdout 只会输出 stderr(未触及 buffer size limit 的前提下)。
如果你把 "hello-std-out" 末尾加上 "\n" 他就能正常输出了。
printf() 的源码实现在 Libc, macOS 使用的版本可以在这里下载。
int
printf(char const * __restrict fmt, ...)
顶层实现比较简单,封装了几层加锁,优化之类的内部实现,我们直接看最底层 __sfvwrite。
/*
* Write some memory regions. Return zero on success, EOF on error.
*
* This routine is large and unsightly, but most of the ugliness due
* to the three different kinds of output buffering is handled here.
*/
int __sfvwrite(fp, uio)
里面判断 fp 传入的 flags,有三种情况要处理:
而 Libc 里提供的 stdin, stdout 和 stderr 定义如下:
#define STDIN_FILENO 0 /* standard input file descriptor */ #define STDOUT_FILENO 1 /* standard output file descriptor */ #define STDERR_FILENO 2 /* standard error file descriptor */FILE __sF[3] = { std(__SRD, STDIN_FILENO), std(__SWR, STDOUT_FILENO), std(__SWR|__SNBF, STDERR_FILENO) };
FILE *__stdinp = &__sF[0]; FILE *__stdoutp = &__sF[1]; FILE *__stderrp = &__sF[2];
#define stdin __stdinp #define stdout __stdoutp #define stderr __stderrp
至此我们可以看到 stderr 带上了 __SNBF flag,表示完全不 buffer,所以只要一调用 printf 它就会写入。
而 stdout 没有带这个 buffer,在 __sfvwrite() 的实现里,它先判断如果非 __SNBF 那就是要 buffer,然后判断 fp 没带上 __SLBF 那就是 fully buffered。
看到这里大家会不会有个疑问,__stdoutp 声明的时候不带 __SLBF flag 那为什么上述例子加上换行它就自动 flush 了?
这里 fp 的 flags 是随时可以被修改的,stdio 封装的 setlinebuf() 接口就可以把当前 fp 加上 _IOLBF mode,也就是带上 __SLBF flag。
在我们上面的那个例子中,如果我们加上
setbuf(stdout, NULL);
或者
setvbuf(stdout, NULL, _IONBF, 0);
fp->flags 就会被修改,stdout 就能及时被打印出来。
所以虽然 Libc 声明的时候默认是 fully-buffered 但是中间可能会被修改。至于内核具体在什么地方修改了我暂时没找到,不过我们可以参考这篇文章
GNU libc (glibc) uses the following rules for buffering:
| Stream | Type | Behavior |
|---|---|---|
| stdin | input | line-buffered |
| stdout (TTY) | output | line-buffered |
| stdout (not a TTY) | output | fully-buffered |
| stderr | output | unbuffered |
我跑上述例子的时候是在 terminal 用 gcc 编译然后 ./a.out 运行的,符合预期。

承接上期节目,我们继续讨论斯塔夫里阿诺斯的《全球通史》下册。
我也写了篇博客介绍这本书,有兴趣的听众朋友可以点开看看:枫影夜读 #3 L·S·斯塔夫里阿诺斯《全球通史》
推荐使用泛用型播客客户端搜索“枫言枫语”来订阅收听本节目。我们也在国内的荔枝FM和喜马拉雅有同步音源。

3月猛男必玩的捡树枝游戏《动物森友会》你玩了吗?
各位听众朋友们好久不见,本期节目我们两个不专业的历史门外汉跟大家一起闲聊历史,主要讨论的是以全球史观看待整个地球人类史的著作,斯塔夫里阿诺斯的《全球通史》。顺便再聊两句动森XD。
我也写了篇博客介绍这本书,有兴趣的听众朋友可以点开看看:枫影夜读 #3 L·S·斯塔夫里阿诺斯《全球通史》
推荐使用泛用型播客客户端搜索“枫言枫语”来订阅收听本节目。我们也在国内的荔枝FM和喜马拉雅有同步音源。

以前历史课老师曾给我们推荐过《全球通史》这部著作,因为老师的教授风格很有趣,所讲述的观点和视角也比课本上的丰富得多,所以一直记得这本书。最近北京大学出版社出版了第7版新校本,在全网热推,正好看到就买了,果然是一部佳作。
本书的全名为《全球通史:从史前到21世纪》(A Global History - from Prehistory to the 21st Century),作者是L·S·斯塔夫里阿诺斯,一位在加拿大出生的希腊裔美国历史学家。这本书共分上下两册,以公元1500年划分,最早于1971年出版。
我们在学校里学习世界史的时候,虽然会提及全球各个大陆的历史,但是通常都是以中国为中心去看的。比如说1840年鸦片战争,虽然前半部分会阐述工业革命以来英国生产力的大发展,但是后续就开始以中国视角面对帝国主义列强的入侵了。反过来说,西方的史学界也经常以“西欧中心论”来看待历史。
作者写这本书时,试图以全球的视角来看待不同地区的文明直接的碰撞与交流,所以相较之下本书会更多着墨于各个地区之间的联系。当然,本作毕竟成书较早,通读下来依然有一种“西欧中心”的感觉,这主要来自于作者本身的局限性。一个人毕竟很难掌握世界上全部的语言,更罔论通读各个地区各个国家的历史资料了。所以编写全球史的人在论及自己不太熟悉的国家与地区时,通常都需要阅读第二手资料,而由此产生的谬误与浅层理解就无法避免。作者书写西欧历史时可谓神采飞扬,中东伊斯兰世界也可圈可点,但是书写中国、印度等地区时则显得较为单薄。
不过作为一个习惯了中国视角的普通读者来说,本作依然能给我带来一个新颖的角度,从中也受到了不少有趣的启发,我以为本作非常适合作入门科普阅读。
人类出现在地球上可以追溯到400万年前,但是文明的出现至今不足6000年,假如把人类历史缩放到一天,那么文明只占最后的两分钟(129秒)而已。
但是人类世界的发展曲线却在最后几千年呈现指数爆炸的变化,越靠近现代变化速度越快,所以本书对历史的断代在时间上是不均匀的。
另外,历史上的时间分界点也只是一个大概的日期,确定这样的时间点只是为了方便起见。事实上历史是过渡渐进的,所以不管把过渡时间指定为1年、10年还是100年都没有意义。
作者为上下两册选择的分界点是公元1500年。在这个过渡阶段里有一个重要的标志性事件:1492年哥伦布发现新大陆。同样,这个事件也是全球各地一系列发展的结果,而不是1492年或者1500年历史的车头突然发生90度转弯。
本书上册主要讲的是史前人类到公元1500年这段时间。
史前人类的历史有两个重要的转变:一是灵长类逐渐转变为有思维能力的真正的人类;二是人类的先祖从坐享大自然恩赐的食物采集者,转变为日益摆脱大自然束缚、掌握自己命运的食物生产者。所以简单来说史前这段数百万年的演化史最重要的是进化为人类和农业革命。
接下来是文明出现(约公元前3500年)到公元500年之前,作者从美索不达米亚的苏美尔文明一路讲到西欧因蛮族入侵导致西罗马帝国灭亡。
然后是公元500年到1500年漫长的中世纪。上册大量的篇幅在讲述欧亚大陆上各种族之间的互相影响,像是古希腊与埃及文化的交流,像是突厥人和蒙古人从东亚一路打到欧洲等等。然后作者又用两个“编”的篇幅讲述了非欧亚大陆世界和诸孤立地区的世界。
相比于我们熟悉的中国中心的历史,这种相对来说更以“西欧”为中心的历史提供了一种不同的视角。虽然作者在本书中试图以“全球史观”来看世界,但是在我读来,全球除欧洲以外各地区的篇幅和分析仍不够西欧地区来得详尽。不过我并不是史学专家,能以这样的视角看世界已经是非常不错的体验了。
如前文所述,人类世界在加速发展,1500年以后虽然只有短短数百年,但是却发生了比过去几千年加起来还多的巨变。其中对当今世界最为深刻的影响就是全球化。西方从文艺复兴时期开始兴起的技术革命、社会革命,在几百年时间里让一度落后于世界的地区,一跃成为征服全球的殖民者。
下册的第一个阶段是1500-1763年。因为伊斯兰世界的兴盛,西方与东方的传统贸易路线被切断,西欧人想要买到东方的香料只能通过威尼斯人中转。于是让威尼斯和阿拉伯商人赚得盆满钵满。现在我们不太了解“香料”这个东西为什么在当时能有这么大的诱惑力,本书也未作详尽解释,这里我们知道“香料”的利润率极高就行。麦哲伦的船队绕地球一圈,整个舰队最后只剩一艘船摇摇欲坠,但是他带回的香料依然足够支付整条舰队的全部费用。可见“香料”是只要肯冒险走一趟就能发家致富的生意。
于是1500-1763这段时间里,巨大利益的诱惑和陆上商路的阻隔促使西方人寻找新的贸易路线,此时积累的航海技术也得到一个正向循环的发展。于是有了寻找通往东方新航线的大航海时代,也即所谓的“地理大发现”时代。
要知道这一阶段西方依然不是最强的地区,不论是与世隔绝的中国还是鼎盛时期的中东伊斯兰世界,都是比西方发达的地区。直到1763-1914时期,作者作为下册的第二阶段:西方据优势地位时期,西方列强才开始征服世界。
我们上面也说过历史是渐进发展的,工业革命并不是一夜之间爆发的。长期以来主要是科学革命、工业革命和政治革命三个相互影响,相辅相成的重大转发在西方发生了,最终给了西方以不可阻挡的推动力和力量。
这段时期包括我们所熟知的英国建立日不落帝国,西方对非洲的瓜分,亚洲的殖民,跟我们息息相关的鸦片战争等等。直到1914年第一次世界大战爆发。
最后一个阶段是1914年以来,西方衰落与成功。一战二战对全球的影响无疑是巨大的,无论是战争摧毁的城市还是旧有国家制度,先破而后立,全世界在战后的废墟之上发生了天翻地覆的变化。
殖民初期盛行的白人至上论,借着达尔文主义的兴起,成为西方殖民的正当理由。但是在几百年时间里建立起来的全球霸权,却在战后短短数十年时间里土崩瓦解。诸殖民地纷纷宣告独立,一个个新兴国家建立起来,最终才有了我们现在所生活中的世界。
作者在每一编的最后都会专门写一章“历史对今天的启示”,在章节里也经常会对历史事件对今日的影响作出评述。像是黑奴在西方盛行的时期,借着达尔文的《物种起源》发展为“白人至上论”,可谓今日种族歧视之根源。其中我觉得最有意思的是技术革命与社会革命之间的脱节。
人们一般都比较容易接受技术革命,因为技术上的发展通常都能提升我们生活的幸福感。这点在互联网兴盛的当下可以说体会至深。但是在人类智力发展的同时,却又不具备掌握该智力的智慧。“环保”这个概念喊了这么多年,但是在生产力发展,经济发展的大趋势下依然没有十分有效的手段可以两者兼备。互联网技术的发展也带来了极大的效率提升,但是机器取代人类之后,人类的工作时间反而增加了。种种矛盾现象实际上无不体现出人类在社会革命上与技术革命的严重脱节。
史前时代的人类通常每周只需15到20小时采集食物,但是随着农业革命的到来,变化开始发生,直到第一次工业革命,直到二战后的高科技革命。个体的生产力在提升,但是工作时间也在剧增。1900年美国人每周平均工作60小时。以前可能听说日本有“过劳死”的案例,现在国内也时不时会有猝死的报道。虽然个人的生产力提升了,但是如果所有人都具备一样的生产力基础,那么相对生产力就没有差异。这时候竞争激烈的公司之间,只能通过堆人力堆时间来互相对抗,这个现象在国内的互联网行业尤为普遍。
在信息时代,形成技术壁垒远比过去困难得多。大航海时代葡萄牙可以通过掌握自己的航线来跟西班牙竞争,先行者们也可以通过控制重要港口来打击对手。但是在信息流通如此迅速的今天,达成技术壁垒的门槛非常高。即使是领跑世界的科技公司Google也在近来开始取消其著名的20%时间,开始削减各项公司福利。
1992年,美国劳工联合会前主席威廉姆·格林断言:“唯一的选择就是失业或休息。”结果选择就是失业。公司高管都拒绝接受缩短工时的建议,因为这样增加的劳动成本将使他们的公司受到国内外竞争对手的攻击。
所以可以说,在人类社会发展出新的正向循环的竞争机制之前,当下的社会就一定是有人会去做堆时间堆人力的竞争,而一旦有人这么做了,其他人就得跟进,最终结局就变成要嘛加班要嘛倒闭。
作者在本书中还讲述了其他意义深刻的观点与启示,比如社会的不公平,比如自我毁灭倾向。可以看到人类历史上作出的重大发展都有其二元性,既有好的方面也有坏的方面,既是破坏也是进步。所以作为生存在地球上的一个普通个体,如何去看待这个世界呢?作者的态度还是比较乐观的。
虽然我们的历史充满了矛盾,技术的进步带来了生产力的发展和“核冬天”的恐怖。但是人类之所以特别,是因为我们学会了利用自然环境来满足自身的需求——使环境来适应我们的遗传特点而不是相反。当然这种做法也引发了对自然环境的极大破坏,但是至少人类不是命运的产物,人类通过高科技已经摆脱了部分可能的灭绝威胁:绕地小行星的爆炸和早已形成的新冰川纪的袭击。
纵观历史,我们不仅要看到人类科技、社会的进步,也要看到我们在公平性,时间上的倒退。作为一个普通的个体,历史的洪流在往前奔涌,我们能做到的无非是以史为鉴,少走弯路,看清自己所处的环境。在无法改变环境之前要适应,在能够改变的时候试图做些改变。这不仅需要勇气,也需要智慧与毅力。
如前所述,编写全球史对作者有着极高的要求,他可能熟悉自己母语国家的历史,却很难精通世上所有语言。所以在讲述东亚诸国历史时,尤其是我们所熟悉的中国历史,总觉得作者的跳跃比较大,而且有些强行把中国历史切分到他所设定的断代节点中的感觉。比如说公元1500年上下,西方虽然开始了地理大发现时代,但是中国却处于闭关锁国之态,虽然1405-1433年间有过郑和下西洋事件可与地理大发现挂钩,但是总的来说并没有明显变化。所以这样的分界可能更靠近西方世界的变革多一些,如果以西方自地理大发现以来积累导致的技术革命,将对未来的全球造成统一的影响来看,那么还是合理的。只是对于与世隔绝的中国来说,硬套进这个框架里还是有点脱节。
本书自1971年出版以后,作者对其进行了多次修订,增添了不少内容,比如冷战时期的内容。作为一部时空跨度极大的史书,可能许多人的刻板印象是按照时间轴陈列事件,内容艰涩生硬。但是本书却浅显易懂,文风流畅,读起来毫不费力。而且虽然主线是从古至今,但作者时时将历史事件与今日时势结合作出点评,发人深省。
本书也需要对不同方面的内容作出取舍,所以如果想单独了解专一领域的历史就不合适了。比如基督教的诞生与发展的历史其实非常的迂回曲折,但是本书并不会过多涉及。这也带来另一个问题,假设你之前并不了解基督教,那么阅读起来在部分章节就会有点不知所云。像是基督教后来怎么分出天主教、东正教之类的,他们的教义有什么区别,教派有什么分歧之类的。这样完全没有背景的读者读起来可能会有点一头雾水。
但是总的来说,我非常欣赏这种全球史观的作品,对于作者着墨于各地区的联系这点也觉得十分有趣。再加上阅读起来非常轻松,所以是值得推荐的好书。

1973 年 Xerox PARC 第一次在 Xerox Alto 这款个人计算机上推出带有 GUI 界面的操作系统,自此让极大地降低个人计算机的使用门槛,也开启了更加丰富多彩的计算机发展。
不过作为一个码农,终端依然是平日不可或缺的生产力工具。在 macOS 上,系统自带的 Termianl.app 或者更加好用的开源的 iTerm2.app 是最受欢迎的终端应用(其他 X Windows 系统也有像 xterm 之类的优秀应用)。他们也都是一个 Cocoa App。那么一个 Cocoa App 是如何把自己变成一个能跟用户通过键盘交互,有标准输入输出的“伪终端”(Pseudoterminal)的呢?
在带有电子显示器的终端发明以前,人们真的就是在一台带键盘的打印机上,一边打字输入,一边等待计算机在纸上打印输出。所以大家写 Hello World! 的时候都是用 print("Hello World!"),因为它是真地在打印。
第一台带有显示器,支持 ANSI escape codes 的终端是 DEC 公司生产的 VT100。在这之前他们已经生产过很多种型号的电子终端,不过这台机器是最成功的。
我们知道 ls 这个命令在 Unix 系统里就是一个 binary,一般放在 /bin 或者 /usr/bin 这样的目录里,用 whereis 可以找到它在哪里。
whereis ls
ObjectiveC 在 Foundation 里提供了 NSTask 这样的高级封装,用它的接口可以非常简单地实现类似 shell command 的效果。
但是首先一个沙盒 App 的能力是有限的,其次就算是沙盒外的 App,NSTask 也不允许直接访问 /usr/bin 目录里的 binaries,直接调用要嘛无响应要嘛直接 crash。
所以我们还得迂回一下,我们不直接运行 binaries,而是利用 bash 来运行:
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/bin/bash"];
[task setArguments:@[ @"-c", @"/usr/bin/killall Dock" ]];
[task launch];
但是即便如此,想要使用 NSTask 的接口来模拟终端还是非常困难的事情。所以,Termianl Apps 们是怎么实现的呢?
iTerm2 的代码是开源的,历史原因内部实现比较复杂,而且 iTerm2 支持在 Cocoa App 里直接和 python 脚本交互,相当于他提供了一套桥接的接口,可以用 python 来实现对 iTerm2 App 的自动化,类似 Hammerspoon 这类 App 的效果。所以阅读过程中我还看到一堆 client/server 的通信,有点绕。
最后我发现真正实现终端功能的地方在这里: iTermPosixTTYReplacements.c,关键函数是:
int openpty(int *amaster, int *aslave, char *name, struct termios *termp, struct winsize *winp);
这个函数的实现在 Libc 里,可以参考苹果开源页面。
openpty() 是 BSD 函数,并不在 POSIX 标准里,不过 Linux 也有把这个函数 port 过去。从应用层的角度来看,openpty() 会跟 open("/dev/ptmx") 获取一个可用的 pseudoterminal。iTerm2 的做法就是通过该函数获得一个 pseudoterminal master 和 slave 的 fd 句柄,后续用户在 UI 界面上的输入都通过这两个句柄来交互。
iTerm 在 openpty() 之后还 fork() 了一下自己,然后父进程释放所有的句柄,这样父进程处理 UI 输入,一个窗口对应一个子进程,一个子进程对应一个 pty。
为什么 Unix 要这么设计 pty 接口呢?历史原因。
早期的计算机比如 1970 年 DEC 生产的 PDP-11,他需要通过一系列的电线跟用户的终端(也就是键盘和打印机)连接到一起。这种只有键盘和打印机的终端也叫做 TTY。后来有了电子显示器之后,就得使用软件模拟一个硬件终端,也叫做"伪终端"(pseudoterminal)。
UNIX 采用的设计是加入了一个中间层,当你使用 openpty() 打开一个伪终端的时候,会给你一个 master 一个 slave 句柄。GUI 软件把键盘输入作为 master 的 input 写入,master 的 output 就会作为 slave 的 input 写入,然后再作为 output 输出。所以对于我们的 Cocoa App 应用层来说,可以简单地把 master fd 作为 writer,把 slave fd 作为 reader。
听起来好像没什么必要但是其实 slave 做了一些特殊的处理。比如 GUI 直接把键盘输入的 CTRL+C(0x03) 写入 master 句柄。这时候 slave 接收到后会把 0x03 转换成 SIGINT signal 发出。对此感兴趣的同学可以参考微软关于 ConPTY 的这篇文章。
所以 iTerm2 既是一个 Cocoa App 又是一个“终端模拟器”,你可以在这个 App 里跑任意 shell 命令。
openpty() 这种 master/slave fd 的设计还体现在 SSH 远程登录上。可以参考 macOS 的 OpenSSH 源码。客户端通过 SSH 协议连上服务端时,服务端的 sshd 进程开了一个 pty 用来跑客户端输入的命令。
另外 VSCode 也基于 Node.js 实现了一个编辑器内的 console,源码在这里。
回到我们的 Cocoa App 来,一个 NSTask 对象在被 launch() 之前我们可以当做是一个数据存储的结构体来对待。通常我们会直接调用它的 launch() 方法,然后使用 NSPipe 来读写。
这里如果要绕过上文所述的 crash 问题,我们可以改用 openpty():
NSCAssert(openpty(&masterFD, &slaveFD, NULL, NULL, NULL) == 0,
@"A pseudoterminal couldn't be opened.");
*readHandle = [[NSFileHandle alloc] initWithFileDescriptor:masterFD closeOnDealloc:YES];
*writeHandle = [[NSFileHandle alloc] initWithFileDescriptor:slaveFD closeOnDealloc:YES];
有兴趣的读者朋友不妨一试。
We love mini apps, we'd like to keep our apps to be useful while in a mini style.
In this update, we've fine-tuned details of the user interface, to make it simpler and cleaner. The weather illustrations are merged into the canvas now, it looks even better in the dark background.
There's a brand new mini mode in this version, it's a square window that only shows current weather illustration with limited information, which is a great way for keeping it on the desktop or in another monitor if you were using multiple displays.
We've added more mouse controls in this version, now you can double click the artwork to toggle between normal and mini mode. The artwork supports control-click for quick operation too. If the default conditions were not your favorite order, you can simply reorder them by drag and drop.
Unlike iOS, many users are still using macOS Majove or even El Capitan, so we've restructured the whole codebase to support that, we hope more and more people can enjoy this mini weather experience on their Mac.

大家好!这是一期怀旧节目,我和自力邀请了好朋友同时也是资深老前端,前端观察的站长——神飞,来跟我们一起漫谈前端史。
前端的发展离不开网络基础的发展,自 1990 年从万维网演变至今天的互联网,短短几十年风云变幻,波澜壮阔。我们三个都曾经是前端开发,时间或长或短,如今在客户端、交互设计以及后台的岗位上继续随着浪潮起伏。前端领域里的技术更迭瞬息万变,有吐不完的槽也有怀不完的旧。各位听友不妨戴上耳机,跟着我们,一起回到过去,重温 96169 拨号上网的时代。
推荐使用泛用型播客客户端搜索“枫言枫语”来订阅收听本节目。我们也在国内的荔枝FM和喜马拉雅有同步音源。

因为最近新冠疫情的关系,很多团队已经开始尝试在家远程办公。但并非所有团队都有远程办公的经验,对我们两位主播来说也是首次尝试。
所以本期节目我们邀请到 9 位业内朋友,听听看他们这次疫情对于他们的团队的影响,以及他们远程办公的看法。
这些朋友来自国内外的创业团队,程序员,设计师,投资人,还有偏传统行业的朋友。每个人的访谈都很有意思,但是由于节目时长的关系,我们没法把所有人的录音都剪到正式节目里,所以我们把完整的采访录音也都放在 Show Notes 里面,大家可以点击链接进行收听。这里有各种不同的角度和看法,值得一听。
顺序不分先后
推荐使用泛用型播客客户端搜索“枫言枫语”来订阅收听本节目。我们也在国内的荔枝FM和喜马拉雅有同步音源。

今年(2020年)的春节对全国人民来说都是非常特别的一个假期,由于"武汉肺炎"(新型冠状病毒)的爆发,全国进入警备状态,人均口罩,窝在家里不出门。同时年轻人劝自己的父母长辈戴口罩、取消家族聚餐等等举措亦成为一种新的流行。
从我个人的角度来说,不仅劝说长辈一事遇到了一些矛盾与冲突,需要学习和尝试新的沟通技巧,而且在过去的几个月时间里,我自身也遇到了不少事情需要我不断打破过去的习惯,学习新的处事方式来应对不断变化的工作与生活。
有些事情在已经掌握方法的人眼里:"这不就是件小事嘛"。但是对于没有门道或者掉进陷阱的人来说,这可能是难以逾越的高墙。如果你具备了解决问题的能力,那么能够理解高墙另一侧的人的心态就成为沟通的关键;如果你不具备解决问题的能力,那么如何提升自己的弹跳能力,或者想办法绕过高墙,就成为自我发展和成长的关键。
上一期我们跟大家分享了 Jordan Peterson 的《人生十二法则》这本书,里面讲了许多切实可行且行之有效的人生道理。但是我在文章与播客都有提及,书中并未指出如何提升自我认知的方法,有些例子也比较北美,中国读者可能比较难感同身受。所以本期节目我们给大家分享另一本关于自我发展的心理学作品,就是心理咨询师陈海贤老师的《了不起的我》。
推荐使用泛用型播客客户端搜索“枫言枫语”来订阅收听本节目。我们也在国内的荔枝FM和喜马拉雅有同步音源。

今年(2020年)的春节对全国人民来说都是非常特别的一个假期,由于"武汉肺炎"(新型冠状病毒)的爆发,全国进入警备状态,人均口罩,窝在家里不出门。同时年轻人劝自己的父母长辈戴口罩、取消家族聚餐等等举措亦成为一种新的流行。
从我个人的角度来说,不仅劝说长辈一事遇到了一些矛盾与冲突,需要学习和尝试新的沟通技巧,而且在过去的几个月时间里,我自身也遇到了不少事情需要我不断打破过去的习惯,学习新的处事方式来应对不断变化的工作与生活。
有些事情在已经掌握方法的人眼里:"这不就是件小事嘛"。但是对于没有门道或者掉进陷阱的人来说,这可能是难以逾越的高墙。如果你具备了解决问题的能力,那么能够理解高墙另一侧的人的心态就成为沟通的关键;如果你不具备解决问题的能力,那么如何提升自己的弹跳能力,或者想办法绕过高墙,就成为自我发展和成长的关键。
上一期夜读我们跟大家分享了 Jordan Peterson 的《人生十二法则》这本书,里面讲了许多切实可行且行之有效的人生道理。但是我在文章与播客都有提及,书中并未指出如何提升自我认知的方法,有些例子也比较北美,中国读者可能比较难感同身受。所以本期夜读我们给大家分享另一本关于自我发展的心理学作品,就是心理咨询师陈海贤老师的《了不起的我》。
1 月 21 日,还在公司加班的我看到"丁香医生"做的"武汉肺炎"的确诊报道页面,以及许多其他关于此病的报道,深感疫情严重。当时我和朋友说起希望劝说家里的长辈戴口罩,最好是春节哪儿都不要去,但是我当时的态度是"这是不可能的"。那天我的朋友已成功劝说他的父母春节不要出去,在家过,同时告诉我他的秘诀:不停往家人群里发各种疫情的报道。
当时劝说戴口罩和春节在家的流行尚未起来,疫情报道还停留在 100 人以下确诊的时候。没有外部舆论的助力,我觉得我的劝说希望渺茫。但是那会儿我已经在阅读《了不起的我》这本书了。除了此前提到过的自我分为"情感的大象"和"理智的骑象人"以外,陈海贤老师还提到,要学会"控制的两分法",即:努力控制自己能控制的部分,不要试图去控制自己无法控制的部分。
所以我当时听朋友这么说,虽然不抱太大期望,但也开始往我的家人群里各种转发疫情相关报道,每天发一两次,同时打电话给父母,告知他们疫情还是很严重的。一开始父母的态度在意料之中:我们老家不在疫区,不用紧张,口罩什么的不要紧的。当时公司前台已开始发口罩,我每天离开办公室都要戴上口罩。于是我戴着口罩发了张自拍发到家人群里,身体力行地告诉家人要重视这件事情。
父母长辈怎么看待疫情我无法控制,他们要不要取消春节聚餐我也无法控制。我能控制的部分是多打电话,多往家人群里普及疫情的资讯。最后的结果是,今年春节的除夕、初一、初二三天的家族聚餐全部取消了。这大大出乎我的意料之外。当然这并不是我的功劳,我在其中起到的作用是微乎其微的。不过这出乎意料之外的结果却让我对本书理论的印象更深了。
在本书第一章,作者援引了心理学家罗伯特·凯根(Robert Kegan)的"心理免疫的 X 光片"方法,分析了为什么我有时候明明想要改变,却总是往相反的方向做事情。我们可以用这个 X 光片的方法来分析一下劝父母取消家族聚餐这件事情。这个方法需要你把自己的心理分为四栏分别填入:
这里当然是希望父母家人全都平安,但这是愿望不是行为,直接的行为是"取消家族聚餐"。
我们正在做哪些跟目标完全相反的行为
一开始我觉得没必要去劝说,说了也没用,所以这个行为是"没有任何劝说,或者劝说力度很小"。
这些与目标相反的行为有哪些隐含的好处或可以避免的损失
我觉得劝说没用,但是没用并没有损失,所以更深一层其实是害怕跟家人发生矛盾。怕这个劝说从"你不要过分紧张啦"之类的敷衍演变成更激烈的冲突。所以这里的好处是:避免冲突。
内心有一个重大的假设,这个假设是什么?
这里 #3 的好处是避免冲突,那么为什么会冲突呢?因为我心里有一个重大假设,这个假设就是:说了也一定不会听。那么这个假设成立吗?从今年的结果来看,这个假设完全不成立。取消初一、初二的聚餐需要我的父亲和外公去行动,他们的行动已经表明了他们的态度:他们非常理解疫情的严重性和取消聚餐的必要性。
根据三段论,大前提都是错的,那么后面不管怎么推理都是错的。所以这个例子里面我的努力仅仅是转发了微信消息,告知疫情的严重性,实际上父母长辈是否真的有了解到疫情的讯息,是否真的觉得有取消聚餐的必要这些完全不是我能控制的。但是最终结果是:家族聚餐真的取消了。
不管我的努力到底有没有作用,至少这个结果在鼓励我的情感大象,告诉我的大象这一小步走出去是有用的。只要能一小步一小步的往前走,跟自己比,总是能变成一个更加强大的自己。
2020 年我立的一个 flag 是想变成一个 Tough Guy。我以前不是,希望未来是,所以这中间就需要"改变"。改变是一件很难的事情,即使对于掌握充分理论知识的心理学家来说也很难,何况一无所知的普通人。
所以学习《了不起的我》,不能让你立刻就产生改变,但是可以帮助各位读者朋友,掌握多一点点的门道,学会多一些可以付诸实践的技巧,我觉得这已经很棒了。
"焦虑感"仿佛已经成为现代人心理健康的头号大敌,各种营销号"贩卖焦虑"收割颇丰。如果你仔细观察你身边的同事你会发现,不管是初入职场的新人还是经验丰富的老人,无论是夹心饼干的基层管理还是收入爆表的公司大佬,多数人的脑门上都常常写着"焦虑"二字。虽然多数时候工作还是要继续,生活还是要照过,但是"焦虑来袭"可以说是在繁忙都市的白领中最常见的一种消极情绪了。
我对这种情绪当然也十分熟悉。不过此前在《人生十二法则》的夜读/播客中我曾提到,遇到情绪是最好的分析自我的时刻。所以消极情绪并不可怕,是大脑给我们正常的也是必须的反馈。关键是我们遇到消极情绪时如何去应对。
"改变"是应对消极情绪最常见的做法,只是我们可能对"改变"的了解不多。我们已经知道,当现实世界不符合我们的预期时,我们就会有怨恨、愤怒、悲伤等情绪出现。有的时候我们在工作中在学习中要做一些自己并不喜欢的事情时,会觉得"我没有选择"。
但其实"每个人都有选择"。你选择了"我没有选择",这也是一种选择。
本书第一章,作者通过"每个人都有选择",打破"我没有选择"的障碍。其实选择有很多,可能性有很多,你说没有,只是因为你已经在众多可能性中选择了当下这种而已。接下来作者用"情感的大象"和"理智的骑象人"作比喻,解释了为什么我们已经下决心要改变却很难做到。
比如一个人如果想戒烟,他可能会说我从今天起不抽烟就好了。但是情感的大象是很强壮的,平时它听话的时候骑象人要它往左它就往左,但是当你要戒烟了,情感的大象平时收到抽烟的那些种种好处的反馈突然没了,它发怒了,这时候骑象人的力量是完全拉不动这头大象的。这就是为什么我们很难改变旧有经验的原因。
再加上前文所述的"心理免疫原理": 人的心理免疫系统会阻碍一切改变,无论好坏。当你想要改变的时候,骑象人想往戒烟的方向走,可大象却会努力阻止骑象人。"就像一辆车,一脚踩着刹车一脚轰油门,只能原地打转,痛苦地消耗而已。"
所以在第一章中,作者先讲述了难以改变旧有经验的原因,然后提出了"小步子原理",像文首的例子一样,不要管长辈听还是不听,先把疫情文章转了,走出一小步。然后像情感的大象感受到这一小步带来的好处,才有助于让它缓和下来,配合骑象人一起往自己心中想要的方向去慢慢修正。
但是在第一章的最后一节,作者又提出了一个打翻本章"改变"主题的观点: 改变真的有效吗?
前面的小节都在说我们为什么难以改变,我们如何能够拉动大象实现改变。但是这个前提是: 改变真的有用,我们需要改变。然而我们的真的遇到什么问题都需要改变吗?不一定,有的时候"改变"反而会成为我们逃避问题的借口。
书中举了一个例子,一个年轻人毕业三年换了五份工作,每次换工作的原因都是觉得这份工作不是自己想要的。所以看上去好像"换工作"是在积极"改变",是在寻找自己的人生意义。但是真的是这样吗?
改变有两层意义:
"有时候,改变作为应对方式本身,也需要改变,在心理学上这叫"第二序列改变"(保罗·瓦茨拉维克 Paul Watzlawick《改变:问题形成和解决的原则》)。例子中的年轻人想要改变的,是工作这个内容,但是他真正想要改变的却一直没有变的,是通过换工作来应对焦虑的这种方式。盲目寻求变化,没法安顿下来踏踏实实积累经验,这才是他真正的问题。"
其实每次有朋友提到工作上遇到了什么什么困难的时候,总会有人说那可以选择换一个公司,然后就会有人说,那换了一个公司也会有啊。接下来的话题就会变成,哪家公司有这个问题哪家公司没有了。这其实没有意义,真正需要讨论的不是哪家公司有同样的问题,而是遇到问题的这位朋友,能否通过改变自身的应对方式来解决这个问题。
如果能够解决,此时再选择什么公司,那都是自发自愿的选择,而不是为了逃避这个问题而选择。选择哪家公司和如何应对这个问题,本质上是两个平行的问题。
本书一共分为五章,每章一个话题,每个话题之间层层递进,从“开启行为的改变”到"推动思维的变化",从"发展关系中的自我"到"走出人生的瓶颈",最后"绘制人生的地图"。前面的章节可以说是为了最后能够把自己的人生故事写好而做的必要准备。
同时每一章都有多个小节,从提出问题,分析问题,到切实可行的解决方法,再到最后一小节"部分推翻"自己前面的论述。
可供具体执行的方法是自我发展类书籍一个非常重要的检验标准,缺乏具体方法的书顶多只能称为"心灵鸡汤",喝过就忘了。《人生十二法则》和《了不起的我》就都是可以付诸实践的好书。而且《了不起的我》还指出了一个非常棒的观点: 知识只是局部的真理,包括本书。
我们知道物理学的发展过程就是在不断地发现新的知识,推翻自己,重建,再推翻自己的过程。心理学和脑神经科学在不同的维度分析我们人类的大脑的运作方式,可能会得出各种看似矛盾的答案。但是我觉得,追求知识的过程就是在这样的矛盾和推翻中不断地向前螺旋滚动。2019 年我遇到也解决了许多问题,在这个过程中,有局部的真理,也有被我后来推翻的真理。从自我认知到接纳自我,自己的人生地图只有自己可以画,自己的人生故事只有自己可以写。
《了不起的我》一共 360 多页,阅读的过程中一半是学习新知识,一半是印证旧想法。但是在学习了这两本书之后,"这不就是xxx嘛"的自大想法不会再有了,毕竟"知道自己什么都不知道",还是挺难的,要学习。
自我发展需要空间,能够退后一步,给自己空间很不容易。所以不要指望看完本书就能成仙,要不然岂不是所有的和尚读完佛经就成得道高僧了。学习是一小步,改变是一小步,用一句烂俗的话讲:每天一小步,天天都有新高度。
全书的前半部分解答了我最近的一些疑惑,也提供了我可以付诸实践的方法,所以读起来感同身受,非常受用。但是到了后半部分,关于转折期与人生各个阶段的阐述,我阅读起来则比较有距离感。每个人阅读时的背景与经历不同,想必读起来自有不同的领悟。
总体来说,作为一个自我发展的心理学读物,行文平易近人,用词简单易懂,少专业术语,多实例举证,很容易为读者所接受。而书中所提出之理论,亦多辅以文献佐证,令人信服。书本末尾更带有全部引用的文献作者和书名,读者可自行查阅。
我觉得这本书更像一本工具书,适合摆在书架上,遇到问题时回头来查阅一番,寻找焦虑出口的路标,让本就焦头烂额的生活稍微平顺一点。毕竟 "shit happens, but life goes on."
2020/01/30 凌晨
于自居
想阅读"枫影夜读"栏目旧文("每周读书")的读者朋友可点此直达。
"The missing clock app in the Dock"
The Dock on the Mac desktop is a convenient place to access apps and features that you’re likely to use every day, and the dock icons look gorgeous, especially on a retina display.
Wouldn't it be great if the dock icons can show even more information?
Clock mini would be a perfect choice:
Clock mini has a simple to use a timer which will send you an alert when the timer reaches zero. You can choose different alarm sounds, or let the dock to bounce repeatedly.
All in all, it is just a simple timer, no big deal.
However, you can fire a timer by just clicking the dock icon any time without switching from different workspaces or applications, which makes Clock mini a really handy timer utility when you were busy multitasking with lots of windows on the desktop.
The timezone dashboard makes it possible for you to bring up all the clocks in different timezones around the world into one place.
You can apply different themes for different clocks in the dashboard, they look just beautiful in full screen.
We love our Mac, we love the dock, we need every icon in the dock to be beautiful, so we are super serious about the app icon, that's why we built themes library.
We are enjoying design new themes, we've even built an app helping us. However there are no scheduled plans for the updates of the themes library, good design takes time.
Please stay tuned :D

各位听众朋友大家好,时间过得真快,转眼来到 2020 年。
2019 年本播客顺利播出了 6 期节目,从 6 月份的 WWDC 开始,录制了有 7, 8 期节目,但是因为“真·技术原因(音频问题)”只播出了 6 期。希望 2020 年能有一个好的开始,今年内至少要播出 12 期节目😂。
最近我读了 Jordan Peterson 的 12 Rules for Life 这本书,中文版是 Steve 说的主播史秀雄翻译的,名为《人生十二法则》。前几天我也发了一篇博客聊这本书。
读书的时候觉得他写得特别好,于是推荐给了本节目的常驻嘉宾自力(@hzlzh)。所以本期节目就是我们俩对这本书的一个读后的讨论。秉持着本节目灌水的态度,我们以心理学门外汉的视角,非常水的讨论了一番。
如果大家想获得一个专业的分析的话可以去听译者史秀雄的播客《Steve 说》。
《人生十二法则》提供了面对残酷且艰难的人生时,我们如何积极应对的一种思路。希望 2020 年大家都能成长为足够坚强的人,在这个其实非常残酷的世界里活出自己人生的意义。
Life is Suffering.
Be a tough guy.
P.S.
我们都生活在阴沟里,但仍有人仰望星空。
We are all in the gutter, but some of us are looking at the stars.
—— 出自 奥斯卡·王尔德《温夫人的扇子》
P.P.S 12 Rules for Life
- Stand up straight with your shoulders back
- Treat yourself like someone you are responsible for helping
- Make friends with people who want the best for you
- Compare yourself to who you were yesterday, not to who someone else is today
- Do not let your children do anything that makes you dislike them
- Set your house in perfect order before you criticise the world
- Pursue what is meaningful (not what is expedient)
- Tell the truth – or, at least, don’t lie
- Assume that the person you are listening to might know something you don’t
- Be precise in your speech
- Do not bother children when they are skateboarding
- Pet a cat when you encounter one on the street
推荐使用泛用型播客客户端搜索“枫言枫语”来订阅收听本节目。我们也在国内的荔枝FM和喜马拉雅有同步音源。

各位读者朋友大家好,转眼来到 2020 年,因为本人拖更成性,“每周读书”系列的“每周”早已名存实亡。所以想着 2020 年我们干脆开一个新系列好了。
其实就算按照本系列更新的巅峰时期我都很难做到真的每个礼拜输出一篇文章。我的输出流程是,一读书,二作笔记,三才是输出文章,这些都需要不少时间。2019 年我的时间分配有了比较大的变化,客观上不足以维持每周的输出。
当然阅读依然是件很棒的事情。2019 年我一共读完了 16 本书,其中也有些我觉得很有趣、很实用,想要分享给大家的书。所以既然做不到,就不再给自己设定“每周”的标题,就叫大白话“枫影读书”吧 (已更名“枫影夜读”),希望能够输出更多的分享给大家。
曾经我们在聊《后物欲时代的来临》一书时提到过人生的意义可能并不是能够被“寻找到”的,而是需要由自己来“拼凑”和“定义”。不过那本书主要讨论的还是“外部”的,而不是“自身”的。我们每个人生活在这世上,一直在探索“自我”,有的人探索得比较快,比较深,有的人可能懵懵懂懂,一辈子也不太有“自我意识”,一直被来自外部的压力、要求、规范、约束驱动着往前跑。
一个婴孩降生于世,他是空白的。成长的过程中需要不断地学习,在课堂练习加减乘除是学习,在社会被生活的车轱辘碾压过去是学习,对自我意识的探索也是学习。曾经我介绍自己的纸笔思维练习方法 FWP 时说过凡事都需要练习,不仅木匠铁匠运动员需要练习,对自我的探索,对思维方式的修正也需要练习。
我们在成长过程中有一个阶段是“大人不希望小孩子看到不该看的东西”,这不仅是现在引起社会关注的“性教育”问题,更基础的我觉得是“大人普遍没有在教授社会的混乱与黑暗”给小孩子。不仅是东方文明,西方社会也普遍希望教授给小孩一个理想的世界,(改编后的)童话故事里的结局大都是美好的,但是现实却并不如此。小孩子不在课堂里学会现实世界的真实样貌,他就得在面对社会的车轱辘时受到更大的冲击和挫折。
前阵子有一个纪录片挺火的,叫做《美国工厂》(American Factory)。我留意到镜头中那些工作在福耀玻璃美国工厂里的美国本地员工们,他们抱怨工资太低,工作时间太长的委屈与无力的表情,像极了初入社会无法接受理想与现实之间巨大落差的年轻人们。
我想这是两方面的问题造成的,一则外部世界并不如想象的那样美好,这个世界本质上是黑暗与残酷的,人生是痛苦且艰难的。在物质丰富,生活稳定的年代成长的孩子,容易产生这个世界很美好,所有的事物都应该像童话般美丽一样的错觉。比如说发达国家美国,比如说出生在摆脱了饥饿、战争等基本生存困境的今天的孩子们。
另一方面,当外部世界不如预期的时候,从我们自身的角度,我们感受到的是混乱,而我们期望的是秩序。
地球诞生至今大约有 45.4 亿年,历史上有过五次极大规模的生物大灭绝,距离我们最近的一次是白垩纪﹣古近纪灭绝事件,也就是我们熟悉的恐龙大灭绝,距今六千五百万年。而智人出现只有几十万年,人类文明有记载的也不过几千年。人类的寿命与这巨大的时间尺度比起来九牛一毛,甚至直到几十年前,全世界的人类还在因为饥饿问题而烦恼,二战距今也才 75 年。这个世界的历史几乎全部主题都是苦难,这才是世界本来的面目。我们的祖先从树上跑到草原,进化出双腿直立行走,能够活到今天是因为智人能够适应环境,能够在自然选择中占到优势生存下来。生存,才是这个世界的主要命题,既不是理想,也不是童话,更不是改变世界。
当然我这里并不是说理想与改变世界不好,这些品质依然是人类所需要的,否则人类历史就没有办法往前进步了。但是我想说的是,想要改变世界是需要付出代价的,如果一心想要用理想来应对现实,没问题,请承担你的代价,而不是一边抱怨这个世界太糟糕,一边又逃避自己应该为此而付出的代价。
说到底,这个世界的本质是痛苦,是混乱。人类通过通过改变自己,适应世界,达成某种程度的秩序。可以说没有混乱就没有秩序,没有秩序就没有历史的发展,如道家的太极,黑为混乱,白为秩序,人生的意义就是一只脚踩在混乱中,一只脚踩在秩序上,在黑白之间的曲线上弯曲前行。
而这就是我所理解的,来自多伦多大学的心理学教授 Jordan Peterson 所著的 12 Rules for Life (中文版译为《人生十二法则》)的基础,在混乱与秩序中交替前行。
人类是动物的一种,动物有情绪,人类也有情绪,我们会愤怒,会开心,会哀伤,会哭泣。在大脑的进化中,这些情绪反应是由比较古老的部分所控制的,而现代人所推崇的所谓“理性”则是由后来发展出来的部分控制。所以当我们面对突发事情的时候,我们会脸红,会愤怒,所谓眼神会出卖你。
通常大家都说要克制,要压抑自己的情绪,男儿有泪不轻弹之类云云。但其实 2019 年我学到非常有用的一句话是:
当你出现情绪的时候,就是你进行自我分析的最佳时机。
情绪并不可怕,情绪并不需要被“压抑”,情绪是因为我们遇到“不符合预期”的事情时,旧脑非常快速的反应,也是我们可以了解自己的最佳时机。只是通常我们任由自己的情绪去直接应对突发事件的时候,都得不到一个好的结果,所以从表象上看似乎我们只要压抑了自己的情绪就好了。但其实不是的,能够从容面对各种状况的人往往不是他能够克制,而是因为他拥有解决一切问题的能力,所以他能够很自信地应对这些状况。
所以每一次情绪出现的时候,我当下或者事后都会好好分析自己,看看自己是什么地方没有自信,或者什么地方没做好,没能给这个问题一个解决方案。而不是说告诉自己下一次一定要压抑住,历史的经验告诉我,这是完全没用的。
当然要做到这点,前提是必须有一定的自我意识,能够从一个剥离的角度回顾自己,这里涉及“自我意识”的训练,可以参考这本书的翻译者——"Steve说播客"的主播史秀雄——曾经介绍的“个人成长史”的练习(这个练习方法也是史秀雄在加拿大留学期间,Jordan Peterson 布置给他们的大作业)。根据我的观察,生活中还有很多人没办法从自己的驱壳中跳出来观察自己,这样他的一切情绪反应都只是本能与旧脑的应激反应,没有办法从中自省与改变,也就无法成长。所以“自我意识”是这一切的基础,我以为 Jordan Peterson 没有在这本书中提到这个前提,故在此一提。
这本书还有个副标题:An Antidote to Chaos,中文翻译为解决混乱的灵药。前面我们分析了遇到“不符合预期”的事情时我们会觉得遇到了“混乱”。这本书的副标题大概是想让读者读完之后能够掌握应对“混乱”的解决方案,从而可以自信从容地,把突发事件消于无形之中。
书里提到的十二条法则涵盖了人生的多个方面。现年 58 岁(1962 年生)的 Jordan Peterson,无论是学术上的成就还是人生阅历,完全有资格给年轻人建议。他也经常在 Quora 上面回答问题,这 12 条法则就是他给问题 "What are the most valuable things everyone should know?" 的回答。
这十二条法则如下:
单看这十二句话应该是云里雾里不知所云的,作者每一个法则都用完整的一章来解释,内容详实,极具深度,有些观点也颇为新颖,当然有些例子和观点跟美国文化相关度较高,我自己阅读的过程中有时候代入感不会很强,但是总体读下来,全书的质量还是很高的,是一本发人深省的好书。
比如第一条法则:"Stand up straight with your shoulders back",中文译为:"获胜的龙虾从不低头:笔直站立,昂首挺胸"。前半句是译者自己根据章节内容加的,后半句是直译。
这一章主要讲的是龙虾(Lobster)的故事。相比起人类的大脑,龙虾的神经系统要简单得多,所以科学家可以根据龙虾的行为和神经系统的反应,相对准确地解释二者的关系。研究这种简单的系统有助于科学家们更加复杂的系统,比如我们的大脑。
龙虾是一种生活在海床上的动物,成年的龙虾每年都会脱壳一两次,脱壳的时候就会变得很脆弱,所以它需要寻找一个合适的地方,既能有足够的食物,又能保护自己免受天敌或者其他威胁的伤害。
这样的地方当然很多龙虾都想去,那当两只龙虾遇到一起的时候,他们就有可能要互相攻击,抢夺地盘。但是如果说每一次遇到其他龙虾他们都要打一架的话,那这个伤亡的代价就太惨重了,如果实力悬殊那获胜的一方可能可以全身而退,但是如果实力相当,可能结果非死即伤,胜利的一方以后面对其他龙虾也会处于弱势。
所以龙虾群体就演化出一种能够更好地生存下去的方法,就是斗争升级机制。第一阶段,两只龙虾会互相张牙舞爪,同时用眼睛下方的喷嘴向对方喷射液体,就跟吐口水似的。如果双方大小差异很大,弱的一方可以从对方的钳子大小以及喷射出的液体的信息知道对方比自己强壮很多,然后落荒而逃。
如果这一阶段势均力敌,那就进入第二阶段:双方拼命抽打触须,钳子向下收起,一只前进,一只后退,然后轮到对方前进,另一只后退。这么来回几次,看看有没有人觉得自己不够打先逃。
如果还不逃,那就继续第三阶段:真正开打了。这一阶段有点像摔跤,双方伸出钳子要把对方掀翻,先被掀翻的一方通常会承认对手厉害,然后逃走。
但是如果都无法掀翻对方,或者说输了的那一方不服输,那么就是最后阶段,真刀真枪地,用钳子夹住对方的腿、触须、眼镜等软弱的部位,斗个你死我活。通常这种情况无论胜负,不管生死,双方都会受到很大的打击,从而在以后的战斗中很可能处于劣势,这也是龙虾们极力想要避免的局面。
在斗争当中,战败的那只龙虾,无论它之前有多厉害,接下来的时间里他都会完全没有斗志,垂头丧气,信心全无。如果说这只龙虾之前还是在某片海域里占有统治地位的大龙虾,他的大脑甚至会彻底重构,以便适应他新的“卑微”的身份,否则他无法承受从“君王”降为“草民”的打击。像是从事业巅峰被打击到谷底的人类一样,也会有类似的情感转变。
科学家就从龙虾的研究中发现了他们的神经元通信传递的化学递质——血清素和章鱼胺。血清素高、章鱼胺低的龙虾往往会变得趾高气昂,斗志满满,反之则垂头丧气,毫无战意。实际上血清素也被用于治疗人类的抑郁症。
Jordan Peterson 在 YouTube 也有一个 Channel,专门放他的 TED,公开课之类的演讲视频,龙虾故事也是他非常受欢迎的一个,所以国内也有人叫他“龙虾教授”。他讲龙虾的故事主要是想带出一个非常重要的知识点:就是统治地位并不来自于人类文化,而是根植于大脑的。
过去我们往往认为人类世界的统治阶级是文化的产物,但是通过龙虾的研究我们发现,其实在动物界这些“统治地位”的例子比比皆是,龙虾是一种,猩猩也是。说明这种等级制度基本上动物世界运行的基础,是这个世界的规则之一,只不过在人类社会,这种等级的划分往往包含了多个维度。一个学生在中国学校里,通常是以成绩论高低,当然家境、样貌、运动能力等都对一个人的综合水平有影响,但是一直以来“分数”才是中国学校里的“正统评价”。这种正统评价其实是非常单一的,所以当一个学生离开学校开始工作了之后,就会突然发现这种单一的评价标准不再管用了。
当然人们又选择了另一个大多数人都认同的单一评价标准:财富。权力、能力、职位等等往往都与财富成正比例关系,所以当财富能够正确反映一个人的综合能力水平时,倒不失为一个不错的衡量标准。可惜的是,学校里的“分数”能够大致反应一个人的智力水平,而社会上公认的“财富”却可以由很多个维度共同决定,这中间当然也包括运气。
于是你就会发现,在不同的组织里,通常会有不同的等级制度,不同的游戏规则。在规则下把游戏玩得好的人就是赢家,跟动物世界的赢家通吃一样,人类社会也是赢家通吃。金字塔顶端 1% 的人掌握的财富跟 50% 的底层人民掌握的财富一样多。
但是今天没有把规则玩好并不代表一直玩不好,而且人类世界各种大大小小的组织非常多,如果留意观察就会发现,每一个稍微有点规模的组织都在构建自己的等级制度和游戏规则。每个人都有自己擅长的事情和不擅长的事情,在一个维度上不如别人做得好并不代表自己什么都做不好。这时候如果陷入战败龙虾的状态,就应该意识到是血清素和章鱼胺在作祟。
只要你笔挺站立,昂首挺胸(Stand up straight with your shoulders back),其他龙虾看到你就会觉得,哇塞,这是一只常胜龙虾诶。从而给自己带来一个良性循环,而不是任由战败情绪作祟,反而陷入恶性循环,掉进穷人陷阱,再也出不来。
这话说起来简单,做起来很难。通常战败的人会告诉自己“我没得选择”,从而获得一种“受害者的安慰”。因为这是最简单,最不费力,最容易接受的一种战败者状态。如果你想要从战败者状态站起来,首先你得自省,得有自我意识,得知道“哦,原来我自己受到了血清素的影响”,然后你得有勇气,得有面对困难挑战极限的勇气。一只脚踩在黑色的混乱,另一只脚踏上白色的秩序,这样你的人生才能在黑白交替中拼凑出意义的地图,而不是浑浑噩噩,自己也不知道自己为什么要活在这个世界上。
这本书讲了很多内容,而且都很深刻,龙虾一章从分析龙虾的神经系统与生理基础,讲到陷入焦虑、脆弱、抑郁的原因和陷阱,再然后告诉读者朋友如何能够走出失败者模式。我觉得 Jordan Peterson 讲的东西是非常具有启发性和实践意义的,但是阅读起来需要有自我意识前提,并且有些分析相对学术,有一定门槛,当然比起学术论文还是通俗得多。
在后面的章节里 Jordan Peterson 还分析了自己如何跟自己和解,好好照顾自己,分析了如何放弃损友,分析了如何让自己不要跟别人比较,而是跟昨天的自己比较等非常具有积极人生意义的话题。
我觉得读起来获益良多。虽然在玩滑板的例子、养狗养猫的例子、以及教育小孩的例子上我没有办法感同身受,但是很多道理在生活中曾经懵懵懂懂一知半解,读书的过程就像一盏灯突然照亮了这些模糊的地方,印证了自己的想法。
我在阅读 Jordan Peterson 的这十二条法则的时候常常映照自己的生活实践来理解,往往会有额外的收获,这些收获并不一定直接来自于这本书,我对于这些法则的理解也是非常主观的个人理解。希望各位读者朋友在阅读这本书的时候,也能收获属于自己的理解。
希望所有人都能昂首挺胸,做一只获胜的龙虾。
2020.01.05/下午
于自居
我购买了本书的英文版和中文版,皆为 Kindle 版本。英文版是 Random House Canada 在 2018 年 1 月出版的,ASIN 为 B0797Y87JC。中文版为史秀雄翻译的《人生十二法则》,由浙江人民出版社于 2019 年 11 月 1 日出版。史秀雄老师在加拿大留学的时候曾经上过 Jordan Peterson 的课,我在听他的播客时曾经听他提起过这本书,所以英文版很早就买了但是一直没读。最近听史秀雄的播客才知道原来他翻译了这本书并且已经开售了,于是完整地读完了中文版,翻译质量很好,非常流畅。
英文原版并不算很难读,但是专门名词有点多,而且 Jordan Peterson 写书有点跳跃,比如第一章开头才刚讲了龙虾几句立刻就跳到鸟类关于领地之争去了。像是这句:
High serotonin/low octopamine characterizes the victor. The opposite neurochemical configuration, a high ratio of octopamine to serotonin, produces a defeated-looking, scrunched-up, inhibited, drooping, skulking sort of lobster, very likely to hang around street corners, and to vanish at the first hint of trouble.
专有名词和非常长的句子对我这种英文水平一般的读者来说确实是一种挑战。所以如果只是想了解 Jordan Peterson 的十二法则的核心,那么阅读中文版是足够的。
不过中文版对于这十二法则的翻译,基本都在前面加了半句译者对章节内容的总结,并非法则原文。除了我们上面提到的第一条法则的翻译之外,第三条是:
Make friends with people who want the best for you.
翻译为:
放弃损友:与真心希望你好的人做朋友
后面是直译,前半句则是总结。我觉得如果不太细心的读者可能会误以为原文就是整句,如果能说明一下更好。(译者曰此半句为编辑所加,我觉得很能理解😂)
另外中文版每个章节前面都会截取一小段原文和翻译,这个英文版也是没有的,比如第一条龙虾法则,中文版截取了一小段:
ATTEND CAREFULLY TO YOUR POSTURE. QUIT DROOPING AND HUNCHING AROUND. SPEAK YOUR MIND. PUT YOUR DESIRES FORWARD, AS IF YOU HAD A RIGHT TO THEM-AT LEAST THE SAME RIGHT AS OTHERS.
谨慎对待你的体态,别再低头徘徊。
说你所想,追你所求,
这是你和他人同样拥有的权利。
这是中英文的一点差异,但是总的来说中文版翻译的很好,值得推荐。
想阅读本栏目旧文("每周读书")的读者朋友可点此直达。
"A dock clock and more."
In the first public beta release of Mac OS X, there was a Clock app built right into the OS. That was the first time Apple introduced the aqua interface to the world, and the clock app was such an elegant utility app. There's no such technology like the Retina display back in 2000, but still, the clock app became my favorite one; it was so beautiful.
Years later, I started teaching myself Cocoa programming, and the Clock app was gone in upcoming updates. So I decided to make one, which was the first version of Clock mini, launched in 2014.
It was just a simple cocoa app without many features, no big deal. To my surprise, when I submitted it to Product Hunt, Clock mini got lots of upvotes, and the app store even promoted it once in "New Apps We Love Right Now".
People were loving and enjoying this little cocoa app!
The retina display was a game-changer. The high pixel density makes the Dock 4x more data-dense than before. I still remember the moment when I opened Xcode and built Clock mini on a Retina MacBook Pro. The richness of details in the clock icon just blew me away.
It was magical!
I think Mac users are enjoying using the Dock. To me, it is not just a container of shortcuts; it is alive there, telling you that the computer you are looking at is more than a tool.
So we keep working on the project, trying to make Clock mini more useful and meaningful.
Recently, we've released version 2.0, in which we've redesigned everything, from code to pixels. We've added lots of new features like a timer and time zones support, a themes library with different styles...
From a tiny little Cocoa app to a dock clock utility with a timer and time zones and a beautiful themes library, the mini clock is getting better.
We've been using Clock Mini for a while now; it has already become a part of our Macs. In the new year, we hope more and more Mac users can enjoy using Clock Mini just as we did.

前两天同事提到苹果去年发布的 A12 芯片支持 arm64e 指令集,提供了指令地址加密功能。说是虽然系统是 64 位的,但是 arm64 指令地址根本用不满,所以把高位的部分(upper bits)拿来存一个指针地址签名。
当时我就很好奇,现在 arm64 的内存指针都是 64 位的,为啥会用不满?于是我学习了一下 ARMv8.3 新增的 PAC 功能。
首先我们来看看 PAC 是啥。PAC 是 Pointer Authentication Code 的缩写,字面意思翻译就是指针验证码。在 CPU 执行指令前的时候先拿指针的高位签名跟低位的实际地址部分做一下校验,如果失败了就直接抛出异常,从而防止指令地址被篡改。
Exception Subtype: KERN_INVALID_ADDRESS at 0x0040000105394398 -> 0x0000000105394398 (possible pointer authentication failure)
为了实现这个 PAC 功能,arm64e 新增了两个指令:
PACIASP 计算 PAC 加密并加到指针地址上AUTIASP 校验加密部分,并还原指针地址并不是所有的指针都需要 PAC 保护。高通的 ARMv8.3文档给这项新技术举了个例子:
| 行为 | 没有栈保护 | 使用 PAC |
|---|---|---|
| 函数入栈(入口) | SUB sp, sp, #0x40 STP x29, x30, [sp,#0x30] ADD x29, sp, #0x30 … |
PACIASP SUB sp, sp, #0x40 STP x29, x30, [sp,#0x30] ADD x29, sp, #0x30 … |
| 函数出栈(返回) | ... LDP x29,x30,[sp,#0x30] ADD sp,sp,#0x40 RET |
... LDP x29,x30,[sp,#0x30] ADD sp,sp,#0x40 AUTIASP RET |
把函数返回地址加密,用于对抗缓冲区溢出攻击(buffer-overflow vulner-ability)。

简单介绍一下缓冲区溢出攻击,上图是一个 App 在内存时的布局(memory layout),在这个 case 中,我们只关注其中的 stack 和 heap。
heap 也就是堆,堆会往上长,stack 也就是栈,往下长。这项攻击利用的就是 stack 的缓冲区增长过程中的漏洞。

一个函数被调用的时候需要在 stack 上入栈很多东西,从内存高位开始,参数名,函数的返回地址,接下来是函数内部要执行的指令。这样当指令执行完就一个个出栈,到了函数返回地址 CPU 就知道该往哪里去了。

可以看到栈底的东西是用来控制 CPU 指令往哪里跳的,而我们代码里分配的 buffer 跟它连在一起。关键点在于 buffer 的填充方向是从低位往高位去的。如果我们先分配一小块 buffer,然后往里面写一段超出 buffer 长度的数据,我们就能直接改变栈顶的数据,比如我们的目标:return address。
雪城大学有一个教程教你怎么利用 fwrite 写一段超过 buffer 长度的数据,然后把准备好的调起 shell 的函数入口塞进去替换到原先的函数返回地址,这样 CPU 执行完写 buffer 指令后就拿到该函数地址,直接出栈打开了 /bin/bash。
我们的程序是由内核运行在用户空间的,默认没有 root 权限。但是当内核执行我们修改过的返回地址打开 /bin/bash 的时候,就是以内核权限打开的。这时候我们就获得了一个有 root 权限的 shell,接下来想干啥就可以干啥了。
有了 PAC 之后,我们编译的 App 就可以带上这个保护,遇到这种篡改过的地址就直接抛出异常。当然这个例子里的攻击很简单,操作系统早就有了多种防范手段,这里只是举一个 PAC 应用的例子。而 PAC 是在 CPU 指令层面加入的保护,理论上只是多耗了一个 CPU 周期而已,性能应该要比在软件层面的保护高得多。
PAC 介绍完了,接下来我们来看看为什么指针地址用不满,还剩一半可以直接用来存 PAC 签名?
翻了苹果的文档,高通的文档都只是轻描淡写地说利用没有用到的高位。
于是我们开脑洞想是不是一个 Mach-O 文件的 (__TEXT,__text) 段(机器码段)最大不能超过 4GB (一个 32 位指针的最大地址),又或者是整个操作系统能够跑起来的所有进程加起来不能超过 4GB 之类的。
但是其实 __text 段里的数据全都是只读的,内核随时可以换出(page out),需要的时候再换入(page in),如果忽略 vm_pressure 的话,理论上应该只要它不要超过虚拟内存大小就行(不可能有人写那么大的代码的)。最后推断其实现在的 App 根本用不了那么多的地址空间。因为用不了那么多,所以才可以利用起高位。
不过这些脑洞都没有道理,其实正确答案是: 系统虚拟内存的寻址设计根本不需要用满 64 位指针。
我们看 AARch64 Linux 的虚拟内存分级设计。一个内存页大小为 4KB,整个虚拟内存被划分为 3 级或 4 级(level),下面我们以 3 级为例。
Start End Size Use
-----------------------------------------------------------------------
0000000000000000 0000007fffffffff 512GB user
ffffff8000000000 ffffffffffffffff 512GB kernel
用户空间的地址把 63:48 位都置为 0,内核空间则都置为 1。
Translation table lookup with 4KB pages:
+--------+--------+--------+--------+--------+--------+--------+--------+ |63 56|55 48|47 40|39 32|31 24|23 16|15 8|7 0| +--------+--------+--------+--------+--------+--------+--------+--------+ | | | | | | | | | | | v | | | | | [11:0] in-page offset | | | | +-> [20:12] L3 index | | | +-----------> [29:21] L2 index | | +---------------------> [38:30] L1 index | +-------------------------------> [47:39] L0 index +-------------------------------------------------> [63] TTBR0/1
这样只需要 L1 + L2 + L3 + in-page offset 就能定位到一个虚拟内存地址。在 AARch64 Linux 的设计里,一个用户空间的内存指针其实只需要用到 0:47 一共 48 位,剩下的就都是没用到的了(是不是回想起大学时计算机课的内容了😂)。
那么 PAC 引入之后剩下的位是怎么利用的呢?参考高通的这份文档,分为两种情况:

有标记位的情况下因为高位部分可能已经被用来存储额外的指针标记了,所以只用了 48:54 一共 7 位来存储。
指针没有标记位

没有标记位的情况就往 63:56 写入 8 位,往 48:54 写入 7 位,一共用了 15 位。
Tagged pointer其实用法很多,本质上跟 PAC 的原理是一样的,都是利用了指针的剩余无效空间。比如苹果在 iOS 7 引入的 NSTaggedPointer,利用指针的剩余空间来存数据的值。比如一个 NSString 如果内容很短,就可以利用指针剩余的 bits 把内容存起来,不需要另外开辟一个内存空间。
高通的文档里如果用上了 15 位那可能剩下的空间就不够 NSTaggedPointer 发挥了,所以如果要对这类指针用 PAC 就只能用 7 位签名。当然一般这些数据应该不需要保护就是了。

因为推友问了一个问题:
@PofatTseng: 發問:要怎麼測量 symbol 在 MachO 裡佔據的大小,如果只看 __Text.__text 後 + 的偏移量準嗎?
@MapleShadow: @PofatTseng 看 Load Command 的 LC_SYMTAB 能满足你的场景吗?like
otool -l xxxx | grep -i LC_SYMTAB -B 10 -A 10
@PofatTseng: @MapleShadow 我想問的是單一個物件的相關 symbol ,比如我有一個 struct Foo {}
怎麼知道他在 MachO 裡佔去了多少空間?Foo 所有symbol 會在連續的位置上嗎
@MapleShadow: @PofatTseng 这个问题是个好问题,本来以为是一个简单的问题但是一点都不简单😂简单说通常情况下我们 App 的符号都被 strip 掉放进 dSYM 所以不占 Mach-O 空间,但是如果你是 debug 版或者动态库就会塞进去。至于长度,symbole table 的指针都是 8 字节(updated: 其实是 16 bytes),但是指向的符号 string 不是定长的,在 string table 里面取
我本以为是一个简单的问题,结果发现自己对 Mach-O 的很多细节都不太了解,于是学习了一下,以此文作为学习笔记。
如果只对上述问题的答案感兴趣的可以直接跳到末尾看结论。
P.S. 学习过程我参考了这篇文章但是因为年代有点久远,里面有些字段已经弃用了,当做字典参考就行。
我们在 macOS 系统如何启动?和 App 如何运行起来均有涉及 Mach-O 文件结构的讨论,但不全面。这里我们再详细介绍一下 Mach-O 的结构。下文使用的例子需要对比 Debug 和 Release 版,所以用我的 Mac 全屏休息提醒工具: Just Focus 为例。
首先我们来看最简单的 64 位单架构 Mach-O 文件(Fat Binary 后面再讨论),相关的数据结构定义在 XNU 源码的 EXTERNAL_HEADERS/mach-o/loader.h 里面。一个 Mach-O 文件有三个主要部分:

dyld 动态链接的符号表,标示初始函数入口,标示动态库的地址等等。segment,每个 segment 包含 0 个或多个 section。内核加载 Mach-O 时会根据 load commands 把相应的数据加载到内存里,根据 XNU 的注释,分 segment 是为了做数据对齐(segment alignment)以优化换页效率,下文分析 section 结构体时会讲到。Header 是定长的,在 64 位 Mach-O 中表现为 mach_header_64 结构体。
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
magic: 大小端兼容性之用,MH_MAGIC_64 就是编译的文件和系统是同样的 byte order,MH_CIGAM_64 则是反过来。原因是曾经兼容 PPC 和 Intel 等多种 CPU,有兴趣的同学可以阅读: macOS 内核之 OS X 系统的起源。cputype: CPU 类型定义,CPU_TYPE_POWERPC 用于 PowerPC CPU,CPU_TYPE_I386 就是 Intel 的 x86,当然还有 iPhone 的 CPU_TYPE_ARM。cpusubtype: 属于 cputype 的细分,比如 i386 全部支持 CPU_SUBTYPE_I386_ALL,或者只支持 armv7 的 CPU_SUBTYPE_ARM_V7。filetype: 文件类型,决定了这个 Mach-O 文件的布局,定义从 MH_OBJECT 0x1 到 MH_DSYM 0xa。
MH_OBJECT: 编译过程产生的中间文件,这个文件比较特殊,其他文件分了多个 segment 和 section 但是这家伙只有一个 segment,把所有的 section 都塞进去。这个中间文件可以在 DerivedData/JustFocus-xxxx/Build/Intermediates.noindex/JustFocus.build/Debug/JustFocus\ Helper.build/Objects-normal/x86_64/ 里面找到。MH_EXECUTE: 标准可执行文件MH_BUNDLE: 动态库,macOS 上跟资源文件打包为 .bundle 或 .plugin,比如 /System/Library/Audio/Plug-Ins/HAL/AirPlay.driver/Contents/MacOS/AirPlay。本质上是动态库,Unix-like 系统叫做 .so,但是在 macOS 历史上曾经有点特殊,可以参考macOS 上 bundle (.so) 和 dylib 的区别。MH_DYLIB: 动态库,比如 /System/Library/Frameworks/AppKit.framework/AppKit 就是 MH_DYLIB 类型。MH_PRELOAD: 不在内核运行的特殊文件格式,比如内核还没加载前就要执行的 Bootloader,参考 macOS 内核之系统如何启动?MH_CORE: core 文件,程序 crash 的时候保存地址空间里的数据,服务端开发的朋友应该很熟悉。不过 macOS 默认不会把 core 信息 dump 到 /cores/ 目录,而是产生 crash log 放在 /Library/Logs/DiagnosticReports。可以参考这里打开 core dump.MH_DYLINKER: 动态链接器类型,一般我们写的 App 都是用系统的 /usr/lib/dyld,这个文件就是 MH_DYLINKER 类型。MH_DSYM: 编译后的 .dSYM 包里最主要的就是用 Mach-O 文件存储的 symbol 信息,比如 Alamofire.framework.dSYM/Contents/Resources/DWARF/Alamofire 就是 MH_DSYM 类型的 Mach-O 文件。ncmds: load commands 个数sizeofcmds: load commands 总长度flags: 这里面有一堆 flags,大部分是跟编译相关的,我也没全部学明白,所以干脆不描述了,感兴趣的朋友可以看这里。reserved: 应该只用来做字节对齐了
mh64->reserved = 0; /* 8 byte alignment */
Mach-O 文件中,读完 Header 和 Load Commands 之后,就是各种 Data 数据了,这些数据是以 segment 组织的。
一个 segment 有起始和终止的 offset,该范围内的数据就是 segment 的数据。segment 的标识是 segment name,宏以 SEG_ 开头。
但是 segment 的数据没有带上起始终止之类的信息,这些信息是在 Load Commands 中定义的。比如 LC_SEGMENT_64 会定义某个 segment 从哪里开始到哪里结束,名字是什么,虚拟内存的属性(比如 read-only),有多少个 section 等等,相当于一个索引,我们要获得有意义的数据就得先解析 Load Commands 然后再去读取对应的数据。
segment 的数据会被 dyld 根据 LC 的布局信息加载到内存里,所以 segment 都是按页对齐的。在 x86 上一页是 4096 bytes 也即 4 KB。
segment 做按页对齐其实就是把它所包含的所有 section 加起来除以 4 KB,不能整除就在最后一个 section 补 0。
理论上 Mach-O 文件里的 segment 有多大,加载后就会占多少的虚拟内存。但是实际上一个 segment 有可能在加载后比它在 Mach-O 里的数据大,比如 __PGAEZERO 这个 segment。在 Mach-O 里它其实是空的,只在 Load Command 记录了一个索引信息,但是加载到内存的时候,内核会给我们的 App 的地址开始端 0x0 分配一个空的页(到 0x1000)。这个空的内存页不带内存保护(声明为 VM_PROT_NONE),不可读写不可执行,我们平时遇到的访问野指针(NULL)就会命中这个区域,然后内核就让我们的 App crash 了。
上面 header 提到过 .o 文件比较特别,他是编译过程的中间文件(intermediate object file),出于文件大小的考虑,他的所有 sections 全部放在一个 segment 里面,并且这个 segment 没有名字。

segment 用名字区分,定义了这么多种:
#define SEG_PAGEZERO "__PAGEZERO" /* the pagezero segment which has no */
/* protections and catches NULL */
/* references for MH_EXECUTE files */
#define SEG_TEXT "__TEXT" /* the tradition UNIX text segment */
#define SEG_DATA "__DATA" /* the tradition UNIX data segment */
#define SEG_OBJC "__OBJC" /* objective-C runtime segment */
#define SEG_ICON "__ICON" /* the icon segment */
#define SEG_LINKEDIT "__LINKEDIT" /* the segment containing all structs */
#define SEG_IMPORT "__IMPORT" /* the segment for the self (dyld) */
/* modifing code stubs that has read, */
/* write and execute permissions */
#define SEG_UNIXSTACK "__UNIXSTACK" /* the unix stack segment */
有些是历史遗留产物,对我们来说有用的字段是这些:
__PAGEZERO 的作用讲过了不再赘述,这个东西是由静态链接器生成的。__TEXT 包含了所有的可执行代码,内存保护设置为 VM_PROT_READ 和 VM_PROT_EXECUTE。因为这一整段都是只读的,所以内核可以在内存不够的时候把这些数据换出(page out),需要的时候再换回来(page in)。__DATA 可写的数据,比如 ObjC runtime 支持的库。像这样的系统库有可能被多个进程链接,因为这一段内存可写,所以写操作会触发 copy-on-write,以此实现逻辑上每个进程有一份 copy (不一定真的要 copy)。__LINKEDIT 动态链接器需要用到的数据,比如 symbol table, string table 之类的下面这些是历史:
__OBJC Objective-C 的 runtime 支持,历史遗留字段,现在都放进 __DATA 里面了__ICON 应该是历史遗留产物,现在图标资源已经分离出去了,我们的 App 一般打包成 .app 文件夹。__IMPORT i386(IA-32) 也就是 32 位 x86 架构才会用到的一个字段,64 位改用 __DATA,__la_symbol_ptr 了。__UNIXSTACK 应该也是历史产物,参考这里。
__TEXT 和 __DATA 一般会包含多个 sections,这些 sections 的命名和用途也会随着系统和编译器更新而变化,想要了解全部 section 及其作用的可以参考 LLVM 项目。这里我们看几个关键 section。
| Segment, Section | 作用 |
|---|---|
| __TEXT,__text | 可执行的机器码 |
| __TEXT,__cstring | 常量定义的 C strings,以 '\0' 结尾。编译器编译时会把所有 C String 合并优化,放在这个地方。 |
| __TEXT,__const | 初始化过的常量。编译器会把所有无需重定向的以 const 声明的常量放在这类。(多数编译器都把未初始化过的常量默认赋值为 0。) |
| __TEXT,__objc_ 开头的 | 以前放在 __OBJC 里 runtime 的支持,现在都放这里了。 |
| __TEXT,__stubs 和 __TEXT,__stub_helper | 动态链接需要用到的信息 |
想要理解完所有 __TEXT 里的 sections,你得学习 llvm 的源码。并且这些字段也经常随着系统和编译器的更新二更新,所以我选择放弃。真的需要的时候再回过来反查就行。在这一个 segment 里最重要的就是 __TEXT,__text,可执行的机器码放在这里。
| Segment, Section | 作用 |
|---|---|
| __DATA,__data | 初始化过的变量,比如一个可变的 C string 或者一个数组 |
| __DATA,__la_symbol_prt | Imported 函数的指针表,比如 libswiftFoundation.dylib 这样的动态库的符号的指针地址 |
| __DATA,__bss | 未初始化的静态变量 |
load command 的定义很简单:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
cmd 就是 LC_ 开头定义的宏,非常多,我们只看关键的,全量的请参考 loader.h 里的定义。
| Command | 结构体 | 作用 |
|---|---|---|
| LC_UUID | uuid_command | 编译出来的 image/dSYM 的 UUID,用于两者互相关联 |
| LC_SEGMENT_64 | segment_command_64 | 定义 segment |
| LC_SYMTAB | symtab_command | 定义 symbol table |
| LC_DYSYMTAB | dysymtab_command | 定义动态链接库需要用到的 symbol table |
| LC_UNIXTHREAD | thread_command | 程序的入口。现在大部分 App 都用 dyld 调起了,内核的 Mach-O 和 dyld 则还是用 LC_UNIXTHREAD 声明入口 |
| LC_MAIN | entry_point_command | 程序的入口,需要配合 LC_LOAD_LINKER 使用,把该地址交给 dyld 然后由它来调起 App 的入口函数 |
| LC_LOAD_LINKER | dylinker_command | 声明用到的 dy linker, iOS/Mac 一般都是 /usr/lib/dyld |
| LC_LOAD_DYLIB | dylib_command | 该 Mach-O 需要用到的动态库 |
通过 Load Command 获取了 segment 的 offset 和 size 之后就可以读取为 segment_command_64 和 section_64 结构体了。
struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* LC_SEGMENT_64 */ uint32_t cmdsize; /* includes sizeof section_64 structs */ char segname[16]; /* segment name */ uint64_t vmaddr; /* memory address of this segment */ uint64_t vmsize; /* memory size of this segment */ uint64_t fileoff; /* file offset of this segment */ uint64_t filesize; /* amount to map from the file */ vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */ uint32_t nsects; /* number of sections in segment */ uint32_t flags; /* flags */ };
struct section_64 { /* for 64-bit architectures / char sectname[16]; / name of this section / char segname[16]; / segment this section goes in / uint64_t addr; / memory address of this section / uint64_t size; / size in bytes of this section / uint32_t offset; / file offset of this section / uint32_t align; / section alignment (power of 2) / uint32_t reloff; / file offset of relocation entries / uint32_t nreloc; / number of relocation entries / uint32_t flags; / flags (section type and attributes)/ uint32_t reserved1; / reserved (for offset or index) / uint32_t reserved2; / reserved (for count or sizeof) / uint32_t reserved3; / reserved */ };
其中比较特殊的是,最后一个 segment 也就是 __LINKEDIT 存储 link edit information,里面有 symbole table, string table, dynamic symbol table, code signature 等信息。
但是他的 LC_SEGMENT_64 里面却没有包含里面的 sections 信息,你需要配合 LC_SYMTAB 来解析 symbol table 和 string table。
// LC_SYMTAB 对应的结构体
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
没有对 Mach-O 文件的符号进行任何处理的时候,所有符号表信息都会放在 Mach-O 文件里。
我们可以用 MachOView 直接查看 Symbol Table。

这是 Just Focus Debug 版的符号表,但是 Xcode 在编译的时候默认会对 Release 版做一个优化: 把符号从 App 的 Mach-O 去掉,写进成对的 dSYM 文件。可以在你的 Xcode Project -> Build Settings -> Build Options -> Debug Information Format 看到各个 scheme 的配置。
DWARF 是 Executable and Linkable Format 配套的一个 Debug 数据格式。ELF 则是 Unix 的一个标准格式,多数 Unix 系统和 Linux 都采用这种格式定义可执行文件。macOS 虽然不支持 ELF 但是用了 DWARF 作为 debug 数据格式。
DWARF 生成 debug 信息并塞进 Mach-O 文件DWARF with dSYM File 生成 debug 信息并放到配套的 dSYM 文件,以 UUID 匹配,App 的Mach-O 里不带符号信息。
可以读取 LC_SYMTAB 然后在最后一个 segment 里找到 symbol table。LC_SYMTAB 数据是一个定长的 16 bytes 数据。
然后通过 symbol table 的 string table index 获取该 symbol 对应的 string,这个就不是定长的了,读到 \0 停止。所以符号的 string 越长占 Mach-O 的 size 就越大。
2019-11-16 updated: 上面的说法是你使用 MachOView 这样的工具时,可以肉眼 filter 已知的 string 所以可以这样查。但是系统执行文件的时候,拿到的是 (__TEXT,__text) 里的一个个指针地址,crash 发生的时候内核会保存当前进程的内存空间快照,crash 时的指令地址反查 symbol 就能得到我们人能阅读的 crash 堆栈。所以如果你想要通过 string 裸读 Mach-O 文件来反查对应指针地址的话,因为 string table 里的存储是连续的 bits,没有索引就无法读出 string,所以只能解出所有结果,然后自己去 filter。
无用 class/struct 会占用 Mach-O 空间吗?
如果是 C/C++ 的符号,编译链接时会知道这个 class/struct 没人用,直接优化删掉,等于没有。
如果是 ObjC 的符号,则还是会保留,因为有 runtime,你不知道它到底有没有被人用。
所以 ObjC 无用的 class/struct 在 release 下不会占用 Mach-O 的 Symbol Table/String Table 空间,但是会占用 Mach-O 的 (__TEXT,__text) 空间。
foo 的所有符号会连续吗?
不连续,link-editor 比如 dyld 可以通过读取 LC_SYMTAB, LC_DYSYMTAB 等 load command,从对应的 Symbol Table 和 Dynamic Symbol Table 找到符号。
比如 Just Focus 有一个 Swift enum JFAppState 在 Symbol Table 上它的符号并不连续。
什么符号可以从 Mach-O 去掉?
默认情况下所有符号都会保留在 Mach-O 里,这样调试的时候就能显示全部符号,但是如前所述发布版本并不需要这些符号,完全可以去掉以节省空间。Xcode 对 Strip Style 也提供了多个选项可供设置: Build Settings -> Deployment -> Strip Style

单独编译静态库是无法 Strip All Symbols 的,不然你引用这个静态库链接器就不知道该怎么链接了。但是打包成一个完成 App 的时候,静态库的符号可以被去掉。
理论上动态库的符号无法去掉,但是编译器可以根据你调用的方法进行优化,只保留用到的符号。但是 ObjC 有 runtime,应该无法确定哪些符号用到哪些没有。llvm 用到的链接器 ld 提供了 -strip-unneeded 的选项,不过我还不知道他是怎么实现的,大概要把编译原理从头学一下然后再学一遍 llvm 才知道了。
主流操作系统 Unix-like, Windows 和 macOS 虽然各有自己可执行文件的格式,但是设计上大同小异。
Mach-O 文件格式随着系统与编译器的升级加入和删除了很多古老的 segment 或者 section,而这些特性都需要编译器(llvm)与执行环境(xnu)的配合开发。
作为一个编译后的产物,Mach-O 里的字段有很多跟编译器的优化相关。这些字段如果要一个个理解清楚需要很多时间,并且需要熟悉编译原理以及 llvm 自家的特性(毕竟很多优化都是独有的)。所以没有必要细究每一个字段的作用,真的用到的时候再查就行了。
但是以鸟瞰的视角了解 Mach-O 文件的结构,对于理解一些古怪的问题还是很有帮助的。

今天和同事讨论到一个问题:
bundle和动态库一样吗?
同事说 bundle 只是包含了其他资源而已,其实就是动态库。
我看 Mach-O 文件类型里 MH_BUNDLE 与 MH_DYLIB 是分开的,所以觉得 .bundle 里面的 Mach-O 文件和 dylib 的 Mach-O 文件应该会有些不一样。不过我也不知道有什么不一样,所以学习了一下,以此文记之。
定义一下动态库为 dylib Mach-O 文件, bundle 指的是 .bundle 文件夹里面的 Mach-O 文件,一般类 Unix 系统叫做 .so 库,不过苹果官方建议叫做 .bundle。
P.S. 这里苹果官方不厚道,它推荐用 .bundle 作为 MH_BUNDLE 类型文件的后缀名但不强制,然后自己还把 .bundle 后缀名用作一个类似 .app 的资源与可执行文件打包。所以很容易就会混淆两个概念。实际上我看到的 MH_BUNDLE 类型的 Mach-O 基本上都没有后缀名,有 .bundle 后缀名的基本上都是资源与可执行文件的打包。
先说结论: 通常语境下 bundle 和 dylib 没有区别。要较真的话也只有在 OS X 10.5 以前才有比较大的区别,所以同事说 bundle 和动态库没有区别是对的。
P.S. ELF 系统(Executable and Linking Format,Unix-like 系统基本都是)上这两者完全相等,只有 Mac 的 dyld 对他们做了点区别对待。
Mach-O 文件的 header 里有一个 type 字段表示当前文件的类型,如果把 .bundle 文件夹解开,里面的 Mach-O 文件的类型是 MH_BUNDLE,而 dylib 则是 MH_DYLIB。
➜ otool -hv AppKit Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags MH_MAGIC_64 X86_64 ALL 0x00 DYLIB 60 8344 NOUNDEFS DYLDLINK TWOLEVEL APP_EXTENSION_SAFE
➜ AppKit.framework otool -hv /System/Library/Audio/Plug-Ins/HAL/AirPlay.driver/Contents/MacOS/AirPlay Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags MH_MAGIC_64 X86_64 ALL 0x00 BUNDLE 21 2544 NOUNDEFS DYLDLINK TWOLEVEL
在 macOS 上,动态加载通过 dyld 进行。bundle 和 dylib 两种文件都可以使用 dlopen 加载。两者的区别要在 dyld 的源码里面找。
dyld 的 dlopen() 实现主要关注是这几个地方:
dlopen()load()loadPhase0()loadPhase1()loadPhase2()loadPhase3()loadPhase4()loadPhase5()loadPhase6()checkandAddImage()
dylib 就从 sAllImages 找到一样路径的 image 先删掉dylib 和 bundle 能使用的 API 不一样,所以这里还得判断 context.mustBeBundle 和 isBundle()是否匹配// some API's restrict what they can load
if ( context.mustBeBundle && !image->isBundle() )
throw "not a bundle";
if ( context.mustBeDylib && !image->isDylib() )
throw "not a dylib";
bundle 就不会加到 global list,因为 bundle 可以只加载但不链接。所以结论是 bundle 可以只加载不链接,而 dylib 加载后就链接了。
NSObjectFileImage 只有 bundle 能用dyld 提供了 NSObjectFileImage 接口,这些接口只有 bundle 能用,只加载不链接就通过这个接口来实现。
NSObjectFileImageReturnCode NSCreateObjectFileImageFromFile(const char* pathName, NSObjectFileImage *objectFileImage)
里面会调用 load() 方法加载 bundle,这类接口的 context.mustBeBundle 为 true,底下判断的时候遇到非 bundle 就会报错。
load() 之后再使用以下方法链接:
NSModule NSLinkModule(NSObjectFileImage objectFileImage, const char* moduleName, uint32_t options)
NSObjectFileImage 相关的接口从 OS X 10.5 开始已经被废弃了。
在 Mac OS X 10.5 (2007 年) 以前,bundle 可以被 unload 但是 dylib 不可以,10.5 开始 dylib 也可以被 unload 了。dlclose() 的实现很简单,调用时减一下引用计数,为 0 就从走垃圾回收接口 garbageCollectImages() 删掉。
经过以上调查,现如今的 bundle 跟 dylib 在使用上几乎可以完全对等。要说区别那就只有编译 dylib 为 shared library 的时候需要加上版本号,而 bundle 只会给自己的 App 用就没有必要了。
libbz2.1.0.5.tbd
libbz2.1.0.tbd
libbz2.tbd
至于 Mach-O Header file type 的区别,只是给 dyld 作 NSObjectFileImage 接口判断而已,这些接口废弃了那自然就没有区别了。
