可能我们平时有过以下疑问:

  1. C++函数和C函数有区别吗?
  2. C函数的调用过程是什么样的?
  3. C++函数的调用过程是什么样的?
  4. 类似 int sum(int, int)这样的函数,可以被正常调用并返回期望的结果吗?
  5. Obj* obj = nullptr; obj->show(); obj->get_name();可以编译通过吗?如果可以,运行后会显示什么?
  6. std::function、std::bind、lambda底层是怎么实现的?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class Obj {
public:
void show() {
std::cout << "Obj::show" << std::endl;
}

virtual const char* get_name() {
return "obj";
}
};

int main(int argc, char* argv[]) {
Obj* obj = nullptr;
obj->show();
obj->get_name();

return 0;
}

后续示例代码编译环境:

  • 操作系统:Windows7及以上(推荐Windows11)
  • 编译器:Visual Studio 2010及以上(推荐Visual Studio 2022)
  • 编译选项:x86 debug版本
  • IDA反汇编工具
  • 一个在线把C++翻译成汇编的网址:https://godbolt.org
    没有Windows环境的,也可以借助这个网站来模拟学习,选择x86 msvc v19.lastest

本文涉及到的知识点包括:

  • C++
  • x86汇编
  • 栈平衡
  • 软件安全(缓冲区溢出攻击)

本文将从编译器的视角分析编译的C++程序。简单的通过利用调试器、调试器里的反汇编功能,来提供一种剖析C++的底层原理的思路,从而帮我们深入理解C++。
文中涉及到的示例代码均可在推荐的开发环境中编译和运行。


在分析开篇的那几个问题前,我们先看一个函数的调用过程

为此,我们采用一段简单的C++代码来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// demo1.cpp

#include <iostream>

int sum(int a, int b) {
return a + b;
}

int main(int argc, char* argv[]) {
int num1 = sum(1, 2);
std::cout << num1 << std::endl;

return 0;
}

假设我们猜测int num = add(1, 2);调用过程如下:(当然也可以跳过假设直接查看反编译代码)

1
2
3
4
5
6
7
8
9
__asm {
pushad // 保存寄存器当前存储的值
push 2 // 把sum(1, 2); 中的参数2压栈
push 1 // 把sum(1, 2); 中的参数1压栈
call sum // 调用sum函数
add esp, 8 // 手动做栈平衡(如果把这行注释掉,栈就不平衡了,程序直接崩溃,可自行测试)
mov num1, eax // 把sum函数的执行结果,赋值给变量num1
popad // 恢复寄存器数值,恢复为pushad保存的数值
}

一些关于汇编的参考资料:
x86汇编指令集大全:https://zhuanlan.zhihu.com/p/53394807
机械指令 对应 汇编指令:https://blog.csdn.net/jzxin0/article/details/106069838
那么写一段完整代码来验证:

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
// demo1.cpp

#include <iostream>

int sum(int a, int b) {
return a + b;
}

int main(int argc, char* argv[]) {
int num1 = sum(1, 2);
std::cout << num1 << std::endl;

int num2 = 0;
// int num2 = sum(1, 2);函数调用过程,在编译器看来是等效于如下过程:
__asm {
pushad // 保存寄存器当前存储的值
push 2 // 把sum(1, 2); 中的参数2压栈
push 1 // 把sum(1, 2); 中的参数1压栈
call sum // 调用函数sum
add esp, 8 // 手动做栈平衡
mov num2, eax // 把sum的执行结果,赋值给变量num2
popad // 恢复寄存器原来的数值,恢复为pushad保存前的数值
}
std::cout << num2 << std::endl;

return 0;
}

上述代码运行结果如下:
pSzGhqI.png
从执行结果来看,num1的值等于3,num2的值等于3,num1等于num2,也就是说这段内联汇编等效直接调用函数。由此我们可以猜测:
函数调用过程:在调用一个__cdecl调用约定的函数前,首先保护当前寄存器的数值,接着是参数入栈,然后调再用该函数,调用结束后接着做栈平衡,最后再恢复寄存的值为函数调用前的值。
那么我们如何验证我们猜测呢?
我们可以借助调试器的反汇编功能,在调用函数前设置一个断点,然后运行程序,等程序在断点处停下来后,点击鼠标右键选择转到反汇编
pSzG5Zt.png
然后我们可以看到:
pSzGfsA.png
可以看到反汇编代码和我们编写的内联汇编代码思想一致,也就证明了我们上述结论是正确的。

这里我们在发散一下:
我知道CPU只能识别机器指令(0和1排列组合)。同时一个汇编指令对应一个机器指令,有没有一种可能让CPU直接执行我们写的0和1机器语言代码呢? 想起来还有点小激动。
为此我们需要补充一些汇编指令对应到机器码的知识,终于在学习若干小时候后(直接快进),我们得到如下代码:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// demo2.cpp

#include <iostream>
#include <Windows.h>

int sum(int a, int b) {
return a + b;
}

int main(int argc, char* argv[]) {
int num1 = sum(1, 2);
std::cout << num1 << std::endl;

int num2 = 0;
// int num2 = sum(1, 2);函数调用过程,在编译器看来是等效于如下过程:
__asm {
pushad // 保存寄存器当前存储的值
push 2 // 把sum(1, 2); 中的参数2压栈
push 1 // 把sum(1, 2); 中的参数1压栈
call sum // 调用函数sum
add esp, 8 // 手动做栈平衡
mov num2, eax // 把sum的执行结果,赋值给变量num2
popad // 恢复寄存器原来的数值,恢复为pushad保存前的数值
}
std::cout << num2 << std::endl;

__asm {
push ebx
lea ebx, sum
}
// 下边的数组shell_code中的代码等效于int num2 = sum(1, 3);
unsigned char shell_code[] {
0x60, // pushad
0x6A, 3, // push 3
0x6A, 1, // push 1
0xFF, 0xD3, // call ebx(因为ebx存储的值是sum)
0x83, 0xC4, 8, // add esp, 8
0x89, 0x45, 0xE8, // mov num2, eax
0x61, // popad
0xFF, 0xE1, // jmp ecx(因为ecx存储的值是end_tag, 等效与jmp end_tag)
};
// 修改数组shell_code这段内存为可读可写可执行
DWORD oldProtect = 0;
VirtualProtect(shell_code, sizeof(shell_code), PAGE_EXECUTE_READWRITE, &oldProtect);

// 我们直接让CPU执行数组shell_code中的内容
__asm {
pushad // 保存寄存器当前存储的值
lea eax, shell_code // 把数组shell_code的地址放入eax
lea ecx, end_tag // 把end_tag地址放入ecx
jmp eax // 跳转到数组shell_code的首地址
}

end_tag:
__asm {
popad // 恢复寄存器原来的数值,恢复为pushad保存前的数值
pop ebx; // 恢复ebx寄存器原来的数值
}
std::cout << num2 << std::endl;

return 0;
}

上述代码执行结果如下:
pSzGWMd.png
到此我们应该庆祝一下,现在我们已经掌握了让CPU直接执行0、1代码的方法。
但也别高兴的太早,似乎软件安全面临挑战,既然我们可以执行一段由0、1组成的shellcode buffer,那么不法分子也一样可以。如果我们代码有strycpy等类似函数,又没有对输入长度做检查,亦或memcpy(a,b,n)将b中的n个字符拷贝到a处。但是如果 n>a,那么都会存在缓冲区溢出攻击风险。只要不法分子精心构建一段由0、1组成的shellcode,那么我们软件、甚至整个操作系统都有可能受到威胁。
有兴趣进一步了解缓冲区溢出攻击的可以自行百度,或者参考文章 https://zhuanlan.zhihu.com/p/32473371 里的思路。

1. C++函数和C函数有区别吗?

1.1 C++类成员函数与C函数调用约定不同

首先我们大家一起回顾一下常见的函数调用约定。
|调用方式|参数入栈顺序|局部变量清理由谁清理|
|:-:|:-:|:-:|
|pascal|从左到右|函数内部清除(由编译器帮忙实现)|
|
cdecl|从右到左|函数外部清除(由编译器帮忙实现)|
|fastcall|从右到左|函数内部清除(由编译器帮忙实现)|
|
thiscall|从右到左|函数内部清除(由编译器帮忙实现)|
|__stdcall|从右到左|函数内部清除(由编译器帮忙实现)|

其他非常见:
nakedcall:一般出现在汇编中,编译器不会给这种函数增加初始化和清理代码。 vectorcall:尽可能利用寄存器来传递函数的参数变量,vectorcall对寄存器的使用数目多于fastcall。

__pascal: 是Pascal语言的函数调用方式,其特点是函数参数是从左到右的压栈方式入栈,局部变量在函数内部清除。(在windows下pascal已经不被msvc编译器所支持,PASCAL实际被编译器替换成了stdcall)
__cdecl:是C和C++程序普通函数的缺省调用方式,其特点是函数参数是从右到左的压栈方式入栈,局部变量在函数外部清除。
__fastcall:调用速度快,前两个(或若干个)参数由寄存器传递,其余参数还是通过堆栈传递。函数参数从右到左的压栈方式入栈,局部变量在函数内部清除。
__thiscall:是C++类成员函数的调用方式,其特点是this指针被放在特定寄存器中(VC使用ecx),函数参数从右到左的压栈方式入栈,最后一个入栈的是this指,局部变量在函数内部清除.
__stdcall:其特点函数参数从右到左的压栈方式入栈,局部变量在函数内部清除。

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
40
41
42
43
44
45
46
47
48
49
50
// demo10.cpp

#include <iostream>
#include <windows.h>

int sum_def_call(int a, int b) {
return a + b;
}

// 在windows下__pascal已经不被msvc编译器所支持,PASCAL实际被编译器替换成了__stdcall
int PASCAL sum_PASCAL(int a, int b) {
return a + b;
}

int __cdecl sum_cdecl(int a, int b) {
return a + b;
}

int __fastcall sum_fastcall(int a, int b) {
return a + b;
}

class Obj {
public:
int __thiscall sum_thiscall(int a, int b) {
return a + b;
}
};

int __stdcall sum_stdcall(int a, int b) {
return a + b;
}

int main(int argc, char* argv[]) {
int num1 = sum_def_call(1, 2);
int num2 = sum_PASCAL(1, 2);
int num3 = sum_cdecl(1, 2);
int num4 = sum_fastcall(1, 2);
int num5 = Obj().sum_thiscall(1, 2);
int num6 = sum_stdcall(1, 2);

std::cout << num1 << std::endl;
std::cout << num2 << std::endl;
std::cout << num3 << std::endl;
std::cout << num4 << std::endl;
std::cout << num5 << std::endl;
std::cout << num6 << std::endl;

return 0;
}

对应的反汇编代码:
pSzGqzQ.png

如果如果入栈参数所占用栈内存,不清理会怎么样?
会破坏栈平衡,可能会导致程序直接崩溃。

1.2 C++支持函数重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// demo3.cpp

int sum(int a, int b) {
return a + b;
}

double sum(double a, double b) {
return a + b;
}

int main(int argc, char* argv[]) {
sum(1, 2);
sum(1.0, 2.0);

return 0;
}

通过IDA,打开编译好的二进制文件,我可以看到:
pSzG2xH.png
sum函数名,被编译器改为了j?sum@@YAHHH@Z和j?sum@@YANNN@Z,C++从而支持了函数重载。

1.3 C++支持默认参数

1
2
3
4
5
6
7
8
9
// demo4.c

int sum(int a, int b = 1) {
return a + b;
}

int main(int argc, char* argv[]) {
return 0;
}

代码demo4.c后缀为.C,启动编译编译,构建结果:
pSzGIdP.png
构建失败,说明C语言不支持默认参数。

1.4 C++支持仿函数

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
// demo5.cpp
#include <iostream>
#include <string>

class PeoPle {
public:
int operator()(const std::string& name) {
if (name == "xiao_ming") {
return 18;
} else if (name == "xiao_hua") {
return 20;
} else {
return 0;
}
}
};

int main(int argc, char* argv[]) {
PeoPle year;

int y1 = year("xiao_ming"); // 通过operator(),使用起来像一个函数一样
int y2 = year("xiao_hua");

std::cout << y1 << std::endl;
std::cout << y2 << std::endl;

return 0;
}

1.5 C++支持std::function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// demo6.cpp
#include <functional>
#include <iostream>

int sum(int a, int b) {
return a + b;
}

int main() {
std::function<int(int, int)> sum1 = sum;
std::cout << sum1(1, 2) << std::endl;

return 0;
}

pSzGbRg.png
可以看到std::function通过std::function类中operator()操作符重载实现。(当然也可以直接std::function源码来学习具体实现)

1.6 C++支持std::bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// demo6.cpp
#include <functional>
#include <iostream>

int sum(int a, int b) {
return a + b;
}

int main() {
std::function<int(int, int)> sum1 = sum;
std::cout << sum1(1, 2) << std::endl;

auto sum2 = std::bind(sum, std::placeholders::_1, 1);
std::cout << sum2(2) << std::endl;

return 0;
}

pSzGOMj.png
可以看到std::bind通过std::bind类中operator()操作符重载实现。(当然也可以直接查看std::bind源码来学习具体实现)

1.7 C++支持lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// demo6.cpp
#include <functional>
#include <iostream>

int sum(int a, int b) {
return a + b;
}

int main() {
std::function<int(int, int)> sum1 = sum;
std::cout << sum1(1, 2) << std::endl;

auto sum2 = std::bind(sum, std::placeholders::_1, 1);
std::cout << sum2(2) << std::endl;

auto sum3 = [](int a, int b) -> int {
return a + b;
};
std::cout << sum3(3, 4) << std::endl;

return 0;
}

pSzGXss.png
可以看到lambda表达式是通过匿名类,并且operator()操作符重载实现的。

2. C函数的调用过程是什么样的?

首先保护当前寄存器的数值,接着是参数入栈,然后调再用该函数,调用结束后接着做栈平衡,最后再恢复寄存的值为函数调用前的值。

3. C++函数的调用过程是什么样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// demo7.cpp
#include <iostream>

class Obj {
public:
int sum(int a, int b) {
return a + b;
}
};

int main() {
int num = Obj().sum(1, 2);
std::cout << num << std::endl;

return 0;
}

查看反汇编:
pSzGoIf.png
由此可以知:
C++普通函数调用过程和C函数是一样,C++类成员函数,调用过程和C函数类似,只是编译器把this指针存入了ecx(msvc编译器是这样,不同编译器可能用不同的寄存器。)

4. 类似 int sum(int, int)这样的函数,可以被正常调用并返回期望的结果吗?

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
// demo8.cpp
#include <iostream>

int sum(int /*a*/, int /*b*/) {
int* base = nullptr;
int* a = nullptr;
int* b = nullptr;

__asm {
mov base, ebp
}

a = (base + 2) + 1; // call sum有1次push,sum头,push esp,也有1此push,所以要跳过这2个数值,到存放a, b栈地址。
b = (base + 2) + 0;

return *a + *b;
}

int main() {

// call指令等效于先push xx, 在jmp xx
int num = sum(1, 2);
std::cout << num << std::endl;

return 0;
}

运行上边代码:
pSzG7i8.png
由此可知,我们可以通过寄存器esp指向的堆栈,直接拿到想要的参数变量的值,类似 int add(int, int)这种函数也就可以正常工作了。(因为函数来说,无论参数变量有无名称,最终都会被放到栈上,那么我们就有办法直接从栈上取到参数对应的变量)

Obj* obj = nullptr; obj->show(); obj->get_name();可以编译通过吗?如果可以,运行后会显示什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class Obj {
public:
void show() {
std::cout << "Obj::show" << std::endl;
}

virtual const char* get_name() {
return "obj";
}
};

int main(int argc, char* argv[]) {
Obj* obj = nullptr;
obj->show();
obj->get_name();

return 0;
}

执行上述代码:
pSzGHJS.png
obj->show();是可以调用的,show()属于类,不管有没有类实例,show()都是存在的(被所有类实例共享)。
obj->get_name();由于需要先找到虚表,再从虚表找到get_name()函数的地址,虚表放在obj的首地址,而obj值是nullptr,程序崩溃

最后,只要我们要学会了善于利用调试器,以及调试器里边的反汇编功能,假以时日,那些看似遥不可及的C++底层原理,也必然为我们所用!


一些题外话:
当下还有什么工程用到了汇编么吗?
1.Linux内核:https://github.com/torvalds/linux/blob/master/arch/x86/boot/copy.S
2.FFmpeg多媒体框架:https://github.com/FFmpeg/FFmpeg/tree/master/libavutil/x86
3.OpenSSL加密库:https://github.com/openssl/openssl/tree/master/crypto/x86
4.GCC编译器:https://github.com/gcc-mirror/gcc/blob/master/gcc/config/i386/i386.cc
5.NASM汇编器:https://github.com/netwide-assembler/nasm
6.libyuv:https://github.com/sayrer/libyuv/blob/master/source/scale_win.cc libyuv是google 开源的用于实现对各种yuv数据之间的转换包括裁剪、缩放、旋转,以及yuv和rgb 之间的转换,底层有一部分代码是基于汇编实现的,大大提高了转换速度。
libyuv被广泛用于 音视频SDK中。

编程就像修高楼,只有夯实基础,万丈高楼才能平地起,这样的大楼自然经得住风吹雨打!

UAC实现原理:

  • 当用户登录系统成功后, 系统会为用户生成一个accessToken。该用户调用的每一个进程都会有一个AccessToken copy。当进程要访问某个securable object 时,系统会比对accessToken拥有的权限(previlages 是否能访问securable object)
  • 如果安全描述符中不存在DACL,则系统会允许线程进行访问。如果存在DACL,系统会顺序遍历DACL中的每个ACE,检查ACE中的SID在线程的AccessTkoen中是否存在。以访问者中的User SID或Group SID作为关键字查询被访问对象中的DACL。顺序:先查询类型为DENY的ACE,若命中且权限符合则访问拒绝;未命中再在ALLOWED类型的ACE中查询,若命中且类型符合则可以访问;如果前两步后还没命中那么访问拒绝。

相关概念:
ntoskrnl.exe: ntoskrnl.exe 是 Windows 操作系统的一个重要内核程序文件,里面存储了大量的二进制内核代码,用于调度系统。
Windows执行体: Windows执行体是ntoskrnl.exe中的上层(内核是其下层)。
对象管理器(Object Manager): 创建、管理和删除Windows执行体对象和抽象数据类型,他们(抽象数据类型)代表了操作系统资源,比如进程、线程和各种同步对象。

环境子系统: 环境子系统是操作系统中名词。环境子系统向应用程序提供环境和应用程序编程接口(Appplication Programming Interface, API)。Windows 2000/XP支持三种环境子系统:Win32、POSIX和OS/2,其中最重要的环境子系统是Win32子系统,其他子系统都要通过Win32子系统接收用户的输入和显示输出。环境子系统的作用是将基本的执行体系统服务的某些子集提供给应用程序。 用户应用程序调用系统服务时必须通过一个或多个子系统动态链接库作为中介才可以完成。
执行体对象: 每个Windows环境子系统总是把操作系统的不同面貌呈现给它的应用程序。执行体对象和对象服务是环境子系统用于构建其自有版本的对象和其他资源的基础。
安全描述符(Security descriptors): 是安全信息的数据结构(SECURITY_DESCRIPTOR),用于可安全(securable)的Windows对象,这些对象可以被唯一名称辨识。安全描述符可用于任何命名对象,包括文件、文件夹、共享、注册表键、进程、线程、命名管道、服务、工作对象以及其他资源。

  • 版本号:创建此描述符的SRM安全模型的版本;
  • 标志:一些可选的修饰符,定义了该描述符的行为或特征。表6.5列出了这些标志;
  • 所有者SID:所有者的安全ID
  • 组SID:该对象的主组的安全ID(仅用于POSIX)
  • 自主访问控制列表(discretionary access control list,DACL):规定了谁可以用什么方式访问该对象。
  • 系统访问控制列表(System Access Control List,SACL)规定了哪些用户的哪些操作应该被记录到安全审计日志中,以及对象的显式完整性级别。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _SECURITY_DESCRIPTOR {  
UCHAR Revision;
UCHAR Sbz1;
SECURITY_DESCRIPTOR_CONTROL Control; //其自身的一些控制位
PSID Owner; //Owner安全标识符(Security identifiers) 相当于UUID,标识用户、用户群、计算机帐户
PSID Group; //Group安全标识符(Security identifiers) 相当于UUID
PACL Sacl; //(System Access Control List),其指出了在该对象上的一组存取方式(如,读、写、运行等)的存取控制权限细节的列表。
PACL Dacl; //(Discretionary Access Control List),其指出了允许和拒绝某用户或用户组的存取控制列表。 如果一个对象没有DACL,那么就是说这个对象是任何人都可以拥有完全的访问权限。
} SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;

typedef struct _ACL {
BYTE AclRevision;
BYTE Sbz1;
WORD AclSize;
WORD AceCount;
WORD Sbz2;
} ACL, *PACL;

安全标识符 (Security Identifier,SID): 是Windows操作系统使用的独一无二的,不变的标识符用于标识用户、用户群、或其他安全主体。
可移植操作系统接口(POSIX): 是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称。
访问控制项( access control entries,ACE):
安全描述符: 包含自主决定的访问控制表(DACL),里面包含有访问控制项(ACE),因此可以允许或拒绝特定用户或用户组的访问。它们还包含一个系统访问控制列表(SACL)以控制对象访问请求的日志(logging)。[2][3]ACE可以显式应用于对象,或者从父对象继承。ACE的顺序在ACL中很重要,拒绝访问的ACE应该比允许访问的ACE更早出现。安全描述符还包含对象所有者。
访问控制列表(access control list,ACL): 访问控制列表(ACL,access-control list)是由一个头和零个或多个访问控制项(ACE, access-control entry)结构组成的。共有两种类型的ACL: DACL和SACL。在DACL中,每一个ACE包含一个SID和一个访问掩码。


GoogleTest环境配置以及应用


1 GoogleTest源码编译:

GoogleTest代码仓库URL:
https://github.com/google/googletest.git
下载源代码:

1
git clone --branch release-1.12.1 https://github.com/google/googletest.git googletest

1.1 Windows下GoogleTest的编译方法(包含example):

这里选择的编译器是Visual Studio 16 2019,需要用别的版本的编译器,请自行重新指定一下编译器版本:

VS2022为:”Visual Studio 17 2022”
VS2019为:”Visual Studio 16 2019”
VS2017为:”Visual Studio 15 2017”
VS2015为:”Visual Studio 14 2015”
VS2013为:”Visual Studio 12 2013”
VS2012为:”Visual Studio 11 2012”
VS2010为:”Visual Studio 10 2010”

1.1.1 debug版本编译:

1
2
3
mkdir debug    # 在源码根目录创建一个名叫debug的文件夹
cd debug # 进入debug文件夹
cmake "../" -DCMAKE_CONFIGURATION_TYPES=Debug -Dgtest_force_shared_crt=ON -Dgtest_build_samples=ON -DCMAKE_INSTALL_PREFIX=D:/SDK/GoogleTest/v1.10.x/debug -G "Visual Studio 16 2019" -A x64

1.1.2 release版本编译:

1
2
3
mkdir release   # 在源码根目录创建一个名叫release的文件夹
cd release # 进入release文件夹
cmake "../" -DCMAKE_CONFIGURATION_TYPES=Release -Dgtest_force_shared_crt=ON -Dgtest_build_samples=ON -DCMAKE_INSTALL_PREFIX=D:/SDK/GoogleTest/v1.10.x/release -G "Visual Studio 16 2019" -A x64

生成install文件:
管理员启动:x64_x86 Cross Tools Command Prompt for VS 2019.lnk
然后执行:msbuild INSTALL.vcxproj (release版本可能会报错)
或者使用用:cmake –build . –target INSTALL –config Release 编译并安装。

1.2 Linux下GoogleTest的编译方法(包含example):

1.2.1 debug版本编译:

1
2
3
4
5
6
7
8
9
10
11
12
mkdir debug    # 在源码根目录创建一个名叫debug的文件夹
cd debug # 进入debug文件夹
cmake "../" \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_DEBUG_POSTFIX=d \
-Dgtest_force_shared_crt=ON \
-Dgtest_build_samples=ON \
-DCMAKE_INSTALL_PREFIX=~/SDK/googletest/v1.12.1/debug \
-DCMAKE_C_COMPILER=/usr/bin/gcc-11 \
-DCMAKE_CXX_COMPILER=/usr/bin/g++-11
make
make install

1.2.2 release版本编译:

1
2
3
4
5
6
7
8
9
mkdir release   # 在源码根目录创建一个名叫release的文件夹
cd release # 进入release文件夹
cmake "../" \
-DCMAKE_BUILD_TYPE=Release \
-Dgtest_force_shared_crt=ON \
-Dgtest_build_samples=ON \
-DCMAKE_INSTALL_PREFIX=~/SDK/googletest/v1.12.1/release \
-DCMAKE_C_COMPILER=/usr/bin/gcc-11 \
-DCMAKE_CXX_COMPILER=/usr/bin/g++-11

编译参数含义说明:

-DCMAKE_BUILD_TYPE:用来制定编译Debug版本,还是Release版本。
-DCMAKE_DEBUG_POSTFIX:debug版本生成lib追加的后缀名(d表示原本为xx.so,编译生成的是xxd.so)
-Dgtest_force_shared_crt=ON:Winodws设置构建库类型为MD。
-DCMAKE_INSTALL_PREFIX=~/SDK/googletest/v1.12.1/debug :指定SDK生成后的保存目录。
-DCMAKE_C_COMPILER=/usr/bin/gcc-11:指定使用的gcc编译版本。
-DCMAKE_CXX_COMPILER=/usr/bin/g++-11:指定使用的g++版本。


2 GoogleTest常用测试宏:

ASSERT宏 EXPECT宏 功能 使用场景

ASSERT_TRUE|EXPECT_TRUE|判真|判断表达式真假
ASSERT_FALSE|EXPECT_FALSE|判假|判断表达式真假
ASSERT_EQ|EXPECT_EQ|相等|数值比较
ASSERT_NE|EXPECT_NE|不等|数值比较
ASSERT_GT|EXPECT_GT|大于|数值比较
ASSERT_LT|EXPECT_LT|小于|数值比较
ASSERT_GE|EXPECT_GE|大于或等于|数值比较
ASSERT_LE|EXPECT_LE|小于或等于|数值比较
ASSERT_FLOAT_EQ|EXPECT_FLOAT_EQ|单精度浮点值相等|数值比较
ASSERT_DOUBLE_EQ|EXPECT_DOUBLE_EQ|双精度浮点值相等|数值比较
ASSERT_NEAR|EXPECT_NEAR|浮点值接近(第3个参数为误差阈值)|双精度浮点值相等、数值比价
ASSERT_STREQ|EXPECT_STREQ|C字符串相等|字符串比较
ASSERT_STRNE|EXPECT_STRNE|C字符串不等|字符串比较
ASSERT_STRCASEEQ|EXPECT_STRCASEEQ|C字符串相等(忽略大小写)|字符串比较
ASSERT_STRCASENE|EXPECT_STRCASENE|C字符串不等(忽略大小写)|字符串比较
ASSERT_PRED1|EXPECT_PRED1|自定义谓词函数,(pred, arg1)(还有_PRED2, …, _PRED5|自定义比较函数


3 GoogleTest使用步骤

3.1 以sample1为例:

sample1源码地址:https://github.com/calm2012/my_sample_code/tree/main/GoogleTest/GoogleTest_sample1

3.1.1 sample1目录结构如下:

1
2
3
4
5
6
7
8
└─sample1
│ CMakeLists.txt
│ sample1.h
│ sample1.cc
│ sample1_unittest.cc

├─debug
└─googletest_lib

CMakeLists.txt文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.14)

project(sample1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(sample1
sample1.h
sample1.cc
sample1_unittest.cc
)

# 链接gtest_maind.lib库
target_link_libraries(sample1 ${CMAKE_SOURCE_DIR}/googletest_lib/lib/debug/gtest_maind.lib)
# 链接gtestd.lib库
target_link_libraries(sample1 ${CMAKE_SOURCE_DIR}/googletest_lib/lib/debug/gtestd.lib)
# 在工程中共引入GoogleTest头文件搜索路径
target_include_directories(sample1 PUBLIC ${CMAKE_SOURCE_DIR}/googletest_lib/include)

sample1.h文件内容如下:

1
2
3
4
5
6
7
#ifndef GTEST_SAMPLES_SAMPLE1_H_
#define GTEST_SAMPLES_SAMPLE1_H_

// Returns n! (the factorial of n). For negative n, n! is defined to be 1.
int Factorial(int n);

#endif // GTEST_SAMPLES_SAMPLE1_H_

sample1.cc文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
#include "sample1.h"

// Returns n! (the factorial of n). For negative n, n! is defined to be 1.
int Factorial(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}

return result;
}

sample1_unittest.cc文件内容如下:

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
#include <limits.h>
#include "sample1.h"
#include "gtest/gtest.h"
namespace {

// Tests factorial of negative numbers.
TEST(FactorialTest, Negative) {
// This test is named "Negative", and belongs to the "FactorialTest"
// test case.
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
}

// Tests factorial of 0.
TEST(FactorialTest, Zero) {
EXPECT_EQ(1, Factorial(0));
}

// Tests factorial of positive numbers.
TEST(FactorialTest, Positive) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}

} // namespace

3.1.2编译构建命令:

1
2
3
4
5
6
7
8
9
mkdir debug
cd debug
cmake "../" \
-DCMAKE_BUILD_TYPE:STRING=Debug \
-DCMAKE_C_COMPILER:STRING=/usr/bin/gcc \
-DCMAKE_CXX_COMPILER:STRING=/usr/bin/g++
cmake --build . --config Debug
cd Debug
运行:sample1

3.2 再看在MessagePush工程中如何引入GoogleTest

3.2.1 最后在你的工程里如CMakeLists.txt中引入:

1
2
3
add_gtest(base_common_unittest)
# 或者:
add_gtest_main(mysql_wrapper_unittest)
  • add_gtest的作用是为了在Windows下添加gtest.lib,或者Linux下添加libgtest.a。同时引入GoogleTest头文件搜索路径
  • add_gtest_main的作用是为了在Windows下添加gtest.lib、gtest_main.lib,或者Linux下添加libgtest.a、libgtest_main.a。同时引入GoogleTest头文件搜索路径

add_gtest宏:

1
2
3
4
5
6
7
8
macro(add_gtest target_name)
target_link_libraries(${target_name}
${googletest_lib_path}/${gtes_lib_name} # googletest_lib_path: gooltest库文件路径
${target_name} ${googletest_lib_path}/${gmock_lib_name}
-lpthread
)
target_include_directories(${target_name} PUBLIC ${googletest_include_path})
endmacro(add_gtest)

add_gtest_main宏:

1
2
3
4
5
6
7
8
9
10
macro(add_gtest_main target_name)
target_link_libraries(${target_name}
${googletest_lib_path}/${gtes_lib_name} # googletest_lib_path: gooltest库文件路径
${googletest_lib_path}/${gtes_main_lib_name}
${googletest_lib_path}/${gmock_lib_name}
${googletest_lib_path}/${gmock_main_lib_name}
-lpthread
)
target_include_directories(${target_name} PUBLIC ${googletest_include_path})
endmacro(add_gtest_main)

3.3 GoogleTest常用测试宏(TEST/TEST_F/TEST_P/TYPED_TEST)的一般使用场景:

  • TEST:通常用于简单的函数测试。
  • TEST_F:通常用于需要访问类对象时的测试。
  • TEST_P:通常用于把测试对象当作参数传入或有大量重复测试case时。
  • TYPED_TEST:通常用于接口测试。

3.4 其它参考网址:


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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include "lst_timer.h"

#define FD_LIMIT 65535
#define MAX_EVENT_NUMBER 1024
#define TIMESLOT 5

static int pipefd[2];
static sort_timer_lst timer_lst;
static int epollfd = 0;

int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}

void addfd(int epollfd, int fd)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}

void sig_handler(int sig)
{
int save_errno = errno;
int msg = sig;
send(pipefd[1], (char*)&msg, 1, 0);
errno = save_errno;
}

void addsig(int sig)
{
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask);
assert(sigaction(sig, &sa, NULL) != -1);
}

void timer_handler()
{
timer_lst.tick();
alarm(TIMESLOT);
}

void cb_func(client_data* user_data)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
close(user_data->sockfd);
printf("close fd %d\n", user_data->sockfd);
}

int main(int argc, char* argv[])
{
if (argc <= 2)
{
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);

int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);

ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);

ret = listen(listenfd, 5);
assert(ret != -1);

epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(epollfd, listenfd);

ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
setnonblocking(pipefd[1]);
addfd(epollfd, pipefd[0]);

// add all the interesting signals here
addsig(SIGALRM);
addsig(SIGTERM);
bool stop_server = false;

client_data* users = new client_data[FD_LIMIT];
bool timeout = false;
alarm(TIMESLOT);

while (!stop_server)
{
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if ((number < 0) && (errno != EINTR))
{
printf("epoll failure\n");
break;
}

for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
if (sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connfd);
users[connfd].address = client_address;
users[connfd].sockfd = connfd;
util_timer* timer = new util_timer;
timer->user_data = &users[connfd];
timer->cb_func = cb_func;
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
users[connfd].timer = timer;
timer_lst.add_timer(timer);
}
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
int sig;
char signals[1024];
ret = recv(pipefd[0], signals, sizeof(signals), 0);
if (ret == -1)
{
// handle the error
continue;
}
else if (ret == 0)
{
continue;
}
else
{
for (int i = 0; i < ret; ++i)
{
switch (signals[i])
{
case SIGALRM:
{
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
}
}
}
}
}
else if (events[i].events & EPOLLIN)
{
memset(users[sockfd].buf, '\0', BUFFER_SIZE);
ret = recv(sockfd, users[sockfd].buf, BUFFER_SIZE - 1, 0);
printf("get %d bytes of client data %s from %d\n", ret, users[sockfd].buf, sockfd);
util_timer* timer = users[sockfd].timer;
if (ret < 0)
{
if (errno != EAGAIN)
{
cb_func(&users[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
else if (ret == 0)
{
cb_func(&users[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
else
{
//send( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
printf("adjust timer once\n");
timer_lst.adjust_timer(timer);
}
}
}
else
{
// others
}
}

if (timeout)
{
timer_handler();
timeout = false;
}
}

close(listenfd);
close(pipefd[1]);
close(pipefd[0]);
delete[] users;
return 0;
}

编译环境:

  • 操作系统:MacOS10.15.7
  • 编译器:XCode12.4

下面还需要一些准备工作:

1. 安装python2.7.16,2.7版本应该都可以
然后在终端中输入:python –version

1
Python 2.7.16

检测安装的版本号,以及是否安装成功。
2. 创建文件夹目录

1
2
3
/Users/calm2012/code/cef/87/automate
/Users/calm2012/code/cef/87/chromium_git
/Users/calm2012/code/cef/87/depot_tools

3. 下载depot_tools。

1
2
cd /Users/calm2012/code
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

4. 添加环境变量。

1
export PATH=/Users/calm2012/code/cef/87/depot_tools:$PATH

5. 下载automate-git。

1
2
cd /Users/calm2012/code
curl -O https://bitbucket.org/chromiumembedded/cef/raw/master/tools/automate/automate-git.py

6. 编译配置项设置。

1
2
cd /Users/calm2012/code/cef/87
python automate/automate-git.py --download-dir=/Users/calm2012/code/cef/87/chromium_git --depot-tools-dir=/Users/calm2012/code/cef/87/depot_tools --branch=4280 --force-clean --no-distrib --no-build --x64-buildy

automate-git.py 参数介绍
–branch 表示你要下载哪个版本的代码,CEF 每个版本都有固定的分支,你去 CEF 项目页查看分支名称指定即可,这里我们编译 2019年9月份目前最新的版本 3809。
–no-build 表示只下载代码而不编译,这里只为下载代码,我们还要修改支持多媒体的参数,所以不进行编译
–no-distrib 不执行打包项目,这里只为下载代码,我们还要修改支持多媒体的参数,所以不进行打包
–force-clean 如果你曾经执行过这个脚本,可能会出错,则加上这个参数,它执行清理残留文件(你也可以手动在 chromium 源码目录执行 git clean -xdf 来清理目录中的多余内容)。
automate-git.py 的其他参数可以手动执行 python automate-git.py –help 来查看

如果出现:AttributeError: ‘NoUsableRevError’ object has no attribute ‘message’ 请尝试以下操作。的编译错误。可以按照下边操作
打开gclient_scm.py,然后注释下面行

#logging.warning(
#    "Couldn't find usable revision, will retrying to update instead: %s",
#    e.message)

7. 音视频编码格式配置
修改配置文件/Users/calm2012/code/cef/87/chromium_git/chromium/src/third_party/ffmpeg/chromium/config/Chrome/mac/x64/config.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
cd /Users/calm2012/code
#define CONFIG_FLV_DECODER 1
#define CONFIG_H263_DECODER 1
#define CONFIG_H263I_DECODER 1
#define CONFIG_MPEG4_DECODER 1
#define CONFIG_MPEGVIDEO_DECODER 1
#define CONFIG_MSMPEG4V1_DECODER 1
#define CONFIG_MSMPEG4V2_DECODER 1
#define CONFIG_MSMPEG4V3_DECODER 1
#define CONFIG_RV10_DECODER 1
#define CONFIG_RV20_DECODER 1
#define CONFIG_RV30_DECODER 1
#define CONFIG_RV40_DECODER 1
#define CONFIG_AC3_DECODER 1
#define CONFIG_AMRNB_DECODER 1
#define CONFIG_AMRWB_DECODER 1
#define CONFIG_COOK_DECODER 1
#define CONFIG_SIPR_DECODER 1
#define CONFIG_FLV_ENCODER 1
#define CONFIG_H263_ENCODER 1
#define CONFIG_MPEG4_ENCODER 1
#define CONFIG_MSMPEG4V2_ENCODER 1
#define CONFIG_MSMPEG4V3_ENCODER 1
#define CONFIG_RV10_ENCODER 1
#define CONFIG_RV20_ENCODER 1
#define CONFIG_AAC_ENCODER 1
#define CONFIG_AC3_ENCODER 1
#define CONFIG_AC3_PARSER 1
#define CONFIG_COOK_PARSER 1
#define CONFIG_H263_PARSER 1
#define CONFIG_MPEG4VIDEO_PARSER 1
#define CONFIG_MPEGVIDEO_PARSER 1
#define CONFIG_RV30_PARSER 1
#define CONFIG_RV40_PARSER 1
#define CONFIG_SIPR_PARSER 1
#define CONFIG_AC3_DEMUXER 1
#define CONFIG_AMR_DEMUXER 1
#define CONFIG_AMRNB_DEMUXER 1
#define CONFIG_AMRWB_DEMUXER 1
#define CONFIG_AVI_DEMUXER 1
#define CONFIG_AVISYNTH_DEMUXER 1
#define CONFIG_FLV_DEMUXER 1
#define CONFIG_H263_DEMUXER 1
#define CONFIG_H264_DEMUXER 1
#define CONFIG_MPEGTS_DEMUXER 1
#define CONFIG_MPEGTSRAW_DEMUXER 1
#define CONFIG_MPEGVIDEO_DEMUXER 1
#define CONFIG_RM_DEMUXER 1
#define CONFIG_AC3_MUXER 1
#define CONFIG_AMR_MUXER 1
#define CONFIG_AVI_MUXER 1
#define CONFIG_FLV_MUXER 1
#define CONFIG_H263_MUXER 1
#define CONFIG_H264_MUXER 1
#define CONFIG_MPEGTS_MUXER 1
#define CONFIG_RM_MUXER 1

然后执行:
export GN_DEFINES="ffmpeg_branding=Chrome proprietary_codecs=true is_official_build=true"

8. 创建Ninja工程文件。

1
2
3
cd /Users/calm2012/code/cef/87/chromium_git/chromium/src/cef
// 执行脚本,生成工程文件
./cef_create_projects.sh

9. Ninja Release1编译。

1
2
cd /Users/calm2012/code/cef/87/chromium_git/chromium/src
ninja -C out/Release_GN_x64 cef

10. 编译 sandbox。

1
2
cd /Users/calm2012/code/cef/87/chromium_git/chromium/src
ninja -C out/Release_GN_x64_sandbox cef_sandbox

11. 打包头文件、binary等。

1
2
3
4
5
6
编译完 Release 版本后开始打包操作:
cd /Users/calm2012/code/cef/87/chromium_git/chromium/src/cef/tools

// --minimal 表示仅发布 Release 版本,不包含 Debug
./make_distrib.sh --ninja-build --x64-build --minimal
在 /Users/calm2012/code/cef/87/chromium_git/chromium/src/cef/binary_distrib 目录下就可以看到打包过的文件了。

注意:编译遇到 libtool: error: unrecognised option: ‘-static’ 错误可以尝试:
brew unlink libtool
rm -rf /usr/local/bin/libtool
which libtool


编译环境:

  • 操作系统:Windows 10
  • 编译器:Microsoft Visual Studio Enterprise 2017 Version 15.9.41

注意事项:

  • 建议Windows 10 在 时间和语言–>语言和区域–>语言 设置为英文,在 时间和语言–>语言和区域–>区域设置为美国避免一些奇怪的错误。
  • VS建议升级最新版本,之前遇到由于VS版本低导致各种编译错误,升级VS后成功编译。
  • 如果QT源码,需要解压,建议安装最新WinRar 防止解压出错。之前遇到因为WinRar版本问题而发生解压错误。

#####下面还需要一些准备工作:
1. 安装python2.7.18,并检查在Windos系统【环境变量】path中对应的路径设置
然后在widnows cmd中输入:python –version

1
Python 2.7.18

检测安装的版本号,以及是否安装成功。
2. 安装安装ruby,并检查在Windos系统【环境变量】path中对应的路径设置
然后在widnows cmd中输入:ruby –version

1
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [i386-mingw32]

3. 安装安装perl,并检查在Windos系统【环境变量】path中对应的路径设置
然后在widnows cmd中输入:perl –version

1
This is perl 5, version 32, subversion 0 (v5.32.0) built for MSWin32-x64-multi-thread

4. 编译oepnssl,编译qt需要用到
oepnssl源码地址:https://github.com/openssl/openssl
编译之前找到configdata.pm和makefile两个文件, 用记事本打开, 搜索 “/MD” 字符串, 替换成 “/MT”,编译选项修改MT
编译配置项:

1
perl Configure VC-WIN32 shared no-asm --prefix="g:\SDK\OpenSSL\openssl-1.1.1" --openssldir="g:\SDK\OpenSSL\ssl"

参数说明:
–prefix是Openssl编译完后的安装路径。
–openssldir是Openssl编译完后的生成的配置文件的安装路径。

1
2
3
a. 打开VS2017中的vcvars32.bat
b. 然后输入 nmake 开始编译。
c. 输入 nmake install 归档头文件等到安装路径

5. 下载depot_tools,下载地址:https://chromium.googlesource.com/chromium/tools/depot_tools.git
并在Windos系统【环境变量】path中设置对应的路径。

1
如:D:\SDK\depot_tools


终于可以开始编译QT了
QT5.15.1源码下载:https://download.qt.io/archive/qt/5.15/5.15.1/single/
Windows编译配置项:

1
configure -opensource -platform win32-msvc2017 -developer-build -mp -debug-and-release -force-debug-info -v -confirm-license OPENSSL_PREFIX=D:\SDK\oepnssl\1.1.1g\debug_md -openssl-linked -I  D:\SDK\oepnssl\1.1.1g\debug_md\include -L D:\SDK\oepnssl\1.1.1g\debug_md\lib OPENSSL_LIBS="libssl.lib libcrypto.lib Ws2_32.lib  Gdi32.lib Advapi32.lib Crypt32.lib User32.lib" -webengine-proprietary-codecs -prefix D:\SDK\build\qt5.15.1

MacOS编译配置项:

1
./configure -opensource -platform macx-clang -developer-build -debug-and-release -force-debug-info -v -confirm-license -skip qtwebengine -prefix ~/SDK/QT/5.15.1/macos

IOS编译配置项:

1
./configure -opensource -xplatform macx-ios-clang -developer-build -debug-and-release -force-debug-info -confirm-license -nomake examples -nomake tests -skip qtwebengine -prefix ~/SDK/QT/5.15.1/ios

1
2
3
a. 打开VS2017中的 x64_x86 Cross Tools Command Prompt for VS 2017
b. 输入nmake
c. 输入nmake install
1
2
3
4
mac和ios都下面命令:
a. 打开终端,进入到源码目录
b. 输入make 或者 make -j20(j20表示用20个线程构建,加快构建速度,最大线程数可以参考cpu的线程数)
c. 输入make install

注意:如果需要重新编译可以删除

1
2
3
config.cache
.qmake.cache
config.tests

重新开始编译。


#####参考文章
Qt for Windows - Building from Source:
https://doc.qt.io/qt-5/windows-building.html
Qt for Windows - Requirements:
https://doc.qt.io/qt-5/windows-requirements.html
Qt for Windows:
https://doc.qt.io/qt-5/windows.html

QT

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#include <iostream>


int show(int);

int a = 111;
int b = 222;
int c = 333;
int* pp1[3]{&a, &b, &c};

decltype(pp1)* Fun1(int nNum);

typedef int int_array_t[3];
typedef int (*int_array_ptr_t)[3];
typedef int (*int_array_ptr_t2[3]);

int_array_t obj = {7, 8, 9};
int_array_ptr_t p_obj;
int_array_ptr_t2* p_obj2 = &pp1;

int_array_t* fun0(int n);
int_array_ptr_t fun1(int n);
int_array_ptr_t2* fun3(int);


int main() {

// 1. 普通指针
int nNum = 6;
int* p0 = &nNum;

std::cout << *p0 << std::endl;

int p1[3]{1, 2, 3};

// 2. 由n个int型指针组成的数组;数组的元素是指针类型
int a = 1;
int b = 2;
int c = 3;
int *p2[3]{&a, &b, &c};

std::cout << std::defaultfloat;
std::cout << std::hex << std::showbase;

std::cout << "&a:\t" << (size_t)&a << std::endl;
std::cout << "p3[0]:\t" << (size_t)p2[0] << std::endl;

std::cout << "&b:\t" << (size_t)&b << std::endl;
std::cout << "p3[1]:\t" << (size_t)p2[1] << std::endl;

std::cout << "&c:\t" << (size_t)&c << std::endl;
std::cout << "p3[2]:\t" << (size_t)p2[2] << std::endl;

// 3. 一个指向由n个元素的数组的指针,数组元素的值为int型
std::cout << std::dec;

int(*p3)[3] = &p1;
std::cout << (*p3)[0] << std::endl;
std::cout << (*p3)[1] << std::endl;
std::cout << (*p3)[2] << std::endl;

// 4. 指向int型指针的指针
int** p4 = &p0;
**p4 = 123456;
std::cout << **p4 << std::endl;

// 5. 函数指针
int (*p5)(int) = show; // 一个指向有一个整型参数, 返回值为整型的函数的指针
p5(1);
(*p5)(1);

// 6. 带一个int型参数返回值为指向n个int型指针元素的数组的函数
int*(*p6(int))[3]; // p是一个函数, 函数的参数为int型;函数的返回值为指针;指针指向是一个数组;数组的元素为指针;数组的元素指针为int型指针
decltype(pp1)* pPP1 = Fun1(0);
std::cout << *((int*)((*pPP1)[0])) << std::endl;
std::cout << *((int*)((*pPP1)[1])) << std::endl;
std::cout << *((int*)((*pPP1)[2])) << std::endl;

int_array_ptr_t2* pAray3 = fun3(0);
std::cout << *((int*)((*pAray3)[0])) << std::endl;
std::cout << *((int*)((*pAray3)[1])) << std::endl;
std::cout << *((int*)((*pAray3)[2])) << std::endl;

// int (*func)(int *p, int (*f)(int*));
// 7. func是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型

// int (*func[3])(int);
// 8. func数组的元素是函数类型的指针,它所指向的函数具有int类型的形参,返回值类型为int
int (*func[3])(int) = {show, show, show};

func[0](0);
func[1](0);
func[2](0);

// int (*(*func)[3])(int);
// 9. func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int形参,返回值为int类型的函数。
int (*(*func9)[3])(int) = &func;
(*func9)[0](0);
(*func9)[1](0);
(*func9)[2](0);

// int (*(*func)(int))[3];
// 10. func是一个函数指针,这类函数具有int类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有3个int元素的数组
int(*(*func10)(int))[3] = fun1;
int_array_ptr_t p10 = func10(0);

std::cout << (*p10)[0] << std::endl;
std::cout << (*p10)[1] << std::endl;
std::cout << (*p10)[2] << std::endl;

return 0;
}

int show(int) {
std::cout << "show function." << std::endl;
return 0;
}

decltype(pp1)* Fun1(int nNum) {
return &pp1;
}

int_array_t* fun0(int n) {
p_obj = &obj;
return &obj;
}

int_array_ptr_t fun1(int n) {
p_obj = fun0(n);
return p_obj;
}

int_array_ptr_t2* fun3(int) {
return p_obj2;
}

随着后PC时代的到来,以及互联网的高速发展,当前环境下拥有多平台设备的用户越来越多,开发者不得不考虑跨平台开发,以期多平台有一致的使用体验、获得更多的用户。现在主流的平台有:Android、IOS、Windows、MacOS、Linux。面对如此多的平台我们改如何面对,是一个值得思考的问题。下面我介绍今天需要讨论的一些主题:

  • 什么是跨平台?
  • 跨平台开发的意义?
  • 跨平台开发需要注意些什么?
  • 跨平台开发的现状?

什么是跨平台?

程序源码不依赖操作系统、不依赖硬件环境(如各种CPU指令集),可以在不同的操作系统上运行。

为了弄明白跨平台底层原理,我们需要从程序的生成看起,而程序又是由编程语言开发的,所以我们先从的开发语言看起:

1
2
3
4
5
6
7
graph LR
A[机器语言] --> B[汇编语言]
B --> C[C/C++]
B --> D[Java]
B --> E[Python]
B --> F[Go]
B --> G[其它...]

机器语言: 由0 1代码组成,可以直接被计算机硬件所识别,与底层硬件高度耦合,显然无法跨平台。
汇编语言: 采用字母、单词来表示一条指令,与硬件高度耦合,无法跨平台。
高级语言: C/C++、Java、Python、Go、… 面向过程或者面向对象,开发简单、高效,做到了与硬件无关,是我们跨平台开发的选择。


从实现方式上跨平台可以分为两大阵营:

#####编译型:
C、C++、Delphi、Pascal 使用不同平台对应的编译器,只需编译一次,生成可以执行文件。由不同平台不同的【编译器】做支撑
优点:程序执行效率高、使用内存低
不点:编译出来的可执行文件与平台相关,不能在其它平台直接运行。

#####解释型:
解释型的语言包括:Javascript、Python、Ruby、Perl、…等 。由不同平台不同的【解释器虚拟机】做支撑
优点:不需要编译,既可以跨平台。
不足:每次运行都需要将源代码解释称机器码并执行,效率较低,内存使用率高。

注意:编译型 + 解释型的混合体 : Java  Java代码要编译,解释运行在JVM上。

跨平台开发的意义?

1. 用户在不同平台有近似的用户体验、提高用户体验。
2. 公司可以获取更多用户。
3. 一份代码多平台部署,提高开发效率、可维护性。
4. 节省时间成本和人员成本。
5. …


跨平台开发需要注意什么?

编程语言、操作系统、代码编码格式、x64、x86、同一操作系统不同操作系统版本、操作系统版本都相同不同机型
1. 选择合适的编程语言。看编程语言本身对跨平台支持的程度,如windows的批出就不行。
2. 操作系统。目标操作系统都有哪些。
3. 代码文件的编码格式。不恰当的格式,可能再其它平台编译失败。
4. 程序是32位,还是64位,还是说在有的平台32位有的平台64位。
5. 需要考虑同一操作系统不同操作系统版本。
6. 需要考虑操作系统版本相同但是机型不同,适配。


跨平台开发的现状?

1
2
3
4
5
6
7
graph LR
A[跨平台开发]
A --> B[Web Java Script派]
A --> C[代码转换派]
A --> D[编译派]
A --> E[中间派]
A --> F[解释器虚拟机派]

1. Web Java Script派

  • React Native
    React Native基于JavaScript和React(用于构建用户界面的JavaScript库)。
    Facebook于2015年4月开源的跨平台移动应用开发框架。
    主要擅长于IOS、Android,也能应用于Windows、MacOS、linux
    官网:https://reactnative.dev/
    MacOS: https://github.com/ptmt/react-native-macos
    Windows: https://gitee.com/mirrors/react-native-windows/
    linux: https://gitlab.com/react-native-linux/react-native-linux
    使用React Native成功案例:https://reactnative.dev/showcase
    Facebook Android • iOS
    Instagram Android • iOS
    Skype Android • iOS
    Tencent QQ Android
    JD(手机京东)Android • iOS

  • Electron:
    2011 年左右,中国英特尔开源技术中心的王文睿(Roger Wang)希望能用 Node.js 来操作 WebKit,而创建了 node-webkit 项目,这就是 NW.js 的前身。
    2012 年,故事的另一个主角赵成(Cheng Zhao)加入到王文睿的小组,并对 node-webkit 项目做出了大量的改进,
    后来赵成离开了中国英特尔开源技术中心,帮助 github 团队尝试把 node-webkit 应用到 Atom 编辑器上,再后来 github 把这个项目开源出来,最终更名为Electron。
    官网:https://www.electronjs.org/
    Electron基于Nodejs和Chromium的跨平台桌面软件开发框架。使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。
    Electron擅长于Mac、Windows、Linux也能支持Android、IOS
    Mac App Store 应用程序提交指南: https://www.electronjs.org/docs/tutorial/mac-app-store-submission-guide
    高版本使用CARemoteLayer 未公开api ios不能上架,AppStore有低版本Electron支持ios。
    使用Electron的成功案例: https://www.electronjs.org/apps
    Visual Studio Code
    GitBlade
    GitHub Desktop
    飞书
    迅雷X

  • PhoneGap:
    采用HTML,CSS和JavaScript的技术,创建移动跨平台移动应用程序的快速开发平台 Android • iOS
    2011年7月29日,PhoneGap发布了1.0版产品,现在由Adobe拥有.
    PhoneGap程序的加载和UI接口的反应都比本地的程序慢。Adobe警告开发者,由于使用PhoneGap框架开发的程序运行速度可能会太慢或使用体验不够“原生”.

2. 代码转换派

3. 编译派

C
C++
QT

4. 中间派(基于js 与 编译流之间):

  • Flutter
    Google开源,上线时间2015年5月。
    Flutter采用Dart语言
    Dart是面向对象的、类定义的、单继承的语言。需要编译、Dart2强类型语言。
    https://www.dartcn.com/
    优势:程序运行效率相对较高、Google开源的技术支持
    不足:开源库相对比较欠缺,更新频次不足

5. 解释器虚拟机:

Java
Python
Perl
Lua

其它资料:
2021最新15个跨平台App开发框架:https://www.jianshu.com/p/3bf119de6c5f


1
2
实验环境: windows10 x64  VS2019
我们来分析最基本的 单继承
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
#include <iostream>


class Base {
public:
virtual void show() {
std::cout << "Base show.\n";
}
virtual ~Base() {}
int a = 1;
int b = 2;
};

class CTest : public Base {
public:
void show() {
std::cout << "CTest show.\n";
}
};

int main() {
Base b;
b.show();

Base* pTest = new CTest();

pTest->show();
delete pTest;
pTest = nullptr;

return 0;
}


1
1.可以看到pTest的地址是0x0021ab4c,也是导入表的地址


1
2.导入第一个地址:0x00211573,即 CTest::show的地址


1
3.切换到汇编验证一下


1
2
4.这里eax就是CTest::show的地址,和 上边通过导入表看到地址是一样的,都是0x00211573,也就演示了虚表中,
成员的存放方式