CMake文件种类
CMake除了配置文件CMakeList.txt 之外,还可以通过include的方式加载文件 类似于头文件
加载公用函数
最常见的使用方法就是用来加载一些公用的函数
1 | include(MultiplexingFunc.cmake) |
根据平台不同组织逻辑
还可以根据内部逻辑来组织CMake代码,例如不同的平台加载不同的文件
1 | if(IOS) |
其中 IOS 和 APPLE 变量来自于CMake官方文档Variables that Describe the System
CMake除了配置文件CMakeList.txt 之外,还可以通过include的方式加载文件 类似于头文件
最常见的使用方法就是用来加载一些公用的函数
1 | include(MultiplexingFunc.cmake) |
还可以根据内部逻辑来组织CMake代码,例如不同的平台加载不同的文件
1 | if(IOS) |
其中 IOS 和 APPLE 变量来自于CMake官方文档Variables that Describe the System
CMake本身整理逻辑可以通过 宏(macro) 和 函数(function),声明十分类似
在宏的官方文档中有提到两者不同的问题,提出了两点主要的差别
1 | The macro command is very similar to the function() command. ...... |
例如我们想输入一个变量获得结果,有两种方式,但是经过对比可以发现 load_message_func 并不能正确输出
原因是因为func拥有单独的scope, variable 和外部的并不通用,C语言里一般会采用传入指针来解决
1 | # 声明Macro |
CMake里的函数是没有返回值的,也就是说需要有方法做到类似JS的作用域提升
1 | # 声明Func,并采用作用域提升 |
添加源文件和头文件之后,对于库的依赖可以使用
1 | target_link_libraries(<target> ... <item>... ...) |
其中item可以使用的范围包括以下这些,其中最常用的就是库文件的路径
| 原文 | 解释 |
|---|---|
| A library target name | 使用 add_library() 定义的对象 |
| A full path to a library file | 一个库文件的路径 |
| A plain library name | 一个库的名字,会在编译过程中默认使用,如果没有就报错 |
| A link flag | 一些编译标记位 |
| A generator expression | 通配符表达式 |
如何找到第三方库的路径,则是整个命令的基础,一般分为两种方式
如果是自行编译的第三方库,无非是放到一个已知的位置(例如工程目录libs下)
1 | target_link_libraries(HelloWorld ${CMAKE_CURRENT_LIST_DIR}/libs/lib.a) |
CMake本身提供了一个指令叫 find_package 来查找存在哪些第三方库
1 | # 参数列表很长此处省略 |
其工作原理是根据输入的名字PackageName,全局搜索三个配置文件,来获取这个库的配置相关配置
三个配置文件分别是 FindPackageName.cmake 、PackageNameConfig.cmake、packagename-config.cmake
Mac系统可以使用HomeBrew安装OpenCV, 不然只能手动编译,配置更加复杂一些
1 | brew install opencv |
安装完成后在CMakeLists.txt 使用指令 find_package 即可获得两个变量
1 | find_package(OpenCV REQUIRED) |
如果打印出来可以看到其输出
1 | # OpenCV include directories: /usr/local/Cellar/opencv/4.1.1_1/include/opencv4 |
为什么OpenCV使用了 OpenCV_INCLUDE_DIRS 、OpenCV_LIBS 这两个变量名
命名规则 PackageName 加上 _INCLUDE_DIRS、_LIBS 是众多第三方库约定俗成的习惯
CMake官方文档并没有强制要求,最终还是取决于其CMake配置文件的内容 我们可以去自己查看
根据CMake的官方要求,OpenCV的配置文件应该是
这三个文件之一,根据查找Homebrew或者源文件我们可以看到其位置,并且使用的是 OpenCVConfig 的名字
1 | # Home brew |
我们可以看到文件中存在说明定义了哪些变量
1 | # This file will define the following variables: |
根据CMake的官方文档,还存在一种备用的Module模式,其配置文件类似于Config的命名规则
1 | The command has two modes by which it searches for packages: “Module” mode and “Config” mode. The above signature selects Module mode. If no module is found the command falls back to Config mode, described below. This fall back is disabled if the MODULE option is given. |
根据编译基本概念,每一个可执行程序(Executable,Target、Package)都是由许多的源文件组成
CMake很直接的表达, 参数分别为 (名称 平台标记 源文件可变参数) ,平台标记一般可以先不考虑
1 | # add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] [source1] [source2 ...]) |
添加头文件有两个命令,一个在2.8.10之后生效,更加突出了Target的概念
1 | # 2.8.10之前 |
在 target_include_directories 最大的改变我们可以对头文件设置分类
PRIVATE 和 PUBLIC 会被放入Target的变量 INCLUDE_DIRECTORIES,PUBLIC 和 INTERFACE 则会被放入 INTERFACE_INCLUDE_DIRECTORIES 变量
当时用到添加源文件和头文件的命令时,随着工程增大会逐渐发现两个问题
为了解决这两个问题,我们可以使用 file 命令的GLOB功能
1 | # 非递归查找 |
例如我们递归选出所有的cpp文件,其中使用到了一些其他知识
1 | # 递归查找当前 CMakeLists.txt 文件下 source 内的以 cpp 结尾的文件 |
CMake会影响编译行为的参数有很多,其中最常用的就是 CMAKE_BUILD_TYPE
可能的值包括空值、非空值Debug, Release, RelWithDebInfo, MinSizeRel 等
其中Debug和Release是最常用的两个选项,直接影响到了可不可以打断点调试
配置 CMAKE_BUILD_TYPE 有两种方式
以HelloWorld的的举例
1 | #切换到文件夹 |
注意在 -D 指令传入参数时有多种方法,以下都是正确的
1 | cmake . -D CMAKE_BUILD_TYPE="Debug" |
在阅读这个系列文章之前,需要掌握一些简单的前置概念
而CMake是用于组织工程结构的面向过程语言,和以上三个概念的关系可以简单解释为
分析一个简单的 Hello World 工程, 首先新建文件夹,并且建立对应的文件
1 | ----HelloWorld |
然后我们可以通过以下三个指令,分别完成上述的文章开头的三个功能
1 | # 切换到文件夹 |
整个过程中有两个入门级的问题
CMake会以输入的目录为参考,根据CMakeLists.txt生成一套 Scheme(Apple的叫法),作用是用来描述如何编译当前的代码
在实际工程中一般不会这么粗暴的使用当前目录 “.” 干扰工程的文件结构 ,而是使用一个专门的Build目录,参考CMake02章节。
执行完CMake会生成不止一个 .make 文件,build.make 位于 “./CMakeFiles/HelloWorld.dir/build.make”
原因在于make本身是一个调度工具,不仅仅只用来编译,可以用于任何的指令调度
根据前文,CMakeLists.txt 是一个对工程文件描述的过程语言,HelloWorld中配置内容如下
1 | # 当前CMake的最低版本 |
和成熟的IDE对比,这份配置文件十分的简陋,而且缺少了很多常见的概念,例如头文件、第三方库等等
后续文章我们会以 Xcode 、Android Studio为对比一步步讲解和对比这些细节
最后补充main.cpp的内容
1 | #include <iostream> |
CMake对其能力划分了功能周期, 更详细的内容可以阅读官方文档,其中大致分为
我们刚刚的指令 “camke .” 就是 Generate, “make .” 就是 Build,官方文档给出了更方便的命令 “cmake –build”
工程本身是对代码的组织方式,有一些公共的概念
| 抽象概念 | 含义 |
|---|---|
| 源文件(Source) | 被编译的文件,最终转化成二进制 |
| 头文件 (Header) | 不会被编译的文件,声明(Declare)文件,用于协助别的源文件找到定义 |
| 源文件的产物 | 一个可执行的程序,由多个源文件编译而来 |
| 目标的管理器 | 可执行程序的管理,例如目标1由A、B、C文件组成,目标2由B、C、D文件组成,其中B、C两个文件被复用 |
| 管理器的空间 | 目标管理器的管理器,用于组织多个目标在一个范围内工作 |
| 头文件目录 | 如果存在多个目录,那么头文件路径的前缀会不一致,前缀不同的源文件就需要知道该头文件所在的位置,头文件目录提供这个位置方便源文件在编译中索引 |
| 源文件目录 | 所有源文件存放的根目录,方便工程以此目录作为绝对目录开始组织源文件 |
| 静态库目录 | 如果依赖第三方库,需要知道该静态库存放的位置,用于编译 |
| 动态库目录 | 如果依赖第三方库,需要知道该动态库存放的位置,用于编译 |
由于上述的公共概念并没有严格的规定,所以对这些概念在不同的平台和语言中也有区别
| 抽象概念 | iOS | Android | C++ |
|---|---|---|---|
| 源文件 | .mm | .java | .cpp |
| 头文件 | .h | .hpp | |
| 源文件的产物 | Target | Package | Executable、Target |
| 目标的管理器 | Project | Module | Project |
| 管理器的空间 | Workspace | Project | Directory |
| 头文件目录 | Include Path | Package | Include |
| 源文件目录 | Source Root | Source Root | Source Root |
| 静态库目录 | Library Path | Dependencies | Library Path |
| 动态库目录 | Framework Path | Dependencies | Library Path |
有一些重点是
这里解释一些微妙的差别
由于 HTTP 是基于 Readable String 的协议体系,取决于其设计原理,传输内容没有校验机制,而且很容易被篡改
最简单的加密方法就是对称加密,使用加密算法和 Key 将明文转化成持有 Key 的用户可以解读的密文
1 | //加密算法逻辑: 明文+Key 通过加密算法得到 密文 |
作为对称加密的经典思路,通过简单思考很容易引发(问题1-1)
而非对称加密算法可以很容易的解决对称加密引发的问题
1 | //加密算法逻辑: 明文+公钥Key 通过加密算法得到 密文 |
根据非对称加密的思路,PublicKey 是可以任意传递和分发的,而 SecretString 也仅仅能被 PrivateKey 解密
那么在对称加密的问题1-1对应的解法就有了
在通信的过程中,按照 C/S 架构设计通常存在 Server 和 Client 两个终端
完成了 Client–>Server 加密逻辑,如何保证返回报文 Server–>Client 也具备加密能力,就是(问题1-2)
在对称加密的设计下 Server 和 Client 公用一个Key,加密解密都使用这个Key即可, 时序图
1 | //ReqStr: Request String ResStr: Response String |
而在非对称加密的设计下
因为 PublicKey 加密后仅有 PrivateKey 可以解密,而 PrivateKey 又不能传递(不然就需要面对和对称加密一样的问题)
那么在非对称加密的设计下,点对点就需要两个密钥对(共计4个密钥)
| 密钥 | 作用 |
|---|---|
| Server-PublicKey | 用于加密Client–>Server的通信 |
| Server-PrivateKey | 用于解密,不应该以任何渠道离开Server |
| Client-PublicKey | 用于加密Server–>Client的通信 |
| Client-PrivateKey | 用于解密,不应该以任何渠道离开Client |
根据 对称加密 和 非对称加密 的设计,我们可以得到时序图
1 | //C-PubKey: Client Public Key C-PriKey: Client Private Key |
通过时序图,我们可以得到
在通常的互联网服务中,大多数都是多个客户端和一个服务器的多对一服务,在多对一的设计中必然存在以下问题:
在对称加密中,因为使用同一个Key,那么
而非对称加密中因为 Server–>Client的 Response 是由 ClientPublicKey 加密的
而ClientPublicKey 必须和 ClientPrivateKey 配对使用,从设计上保证了问题1-3
而非对称加密对于问题1-4的解决方案,则是 HTTPS 的核心理论密钥交换机制 从结论上说,密钥交换从根本上解决了非对称加密的问题1-3和问题1-4
HTTPS 的密钥交换设计保证加密通信的以下特性
密钥交换原理中最重要的是 一旦密钥交换完成,通信就可以认为没有任何被破解攻击的可能,时序图如下
1 |
|
由于 Client 每次发起通信请求,都可以先去服务器获取到最新的 ServerPublicKey
而 Client 在获取到 ServerPublicKey 之后可以即时生成 ClientKeyPair 保证了 Client 的更新
在传递 ClientPublicKey 的过程中,已经被加密过了,仅有 Server 可以用 S-PriKey 读取
Server 读取 ClientPublicKey 成功后,之后的通信就可以完成点对点的加密能力
根据密钥交换设计,每次通信建立 Server/Client 均使用的是最新的私钥,在通信关闭后可以直接丢弃不需要储存,从而得到问题1-3、1-4的解法
解法1-3、1-4: 非对称加密可以通过密钥交换机制来保证不同加密会话的独立,并且规避了密钥更新同步的问题
根据密钥交换的假设,一旦交换成功,会话就是99.9%安全的,那么加密问题被转化为一个简单的问题
问题2-1: 密钥交换过程中哪里会被攻击
密钥交换过程最大的攻击方式,就是中间人攻击
而攻击的方式就是在第一步,由一个中间人伪装服务器和客户端,分别和两方完成密钥互换,从而完成会话攻击
1 | // M-PubKey: Middle Public Key , use for cheat client as S-PubKey, cheat server as C-PubKey |
根据时序图,我们可以发现,第一步就存在一个中间服务器,使用一套密钥对 M-PubKey、M-PriKey 来进行伪装,那么他就可以截获所有会话内容
| 中间人密钥 | 对客户端 | 对服务端 |
|---|---|---|
| MiddlePublicKey | 伪装成ServerPublicKey下发给客户端 | 伪装成ClientPubKey发送给服务端 |
| MiddlePrivateKey | 用来解密Client的消息,相当于ServerPrivateKey | 用来解密Server的消息,相当于ClientPrivateKey |
通过以上分析可以得出,密钥交换的过程中进行攻击的基础,问题2-1的答案就是
在分析了攻击点之后,问题2-1就会引出问题2-2
首先给出答案的思路,问题2-2 的解决方案需要两个基础
首先需要明确的概念,在非对称加密通信中的常规加密思路, PublicKey 加密, PrivateKey 解密
而数字签名技术,则是 PrivateKey 加密, 而 PublicKey 解密,整个逆向的过程称之为签名
| 功能 | 加密密钥 | 解密密钥 |
|---|---|---|
| 非对称加密 | PublicKey | PrivateKey |
| 数字签名 | PrivateKey | PublicKey |
两种使用方式,都基于非对称加密,其中签名基于的前提是 PrivateKey 永远只能被终端持有,不应该以任何方式传播出去
1 |
|
回到问题2-2本身,如何让 Client 收到 S-PubKey 之后,能够确定这个 PublicKey 是没有被篡改过的
最简单的一个思路就是,存在一个第三方,可以明确告诉 Client 这个 S-PubKey 是不是可信的,这个第三方就是CA(Certificate Authority)
CA运作的技术基础就是数字签名技术,通过引入第三个密钥对 CA-PublicKey、 CA-PrivateKey 然后分为两个模块来完成
| 模块 | 作用 |
|---|---|
| 证书 | 颁发给各个互联网服务商,也就是每一个HTTPS的服务器,通过 CA签名证明 S-PubKey 是真实有效的 |
| PKI | 委托生产商把 CA-PublicKey 安装到每一个终端(每一个笔记本、每一个iPhone),用于验证证书签名 |
其中PKI是(Public Key Infrastructure)的缩写,中文叫 公钥基础建设 是一个非盈利组织在管理
PKI 的基本要求是把 CA 的证书预装到每一个合法生产的设备上, 通过联合设备生产商规避了问题1-1
每个设备都有一个默认的列表,比如iPhone好像是有8个
这些 CA-PubKey 也是以证书的形式存在于手机中,是 CA 自己颁发给自己的证书,对自己的 PublicKey进行了签名
1 |
|
通过引入 CA的第三个密钥对 和 基于数字签名技术,我们得到了问题2-2的解决方案
通过以上分析,如果再深层次分析引出了问题2-3
问题2-3: 整个PKI+证书的体系是不是完美的,存在不存在攻击的可能
解法2-3: 整个体系其实是非常脆弱的,攻击点大概率存在于Client端,其次是Server端
首先给出答案, 整个体系其实是非常脆弱的,攻击点首先存在于Client端,稍后会列举几个常见的例子
继续思考,除了Client之外,Server存在不存攻击的点,比如DNS劫持等等,不管任何手段都想达到让 Client 误认为假的Server为真,那么就可以引出问题2-4
如果相对证书进行伪造,需要考虑两种可能
| 攻击方案 | 方案条件 |
|---|---|
| 方案-A | Client 终端受到控制,终端上 PKI 已经被攻击者攻破 |
| 方案-B | Client 终端不受控制,并且 PKI 完善 |
首先来考虑 方案-A 的前提条件,就是攻击设备的 PKI 体系
攻击 PKI 体系是最简单的,整个 PKI+Cert 的前提是设备内由生产商预装 CA-PubKey 是没按照预期安装好的,没有被篡改过的,如果想要篡改
| 手段 | 例子 |
|---|---|
| 恶意软件在越狱的手机直接修改 | Android Su权限、iPhone越狱 |
| 诱导 Client 信任一个不是CA的证书(非法的PublicKey) | Charles、铁道部12306 |
在完成了 PKI 攻击之后,我们可以添加任意的 PublicKey, 假设存在攻击密钥对 Fake-PublicKey、Fake-PrivateKey
我使用 Fake-PriKey 签名的任何证书都可会被设备认定成合法的CA证书
在方案-B中,我们无法控制设备的 PKI 列表, 也就是说必须有一张被真实CA机构签发对证书
那么就需要先解决 问题2-5:
最简单的解决方案就是买
其次是通过CA机构的工具联网申请,例如 LetsEncrypt 是一个2015年成立免费SSL证书组织,其工具叫 certbot 可以在各大软件源install
申请和申请QQ号没什么区别,主要是提交你想申请的域名,以及最重要的是验证方式(Challenge)
1 | # --manual 代表手工更新,推荐使用自动更新,这里仅作讲解 |
可见如果想通过CA的证书申请,其中 challenge 的方式决定了我们如何通过验证
| Challenge | 验证方式 |
|---|---|
| dns | 在域名的 DNS 解析服务商里添加一条指定的记录,一般是一个TEXT记录的加密文本 |
| tls-sni | 在域名指向的服务器443端口修改服务器配置文件 |
| http | 在域名指向的服务器80端口放置指定的文件 |
此时我们可以得出问题2-5的答案
那么回到问题2-4,在申请机制的过程中,我们如何进行伪造来通过 Challenge 从而获取合法的证书
首先我们可以发现 CA 的验证逻辑是,如果申请者可以控制域名的DNS服务(dns)或者主机文件(http、tls-sni),就认为该申请者是域名的合法持有者
至于可以通过何种方式获取到所需业务的账户名和密码,以及如何物理接触到主机,常见的有弱密码破解或者钓鱼邮件等方式,就不展开讨论了
通过总结,我们可以发现整个 PKI + CERT 安全体系,是需要多方配合的,而且是十分脆弱的
攻击终端的成本比起伪造证书要低很多,不管是 Client 自己操作不当导致终端的 PKI体系 被破坏,还是由于 Server 管理不善造成证书伪造,都有可能进行中间人攻击
为了防止设备被破解,我们可以通过 SecTrustSetAnchorCertificates 设置PKI列表,通过官方文档可以看到,如果传入NULL则是使用默认的预装证书
1 | //A reference to an array of SecCertificateRef objects representing the set of anchor certificates ...... |
固件升级验签应该在哪里,固件是由网络下载到终端,安装前为了防止固件被修改过,肯定需要验证这个固件的签名,按照逻辑终端PKI本身是可能被攻击的,所以选择网络验证签名
1 |
|

宏在很多语言里都有,是一种预处理(Preprocessor)阶段的流程的能力,预处理主要知识包含
条件(conditionally)、替换(replace)、包含(include)、错误(error)、实现定义(implementation defined)、文件和行定义(file name and line information)
宏的作用主要是替换(replace)其灵活性极高可读性极差,不太推荐使用过于复杂的技巧,可以拆分为以下几点
宏的定义可以分为 Object-like macros 和 Function-like macros, 一种类似变量的定义一种类似方法的定义
1 | #define identifier replacement-list(optional) (1) |
可以看到 Function-like macros 定义中最关键的点在于可变参数
C语言本身 stdarg.h 头文件提供可变参数的能力,例如print函数是一个典型的应用
1 | double average(int num,...) |
宏本身也具备和C语言类似的可变参数能力,只需要在宏定义中包含关键词 VA_ARGS
1 | #define AVERAGE(...) average(__VA_ARGS__) |
即可表达可变参数的传递
在 Function-like macros 中除了关键词,还包含以下主要的操作符
| 操作符 | 符号 | 作用 |
|---|---|---|
| “Stringification” | # | 如果在 replacement-list 的某个 identifier添加操作符 |
| “Concatenation” 、”Token Pasting” | ## |
根据官方文档原文,字符串化操作符的主要作用是使用双引号把某个定义括起来,以字符串解析出来进行展开(Expand)
1 | a # operator before an identifier in the replacement-list runs the identifier through parameter replacement and encloses the result in quotes |
一个简单的举例, 其字符串化某个 identifier 时,如果identifier本身就是字符串,则不会有任何影响
1 | #define showtext(num) puts(#num) |
一个关键的点在于,如果和可变参数__VA_ARGS__共同使用时,会将整个可变参数变为一个字符串参数
1 | #define showlist(...) puts(#__VA_ARGS__) |
链接操作符的作用主要是将还未展开的先切断replacement-list,对前后的identifiers分别进行展开然后再拼接到一起,原文如下
1 | A ## operator between any two successive identifiers in the replacement-list runs parameter replacement on the two identifiers (which are not macro-expanded first) and then concatenates the result. |
这个操作符需要注意的地方是,操作符前后的定义必须是已知的identifiers,不然会报错,大多数中文博客将其理解成
“将宏定义中的多个参数连接形成一个参数”
1 | #define showtext(x,y) puts(x##y) |
还存在两种并不是所有编译器都支持的操作符 “#@” 和 “# #” 这里就不展开介绍了
曾经有个师哥问我32位系统和64位系统中一个字节分别对应几位。
当时我还没毕业,想你说的什么玩意儿。。
写了一年多传输层的代码之后才牢牢记住,无论哪个系统一个字节永远都对应8位。
字节 = byte, 位 = bit, 1byte = 8bit
byte是计算机的最小储存单位,bit为计算机的最小逻辑单位
32位CPU代表每次(不是每秒)CPU可以运算32个0/1逻辑,64位则是64个
在TCP层经常会使用到少于一个byte (8bit) 的标记位,因为计算机储存的最小单位是byte
所以需要对byte拆分的时候就会用到位域
1 |
|
分析输出我们会发现
1 | value: first = 1 |
字节对齐是另一个常见问题,原因在于CPU为了提高效率,会利用一些冗余空间把字节对齐成某个固定值的整数倍
但是对于我们操作byte结果很不方便,比如以下三个结构体在内存中占用的空间
1 | //char 位于 int 之前 |
通过对sizeof来看他们在内存中实际占用的空间,例如
1 | #include <iostream> |
分析输出,首先打印基本类型在内存中的空间
char(1byte = 8bit), int(4byte = 32bit), short(2byte = 16bit), double(8byte = 64bit)
然后无论 value_1 还是 value_2 无论我的数据组成顺序,都是 8byte > 1byte + 4byte (char + int)
这种利用3byte的冗余空间来保存结构体的优化,就是称为字节对齐,字节对齐的规则一般是使用整个结构体中最长的那个
如果我们声明使用 pack(1) 使用1byte进行对齐,则可以获得不包含冗余空间的内存结构 5byte = 1byte char + 4byte int
1 | sizeof char : 1 |
tag:
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent:
meta: false
pages: false
posts:
title: true
date: true
path: true
text: false
raw: false
content: false
slug: false
updated: false
comments: false
link: false
permalink: false
excerpt: false
categories: false
tags: true