利用汇编挖掘编程语言的本质
汇编挖掘编程语言的本质
- Visual Studio 查看反汇编
- 程序的本质
- 编程语言的发展
- 一些编程语言的本质区别
- 代码的执行效率
- if-else 反汇编
- switch 反汇编
- 条件比较少的情况
- 条件比较多的情况
- case 值连续
- case 值不连续
- case 值跨度很大
- 总结
- a++ 和 ++a
- ++a 反汇编
- a++ 反汇编
- 构造函数
- 构造函数反汇编
- 函数和方法
- 函数的内存布局
- 递归函数
学习视频:你了解每一行代码的本质么?利用汇编挖掘本质
Visual Studio 查看反汇编
在要执行的程序中打个断点(F9),点击开始调试(F5),右键 > 转到反汇编(CTRL + K, G)
程序的本质
通常,CPU 会先将内存中的数据存储到寄存器中,然后再对寄存器中的数据进行运算
假设内存中有块红色内存空间的值是 3,现在想把它的值加 1,并将结果存储到蓝色内存空间
CPU 首先会将红色内存空间的值放到 EAX 寄存器中:
mov eax, 红色内存空间
然后让 EAX 寄存器与 1 相加:
add eax, 1
最后将值赋值给内存空间:
mov 蓝色内存空间, eax
总结:程序中任何操作都是在 CPU 中进行的,哪怕内存中的两个变量进行运算,也会先拿到 CPU 中进行计算,然后再放回内存空间。
编程语言的发展
编程语言的发展史:
- 机器语言:由 0 和 1 组成
- 汇编语言:用符号代替了 0 和 1,比机器语言便于阅读和记忆
- 高级语言: C \ C++ \ Java \ JavaScript \ Python 等,更接近人类自然语言
例如对于这个操作:将寄存器 BX 的内容送入寄存器 AX
- 机器语言:1000100111011000
- 汇编语言:
MOV AX, BX
- 高级语言:
AX = BX;
高级语言不允许直接操作寄存器,以上仅仅是举个例子
高级语言的运行步骤:
- 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
- 高级语言可以通过编译得到汇编语言 / 机器语言,但汇编语言 / 机器语言几乎不可能还原成高级语言
验证汇编语言 / 机器语言不能还原成高级语言
对于以下两段 C++ 代码,分别查看它们生成的汇编
struct Date {int year; int month; int day; }; Date d = { 1, 2, 3 };
int array[] = { 1, 2, 3 };
可以发现,两段不同的 C++ 代码,生成的汇编一模一样,因此汇编语言几乎不可能准确的还原成高级语言。
有一些软件可以实现汇编转高级语言,只能展现一些基本的逻辑代码。
CPU 架构不同,生成的汇编也是不同的(之前有说过,程序其实都是通过 CPU 执行的)
一些编程语言的本质区别
C++:可以轻易的反汇编
JavaScript:脚本语言,由浏览器进行解析
PHP:脚本语言,由 Zend Engine (ZE) 进行解析
Java:由 JVM 进行装载字节码
以上介绍可以将编程语言总结为三类:
- 编译型的语言(不依赖虚拟机):C \ C++ \ OC \ Swift
- 编译型的语言(依赖于虚拟机):Java \ Ruby
- 脚本语言:Python \ JavaScript
代码的执行效率
if-else
和 switch
,谁的效率高?
int no = 4;if (no == 1) {printf("no is 2");
} else if (no == 2) {printf("no is 2");
} else if (no == 3) {printf("no is 3");
} else if (no == 4) {printf("no is 4");
} else if (no == 5) {printf("no is 5");
} else {printf("other no");
}
int no = 4;switch (no) {case 1:printf("no is 1");break;case 2:printf("no is 2");break;case 3:printf("no is 3");break;case 4:printf("no is 4");break;case 5:printf("no is 5");break;default:printf("other no");break;
}
定义一个变量,来计算代码的运行时间,这种方法是 “事后统计法”,它的缺点很显著:
- 严重依赖机器的性能
- 需要编写额外的测试代码
汇编代码简单知识:
cmp
:compare,比较jne
:jump not equal,不相等才跳转jmp
:jump,无条件跳转
if-else 反汇编
if-else 反汇编的情况是固定的,不会随着条件多少变化:
每个 if 语句都会经历
cmp
比较操作,然后进行jne
,不相等才跳转可见越后面满足条件的 if 语句,代码执行效率越低
将可能性大的 if 条件提前,可以优化代码效率
int no = 4;
00007FF793D95FAB mov dword ptr [no],4 if (no == 1) {
00007FF793D95FB2 cmp dword ptr [no],1 // jne 后面的地址,代表跳转到下个执行的地方,即 else if (no == 2) 处地址
00007FF793D95FB6 jne test0+36h (`07FF793D95FC6h`) printf("no is 2");
00007FF793D95FB8 lea rcx,[string "no is 2" (07FF793D9AC28h)]
00007FF793D95FBF call printf (07FF793D91190h) // 07FF793D96022 这个地址已经跳出 if-else 语句
00007FF793D95FC4 jmp test0+92h (07FF793D96022h) } else if (no == 2) {
`00007FF793D95FC6` cmp dword ptr [no],2
00007FF793D95FCA jne test0+4Ah (07FF793D95FDAh) printf("no is 2");
00007FF793D95FCC lea rcx,[string "no is 2" (07FF793D9AC28h)]
00007FF793D95FD3 call printf (07FF793D91190h)
00007FF793D95FD8 jmp test0+92h (07FF793D96022h) } else if (no == 3) {
00007FF793D95FDA cmp dword ptr [no],3
00007FF793D95FDE jne test0+5Eh (07FF793D95FEEh) printf("no is 3");
00007FF793D95FE0 lea rcx,[string "no is 3" (07FF793D9AC38h)]
00007FF793D95FE7 call printf (07FF793D91190h)
00007FF793D95FEC jmp test0+92h (07FF793D96022h) } else if (no == 4) {
00007FF793D95FEE cmp dword ptr [no],4
00007FF793D95FF2 jne test0+72h (07FF793D96002h) printf("no is 4");
00007FF793D95FF4 lea rcx,[string "no is 4" (07FF793D9AC48h)]
00007FF793D95FFB call printf (07FF793D91190h)
00007FF793D96000 jmp test0+92h (07FF793D96022h) } else if (no == 5) {
00007FF793D96002 cmp dword ptr [no],5
00007FF793D96006 jne test0+86h (07FF793D96016h) printf("no is 5");
00007FF793D96008 lea rcx,[string "no is 5" (07FF793D9AC58h)]
00007FF793D9600F call printf (07FF793D91190h) } else {
00007FF793D96014 jmp test0+92h (07FF793D96022h) printf("other no");
00007FF793D96016 lea rcx,[string "other no" (07FF793D9AC68h)]
00007FF793D9601D call printf (07FF793D91190h) }
switch 反汇编
条件比较少的情况
int no = 4;switch (no) {case 1:printf("no is 1");break;case 2:printf("no is 2");break;default:printf("other no");break;
}int age = 4;
反汇编分析:
- 条件比较少的情况,编译器不会生成优化代码,代码和 if-else 一样,与每个 case 的值进行比较
- 此时 switch 和 if-else 效率差不多
switch (no) {
00BB1A3C mov eax,dword ptr [no]
00BB1A3F mov dword ptr [ebp-0DCh],eax
00BB1A45 cmp dword ptr [ebp-0DCh],1
00BB1A4C je _$EncStackInitStart+3Dh (0BB1A59h)
00BB1A4E cmp dword ptr [ebp-0DCh],2
00BB1A55 je _$EncStackInitStart+4Ch (0BB1A68h)
00BB1A57 jmp _$EncStackInitStart+5Bh (0BB1A77h)case 1:printf("no is 1");
00BB1A59 push offset string "no is 1" (0BB7B6Ch)
00BB1A5E call _printf (0BB10CDh)
00BB1A63 add esp,4 break;
00BB1A66 jmp _$EncStackInitStart+68h (0BB1A84h) case 2:printf("no is 2");
00BB1A68 push offset string "no is 2" (0BB7B30h)
00BB1A6D call _printf (0BB10CDh)
00BB1A72 add esp,4 break;
00BB1A75 jmp _$EncStackInitStart+68h (0BB1A84h) default:printf("other no");
00BB1A77 push offset string "other no" (0BB7B60h)
00BB1A7C call _printf (0BB10CDh)
00BB1A81 add esp,4 break;}
条件比较多的情况
case 值连续
反汇编分析:
- switch 在一开始执行了多句汇编,用于计算
jmp
的地址 - 不存在越后面满足条件的 case 语句效率越低的情况
int no = 5;
00451AF5 mov dword ptr [no],5 switch (no) {
00451AFC mov eax,dword ptr [no]
00451AFF mov dword ptr [ebp-0DCh],eax
00451B05 mov ecx,dword ptr [ebp-0DCh]
00451B0B sub ecx,1
00451B0E mov dword ptr [ebp-0DCh],ecx
00451B14 cmp dword ptr [ebp-0DCh],4
00451B1B ja $LN8+0Fh (0451B75h)
00451B1D mov edx,dword ptr [ebp-0DCh]
00451B23 jmp dword ptr [edx*4+451BA0h] case 1:printf("no is 1");
00451B2A push offset string "no is 1" (0457B6Ch)
00451B2F call _printf (04510CDh)
00451B34 add esp,4 break;
00451B37 jmp $LN8+1Ch (0451B82h) case 2:printf("no is 2");
00451B39 push offset string "no is 2" (0457B30h)
00451B3E call _printf (04510CDh)
00451B43 add esp,4 break;
00451B46 jmp $LN8+1Ch (0451B82h) case 3:printf("no is 3");
00451B48 push offset string "no is 3" (0457B3Ch)
00451B4D call _printf (04510CDh)
00451B52 add esp,4 break;
00451B55 jmp $LN8+1Ch (0451B82h) case 4:printf("no is 4");
00451B57 push offset string "no is 4" (0457B48h)
00451B5C call _printf (04510CDh)
00451B61 add esp,4 break;
00451B64 jmp $LN8+1Ch (0451B82h) case 5:printf("no is 5");
00451B66 push offset string "no is 5" (0457B54h)
00451B6B call _printf (04510CDh)
00451B70 add esp,4 break;
00451B73 jmp $LN8+1Ch (0451B82h) default:printf("other no");
00451B75 push offset string "other no" (0457B60h)
00451B7A call _printf (04510CDh)
00451B7F add esp,4 break;}
细节研究: 这种情况下是如何计算并跳转的
jmp 451BB0h
:直接跳转到 451BB0h 这个地址所对应的代码jmp [451BB0h]
:去 451BB0h 这个地址的内存空间取出一个地址值,再跳转到取出的地址对应的代码
int no = 5;
00451AF5 mov dword ptr [no],5 switch (no) {
00451AFC mov eax,dword ptr [no]
00451AFF mov dword ptr [ebp-0DCh],eax
00451B05 mov ecx,dword ptr [ebp-0DCh] // ecx == no == 5// ecx = ecx - 1
00451B0B sub ecx,1
00451B0E mov dword ptr [ebp-0DCh],ecx // no不在case的范围中的情况
00451B14 cmp dword ptr [ebp-0DCh],4
00451B1B ja $LN8+0Fh (0451B75h) // edx == ecx == 4
00451B1D mov edx,dword ptr [ebp-0DCh] // edx * 4 + 451BA0h == 4 * 4 + 451BA0h == 451BB0h// jmp [451BB0h] // jmp 451BB0h 这个地址存储的地址值
00451B23 jmp dword ptr [edx*4+451BA0h]
switch 跳转前其实已经将每个 case 代码的首地址算好并存储,并且其之间相差 4 字节
no 是否在 case 范围的计算:比较
no - min
与max - min
,min
和max
是 case 的最小最大值以上汇编使用的公式:
jmp [(no-x) * 4 + 某个地址]
,x
是 switch 中最小的 case 的值所以 switch 中 case 乱序对效率没有影响!这点和 if-else 不同
总结:case 值连续的情况下,每个 case 代码的地址值已经在内存中提前存好,利用公式 (no-x) * 4 + 某地址
直接跳转。哪怕有 100 个 case,只要连续,也是以上流程。相比 if-else 最坏可能要算 100 次,switch 效率更高。
case 值不连续
case 值不连续的情况,其实算法和连续是一样的。
区别是:会把最小的 case 到最大的 case 跳转的地址先算出来(4 个字节)。
例如给的是 case 1、3、5、6,其实提前算好值是 case 1、2、3、4、5、6,其中 2、4 对应跳转到 default。
case 值跨度很大
提前将 case1、case2 … case12 的要加的值存储在内存中(用 1 个字节存储)
no 是否在 case 范围的计算,与之前一样:比较
no - min
与max - min
,min
和max
是 case 的最小最大值。因此如果输入 100,经过判断不在 case 范围内,直接跳转 default
特殊情况:跨度极其大,case 1,2,3,4,10000 这种,那么 switch 就无法做优化,其底层和 if-else 是一样的,每个条件进行比较再判断是否跳转。
switch 底层本质上是空间换时间的优化,跨度极其大情况下的空间换时间是很亏的事情
总结
对比 if-else,编译器会对 switch 做一定的优化,提高执行效率。
a++ 和 ++a
++a 反汇编
int a = 5;
int b = ++a + 2;
反汇编分析:
int a = 5;
00291F55 mov dword ptr [a],5 int b = ++a + 2;// eax = a, eax == 5
00291F5C mov eax,dword ptr [a]// eax = eax + 1, eax == 6
00291F5F add eax,1// a = eax, a == 6
00291F62 mov dword ptr [a],eax // ecx = a, ecx == 6
00291F65 mov ecx,dword ptr [a]// ecx = ecx + 2, ecx == 8
00291F68 add ecx,2 // b = ecx, b == 8
00291F6B mov dword ptr [b],ecx
a++ 反汇编
int a = 5;
int b = a++ + 2;
反汇编分析:
int a = 5;
00241F55 mov dword ptr [a],5 int b = a++ + 2;// eax = a, eax == 5
00241F5C mov eax,dword ptr [a]// eax = eax + 2, eax == 7
00241F5F add eax,2 // b = eax, b == 7
00241F62 mov dword ptr [b],eax// ecx = a, ecx == 5
00241F65 mov ecx,dword ptr [a]// ecx = ecx + 1, exc == 6
00241F68 add ecx,1 // a = ecx, a == 6
00241F6B mov dword ptr [a],ecx
构造函数
构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作
简单使用:
class Person {public:int m_age;void run() {cout << "age is " << m_age << ", run()------" << endl;}// 构造函数Person() {m_age = 0;cout << "Person()------" << endl;}Person(int age) {m_age = age;cout << "Person(int age)------" << endl;}
};Person *p = new Person();
p->run();Person *p1 = new Person();
p1->run();Person *p2 = new Person();
p2->run();
构造函数反汇编
这种情况下,我们手动书写了 Car()
的无参构造函数。
class Car {public:int m_price;Car() {cout << "Car()" << endl;}
};int main() {Car *car = new Car();cout << car->m_price << endl;return 0;
}
反汇编:可以发现代码中有 call Car::Car (07FF7E5D0123Fh)
,确实调用了汇编
Car *car = new Car();
00007FF7E5D027FB mov ecx,4
00007FF7E5D02800 call operator new (07FF7E5D0104Bh)
00007FF7E5D02805 mov qword ptr [rbp+108h],rax
00007FF7E5D0280C cmp qword ptr [rbp+108h],0
00007FF7E5D02814 je main+4Bh (07FF7E5D0282Bh)
00007FF7E5D02816 mov rcx,qword ptr [rbp+108h] // 此处调用了构造函数
00007FF7E5D0281D call Car::Car (07FF7E5D0123Fh)
00007FF7E5D02822 mov qword ptr [rbp+118h],rax
00007FF7E5D02829 jmp main+56h (07FF7E5D02836h)
00007FF7E5D0282B mov qword ptr [rbp+118h],0
00007FF7E5D02836 mov rax,qword ptr [rbp+118h]
00007FF7E5D0283D mov qword ptr [rbp+0E8h],rax
00007FF7E5D02844 mov rax,qword ptr [rbp+0E8h]
00007FF7E5D0284B mov qword ptr [car],rax
下面这种情况,我们没有手动写构造函数:
class Car {public:int m_price;
};int main() {Car *car = new Car();return 0;
}
反汇编:可以发现没有调用call Car::Car
,显然没有生成无参构造函数
Car *car = new Car();
00512495 push 4
00512497 call operator new (0511140h)
0051249C add esp,4
0051249F mov dword ptr [ebp-0D4h],eax
005124A5 cmp dword ptr [ebp-0D4h],0
005124AC je __$EncStackInitStart+4Ah (05124C6h)
005124AE xor eax,eax
005124B0 mov ecx,dword ptr [ebp-0D4h]
005124B6 mov dword ptr [ecx],eax
005124B8 mov edx,dword ptr [ebp-0D4h]
005124BE mov dword ptr [ebp-0DCh],edx
005124C4 jmp __$EncStackInitStart+54h (05124D0h)
005124C6 mov dword ptr [ebp-0DCh],0
005124D0 mov eax,dword ptr [ebp-0DCh]
005124D6 mov dword ptr [car],eax
结论:
可以理解为,要做一些初始化操作的时候才会生成构造函数,没有任何操作就无需生成
在 Java 中,默认会给成员变量赋初值,默认应该是会生成构造函数的
以下情况下会生成构造函数,可以反汇编验证(这里不放汇编代码了):
- 成员变量在声明的同时进行了初始化,会生成构造函数
class Car {public:int m_price = 5;
};int main() {Car* c = new Car();return 0;
}
- 包含了对象类型的成员,且这个成员有构造函数,会生成构造函数
class Car {public:int m_price = 5;Car() {cout << "Car()---" << endl;}
};class Person {Car car;
};int main() {Person* p = new Person();return 0;
}
- 父类有构造函数,子类没有构造函数,子类会生成构造函数,在里面调用父类的构造函数
父子都有构造函数时,创建子类对象会先调用父类的构造函数,再调用自己的构造函数
class Person {public:Person() {}
};class Student : public Person {public:
};int main() {Student* stu = new Student();
}
函数和方法
很多开发者都会这样去定义
- 方法是定义在类里面的
- 函数是定义在类外面的
在汇编层面看来,函数和方法没有任何区别,都存储在代码区,都是 call
一个内存地址
函数的内存布局
调用一个函数,会开辟一段栈空间给函数
寄存器:
esp:永远指向栈顶,push 和 pop 操作会自动控制其指向
push 操作会往栈顶新增数据,同时 esp 指向其内存地址
pop 会弹出栈顶数据,同时 esp 指针指向上一个地址,被弹出数据的地址中数据仍在,相当于垃圾数据,等待以后 push 的数据将其覆盖。
初始
--------------------
esp -> 0x2009 |
--------------------执行 push 4
--------------------
esp -> 0x2005 4 |0x2009 |
--------------------再执行 push 5
--------------------
esp -> 0x2001 5 |0x2005 4 |0x2009 |
--------------------执行 pop eax, 此时相当于 eax = 5
栈顶的内存空间不再被指向,相当于垃圾数据,等待被覆盖
--------------------0x2001 5(此时这段内存相当于垃圾数据)
esp -> 0x2005 4 |0x2009 |
--------------------执行 pop ebx, 此时相当于 ebx = 4
--------------------0x2001 5(此时这段内存相当于垃圾数据)0x2005 4(此时这段内存相当于垃圾数据)
esp -> 0x2009 |
--------------------执行 push 3, 把之前等待被覆盖的垃圾数据覆盖了
--------------------0x2001 5(此时这段内存相当于垃圾数据)
esp -> 0x2005 3 |0x2009 |
--------------------
- ebp:永远指向栈底
- 栈指针寄存器
栈平衡:函数调用前后其栈顶指针指向是相同的,即调用完后栈 esp 指针会回到原来位置
栈空间是系统不断覆盖读写的,不存在释放操作
void test1(int v1, int v2) {}
递归函数
递归的层次太深会导致栈空间不够用,栈空间溢出
利用汇编挖掘编程语言的本质相关推荐
- 探索编程语言的本质:了解编程语言的定义与分类
前言: 由于我看了一眼我的粉丝列表,发现好像关于开发语言的童鞋占比较多哈,所以出一下这篇专栏. 要关注的小伙伴可以提前订阅哈. 目录 前言: 引言 1.1. 编程语言的重要性 1.2. 本文的目的与结 ...
- 安全学习概览——恶意软件分析、web渗透、漏洞利用和挖掘、内网渗透、IoT安全分析、区块链、黑灰产对抗...
1 基础知识 1.1 网络 熟悉常见网络协议: https://www.ietf.org/standards/rfcs/ 1.2 操作系统 1.3 编程 2 恶意软件分析 2.1 分类 2.1.1 木 ...
- java实现系列化的jdk_Java反序列化之与JDK版本无关的利用链挖掘
原标题:Java反序列化之与JDK版本无关的利用链挖掘 Java反序列化之与JDK版本无关的利用链挖掘 一.前言: 总感觉年纪大了,脑子不好使,看过的东西很容易就忘了,最近两天又重新看了下java反序 ...
- xss挖掘思路分享_视频分享:XSS的利用与挖掘
点击上方"公众号" 可以订阅哦! Hello,各位小伙伴大家好. 这里是一名白帽的成长史~ 公众号成立至今也一年了,在此感谢各位的支持~ 今天是第一次给大家视频分享,还请多多包涵~ ...
- 速卖通关键词挖掘工具_sem竞价怎么利用工具挖掘更多的关键词?拓词技巧!
之前,欢哥sem曾多次提及关键词是账户的根本,但是在对客户广告账户进行检测时,发现都存在一个问题:广告账户的关键词寥寥无几! 关键词是我们流量布局的关键,如果账户内关键词不足,往往导致展现量过低,流量 ...
- 根据程序流程图化程序流图_如何利用小程序将零售本质较大化?
小程序是为线下情景为之的,它是线下商户的福利.线下零售业一直以来扩展比较有限,一直困于线下的模式,而没法精准推送网上的大量用户.小程序出示了连接网上线下的安全通道,强有力推动用户引流方法,数据流量变现 ...
- php嵌套序列化输出tp5.0,ThinkPHP v5.0.x 反序列化利用链挖掘
前言 前几天审计某cms基于ThinkPHP5.0.24开发,反序列化没有可以较好的利用链,这里分享下挖掘ThinkPHP5.0.24反序列化利用链过程.该POP实现任意文件内容写入,达到getshe ...
- 利用汇编与机器码定位崩溃点
由于在公司自己写的代码交给测试时老是有那种我这边正常测试那边老是崩溃的情况.调试除了windbg OD等其时还可以简单定位崩溃点.这是在自己搞linux时产生的想法.linux有非常强大的调试机制. ...
- 利用关联规则挖掘中医证素与恶性肿瘤的关系
目标: 借助病理信息,挖掘各中医证素与乳腺癌TNM分期之间的关系 思路与流程: 目的是为了挖掘各中医证素与乳腺癌TNM分期之间的关系,故采用关联规则模型 确定模型之后,需要整理患者的各中医证素与乳腺癌 ...
最新文章
- LSM 优化系列(六)-- 【ATC‘20】MatrixKV : NVM 的PMEM 在 LSM-tree的write stall和写放大上的优化
- “std::invoke”: 未找到匹配的重载函数
- mac 更换默认蓝牙适配器_Win7连接低功耗蓝牙(BLE)鼠标
- CentOS 7下安装Python3.6.4
- Storyboard.storyboard could not be opened. Could not read the archive.
- mysql数据库连接地址utf8_在Python中连接到MySQL数据库时UTF8不工作
- ShadeGraph教程之节点详解4:Master Nodes
- thinkphp开启子域名无法正常访问_解决TP6报错“当前访问路由未定义或不匹配”...
- My in 2007
- 国内首家VR虚拟现实主题公园即将在北京推出
- [转]Android编程之BitmapFactory.decodeResource加载图片缩小的原因及解决方法
- 转载:python能用来做什么?
- kdd数据集_learning from imbalanced data sets—第一章——KDD与数据科学概述
- Nacos配置热更新的4种方式、读取项目配置文件的多种方式,@value,@RefreshScope,@NacosConfigurationProperties
- 健康体检信息管理系统方案/案列/软件/APP/小程序/网站
- Python如何实现图片显示
- spine 局部换装
- 苹果电脑键盘没反应_MAC PRO type-c接口无反应,充电无反应或一直在充电解决方案...
- 天之博特 多车协同:Waiting for subscriber to connect to /tianbot_1/cmd_vel 解决办法
- 对于多线程程序,单核cpu与多核cpu是怎么工作的
热门文章
- vector常见用法
- ecos中的spl同步机制
- ubuntu卸载fcitx后引发的问题修复
- [leetcode]84. Largest Rectangle in Histogram c语言
- sphinx 入门_Sphinx搜索引擎入门
- azure云数据库_配置Azure SQL数据库防火墙
- sql etl_使用SQL TRY函数进行ETL优化
- azure云数据库_Azure SQL数据库上的透明数据加密(TDE)
- 创建java类并实例化类对象
- [GO]结构体成员的使用:普通变量