深入理解计算机系统(CSAPP)bomblab实验进阶之nuclearlab——详细题解
前言
本实验是难度高于bomblab的一个补充实验,该实验部分题目难度已经达到CTF入门水平,且这个实验据说是上一届的某个学长原创,因此互联网上几乎找不到类似的题目。在间断地思考了几周后我最终完成了所有题目,并打算在这篇随笔里详细地给大家分享我的解题过程。
核弹样本(可本地断网运行):https://wwi.lanzoup.com/ifFGo0kmrndg
第0关:准备工作
将文件拖入Detect It Easy进行查壳:64位程序,无壳
这个时候按照我们做bomblab的经验,应该会先objdump这个程序,然后用gdb在main函数下断点,但运行程序会发现如下的异常:
如果继续执行会发现程序由于除以0错误引发异常,从而进入explode函数,结合check_debugger名称我们可以推断出这个程序具有反gdb调试功能,因此我们可以考虑更换其他工具继续实验,这里我选择了IDA pro。
(IDA pro介绍:https://blog.csdn.net/yi_rui_jie/article/details/127865485)
理论上而言我们应该阅读汇编代码再进行逆向分析,但IDA pro的强大之处在于它可以自动帮助我们把汇编语言翻译成高级语言,按F5快捷键即可。
特别说明:
1. IDA的汇编代码是Intel风格,且实际中掌握Intel风格的汇编语言是非常必要的。
2. 高级语言分析不代表不再需要看懂汇编代码,相反地,许多关键之处IDA的分析往往不能令人满意,需要进行修补,对汇编能力有较高的要求。下文中你就可以更好地明白这一点的意义。
3. 这篇题解不会具体介绍IDA的用法(如查看汇编代码和高级代码、创建函数、修改字节、保存文件、远程调试等),请自行查阅资料。
我们首先查看main函数的c语言代码:
1 int __cdecl main(int argc, const char **argv, const char **envp)
2 {
3 if ( argc != 3 )
4 {
5 __fprintf_chk(stderr, 1LL, "Usage: %s studentId password\n", *argv);
6 exit(8);
7 }
8 strncpy(userid, argv[1], 0x80uLL);
9 byte_4045FF = 0;
10 strncpy(userpwd, argv[2], 0x80uLL);
11 byte_40467F = 0;
12 ((void (*)(void))bomb_initialize)();
13 __printf_chk(1LL, "%s: ", "pupil");
14 readNextLine(128);
15 pupil();
16 bomb_defused();
17 __printf_chk(1LL, "%s: ", "tr1vial");
18 readNextLine(128);
19 tr1vial();
20 bomb_defused();
21 __printf_chk(1LL, "%s: ", "rainb0w");
22 readNextLine(128);
23 rainb0w();
24 bomb_defused();
25 __printf_chk(1LL, "%s: ", "q_math");
26 readNextLine(128);
27 q_math();
28 bomb_defused();
29 __printf_chk(1LL, "%s: ", "hothothot");
30 readNextLine(128);
31 hothothot();
32 bomb_defused();
33 __printf_chk(1LL, "%s: ", "tran$f0rm");
34 readNextLine(128);
35 ((void (*)(void))tran_f0rm)();
36 bomb_defused();
37 return 0;
38 }
不难看出这个程序的大致流程是:首先读取两个命令行参数并存储,接着第12行进入bomb初始化函数,然后和bomblab一样需要完成6个小实验。由于该实验需要联网,我们需要先对bomb_initialize函数(初始化函数)进行修改。
找到初始化函数的位置,发现IDA无法识别该函数的后半段代码:
仔细观察该程序逻辑,在call语句后面的三句指令对程序自身进行了修改:
于是我们在0x4035EA处下断点并运行到此,观察al的值发现是1:
这个时候继续调试会出现如下提醒(实际上每次调试都会出现),说明该程序会检查程序运行时间,过长(说明有调试)则抛出信号,这里我们选择No即可放弃将信号传递给程序处理,从而继续调试:
然后我们就会发现程序跳入0x4035F3处执行,而0x4035F2处的指令永远不会执行,是故意塞入而干扰分析的指令,这种干扰的方法也被称为花指令。
后面的代码分析同上,如下图所示,我们只需将两个check函数nop掉,即可完成对初始化函数的修补:
下面我们再对结果判断的两个函数完成修补:
如下图,将bomb_defused和bomb_explode两函数矩形内的联网代码nop掉,即可使该程序能在本地完成调试,且用户名和密码可以随意输入:
建议以文本文件作为输入,因为后续题目可能需要输入非ASCII字符。如./nuclearlab user pwd < answer.txt
第1关:pupil
1 unsigned __int64 pupil()
2 {
3 int v0; // edi
4 unsigned int v2; // [rsp+4h] [rbp-14h] BYREF
5 unsigned __int64 v3; // [rsp+8h] [rbp-10h]
6
7 v0 = (int)now_input;
8 v3 = __readfsqword(0x28u);
9 if ( (unsigned int)__isoc99_sscanf(now_input, "%d", &v2) != 1 )
10 bomb_explode(v0);
11 if ( (unsigned int)pupil_pow_mod(233333LL, v2) != 1 )
12 bomb_explode(233333);
13 return __readfsqword(0x28u) ^ v3;
14 }
由代码可知,我们需要pupil_pow_mod(233333, v2)值为1,而v2是我们输入的值,下面查看该函数代码:
1 __int64 __fastcall pupil_pow_mod(__int64 a1, int a2)
2 {
3 __int64 result; // rax
4 int v3; // eax
5 __int64 v4; // rcx
6
7 result = 1LL;
8 if ( a2 )
9 {
10 v3 = pupil_pow_mod(a1, (unsigned int)(a2 >> 1));
11 v4 = v3 * (__int64)v3 % 998244353;
12 result = v4;
13 if ( (a2 & 1) != 0 )
14 return (unsigned int)(v4 * (int)a1 % 998244353);
15 }
16 return result;
17 }
可以看到result本身值为1,因此只要输入的值为0,函数便会直接返回1,从而通过该关,确实挺符合它的名字,,,,,,
第2关:tr1vial
1 unsigned __int64 tr1vial()
2 {
3 char *v0; // r12
4 size_t v1; // rcx
5 unsigned __int64 v2; // rdx
6 __int64 *v3; // rdi
7 __int16 v4; // dx
8 signed __int64 v5; // rdx
9 void *v6; // rsp
10 bool v7; // cf
11 bool v8; // zf
12 const char *v9; // rsi
13 __int64 v10; // rcx
14 const char *v11; // rdi
15 _BYTE v14[4088]; // [rsp+8h] [rbp-1020h] BYREF
16 __int64 v15; // [rsp+1008h] [rbp-20h] BYREF
17 unsigned __int64 v16; // [rsp+1010h] [rbp-18h]
18
19 v0 = now_input;
20 v16 = __readfsqword(0x28u);
21 v1 = strlen(now_input);
22 v2 = 4 * ((v1 + 2) / 3) + 16;
23 v3 = (__int64 *)((char *)&v15 - (v2 & 0xFFFFFFFFFFFFF000LL));
24 v4 = v2 & 0xFFF0;
25 if ( &v15 != v3 )
26 {
27 while ( v14 != (_BYTE *)v3 )
28 ;
29 }
30 v5 = v4 & 0xFFF;
31 v6 = alloca(v5);
32 if ( v5 )
33 *(_QWORD *)&v14[v5 - 8] = *(_QWORD *)&v14[v5 - 8];
34 EVP_EncodeBlock(v14, v0, (unsigned int)v1);
35 v9 = v14;
36 v10 = 17LL;
37 v11 = "fkdM8J+Sr0hGfg==";
38 do
39 {
40 if ( !v10 )
41 break;
42 v7 = *v9 < (unsigned int)*v11;
43 v8 = *v9++ == *v11++;
44 --v10;
45 }
46 while ( v8 );
47 if ( (!v7 && !v8) != v7 )
48 bomb_explode((int)v11);
49 return __readfsqword(0x28u) ^ v16;
50 }
首先关注第47行,分析可知只有v7=0,v8!=0时不会进explode函数,而38-45行的循环分析可知,当v9和v11所指向的字符串完全相等时才能满足这个要求。v9=v14=
EVP_EncodeBlock(v14, v0, (unsigned int)v1),其中v0是输入的字符串。查询资料可知这个函数对v0进行base64加密,因而我们对v11解密即可。
base64介绍:https://blog.csdn.net/qq_19782019/article/details/88117150
python解密脚本:
1 import base64
2 import math
3
4 ss="fkdM8J+Sr0hGfg0="
5 print(base64.b64decode(ss))
可得答案为~GL\xf0\x9f\x92\xafHF~,由于字符串中含有非ASCII字符,因此需要先写入文本,再读入文本作为输入。如果直接到网站上解密可能无法显示不可见字符(被坑惨了炸了好几次),事实上中间的字符是,这个字符串"~GLHF~"意思是:Good Luck and Have Fun,祝你取得满分。
第3关:rainb0w
1 unsigned __int64 rainb0w()
2 {
3 char *v0; // r12
4 int *v1; // rbx
5 size_t v2; // rax
6 int *v3; // rdi
7 char vars0; // [rsp+0h] [rbp+0h] BYREF
8 int vars10; // [rsp+10h] [rbp+10h] BYREF
9 __int16 vars14; // [rsp+14h] [rbp+14h]
10 char vars30; // [rsp+30h] [rbp+30h] BYREF
11 unsigned __int64 vars38; // [rsp+38h] [rbp+38h]
12
13 v0 = now_input;
14 vars38 = __readfsqword(0x28u);
15 v1 = &vars10;
16 v2 = strlen(now_input);
17 MD5(v0, v2, &vars0);
18 do
19 {
20 v3 = v1;
21 v1 = (int *)((char *)v1 + 2);
22 __sprintf_chk(v3, 1LL, -1LL, "%2.2hhX", (unsigned int)vars0);
23 }
24 while ( v1 != (int *)&vars30 );
25 if ( vars10 != 859124024 || vars14 != 16691 )
26 bomb_explode((int)v3);
27 return __readfsqword(0x28u) ^ vars38;
28 }
首先还是关注explode的条件。由于vars10和vars14与其它变量似乎没有直接关联,我们转到其汇编代码0x4019FB处下断点并运行到此处,以123456作为试探输入:
这时我们点击vars10,跳转到它对应的栈位置:
由cmp内容可知,我们需要把所指位置(实际上是rbp处)当成一个int指针,解该指针得到的值为0x33353138,根据小端顺序,我们可以判断出前四个字符为8153。再点击0x401A30处的vars14,发现跳转至第5个字符'D'处,同理可得第5、6个字符为3A:
而根据前面的MD5函数,我们有理由猜测这个字符串"E10ADC3949BA59ABBE56E057F20F883E"就是123456的MD5加密32位值,在MD5加密网站上输入123456可得到相同结果,证明猜想正确。
于是题目转化为,输入一个字符串,使得它的MD5加密值前六位是字符串“81533A”,这样的字符串有很多,但由于MD5的不可逆性,即我们不能通过算法逆向解密某个加密后的值,因此我们只能从正向突破,其中一种方法是尝试从数字开始枚举,得到一个答案为1914718:
1 import hashlib
2
3 for i in range(10000000):
4 s=str(i)
5 if(hashlib.md5(s.encode()).hexdigest()[:6]=='81533a'):
6 print(i)
7 break
第4关:qmath
1 unsigned __int64 q_math()
2 {
3 int v0; // edi
4 unsigned int v1; // edx
5 int v2; // ebp
6 unsigned int v3; // ebx
7 int v5; // [rsp+4h] [rbp-24h] BYREF
8 unsigned __int64 v6; // [rsp+8h] [rbp-20h]
9
10 v6 = __readfsqword(0x28u);
11 __isoc99_sscanf(now_input, "%u", &v5);
12 magic = (1431655766 * (unsigned __int64)(unsigned int)magic) >> 32;
13 v0 = v5;
14 v2 = t00rerauqs((unsigned int)v5) + 1;
15 if ( v2 <= 29999 )
16 bomb_explode(v0);
17 if ( v2 > 40000 )
18 bomb_explode(v0);
19 v3 = 2;
20 while ( 1 )
21 {
22 if ( !(v1 % v3) )
23 bomb_explode(v0);
24 if ( ++v3 > v2 )
25 break;
26 v1 = v5;
27 }
28 return __readfsqword(0x28u) ^ v6;
29 }
从第14行我们可以看出v2是把我们输入的值通过一个函数进行处理而得到的,由于具体找出公式较复杂,我们可以在15行处下断点,然后动态地输入一组数据来观察v2的变化规律,这里我发现v2随输入值的增大而增大。那么我们不断地以10的指数去尝试,使得v2最终落在30000-40000之间,这时我们可以确定一个相对的范围。然后我们再观察while循环,不难看出它要满足[2,v2]之间的所有整数都不是输入值的因子,那么我们在区间内写一个循环判断即可得到答案,其中一种为1000166183。
第5关:hothothot
1 unsigned __int64 hothothot()
2 {
3 __int64 v1; // [rsp+0h] [rbp-98h] BYREF
4 unsigned __int64 v2; // [rsp+88h] [rbp-10h]
5
6 v2 = __readfsqword(0x28u);
7 ((void (*)(void))hothothot_you_shouldnt_read_this_part_0)();
8 convert_text_enc0ding(now_input, &v1, 128LL);
9 if ( !__sigsetjmp(&buf, 1) )
10 return hothothot_cold();
11 ((void (__fastcall *)(_QWORD))hothothot_you_shouldnt_read_this)(0LL);
12 return __readfsqword(0x28u) ^ v2;
13 }
这道题应该是6个关卡中难度最高的一个,本质上来说它已经属于pwn的范畴了,,,,,,
首先我们会进入一个hothothot_you_shouldnt_read_this_part_0函数,从字面上看它不要我们阅读这段代码,但很显然我的好奇被它激发了,,,,,这段代码汇编如下:
我们可以看到与准备阶段相似的花指令,dword_404468被设置为1来使10变成01,从而使jmp语句跳入0x401705处,后面是一些信号处理函数,对解题没有什么影响,看来的确没啥读的必要。接着我们重点关注convert函数:
1 unsigned __int64 __fastcall convert_text_enc0ding(char *a1, char *a2, size_t a3)
2 {
3 iconv_t v3; // rax
4 size_t outbytesleft; // [rsp+8h] [rbp-30h] BYREF
5 char *outbuf; // [rsp+10h] [rbp-28h] BYREF
6 char *inbuf; // [rsp+18h] [rbp-20h] BYREF
7 size_t inbytesleft; // [rsp+20h] [rbp-18h] BYREF
8 unsigned __int64 v9; // [rsp+28h] [rbp-10h]
9
10 outbytesleft = a3;
11 inbuf = a1;
12 outbuf = a2;
13 v9 = __readfsqword(0x28u);
14 inbytesleft = strlen(a1);
15 v3 = iconv_open("GBK", "UTF-8");
16 iconv(v3, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
17 return __readfsqword(0x28u) ^ v9;
18 }
查询iconv函数可知,这个函数会把我们的输入以UTF-8编码读入,然后转换为GBK编码。当输入的为ASCII字符时,转换后没有任何变化;而当输入其它字符时(如\xe8\x8a\x92在UTF-8下表示“芒”),会把它转换为GBK编码(\xc3\x92在GBK下表示“芒”),因而我们输入\xe8\x8a\x92就能得到\xc3\x92。
附上GBK编码表:https://blog.csdn.net/itnerd/article/details/118615080
接下来回到hothothot函数,查询资料可知sigsetjmp函数在正常情况下会返回0,因而我们进入hothothot_cold函数查看:
看了以后直接傻眼,没有字符串比较之类的语句??这个函数既然直接让rip指向rsp的位置,那我们只能动调来看看它到底想干什么,不妨输入12345678:
观察栈结构我们可以发现,这个函数直接跳入了我们输入的字符串开头,并将我们的输入当作指令执行!!!因而我们有理由推断,这一关要求我们输入一道字符序列,它从UTF-8转换为GBK后的指令能够正常返回函数的0x401CEB处即可成功。
特别注意以下几点:
1. GBK总体编码范围为 8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 xx7F 一条线。因此最后构造的指令中如果有非ASCII字符,则必须两两成对且落在指定区间内,再找到GBK对应的汉字,以这个汉字的UTF-8编码输入。
2. 构造指令中不能含有空字符(\x00),它会被转换为\x20等其它非空字符。
基于上述两点,一些简单的构造都失效了:
jmp 寄存器 出现FF字符,找不到对应GBK编码
push 0x401CEB 出现00字符,空字符失效
mov 寄存器,0x401CEB 出现00字符,空字符失效
mov 寄存器,[内存地址+偏移] 偏移为0时出现00字符,空字符失效
我们可以利用栈中已有的地址和空闲寄存器(如r8)的地址,把它们不断累加得到0x401CEB(注意让偏移不为0),然后再push+ret即可,下面是我的构造方法:
48 e5 86 ad 09 4c 03 40 01 49 e5 85 9d 0c 41 50 e8 8a 92 UTF-8
48 83 e8 09 4c 03 40 01 49 83 c0 0c 41 50 c3 a2 GBK
GBK的字符串为机器码,执行指令如图,实际上retn只需要c3即可,后面的a2是不会执行的,可以替换为其它字符:
最终成功返回所需地址。
第6关:tran$f0rm
观察发现同样的花指令混淆,我们把10改为01,再nop掉0x401D53处的48,之后我们即可创建transform函数,F5得到如下代码:
1 unsigned __int64 tran_f0rm()
2 {
3 int v0; // edi
4 int v1; // edx
5 int v3; // [rsp+Ch] [rbp-1Ch] BYREF
6 int v4; // [rsp+10h] [rbp-18h] BYREF
7 int v5; // [rsp+14h] [rbp-14h]
8 unsigned __int64 v6; // [rsp+18h] [rbp-10h]
9
10 v0 = (int)now_input;
11 v6 = __readfsqword(0x28u);
12 __isoc99_sscanf(now_input, "%d%d", &v3, &v4);
13 *(&loc_401D51 + 1) = 16;
14 ma1f0rm();
15 v5 = v1;
16 if ( v3 <= 100 )
17 bomb_explode(v0);
18 if ( v4 != v5 )
19 bomb_explode(v0);
20 return __readfsqword(0x28u) ^ v6;
21 }
这道题甚至比qmath还简单,我们先输入一个大于100的整数,再动调得到v5的值,即可得到一组答案。我的答案是:120 12869
反gdb调试分析
由之前的分析可知,该文件在debugger_check_caller函数内检测gdb环境,进入其函数查看:
显然核心代码在check_debugger内,进入该函数:
又是熟悉的花指令混淆,我们运用之前的方法去除所有的混淆代码,再创建函数:
查阅文档可知,getppid函数取得父进程的标识码整数x,sprintf使v3字符串为"/proc/x/exe",readlink函数会把参数v3的符号链接内容存储到参数v4所指的内存空间,并返回字符串个数,最终使v4成为一个代表调试器路径的字符串,而basename函数则取v4最后一个'/'之后的内容。
例如我们用IDA调试时,在第15行下断点可得到各个参数值:
v3=221066,v4="/home/kali/Desktop/linux_server64"(IDA调试文件的路径),s="linux_server64",这样所得的s在第15行运算时不会发生除以0的异常。
观察对应的汇编代码,我们容易发现返回值rax就是字符串s,而第15行的运算只用到s,因此下面我们用gdb在0x4015EF处下断点观察rax值:
我们发现rax = s = "gdb",因此*(_WORD *)&s[strlen(s) - 2] = *(short*)&s[1] = 0x6264 = 25188,代入(*(_WORD *)&s[strlen(s) - 2] | 0x2020) - 25188) & 0x7FFFFFFF可得该式值为0,从而引发了除以0异常。不过相应的处理方法也很简单,在0x401622指令处下断点,将ecx的值改成一个正数即可。
那可能有人要问了,你为什么一定要把ecx改成正数,负数不行吗?答案是:不行!因为根据&0x7fffffff可知,正常逻辑下除数一定是一个非负数,既然除数不能为0,那么只能修改为正数。如果修改ecx为负数,会出现什么结果呢?我们继续探究:
由第15行可知,正常情况下,被除数是一个负数,除数是一个正数,因此此时该变量是一个负数;第17行使该变量成为-1,第19行使该变量成为1。
如果我们修改ecx为负数,那么第15行该变量是正数,第17行成为0,第19行依然为0,故该变量的值成为0。
再次观察一下C代码,这个dword_404468变量似乎在前面的某处分析出现过??如果不记得了请看下面这张图片:
在准备阶段时,我们动调发现了al的值是1,但是当时并没有解释它的由来。现在根据0x4035E2处指令可知这个值正是dword_404468,接着我们查找一下这个变量的交叉引用:
可以发现只有3处位置写了这个变量,它们正是check_debugger函数C代码的15、17、19行处,也就是说如果我们把ecx修改为负数,会导致这个全局变量始终为0,从而使程序无法正确跳过干扰指令,最终产生SIGILL异常而进入bomb_explode函数:
总结:
1. 当正向分析困难时就多尝试动调得到结果,省时省力。
2. 对于花指令、反调试等混淆技术要学会辨认、清除,这样才能让IDA发挥最好的作用。
最后附上成功拆弹图:
深入理解计算机系统(CSAPP)bomblab实验进阶之nuclearlab——详细题解的更多相关文章
- CS:APP3e 深入理解计算机系统_3e bomblab实验
bomb.c /*************************************************************************** * Dr. Evil's Ins ...
- <深入理解计算机系统> CSAPP Tiny web 服务器
本文是我学习<深入理解计算机系统>中网络编程部分的学习笔记. 1. Web基础 web客户端和服务器之间的交互使用的是一个基于文本的应用级协议HTTP(超文本传输协议).一个w ...
- CS:APP3e 深入理解计算机系统_3e Attacklab 实验
详细的题目要求和资源可以到 http://csapp.cs.cmu.edu/3e/labs.html 或者 http://www.cs.cmu.edu/~./213/schedule.html 获取. ...
- CS:APP3e 深入理解计算机系统_3e CacheLab实验
详细的题目要求和资源可以到 http://csapp.cs.cmu.edu/3e/labs.html 或者 http://www.cs.cmu.edu/~./213/schedule.html 获取. ...
- CS:APP3e 深入理解计算机系统_3e Datalab实验
由于http://csapp.cs.cmu.edu/并未完全开放实验,很多附加实验做不了,一些环境也没办法搭建,更没有标准答案.做了这个实验的朋友可以和我对对答案:) 实验内容和要求可在http:// ...
- CS:APP3e 深入理解计算机系统_3e MallocLab实验
详细的题目要求和资源可以到 http://csapp.cs.cmu.edu/3e/labs.html 或者 http://www.cs.cmu.edu/~./213/schedule.html 获取. ...
- 【深入理解计算机系统CSAPP】第六章 存储器层次结构
6 存储器层次结构 存储器系统(memory system)是一个具有不同容量.成本和访问时间的存储设备的层次结构.CPU 寄存器保存着最常用的数据.靠近 CPU 的小的.快速的高速缓存存储器(cac ...
- csapp 深入理解计算机系统 csapp.h csapp.c文件配置
转载自 http://condor.depaul.edu/glancast/374class/docs/csapp_compile_guide.html Compiling with the CS ...
- 深入理解计算机系统bomb炸弹实验
1. You can Russia from land here in Alaska. x /s 0x804a26c 0x804a26c: "You can Russia from la ...
- 深入理解计算机系统_3e 第九章家庭作业 CS:APP3e chapter 9 homework
9.11 A. 00001001 111100 B. +----------------------------+ | Parameter Value | +--------------------- ...
随机推荐
- 一步步带你设计MySQL索引数据结构
前言 MySQL的索引是一个非常重要的知识点,也基本上是面试必考的一个技术点,所以非常重要.那你了解MySQL索引的数据结构是怎么样的吗?为什么要采用这样的数据结构? 现在化身为MySQL的架构师,一 ...
- JavaScript:函数:如何声明和调用函数?
首先,理解什么是函数? 通俗的说,函数就是用大括号括起来的一组JS语句的集合体,是一个代码块,表达一种行为逻辑. 当我们调用函数的时候,我们就是在执行这一组JS语句. 然后,确定一点,在JS中,函数也 ...
- MongoDB - 模式设计
注意事项 模式设计,即在文档中表示数据的方式,对于数据表示来说时非常关键的. 为 MongoDB 做模式设计时,在性能.可伸缩性和简单性方面是重中之重,也需要考虑一些特别的注意事项. 限制条件 与常见 ...
- JS如何返回异步调用的结果?
这个问题作者认为是所有从后端转向前端开发的程序员,都会遇到的第一问题.JS前端编程与后端编程最大的不同,就是它的异步机制,同时这也是它的核心机制. 为了更好地说明如何返回异步调用的结果,先看三个尝试异 ...
- 深入理解 Linux 物理内存分配全链路实现
前文回顾 在上篇文章 <深入理解 Linux 物理内存管理>中,笔者详细的为大家介绍了 Linux 内核如何对物理内存进行管理以及相关的一些内核数据结构. 在介绍物理内存管理之前,笔者先从 ...
- [机器学习] Yellowbrick使用笔记6-分类可视化
分类模型试图在一个离散的空间中预测一个目标,即为一个因变量实例分配一个或多个类别. 代码下载 分类分数可视化工具显示类之间的差异以及一些特定于分类器的可视化评估.我们目前已经实施了以下分类器评估: 分 ...
- Java基础篇——注解和反射
注解 注解Annotation可以被其他程序(编译器)读取,常见的有@override 内置注解 @Override 适用于修饰方法,表明重写父类中的一个方法 @Deprecated 用于修饰类.方法 ...
- 题解P3847 [TJOI2007]调整队形
简要题意 给出一个长度为 \(n\) 的序列 \(A\),你需要执行下面的操作,将这个序列变成回文序列: 在序列左右侧或中间插入一个元素,元素数值任意. 删除一个元素. 更改一个元素的值. \(1 \ ...
- 数字IC设计流程
数字IC设计流程 简单介绍数字IC设计流程
- 让你的手,不再抽筋,速度:修改mac的快捷键,以及鼠标滑动
我是资深️windows系统爱好者️ 最近开始尝试使用MAC系统进行代码运行测试,从配置环境,到写博客,一天按下了n次的复制粘贴快捷键, 妈耶,感觉要抽筋了 呜呜--- 这简直无法忍,我决定修改这个快 ...