蜀道之难,难于上青天

0%

最近对 CI/CD 有一些实践,记录一下。
每个人对事物的认识都是有一个发展过程的,我也不例外。

手动时代

刚开始 iOS 开发的我,工作流是这样的

  1. 开发功能自测功能没问题,提测
  2. 接着通过 Xcode 手动出包,完了导出来,上传到平台供测试下载
  3. 测试发现 bug,修复 bug,再重复一次步骤 2

这个流程的缺点是耗费人力,以及没有单元测试,无法确保质量。

半自动时代

后来,接触到了 Jenkins 以及一些脚本知识
于是我就写了个发包的脚本,依赖安装、编译、打包、发布一气呵成。配合 Jenkins 只需要点击一下就行了。
我的工作流变成了如下流程:

  1. 开发
  2. 完成功能自测功能没问题就提测
  3. Jenkins 出包
  4. 修改 bug,重复步骤 3

这个流程让我尝到了脚本的甜头,省去了跑 Xcode 出包的繁琐过程。但是仍然没有单元测试,无法确保质量。

自动时代

这里要提一件事,有很长一段时间我在用 Go 刷 Leetcode,加上 Go 对测试的原生支持,我养成了写单元测试的习惯。

这段时间换了公司,为了熟悉项目,写起了单元测试。同时配合 fastlane 把单元测试、打包、发布等任务都脚本化了。虽然命令已经简化到极致,然而同事们并没有使用的欲望,于是我开始思考是否有更好的方式。

通过一次公司的分享,了解到了 Gitlab 的 CI/CD,我立马就开始尝试。因为脚本都写好了,集成起来就是写个配置文件,很快就搭建好了,同时也集成了飞书。
现在的工作流如下

  1. 开发,push 代码
  2. Gitlab 触发 依赖安装编译单元测试 流程,将结果通过飞书送达,如失败则重复步骤 1
  3. 发布时,push tag
  4. Gitlab 触发 依赖安装编译单元测试发布 流程,将结果通过飞书送达,如失败则重复步骤 3 或 1

这样的工作流,可以持续对项目进行验证,在流程上就能提早发现问题;同时发现问题之后,也能根据 push 信息落实到相关开发人员进行修改。
而且更进一步,Gitlab 还支持配置单元测试覆盖率,我们是一个 Framework 服务,接入方看到覆盖率会更加有信心接入。

总结

一个工程会涉及到的任务

  • 依赖安装
  • 编译
  • 单元测试
  • 打包
  • 发布到测试环境
  • 发布到生产环境

什么是 CI/CD,我理解就是根据预先定义的规则以及程序,让以上的任务都自动化的工作流。
目前已经有很多 CI/CD 的方案了,如 Github 的 Actions、Travis CI,Gitlab 的 CI/CD。读者们都可以尝试一下。

背景

目标文件,又称为可重定向目标文件,是源代码经编译之后的产物。目标文件是可执行文件、静态库、共享库的原材料。MacOS 系统的目标文件采用的是 Mach-O 格式。本文以一个简单的可重定向文件为例来描述 Mach-O 格式。

生成目标文件

新建 main.c 文件

1
2
3
4
//main.c
int main(int argc, char const *argv[]){
return 2;
}

编译 main.c 得到 main.o 文件。

1
$ gcc -c main.c -o main.o

查看目标文件

文件的本质,包括 main.o 在内,实际上是数据的集合。通过 xxd 查看该文件的十六进制形式

1
xxd main.o

main.o 的十六进制形式的完整数据如下,其中第一列的十六进制数字代表所在行的偏移量,用来方便的定位数据;第二列到第九列为文件数据;最右为数据的 ASCII 表示,非 ASCII 码显示为 .。比如第一行开始数据 cf 的偏移量为 0x00000000,数据 fa 的偏移量则为 0x00000001。了解了数据以及偏移量,我们就知道如何读文件了,在下文我会用 offset 代表偏移量。接下来我们可以按照字节来解析 main.o 文件了。
这种原始的读取方式,可以让我们加深对文件的理解,读者也可以采用 MachOView 来查看,更加直观方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
00000000: cffa edfe 0700 0001 0300 0000 0100 0000  ................
00000010: 0400 0000 b801 0000 0020 0000 0000 0000 ......... ......
00000020: 1900 0000 3801 0000 0000 0000 0000 0000 ....8...........
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 7800 0000 0000 0000 d801 0000 0000 0000 x...............
00000050: 7800 0000 0000 0000 0700 0000 0700 0000 x...............
00000060: 0300 0000 0000 0000 5f5f 7465 7874 0000 ........__text..
00000070: 0000 0000 0000 0000 5f5f 5445 5854 0000 ........__TEXT..
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 1200 0000 0000 0000 d801 0000 0400 0000 ................
000000a0: 0000 0000 0000 0000 0004 0080 0000 0000 ................
000000b0: 0000 0000 0000 0000 5f5f 636f 6d70 6163 ........__compac
000000c0: 745f 756e 7769 6e64 5f5f 4c44 0000 0000 t_unwind__LD....
000000d0: 0000 0000 0000 0000 1800 0000 0000 0000 ................
000000e0: 2000 0000 0000 0000 f001 0000 0300 0000 ...............
000000f0: 5002 0000 0100 0000 0000 0002 0000 0000 P...............
00000100: 0000 0000 0000 0000 5f5f 6568 5f66 7261 ........__eh_fra
00000110: 6d65 0000 0000 0000 5f5f 5445 5854 0000 me......__TEXT..
00000120: 0000 0000 0000 0000 3800 0000 0000 0000 ........8.......
00000130: 4000 0000 0000 0000 1002 0000 0300 0000 @...............
00000140: 0000 0000 0000 0000 0b00 0068 0000 0000 ...........h....
00000150: 0000 0000 0000 0000 3200 0000 1800 0000 ........2.......
00000160: 0100 0000 000f 0a00 040f 0a00 0000 0000 ................
00000170: 0200 0000 1800 0000 5802 0000 0100 0000 ........X.......
00000180: 6802 0000 0800 0000 0b00 0000 5000 0000 h...........P...
00000190: 0000 0000 0000 0000 0000 0000 0100 0000 ................
000001a0: 0100 0000 0000 0000 0000 0000 0000 0000 ................
000001b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000001c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000001d0: 0000 0000 0000 0000 5548 89e5 c745 fc00 ........UH...E..
000001e0: 0000 00b8 0200 0000 5dc3 0000 0000 0000 ........].......
000001f0: 0000 0000 0000 0000 1200 0000 0000 0001 ................
00000200: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000210: 1400 0000 0000 0000 017a 5200 0178 1001 .........zR..x..
00000220: 100c 0708 9001 0000 2400 0000 1c00 0000 ........$.......
00000230: a8ff ffff ffff ffff 1200 0000 0000 0000 ................
00000240: 0041 0e10 8602 430d 0600 0000 0000 0000 .A....C.........
00000250: 0000 0000 0100 0006 0100 0000 0f01 0000 ................
00000260: 0000 0000 0000 0000 005f 6d61 696e 0000 ........._main..

Mach Header

Mach-O 文件的开头是个 header,32 位下按照结构体 mach_header 布局,64 位下是 mach_header_64。可以在 apple 开源的 mach-o/loader.h 里找到详细的结构体信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
struct mach_header {
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 */
};

/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */

/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
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 */
};

/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */

不论是 mach_header 还是 mach_header_64,其开头都是一个 uint32_t 的成员 magic,读文件前 4 个字节来查看 magic 的值

1
00000000: cffa edfe

cffa edfe,即 MH_CIGAM_64, 需要按照小端序的方式来解析该文件。确定了读取方式之后,可以参照 mach_header_64 接着往下解析,读出其余成员的值。

name offset value description
magic 0x0 0xcffaedfe MH_CIGAM_64
cputype 0x4 0x01000007 CPU_TYPE_X86_64
cpusubtype: 0x8 0x00000003 CPU_SUBTYPE_X86_64_ALL
filetype 0xc 0x00000001 MH_OBJECT,代表可重定位目标文件
ncmds 0x10 0x00000004 代表有 4 条 Load Commands
sizeofcmds 0x14 0x000001b8 表示 Load Commands 的size
flags 0x18 0x00002000 MH_SUBSECTIONS_VIA_SYMBOLS
reserved 0x1c 0x00000000 保留字

Load Commands

紧接着 mach_header,就是 Load Commands 了。同样是先读取头文件,从 ncmds 可以看出接下来会存在 4 个 load_command ,其结构如下

1
2
3
4
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

cmd 表示 load_command 的类型,根据类型来使用不同的结构来解析。cmdsize 表示该 load_command 的 size,通过 offset + size 可以得出下个 load_commandoffset 地址。main.o 文件的四个 load_command 如下,Load Commands 紧接着 mach_header,所以第一个 load_commandoffset 就是 mach_header_64 的 size 0x20

index cmd offset cmd value cmdsize offset cmdsize value type next cmd offset
0 0x00000020 0x0000019 0x00000024 0x00000138 LC_SEGMENT_64 0x00000158
1 0x00000158 0x00000032 0x0000015c 0x00000018 LC_BUILD_VERSION 0x00000170
2 0x00000170 0x00000002 0x0000174 0x00000018 LC_SYMTAB 0x00000188
3 0x00000188 0x0000000b 0x000018c 0x00000050 LC_DYSYMTAB

LC_SEGMENT_64

第一个 load_command 类型是 LC_SEGMENT_64,从 mach-o/loader.h 可以查到其数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* The 64-bit segment load command indicates that a part of this file is to be
* mapped into a 64-bit task's address space. If the 64-bit segment has
* sections then section_64 structures directly follow the 64-bit segment
* command and their size is reflected in cmdsize.
*/
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 */
};

解析如下,根据 nsects 可以看出该 segment 包含有 3 个 Section,接下来我们看 Section。

name offset value description
cmd 0x00000020 0x00000019 LC_SEGMENT_64
cmdsize 0x00000024 0x00000138 该 command 所占的 size
segname 0x00000028 0 16 位长度的字符串
vmaddr 0x00000038 0 VM Address
vmsize 0x00000040 0x0000000000000078 VM Size
fileoff 0x00000048 0x00000000000001d8 main.o 文件里的偏移量 offset
filesize 0x00000050 0x0000000000000078 File Size
maxprot 0x00000058 0x00000007 Maximum VM Protection,VM_PORT_READ,VM_PORT_WRITE,VM_PORT_EXECUTE
initprot 0x0000005c 0x00000007 Maximum VM Protection,VM_PORT_READ,VM_PORT_WRITE,VM_PORT_EXECUTE
nsects 0x00000060 0x00000003 Section 的数量,包含 3 个 Section
flags 0x00000064 0x00000000 Flags

代码段 __TEXT

同样,第一个 Section 的 header 紧挨着 segment_command_64,从 0x00000068 开始解析,其结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 */
};

解析如下,根据 sectname__text,可以看出这是一个代码段。根据 offsetsize 可以知道该代码段在文件中的具体值

name offset value description
sectname 0x00000068 5f5f 7465 7874 0000 0000 0000 0000 0000 ascii 解析为 “__text”
segname 0x00000078 5f5f 5445 5854 0000 0000 0000 0000 0000 ascii 解析为 “__TEXT”
addr 0x00000088 0x0000000000000000 Address,内存地址,目前为 0,链接时会重定向
size 0x00000090 0x0000000000000012 Size,该 Section 的 size
offset 0x00000098 0x000001d8 该 Section 的 offset
align 0x0000009c 0x00000004 内存对齐数值
reloff 0x000000a0 0x00000000 Relocations Offset,重定向偏移地址
nreloc 0x000000a4 0x00000000 Number of Relocations,重定向数
flags 0x000000a8 0x80000400 Flags,S_REGULAR,S_ATTR_PURE_INSTRUCTIONS,S_ATTR_SOME_INSTRUCTIONS
reserved1 0x000000ac 0x00000000 Reserved1
reserved2 0x000000b0 0x00000000 Reserved2
reserved3 0x000000b4 0x00000000 Reserved3

我们从 0x000001d8 来解析 0x12 个字节

1
2
000001d0: 0000 0000 0000 0000 5548 89e5 c745 fc00  ........UH...E..
000001e0: 0000 00b8 0200 0000 5dc3 0000 0000 0000 ........].......

这是一段机器码,翻译为汇编如下,可以看出这个与 main.c 里的 main 函数的函数体对应。

1
2
3
4
5
6
0x000001d8 55            pushq %rbp
0x000001d9 4889e5 movq %rsp,%rbp
0x000001dc c745fc00000000 movl $"0x0 (_main)",-0x4(%rbp)
0x000001e3 b80x000000 movl $0x2,%eax
0x000001e8 5d popq %rbp
0x000001e9 c3 ret

符号表 LC_SYMTAB

第三个 Load Command 表示符号表,对应的结构体如下

1
2
3
4
5
6
7
8
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 */
};

解析如下,从中我们可以得出符号表的 offset0x00000258 ,符号数量 1 ,以及字符串表的 offset0x00000268size0x0000000b

name offset value description
cmd 0x00000170 0x00000002 LC_SYMTAB
cmdsize 0x00000174 0x00000018 Command Size
symoff 0x00000178 0x00000258 符号表在文件中的 offset
nsyms 0x0000017c 0x00000001 符号数量
stroff 0x00000180 0x00000268 字符串表在文件中的 offset
strsize 0x00000184 0x0000000b 字符串表长度

我们从 0x00000258 来查看符号表,根据结构体 nlist_64 来解析。

1
2
3
4
5
6
7
8
9
10
11
12
/*
* This is the symbol table entry structure for 64-bit architectures.
*/
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};

解析如下,可以根据 n_sectn_value 得到该符号的具体位置,也就是 0x000001d8,根据 n_un.n_strx 从字符串表得到符号名。

name offset value description
n_un.n_strx 0x00000258 0x00000001 在字符串表中的偏移量
n_type 0x0000025c 0x0000000f 代表 N_EXT,N_SECT。N_EXT 代表支持外部符号引用,N_SECT 表示该符号定义在索引为 n_sect 的 Section
n_sect 0x0000025d 0x00000001 符号表在 Section 里的索引号
n_desc 0x0000025e 0x0000
n_value 0x00000260 0x0000000000000000 所在 Section 的偏移量

字符串表

从符号表解析得知字符串表的 offset0x00000268

1
0x00000268 00 5f 6d 61 69 6e 00 00

而唯一的符号,在字符串表的位置是 1,从 0x269 开始读,读到 00 终止,根据 ascii 可以得知其符号名为 _main,对应源文件里的main函数。

其他 Section

第一个 Segment 还包括的 Section 有 __compact_unwind、__eh_frame、reloadcations,跟 debug 调试有关,这里就不多讲了。

总结

以上,我们可以得知 main.o 为 x86_64 平台下的 Mach-O 文件,包含了符号 _main 及其代码段,映射到 main.c 里就是 main 函数。以及一些尚未确定的值,如 vmaddr,这些值只有在链接的时候才能确定。

参考

子在川上曰:逝者如斯夫!不舍昼夜。

读过的书

今年买了很多书,却没有读多少。
《计算机组成原理与汇编语言程序设计》和《程序员的自我修养》补了计算机的基础知识,《剑指 Offer》补了算法知识。
《万历十五年》现在回忆起来,只记得一代首辅张居正、模范官员海瑞、抗倭英雄戚继光等人的名字了,惭愧。
《他改变了中国》让我知道现在的幸福生活来之不易,一个人的一生呐真的是要靠自己的奋斗的。
《乔布斯传》了解了这位改变了互联网的天才的生平。他非凡的运营能力、偏执狂一般对硬件以及软件的把控欲,造就了如今的苹果帝国。Think Different 震撼到我了,我决定打印出来挂到墙上。
《上帝掷骰子吗:量子物理史话》,一次从古希腊到二十一世纪的量子之旅,在这里我重新认识了光,也见识到了物理的伟大。
《球状闪电》在我看来是一次理工男的量子物理的浪漫旅途。
还读了兰陵笑笑生的《金瓶梅》。人性呐,从人类社会起,就没变过。

博客

今年产出了三篇博客,数量较少。
在配置 Nginx 主页的时候,碰到了 403 问题,寻根究底,产出了NGINX 用户权限引起的 403 问题
工作中尝试了 RxSwift,给同事们做了分享,产出了响应式编程 与 RXSwift(一),当时是想写一个系列的,现在看来应该是烂尾了。
在看《程序员的自我修养》时,了解了目标文件的结构,于是对计算机的字符编码比较好奇,产出了UTF-8 字符编码
以后我要多写博客,不管是生活上还是专业上,这其实也是一个归纳总结的过程,自己也能受益良多。否则就像春风拂面一样,过程很爽,但是过去就忘了。

刷题

今年花了一些精力刷题,维护了一个使用 GoLeetCode开源项目,记录自己刷题的过程,同时勉励自己坚持下去。
之前有人问我,刷算法除了面试之外有啥用?我觉得有很多好处,一个是培养解决问题的思路,一个是保持思考的习惯,同时代码也会写的越来越健壮。

罗大佑 & 北京

跟媳妇都是罗大佑的脑残粉。在今年六月份,特地去北京工体看了罗大佑的演唱会,满足了自己的小心愿。
顺道也游览了故宫、圆明园。同时作为一个吃货,在大众点评上找了家卤煮火烧过了嘴瘾。

交房

国庆的时候交房了。接着就忙着看家具、打柜子、做窗帘、封阳台等一系列事情。
刚开始买床买家具的时候,东挑西挑,各种参数对比,查询各指标的意思,浪费了很多精力。后来我想开了,就买品牌货,价钱符合预期,就行了。
空空的房子慢慢的被填满,渐渐的有了家的感觉。终于要住进自己的窝了,嗨,不容易。

健康问题

年末体检,查出了一点小问题。于是下决心找了私教,逼自己体重降下来,每次咔咔练完,全身一阵酸爽。同时对健康饮食也有了一些认识,以少油、少盐、少糖、营养均衡的原则来吃就对了。

总结

读的书比去年多,刷的题比去年多,写的博客比去年多,哈哈,总的来说,凑合。
同时,我也迈进了三十岁的门槛,来到了能力最强以及时间最宝贵的关口,只有继续战斗了。

修改
  1. 2020-1-6,翻阅亚马逊书籍,发现自己在2019年有读过《上帝掷骰子吗:量子物理史话》,并且做过分享。

问题

11100110 10110001 10011111表示什么?

ASCII

American Standard Code for Information Interchange,美国信息交换标准代码

ASCII最初由美国提出,解决了英语世界在计算机里的编码问题。
ASCII00开始,一直到7f,包含了128个字符,其中33个控制字符,95个可见字符。

  1. 控制字符比较常见的如换行键、空字符、退出键等。
  2. 可见字符如空格、数字、大小写字母、符号等。

可以通过echo配合xxd命令很简单的获取字符的ASCII值,如下我们就拿到了aASCII值为0x61

1
2
3
4
$ echo a | xxd
00000000: 610a a.
$ echo a | xxd -b
00000000: 01100001 00001010 a.

随着互联网在全世界的普及,ASCII的局限性慢慢凸显,其无法表示其他语种如汉语、日语的字符。于是发展出了Unicode

Unicode

Unicode,又称为万国码、国际码、统一码、单一码

Unicode是一项业界标准,为每一个字符都制定了唯一的编码,统一了各语种的编码。
比如U+0061表示aU+660e代表汉字的字,U+1f600表示露齿而笑的脸😀,U+1F1F3表示中国国旗🇨🇳

Unicode里一个字型可以代表多种字符编码,甚至是某些编码的组合。
é既可以用U+00e9表示,也可以用U+0065U+0301的组合表示。所以我们使用的时候,还是得根据实际使用的意义来输入,否则在计算长度以及字符串比较等方面就会不一致,造成不必要的麻烦,而且这些问题排查起来也很困难。

UTF-8

8-bit Unicode Transformation Format

UTF-8是一种针对Unicode的可变长度字符编码,是一种前缀码。

  1. 128US-ASCII字符只需一个字节编码(Unicode范围由U+0000U+007F
  2. 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080U+07FF
  3. 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800U+FFFF
  4. 其他极少使用的Unicode辅助平面的字符使用四至六字节编码(Unicode范围由U+10000U+1FFFFF使用四字节,Unicode范围由U+200000U+3FFFFFF使用五字节,Unicode范围由U+4000000U+7FFFFFFF使用六字节)。
代码范围 标量值 UTF-8 注释 个数
000000 - 00007f 00000000 00000000 0zzzzzzz 00-f7 ASCII,字节由0开始 128
000080 - 0007ff 00000000 00000yyy yyzzzzzz 110yyyyy 10zzzzzz 第一个字节由110开始,接着的字节由10开始 1920
000800 - 00d7ff , 00e000 - 00ffff 00000000 xxxxyyyy yyzzzzzz 1110xxxx 10yyyyyy 10zzzzzz 第一个字节由1110开始,接着的字节由10开始 61440
010000 - 10ffff 000wwwxx xxxxyyyy yyzzzzzz 11110www 10xxxxxx 10yyyyyy 10zzzzzz 第一个字节由11110开始,接着的字节由10开始 1048576

Unicode在范围D800-DFFF中不存在任何字符,基本多文种平面中约定了这个范围用于UTF-16扩展标识辅助平面(两个UTF-16表示一个辅助平面字符)。当然,任何编码都是可以被转换到这个范围,但在Unicode中他们并不代表任何合法的值。

根据上面的表可见,UTF-8完美兼容ASCII编码,Unicode范围很大,而且是可以继续扩展的
同时我们可以根据Unicode值推导出UTF-8的编码

1
2
3
4
graph TD
A[Unicode:黄,U+9ec4] -->|二进制| B(1001 1110 1100 0100)
B --> |UTF-8| C(11101001 10111011 10000100)
C --> |十六进制| D(UTF-8:e9bb84)

现在我们回到文章开头的问题

11100110 10110001 10011111表示什么?

可以看出这段二进制编码实际上是UTF-8编码,按照上表的规则,可以推导出其Unicode字符

1
2
3
graph TD 
B(11100110 10110001 10011111) --> |Unicode| C(0110 1100 0101 1111)
C --> |十六进制| D(Unicode:江,U+6c5f)

应用:修改机器码字符段

学以致用,这样效果才会好
这是一段简单的输出字符串的程序

1
2
3
4
5
6
7
#include "stdio.h"

int main(int argc, char const *argv[])
{
printf("hello,world!黄");
return 0;
}

编译
$ gcc main.c | ./a.out
输出:hello,world!黄

查看目标文件的段信息
$ objdump -s a.out
输出里的字符段如下:

1
2
Contents of section __cstring:
100000fa2 68656c6c 6f2c776f 726c6521 e9bb8400 hello,world!....

我们可以知道,这一串十六进制编码代表了UTF-8编码的hello,world!黄。可以尝试对其进行修改
vim -b a.out
打开十六进制模式
%!xxd
查找到hello部分进行修改,将 e9bb84(黄) 改成 e6b19f(江)。
/hello
恢复成二进制模式
%!xxd -r
保存退出
wq!
查看字符段
$ objdump -s a.out
输出:
1
2
Contents of section __cstring:
100000fa2 68656c6c 6f2c776f 726c6521 e6b19f00 hello,world!....

$ ./a.out
输出:hello,world!江
这样,我们就修改了机器码。

到这里,笔者也想通了一些问题,比如hardCode进代码里的密钥肯定是不安全的,存在被查看以及被篡改的风险;二进制文件也是不安全的,所以Apple使用了代码签名的机制,来保证设备上运行的代码是未被篡改过的。

参考链接:

我想从 whathowwhy 几个方面,简单的写一下如何用 RXSwift 写响应式代码。

what is RxSwift?

在计算机中,响应式编程是一种面向数据流变化传播异步编程范式
举个例子:

1
let a = b + c

命令式编程中,这个表达式是计算 b 和 c 的和 a。假如之后 b 和 c 变化了,a 也不会变化。
而在响应式编程里,a 会随着 b 和 c 的变化而变化,不用再次执行这块代码求出一个值,再赋值给 a。
响应式编程可以简化交互式用户界面的开发,比如将视图与数据流绑定,数据流有新的值的时候,视图则会自动响应变化。
ReactiveX 是一组提供异步的数据流的 api,在 ReactiveX 的眼中,一切都可以当作数据流,我们可以用它来轻松的开始响应式编程。
RxSwift 就是 ReactiveX 的 Swift 实现。

how to use RxSwift?

怎么用好 RxSwift 呢?使用 RxSwift 的关键,是如何转变思维,写出响应式代码。
举个例子,在登录页面,会对账号和密码的输入框做校验,账号限制为 11 位字符,密码限制为 6 到 20 个字符,只有在账号和密码都有效的情况下,登录按钮才可点击。

命令式编程

在命令式编程下的典型思路是这样的

1
2
1. 实现账号 TextField 以及密码 TextField 的 delegate 的字符串变化的方法,获取到他们的字符串
2. 根据字符串的变化,设置登录按钮的enable状态

具体实现我就不写了,相信大家已经写了很多遍了

响应式编程

在响应式编程下的典型思路是这样

1
2
3
4
1. 获取账号和密码的字符流 userNameInputStream、passWordInputStream
2. 将账号流转换为账号是否有效的流 userNameValidStream、passWordValidStream
3. 将 userNameValidStream、passWordValidStream 两个流合并为判断账号和密码是否有效的流 loginValidStream
4. 将 loginValidStream 与登录按钮绑定

流程如图
登录按钮流

show you the code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@IBOutlet weak var passWordField: UITextField! 
@IBOutlet weak var userNameField: UITextField!
@IBOutlet weak var loginButton: UIButton!
let bag = DisposeBag()

private func bindView(){
//生成流
let passWordInput = self.passWordField.rx.text
let userNameInput = self.userNameField.rx.text
//转换流
let passWordValid = passWordInput.map { (passWord) -> Bool in
if let passWord = passWord{
return passWord.count > 6 && passWord.count < 20
}
return false
}
let userNameValid = userNameInput.map { (userName) -> Bool in
if let userName = userName {
return userName.count == 11
}
return false
}
//合并流
let loginValid = Observable<Bool>.combineLatest(userNameValid, passWordValid) { (userName, passWord) -> Bool in
return userName && passWord
}

//绑定
loginValid.bind(to: self.loginButton.rx.isEnabled).disposed(by: bag)
}

可以看出这些代码主要在做的事情,就是流的生成、转换、合并,以及最后与 UI 的绑定。代码看起来也很直观,业务逻辑一目了然,减少了潜在性的错误。

why RxSwift?

RxSwift 是一个利器,而且它的扩展库 RxCocoa 将 UIKit 里的大部分 UI 控件做了 Reactive 化,可以很方便的对控件进行数据绑定,以及监控控件的变化。使用 RxSwift 我们可以省去命令式编程下的很多胶水代码,通过流的转换合并,轻易的写出复杂的逻辑。
响应式编程的难点一个是学习曲线比较陡峭,一个是思维转换比较困难,踏过这两个坎之后,就会发现一个新世界了。

问题

在 CentOS 上安装完 NGINX 之后,尝试修改 NGINX 的 root 地址

1
2
3
4
5
6
7
...
http{
server{
root /root/blog;
}
}
...

在 root 用户下,新建文件夹 blog 如下

1
2
3
--root
--blog
index.html

重启 NGINX 服务

1
service nginx reload

访问 HTTP 80 端口

1
2
3
4
5
6
7
8
$ curl 0.0.0.0:80
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.12.2</center>
</body>
</html>

排查

排查错误日志,发现为Permission denied权限错误

1
2
3
$ tail /var/log/nginx/error.log

2019/04/22 20:36:47 [error] 26764#0: *68 "/root/blog/index.html" is forbidden (13: Permission denied), client: 127.0.0.1, server: _, request: "GET / HTTP/1.1", host: "0.0.0.0"

查看 NGINX 的进程用户,发现 NGINX 的 worker 进程为 NGINX 用户启动的

1
2
3
4
5
$ ps -aux | grep nginx
root 26085 0.0 0.0 125720 5268 ? Ss 17:05 0:00 nginx: master process /usr/sbin/nginx
nginx 26764 0.0 0.0 126140 4224 ? S 19:34 0:00 nginx: worker process
nginx 26765 0.0 0.0 126140 4224 ? S 19:34 0:00 nginx: worker process
root 26955 0.0 0.0 112660 976 pts/5 S+ 20:41 0:00 grep --color=auto nginx

查看 root 文件夹的用户权限,发现 root 文件夹的其他用户只有读权限,这样 NGINX 用户是无法访问的,于是可以理解为何报错 403 了。

1
2
3
$ ls -l

dr-xr-xr--. 10 root root 4096 Apr 22 20:35 root

解决

简单粗暴法

将 nginx.conf 的 user 修改为 root,这样 worker 进程是在 root 用户下启动的,可以顺利访问 /root/blog 文件夹。

1
user root;

更安全的方法

上述方法让 root 用户跑 worker 进程,太危险了。其实可以新建一个账号,在该账号下放置静态文件,然后将其权限暴露,这种做法更加安全。

1
2
3
4
$ adduser www
$ passwd www
$ cp -R /root/blog /home/www/
$ chmod 755 /home/www

再将 nginx.conf 的 root 地址修改为/home/www/blog

1
2
3
4
5
http{
server{
root:/home/www/blog;
}
}

这样,就能正常访问 root 主页了

思来想去,还是要对 2018 年做个记录。

生活

在今年的十月六号,在老家举办了婚礼,跟对象走进了婚姻的殿堂。
感谢我的妻子的理解,以及对我的包容。
从此以后,我的人生之路,有了贤妻陪伴,特别好。
还有一件事情就是开始还房贷了,每个月五千的房贷,三千的装修贷,两千的房租。
人的一生呐还是要靠自己的奋斗,要是在 2017 年,我是不敢想象我能扛起来的。
2019 年 9 月交房。

旅游

跟老婆一起去了趟美丽的开远参加昂然兄的婚礼,多年未见的大学同学聚了聚。
吃了米线、卷粉、烧烤、烧豆腐、竹荪山药鸡,非常感谢昂然兄的款待,云南的吃,特别好。
顺道去了普者黑,普者黑的水,特别好。
倒是没有去云大看看,有点遗憾。

游戏

血源很合我的胃口,玩通了一周目,虽然虐的我不要不要的,但是那种痛快的杀戮感,特别好。
入了怪物猎人,玩不下去,还是喜欢魂系列。
还有 Diablo3,玩了下 NEC,最近开始玩 DH,真香。
下载了很多其他游戏,都没时间或者精力去玩了。

工作

一直以来,生活一直向工作倾斜。
工作中,说的最多的一个字就是
家和公司两点一线。
晚上十一二点回家,也是家常便饭,某些地方肯定是出了问题。
这样难免少了对妻子的陪伴,所以我特别感谢我的妻子的理解。
为了补偿,在我妻子生日那天,我特地请假,陪她去中山公园、万松园路转悠了下。吃了美味的公安石锅鱼杂,特别美好的一天。

蜀道难

技术上,今年只是在搞工程了,没有深入。
接触了 Golang,不过都没有坚持下去,运用起来。可以说看完就忘,一方面自己没有去找应用场景,一方面也是往工作太倾斜,导致自己的时间比较少。
书籍买了不少,真看了的不多。
看过的有《程序员的自我修养》《高效能人士的7个习惯》《白鹿原》,一直在看《重构》、《HTTP权威指南》。
粗浅看了《海子的诗》,记住了如下两段:

山后面是山,
天空上面是天空,
道路前面,还是道路。

走在路上,
放声歌唱,
大风吹过山岗,
上面是无边的天空。

我很敬佩能坚持的人,粗浅看了《胡适留学日记》,胡适大佬每天记录一篇日记,记录了很多年。

Flag

根据以往的经验,flag 仅仅是 flag,但是我觉得还是要说一下,立足长远来看。
三年的 flag 是不上班。
一年的 flag 是开一家淘宝店并且运营起来。
这个一年的 flag 是三年 flag 的一个具体实现途径。

以上,2018年大年三十晚。

rule 1

  • Sequences can have 0 or more elements.
  • Once an error or completed event is received, the sequence cannot produce any other element.
  • When a sequence sends the completed or error event all internal resources that compute sequence elements will be freed.
  • To cancel production of sequence elements and free resources immediately, call dispose on the returned subscription.

rule 2

  • onNext 没结束之前,onComplete、onError、onNext都不会开始执行
  • dispose被释放之后,其相关的订阅都会执行dispose
  • Observerable,subscribe是同步的
  • 可被订阅者,被订阅者,可被订阅,之后发布消息
  • 订阅者,订阅者发布消息

other

现在只是用熟练了绑定,监听。

一八年已经过去了百分之四,之前没有做过什么总结,不过去年确实值得说道说道。

我本打算年初的时候回武汉,但二月份的时候恋爱了,就是这么猝不及防。由于对象在无锡,我选择暂时不回武汉,继续在上海待着。

期间就是恋爱工作看书了。一直到下半年,跟女朋友达成回乡的共识,于是着手实施回武汉。由于工作原因,需要帮公司招个人,以及做一下工作交接,从原定的九月份,一直到十月份才终于算是完成任务。招人挺难的,主要是看缘分。最后招了一位当时从武汉来上海找工作的小伙子,虽然期间发生了一些意外,所幸最后还是顺利入职了。写到这里不禁要感慨下几年前自己背着个包来到上海,脑袋里空空如也,能走到现在这般,真是幸运。

十月国庆假期过后,武汉出台了大学生落户政策,大学生可以无门槛购房了。

回武汉的第一时间,我就把户口落了。然后紧锣密鼓的开始看房,一开始在江夏那边看,定了个楼盘,然后就等着开盘。

在等待的过程中,我开始泡图书馆,每天从庙山坐城铁到武昌,然后乘公交到省图书馆。省图挺好的,有暖气、开水、Wi-Fi、插座,自习区里基本都是满的,氛围非常好,十分适合看书学习。过了几天寻思投投简历,招聘信息里看到了现在的公司,想起之前招的那位小伙子有跟我推荐这家公司,于是毫不犹豫的投了,顺利入职。

后来,江夏的那个楼盘通知我说不开盘了,得,继续看房吧。在同事的介绍下,去看了个光谷南的楼盘,捡了个漏,还剩几套没有卖出去。跟家里商量了下,决定买。但当时的钱离首付还差点,记得那天晚上,列了个名单,挨个打电话借钱,于是房子就这么地了,感谢借给我钱的亲朋好友。剩下的就是好好工作,好好睡觉,好好还债了。

其实回武汉的时候我是怀着一种苦大仇深的情绪的,因为印象中武汉的互联网环境真的不乐观,算是跟外面漂泊的自己和解了,有一种妥协的成分在里面。回来之后,很幸运的加入了现在非常棒的团队。武汉这几年招商引资,很多一线公司都来投资建厂,不得不说环境确实是变好了。个人的发展真的是跟历史的大进程息息相关呐,感谢武汉,感谢互联网时代。

好像没有说以后的事。最近写一些基础设施项目,有一些感悟。我觉得把大的方向定下来了,局部模块都是一个渐渐优化、渐渐积累的过程。比如这篇总结,我也是先搭个架子,优化了好多个版本,算是写出来了。生活也是一样,我也是满怀希望,相信未来的。扛着这么大的担子,真的只有继续努力了。

随着硬件的发展和多核 CPU 的普及,CPU 的性能越来越强了。为了让 CPU 充分运转,聪明的计算机科学家设计出了多线程模式,一个线程也就是相应的一条代码路径,多线程就是多条代码路径。

线程队列

那么怎么控制这些代码路径呢,于是就应运而生了一些代码路径的组合,叫做线程队列。这些队列有不同的执行代码路径的方式,比如窜行执行、并行执行等。

##窜行队列
可以把窜行队列当作并行队列的特殊情况,也就是最大并发数量为1。执行完了一个代码路径之后,根据优先级等因素,接着执行下一个,并不保证严格按照先进先出的顺序执行。那么问题来了,假如要控制窜行队列的执行顺序该怎么办呢,于是有了依赖这个概念,也就是一个代码路径依赖于另外一些代码路径,等到它依赖的路径都执行完了,这一个代码路径才能上路,那么这样就能严格控制执行顺序了。

并行队列

并行队列呢,也能通过依赖控制好某些路径的先后顺序,但是这个并行出现的问题就是要知道并发出去的代码路径都已经执行完毕,要做下一步工作了,于是并行队列就提供了这个功能。并行队列的应用场景可以是一些相互之间没有影响的工作。

取消队列执行

怎么让一个正在执行的代码路径停止呢,建议的做法是,要经常性的在关键性的点检查当前的状态,假如当前处于canceled状态,则停止执行下去,而且这个代码路径也没有机会再次执行了。让一个队列停止,这个队列也只是将当前的代码路径的状态置为canceled,也不发配新的代码路径执行了,恢复之后会继续分配。

既然是多线程,那么不可避免的会出现资源抢夺,于是锁出现了。有资源竞争的互斥锁,有适用于生产者消费者的条件锁,有适用于递归的递归锁。使用锁的过程中要时刻注意防止死锁的发生。把用于访问共享事物的代码块称为临界区。不能让一个临界区始终处于等待状态,也不能让一个临界区始终占有共享区间。

互斥锁

互斥锁,只能对临界区加锁一次,不能对同一个线程加多次锁,这样会造成死锁。对临界区加锁的时候,如果临界区被使用了,则当前线程被阻塞,直到临界区被释放,轮到当前线程进入临界区。假如有多个线程处于等待状态,则根据优先级进行队列处理。

条件锁

比互斥锁多的一个是只有锁在特定条件下才能加锁,释放锁的时候可以指定锁的当前条件。适用于生产者消费者模式,一个线程负责生产,其他线程负责消费。负责生产的轮询进入临界区,条件则是一个判定是否要生产的条件,比如货物数量为10则开始生产,生产完毕后将条件设置为shouldProduct = false,消费线程可以进入临界区消费。负责消费的线程出临界区的时候,根据剩余数量判断是否需要生产,如需生产,则将条件shouldProduct = true,则生产线程进入临界区生产。

递归锁

当在递归情况下,使用互斥锁的时候,会造成死锁。这种情况下适用递归锁,允许在一个线程里多次加锁和解锁,当解锁次数跟加锁次数相等时,出临界区,由于要做额外的加锁次数判断,开销会比互斥锁大。