[{"content":"解除 22 与 80 端口占用 一个 Git 远程仓库需要有两个默认端口，80端口作为 web 页面，22端口用于git ssh连接。如果使用其他端口，就需要手动指定端口访问web页面与git url。\n即通过 git@[ip]:[port]:[username]/[repo].git 作为ssh url访问仓库\n通过http://[ip]:[port]访问服务\n不在意的读者可以忽略这一章节。\n80 端口通常为nginx占用，使用systemctl停止nginx的运行即可。\n解除 nginx 对 80 端口的占用 1 2 3 4 5 6 7 # 停止 nginx 的运行 systemctl stop nginx # 停止 nginx 开机自启动 systemctl disable nginx # 查看nginx状态 systemctl status nginx 1 2 3 # 查看80端口的占用状态 lsof -i:80 # 预期输出为空 我这里的 Gitea 服务已经部署成功，80端口由 docker监听，转发给容器内的gitea web服务。\n解除 sshd 对 22 端口的占用 使用 vim 编辑 /etc/ssh/sshd_config，找到#Port 22行。删掉井号解除注释，把22端口改为其他你喜欢的端口。\nvim非输入状态，输入\u0026rsquo;/\u0026lsquo;后进入搜索模式，输入指定字符串后按回车即可跳转到对应位置。\n在确认可以通过新端口连接服务器前，不要退出当前的ssh会话！！！\n修改 sshd 监听的端口后，需要在防火墙放行新的 sshd 端口。如果是有安全组的云服务器，还需要在云服务器提供商处配置安全组。\n1 ufw allow [port] 修改配置并放行新端口后，重启 sshd 以应用新配置。\n1 systemctl restart sshd 此时可以尝试是否可以通过新的端口ssh连接到服务器。然后查看 22 端口是否被释放。\n1 lsof -i:80 我在进行到这一步时，sshd同时监听了 22 端口和我自己指定的端口。lsof可以显示出占用端口的进程pid。这里直接kill掉对应的进程即可。\n宝塔面板创建 Gitea 容器 安装 Gitea 时输入的两个端口就是在服务器主机上监听的两个端口。\n","date":"2026-07-14T15:03:48Z","permalink":"/post/rainyun_git_remote/","title":"雨云服务器宝塔面板搭建个人Git仓库托管实录"},{"content":"pragma #pragma是 C 语言的一种预处理指令，类似于#include。用于告诉编译器进行某种操作。pragma 单词本身的释义为“编译指令”。\n最简单的一种 pragma 可以在编译时输出文本提示。\n1 2 3 4 5 #pragma message(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\u0026#34;) int main(void) { return 0; } 编译上面的 C 文件结果输出如下。\n1 2 3 4 5 ❯ gcc pragma.c pragma.c:1:9: warning: \u0026gt;\u0026gt;\u0026gt; Compiling ... [-W#pragma-messages] 1 | #pragma message(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\u0026#34;) | ^ 1 warning generated. _Pragma 由于宏定义之间不能嵌套，当某种 pragma 想要复用时，无法通过#define复用，此时可以通过_Pragma指令，如下所示。\n1 2 3 4 5 6 7 8 9 #define MSG _Pragma(\u0026#34;message(\\\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\\\u0026#34;)\u0026#34;) MSG _Pragma(\u0026#34;message(\\\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\\\u0026#34;)\u0026#34;) int main(void) { return 0; } 编译后输出如下提示信息。其中的 note 消息可以忽略，关注warning: \u0026gt;\u0026gt;\u0026gt; Compiling ... [-W#pragma-messages]段即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ❯ gcc pragma.c pragma.c:3:1: warning: \u0026gt;\u0026gt;\u0026gt; Compiling ... [-W#pragma-messages] 3 | MSG | ^ pragma.c:1:13: note: expanded from macro \u0026#39;MSG\u0026#39; 1 | #define MSG _Pragma(\u0026#34;message(\\\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\\\u0026#34;)\u0026#34;) | ^ \u0026lt;scratch space\u0026gt;:4:2: note: expanded from here 4 | message(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\u0026#34;) | ^ pragma.c:5:1: warning: \u0026gt;\u0026gt;\u0026gt; Compiling ... [-W#pragma-messages] 5 | _Pragma(\u0026#34;message(\\\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\\\u0026#34;)\u0026#34;) | ^ \u0026lt;scratch space\u0026gt;:6:2: note: expanded from here 6 | message(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\u0026#34;) | ^ 2 warnings generated. 根据上面的编译输出结果，我们可以知道，如下两种编译指令是近似等价的。但是_Pragma语法本身可以放在#define中复用。\n1 2 #pragma message(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\u0026#34;) _Pragma(\u0026#34;message(\\\u0026#34;\u0026gt;\u0026gt;\u0026gt; Compiling ...\\\u0026#34;)\u0026#34;) pragma 指令功能表 指令 功能 clang diagnostic push 保存当前的诊断设置（哪些警告开、哪些关）到内部栈 clang diagnostic ignored \u0026ldquo;-Wexcessive-regsave\u0026rdquo; 告知编译器对接下来遇到的 -Wexcessive-regsave 警告保持沉默 clang diagnostic pop 从栈中恢复诊断设置，这样后面的代码不受影响 -Wexcessive-regsave: 这是一个在某些架构上（尤其是 ARM64）编译器生成代码时可能触发的警告：函数太复杂，编译器需要把大量寄存器保存/恢复到栈上，影响性能。通常在非常大的内联函数或深层嵌套调用时出现。\nattribute __attribute__ 是 GCC（GNU Compiler Collection） 提供的一种语言扩展，用于在 C/C++ 代码中指定函数、变量、类型的特殊属性（attributes），从而告诉编译器对这些实体进行特定的优化、检查或行为控制。 它也被 Clang 等兼容 GCC 的编译器支持。其基本语法如下\n1 __attribute__((attribute-list)) 可放在声明末尾，或紧跟在标识符之后。 多个属性用逗号分隔，例如 __attribute__((aligned(8), packed)) 常见属性表 函数属性 属性 说明 noreturn 函数永不返回（如 exit、abort），帮助消除 unreachable-code 警告 format(archetype, string-index, first-to-check) 声明类 printf/scanf 函数的格式串检查，例如 format(printf, 1, 2) nonnull(arg-index, ...) 声明指针参数不可为 NULL const 纯函数 - 只依赖参数值，不读全局内存，无副作用（比 pure 更强） pure 函数无副作用，返回值仅依赖参数和全局内存（可被 CSE 优化，但调用次数不可消） malloc 返回新分配的内存指针，且该指针不会与任何已有指针别名 warn_unused_result 调用者忽略返回值时发出警告 used 即使未引用也保留函数代码（防止被链接器丢弃） unused 抑制\u0026quot;未使用函数\u0026quot;警告 weak 弱符号 - 可被同名强符号覆盖，用于提供默认实现 constructor 在 main() 之前自动执行（可带优先级参数） destructor 在 main() 返回后或 exit() 时自动执行 noinline 禁止内联此函数 always_inline 强制内联（即使优化级别不支持） flatten 将此函数内的每个调用都视为 always_inline hot 标记为热点路径，优化分支预测和指令布局 cold 标记为冷路径，优化代码体积而非速度 no_sanitize(\u0026quot;sanitizer-name\u0026quot;) 对此函数禁用指定 sanitizer（如 address、thread） interrupt / interrupt_handler 声明中断处理函数（自动保存/恢复寄存器，使用特殊返回指令） sentinel 参数列表必须以 NULL 结尾（如 execl） alloc_size(pos) / alloc_size(pos1, pos2) 标明第几个参数控制分配大小，辅助优化和安全检查 returns_nonnull 函数保证返回非 NULL 指针 leaf 函数不会回调当前编译单元内的任何函数 nothrow 函数保证不抛出 C++ 异常 target(\u0026quot;feature\u0026quot;) 为特定 CPU 特性编译此函数（如 target(\u0026quot;avx2\u0026quot;)） 类型属性 属性 说明 aligned(N) 指定类型或变量的对齐字节数（N 为 2 的幂） packed 紧凑布局 - 移除成员间的填充字节 deprecated / deprecated(\u0026quot;message\u0026quot;) 标记为已弃用，使用时产生警告 unavailable 标记为不可用，使用时产生错误 visibility(\u0026quot;type\u0026quot;) 符号可见性：default、hidden、internal、protected mode(TI/QI/HI/SI/DI) 指定整数类型的宽度（如 __attribute__((mode(DI)))） may_alias 声明此类型可能与其他类型别名，抑制严格别名优化 transparent_union 透明联合体 - 传参时各成员类型可隐含转换 designated_init 只能通过指定初始化器初始化（可用于 union） objc_root_class 标记 Objective-C 根类 变量属性 属性 说明 section(\u0026quot;name\u0026quot;) 将变量放入指定段（如 .init_array、.bss.my_section） used 即使未引用也保留变量 unused 抑制\u0026quot;未使用变量\u0026quot;警告 aligned(N) 指定对齐（同类型属性，直接作用于变量） packed 去除结构体成员对齐填充（作用于变量声明时） cleanup(function) 变量离开作用域时自动调用给定函数（类似 C++ RAII） weak 弱符号 - 可被同名强符号覆盖 tls_model(\u0026quot;model\u0026quot;) 指定 TLS 模型：global-dynamic、local-dynamic、initial-exec、local-exec noinit 变量不初始化为零（用于保持复位后的数据） 用法示例 1 2 3 4 5 6 7 8 9 typedef struct { acpi_sdt_header_t header; uint32_t PointerToOtherSDT; } __attribute__((packed)) rsdt_t; __attribute__((noinline)) void compiler_barrier(void) { __asm__ volatile(\u0026#34;\u0026#34; ::: \u0026#34;memory\u0026#34;); } volatile float __x __attribute__((unused)); aligned(N) 做了两件事：\n效果 说明 起始地址 变量/结构体地址满足 addr % N == 0 sizeof 补整 末尾补 padding 使 sizeof % N == 0 volatile 关键字 volatile 是 C 语言的一个类型修饰符，用于告诉编译器：该变量的值可能在程序控制流之外被意外修改，因此编译器不能对该变量的访问进行某些优化（如缓存到寄存器、消除冗余读取等）。\n__asm__和volatile可以一起使用，如下。\n1 __asm__ volatile(\u0026#34;inw %1, %0\u0026#34; : \u0026#34;=a\u0026#34;(ret) : \u0026#34;dN\u0026#34;(port)); 编译器必须：\n保留这段汇编，即使输出没用 每次都生成，不能合并 保持原位，不和其它 volatile asm 重排 ","date":"2026-06-26T08:04:20Z","permalink":"/post/c_extension_study1/","title":"c拓展语法学习"},{"content":"林景 我和她的渊源可能从爷爷辈开始。我们的父辈是发小，不过我们并没有“打娘胎里认识”。\n她的名字起得很好听，也很有水平——一个动词化的形容词，加一个名词，足以引发对她本人的美好想象。“林景”是与她名字首字母相同的词，我曾用它指代过她，所以它成了本文的标题。\n合并的体育课 我们在同一个小学上学，同一个年级，她在4班，我在5班。有次4班和5班的体育课合并在一起上，也就有了我和她的第一次正式接触。体育课自由活动期间，我已经记不清我到底做了什么，相比于以后发生的事情，应该并不算是严重，却仍然成功地将她惹哭了。\n放学后，我被她的家长找上门了。\n我并不知道她是什么时候住进我的内心的，如果一定要有个确切的时刻的话，我想应该就是她泪眼婆娑站在我面前的时候吧。就是这么简单，就是这么“流氓”。自此以后，我就开始了近8年的单相思。\n同船渡 或许是因为我们都是学校老师的孩子，初中我们俩被分到了同一个班级中。“百年修得同船渡”，能和她做同班同学曾是我梦寐以求的。\n距离$\\sqrt{5}$ 很长的一段时间，我和她的距离都是$\\sqrt{5}$——她坐在我左前方，隔了一列两行。明明只是$\\sqrt{5}$个座位的距离，但却好似远在天边。看黑板的时候，余光刚好可以覆盖到她。她的身影自此烙印在那里，像烧屏，移开视线依然在那。\n很长的一段时间，我和她放学回家时都可以同行一段路。她总是以背影的形式存在我的视线中。夕阳下我们前后走着，影子被拉得很长。\n这段路很长，长到每次和她同行时，时光好像在此静止；这段路也很短，短到这段路已经走完近7个年头了。\n回眸 她和一个不靠谱的男生分在同一个值日小组。有天我发完校讯通走出办公室，发现倒垃圾的活被他甩给了她。我当然见不得，直接从她手中接过垃圾桶，说了句“我来倒”。——这样看，我还是对她有过微不足道的付出。\n其实那天我也期待着能和她继续同行回家，毕竟尽管路线存在重合，但是如果离校时间不同，还是不能看见她的背影。\n如果我选择帮她倒了这个垃圾，我的离校时间必然会和她错开。我也就错过了当天和她的同行机会。\n当天放学后的这段路也因此变得不同，她碰到不熟的同学，寒暄了几句，然后停下来回头等我，我想这是她刻意如此的。她对我表示了感谢，但是其实我不喜欢别人对我说谢谢。\n这是我第一次，也是唯一一次和她并肩同行。可惜我当时并不善与女生交谈，我也与她并没有多少共同话题。我甚至不会创造下一次接触的机会。\n这次少有的同行机会就在简单的几句交流后以沉默告终。——我们从未如此靠近，但我们的心依旧相距很远。\n比邻 初中阶段，我们数学老师按考试成绩排了个考试模式的座位表，每次由他自己组织的测试都会要求我们换座位去考（有些其他科目的考试也会蹭一下这个座位表）。\n她的成绩很好，应该是被排在了第三个位置，我则次一点，被排在了第十个位置。我的座位在第二列，我恰好坐在她的右边。因此每次数学考试我都十分期待，这是我少有的和她物理上接近的机会。\n当然也少不了试图吸引她注意的“嘉豪”行为：飞快写完试卷，然后拿出物理作业开始写，或者干脆直接提前回家等等。如果能成功引起她的注意并发出惊叹，确实也会让我产生些许成就感。\n“家人” 非常幸运，我和她也是高中阶段的同学——无论是分班前后。\n我们的班主任常以“家人”比喻同学间的关系，以阻止早恋的发生。因此本节起作“家人”。\n我也知道高中阶段紧张，此外本身也成长了一些，藏起了对她的心意。\n相较于初中阶段，高中阶段的我和她显得就更加平淡，接触也更少。\n我看向她时，她几乎都是在埋头苦学，当然我自己也是如此，只是偶尔抬头，想起来就看她一眼。我依稀记得她的坐姿不是很健康，头埋在桌子上，我时常心疼于她眼底的青黑，但心疼之余更多的是祝福她的夙兴夜寐能获得回报，能取得成绩上的进步。\n分班前文理都考，她依旧名列前茅，但是分班后只考理科，她似乎有些力不从心了，我们的名次也就发生了颠倒。我开始稳定于年级20名，而她却会被排到第二考场。每当班主任给每个人都发一份排名表时，我都会认真分析她的各科成绩。我很想以某种方式，将我的所学传递给她，但我不仅缺乏那样做的时机，更缺乏那样做的身份。\n高考后，她报了青岛的学校，而我前往武汉上学。这份近8年的单相思自此无疾而终，结束的方式比想象中安静——没有告别，只是地图上的两个点不再重叠。某天翻看到她的 QQ 动态，她也认识了新的朋友，也似乎找了一个值得的异性并与之交往。\n其实我当时心中确也揪了一下，到底该遗憾什么呢？说不清楚。遗憾自己不够好？遗憾从未开口？但肯定不是遗憾失去她，因为我从未拥有过……八年的时间里，我始终只是一个旁观者。比起没有把握住，不如说根本没有把握住的能力。\n建联 “应似飞鸿踏雪泥”，我原以为我和她再不会有重逢的时机。大学四年的经历彻底地改变了我，我对她的感情也早已消散，偶尔有想起时，涌上心头的，多半还是愧疚与亏欠。\n焉知非福……？ 我原本并没有打算长期玩《洛克王国·世界》，只准备创个账号占个uid。也算是为了追求新认识的一个女生才认真去玩下去的吧，近期的痛苦也基本源自于这个女生，但这并不是本文的重点。\n2026年6月8日，我在翻看好友列表时看到了她的名字——一个我从未想过会出现在游戏中的人。她很少玩游戏，至少我记忆中如此。\n当天晚上，在做了番心理准备后，我还是在游戏内与她打了招呼。此时我无比庆幸当初花了点小钱，抓到了“古希腊掌管加好友的神”——三种形态的鸭吉吉，并生了很多蛋出来，给我创造了个主动的机会。我借由赠送鸭吉吉给她和她的朋友，成功和她搭上了话。\n非常简单的寒暄后，我们顺理成章地加上了游戏好友，此时我就有了足够的发挥空间：现在的我早已经可以得心应手地处理人际关系。\n阴差阳错，我为了别人才坚持玩的游戏，却让我与她重新建联。只是“塞翁失马”的过程足够痛苦——焉知非福，焉知非祸？\n致歉 我对不起她，无论从什么方面。\n她承受了我开智前的多数莫名其妙的行为，何止骚扰与冒犯。作为我第一个且喜欢最久的异性，我竟对她没有任何付出，甚至对她造成困扰。\n2026年6月10日，我传送到了她挂机的广场，很突兀地向她道了歉，和她当初进入我的心中一样突兀，和我曾经对她的冒犯一样突兀。道歉的内容并没有截图，我说了“真的很对不起”，说了“以前很多冒犯到你的地方”，说了“现在自己都觉得下头”。她回得很轻：“没有在意”、“过去很久了”、“没记在心里”。\n我并不知道她是否是真的不在意，但是冒犯到她的当时一定会给她带去很大的困扰。其实每当想起来我的所作所为，歉疚就会环绕在我的心头。非常幸运有这个机会能够直面她，明确地表示歉意。\n\u0026ldquo;re\u0026quot;connect 道完歉之后，我顺势提起了加微信好友的请求。但是因为洛克王国的屏蔽政策，似乎发了很多次都没有被她看见。游戏的屏蔽词过滤了所有我能想到的说法，我在对话框里反复尝试，她似乎一条都没看见。每一次我发送出去，我都在期待她角色头顶上的、代表输入中的省略号能变成令我惊喜的言语。\n但是她几次欲言又止，我以为她并不想和我建联，我的心也逐渐沉到谷底，我说“没事的，不加也没关系的”——我的目标已经达成了，我已经向她表达了歉意，但是我内心还是难免难过。\n她说没看见我发的消息，我假装不知道已经有了她的 QQ 好友，因为我不想直接说“去 QQ 聊”，这样给双方一种目的性很强的感觉。我说“我们俩是有企鹅好友的吧？”，她揶揄地答到“没有 QQ 好友你怎么在游戏里面看到我的”。我便顺势把微信二维码通过 QQ 发了过去。\n我当然是有私心的，除了加上常用的联系方式之外，我也希望能通过朋友圈了解她的近况，她的 QQ 动态并不活跃。\n我留下了一个保持联络的点：有机会一起玩洛克。但刚建联第一天并不需要很多交流，此时就需要个合适的理由结束对话。我谎称要去“拿奶茶”——这是我结束对话的常用理由，像关闭服务器时的“优雅退出”。\n续写从未开始的故事 过去的我呀，如果你知道现在我和她已经有简单的联络了，会有什么样的感受呢？我不知道现在我的社交能力是否足以让我和她建立起深厚的情谊，但是起码现在我已经有了一个着力点，我也向你承诺我会尽力弥补你的遗憾。\n现在的我已经失去了接触新人的动力，但是她不一样，她对你我都意义非凡，她是泪眼婆娑站在我的面前的人，是夕阳下走在我前面的背影，更是我最对不起的人。因此我愿意尝试、也愿意努力（即使是单方面的）将她写入我的日常中。\n但我也要向你道歉，你的单相思不会得到来自未来的回应——我不会以发展为目标与她相处。不是因为她没有过去美好了，恰恰相反，她的行为仍然符合你的想象，只是我和你的区别实在太大了。我能做的只是一带而过的提及你对她的喜欢，我说不出情真意切的告白。那些汹涌的感情，是你的，不是我的。\n","date":"2026-06-12T14:34:18Z","image":"/post/forest_scenery/head.png","permalink":"/post/forest_scenery/","title":"林景"},{"content":"　回首过去，珞珈山的樱花已几度枯荣，从武昌区到东西湖区，从桂园四舍到C栋5单元， 校区的更换将我的大学生涯划分为两段截然不同的体验，也因此大一大二的回忆也就尤为珍贵。\n念及往昔，晚课结束骑共享单车，从图书馆回到桂四路上耳畔的风似乎还在猎猎作响； 疫情期间每日中午核酸检测时，卓尔体育馆排起的长队也犹在昨日。 每当“最后”的节点如“最后一节公选课”、“最后一场期末考试”发生时，我都会与我的朋友们感慨光阴似箭、岁月如梭，不知不觉间“最后一天”的节点竟也悄然来临。\n落笔至此，我的大学时光确已步入尾声。 回望来路，我深知自己并非完美的旅人，沿途既有欢声笑语，亦不乏遗憾叹息。 此刻，歉疚与感恩两种情绪在心头反复交织，难以平复。\n首先感谢的是我的父母，是你们无条件的支持与包容，让我有勇气去探索未知的世界，成长为更好的自己。 奈何我资质愚钝，难以在学业上更进一步，未能满足你们的期许，念及此时我心中常怀愧疚，唯愿以未来之努力报答父母养育之恩。\n衷心感谢罗敏老师。从大一《程序设计》课程的初窥门径，到毕业设计阶段的悉心指导，这一路走来，离不开您的谆谆教诲。 您不仅启蒙了我对计算机科学的浓厚兴趣，更以严谨的治学态度和深厚的学术造诣，为我树立了为学与为人的榜样。\n感谢并肩同行的同窗与好友。是你们的陪伴与鼓励，让这段旅程充满欢笑与温暖。 每当我内心郁结、迷茫彷徨之时，是你们耐心的倾听与真诚的话语驱散我心头的阴霾，用坚定的支持助我重拾前行的勇气。\n在这里我还要感谢一位特别的人，尽管你我的联络不若其他好友般频繁，但于我而言你的存在却如亲生姐姐般重要。 每当我最为迷茫、最为挣扎、最为痛苦之时，是你充满善意的抚慰，指引我走出黑暗。\n正如王勃在《滕王阁序》中所叹：“天高地迥，觉宇宙之无穷；兴尽悲来，识盈虚之有数”。 本科毕业，恰是我们初识天地广阔、直面宇宙无垠的起点。此去经年，同窗各奔山海，聚散离合，亦如月相盈亏，皆有定数。 愿我们都能在各自的道路上坚定前行，不负韶华，不负相遇。\n“回首向来萧瑟处，归去，也无风雨也无晴”。 最后，感谢这段独一无二的大学时光，感谢四年来的每一段相遇与离别。它塑造了今天的我，也将照亮我未来的路。\n","date":"2026-05-28T12:52:01Z","permalink":"/post/undergraduate_thesis_acknowledgment/","title":"本科毕业论文致谢"},{"content":" All In: 意为\u0026quot;全押\u0026quot;——将手中所有筹码一次性推入牌局。这是孤注一掷的瞬间，没有退路，也不留余地。放在生活里，它代表一种信念：当你认定手中的牌值得，就把全部押上去，坦然接受命运的翻转。\nNice Fold: \u0026ldquo;Fold\u0026quot;意为\u0026quot;弃牌\u0026rdquo;，主动放弃当前这局。一句 \u0026ldquo;nice fold\u0026rdquo; 是对手的赞许——承认你做出了聪明的撤退。不是所有的坚持都叫勇敢，有时候及时放手，才是对自己最大的负责。\n何以为我？ 我是一个真诚的人。我已经学会对朋友等直球地、毫不吝惜地表达爱，我愿意对我认准的人付出全部的真心。\n我是一个恋旧的人。生活中的绝大多数人和物我都做不到“断舍离”。我会保留每一场电影的票据、景区公园的门票、一些具有纪念意义的杂物。\n我应该是一个果决的人。面对不需要的杂物和讨厌的人，绝大多数我都能做到“直接扔掉”和“一脚踢开”。沉没成本从来不会影响我的决策，无论我过去付出了多少。\n我也应该是一个犹豫不决的人。我很难做到“一脚踢开”我认准的人，我会挽留每一个我付出真心对待的人，但我挽留的目的也不完全是挽留，我希望为每一段关系留下一个明确的句号。\n我的真诚与真心从不廉价，我只会将它交付给我认为值得的人。\n我的感情经历非常曲折，每一次经历都没有得到善终。但每一次都从根本上重塑了我。我不想在这里说出我的故事，我只想介绍我是什么样的人，我期待着什么，什么把我塑造成了现在的样子。\n我的文笔并不出彩，无法描绘出我的幸福与美满、挣扎与痛苦，它像我现在的性格一样——死水一般平静，却弥漫着淡淡的痛苦。\n阳星 我不想和你做朋友。\n或许提前表明心意真的会有不一样的结果，我也愿意相信曾经有那么一瞬间，会分支出我们的美满结局🥹。\n当然也或许我感受到的全部都是我的臆想、我的自以为是。\n我和她是两三年的朋友。或许在外人看来我们风轻云淡，实际上我们也并没有“暗通款曲”。只是以朋友的身份相处，但可能也偶有暧昧和甜意。\n下图是她曾经艾特我的一条图集，我当时只是简单地点赞了她的评论，但内心却是五味杂陈。“我不想和你做朋友”，我如是想，但是那时的我并没有这样说的勇气。\n2023年\n“阳星”源于群内一次拿群友昵称组合出情侣组合名的事件。她姓杨，我的昵称叫“星分”，她在尝试使用我们的昵称拼出组合名。当我看见她发出“羊星”、“星羊”的消息时，天知道我心里有多开心。也怪当时的自己榆木脑袋——那本是一个推进关系的完美机会，我却视而不见。\n2023年8月《王者荣耀》瑶无双皮肤上架时，有过很大的节奏，当时大批玩家流向《曙光英雄》这款竞品游戏。我和她也不例外。当时也恰逢七夕将近，《曙光英雄》推出了针对情侣关系的皮肤活动。为了蹭到这个福利，我和她也就顺理成章地绑定了《曙光英雄》内的情侣关系。是的，这并不能说明什么，但也确实是我们曾亲密无间的一个回忆锚点。\n我已经记不清我们是因为什么首次断联的了。我只记得当时我们的最后一条消息正是本系列的封面图—— nobi 小兔的“不喜欢我你真的没品”的表情包，回应我的只有一个红色惊叹号。我当时也并未认识到她对我的意义，竟草率地顺势切断了和她的其他联系方式。我无法知道当时的她内心会经历什么样的波澜，如果会痛苦良久，那我难辞其咎。\n已经很久了，为了想起首次断联的原因我又去翻了和朋友的聊天记录，看见和她相关的竟还是会内心悸动，也难免看见后期锋利的话语，像刀刃一样重新扎入心里。\n雨季 2024年\n机缘巧合间，我刷到了你妈妈的抖音。在她的作品中看见你背影的一瞬间，我们间的回忆如同潮水一般将我淹没，紧随其后的就是延迟发作的断联之痛。\n我把你找回来时，恰逢你奶奶的离世，你说：“亲人的离世像一场漫长的雨季，大部分的时间没有感觉，只是会在一瞬间意识到她已经不在了”。\n我说：“其实朋友之间也像这样，会在某个瞬间发现分享的对象已经没有陪伴在身旁了”。所以我才把你找回来了，至少这次我要表明我的心意，无论结果如何，我不想再“意难平”。但……确实不是一个天时、地利、人和的时机。\n我朋友很多，我善于与人交往。但是我真的不想和你只是做朋友🥹，是的，我喜欢你，yj。\n你做出了你的选择；你给了我足够的尊重。我感受到了你的痛苦——我对你也很重要。\n“人生南北多歧路”。时至今日，我仍然觉得当时的我做得非常漂亮、非常完美：了结了过往的意难平，也为当下和将来的自己足够负责。一刀两断，斩断了一切能想到的复燃的途径，唯一留下的念想，竟是给截图里的那条图集点了一颗红心。期间的每一个决定我都不会后悔，唯一最后悔的就是没在最有机会的时候大胆表明心意吧。\n我也不愿意让彼此成为对方的包袱，所以决定彻底放手。虽然确实会互相伤害，但是希望这样决绝的结局也能让你满意。（起码在我的理解里，这总比莫名其妙的结束更完美一点。）\n我知你秉性。你向往自由，渴望独立。\n但如果遇到值得的人，我也希望你能改变过去一贯的信念，托付出自己脆弱柔软的一面。\n第21我 2025年\n2025年1月1日，我们再次断联，从此开始是全新的第21我：他再也不会遮掩真心，变得直接、真切与热烈。他学会了勇敢表明心意，也能做到果决斩断关系。他怕因为遮掩而错过最佳时机，也怕因为一时的怯懦就错过认准的人。这就是从与你的相处中学到的、第21我的全新特质——“All In And Nice Fold”。\n可悲、可笑又可叹的是，与她的首次分别竟然并未引发我内心的剧烈波动，这也算我对她的亏欠之一吧。\n除此之外，“第21我”也受到了严厉的惩罚：他将加倍深切地感受到失去的痛苦，此后的每一次分别都将如剜心剔骨般疼痛……\n我的爱直接而真诚。\n我从不后悔给予我全部的真心，只为寻找到同样真心待我的人。\n慢一点忘记 2026年6月2日更新\n每到感慨万千的时候就想“大赦天下”。正如我致谢中写道“最后，感谢这段独一无二的大学时光，感谢四年来的每一段相遇与离别。它塑造了今天的我，也将照亮我未来的路”。幸福的相遇与痛苦的离别都是我人生的重要组成部分。\n距离我们第二次断联也过去一年半了，临近毕业，难免想起故人。深夜里我想尽办法找到了你的抖音，犹豫良久还是发出去了这条评论。过往种种都会被时间冲淡，剩下来的竟更多是感谢与祝福。\n非常遗憾没能牵起你的手，我们也确非 Best Match，但毫无疑问，你的出现在我的生活中留下了浓墨重彩的一笔。\n如果你也有因我而改变的部分，那我荣幸之至；如果你幸福的话，我会比你先泪流满面。\n“感谢每一段相遇与离别”，尤其是你。\n2026年6月28日更新\n20岁的我曾说过一定会到达你的城市，无论是否与你相见。今天22岁的我来赴约了。\n不必相见，我也不会告诉你我的到来。我不会前往你在的区域，更不会尝试寻找你的足迹。\n“你那里是怎么样的天气，夜空是否还是那么透明……其实对天气不感兴趣，想知道你会不会太冷。”\n现在我终于与你同在一片天空下，与你共享相同的体验了。\n广州的夜晚真的很美，每当你抬头望向夜空时，又会想起什么呢？当年你走入社会，第一次看见夜晚广州塔身的霓虹时，内心中更多的是对未来的迷茫，还是对成长与独立的期盼呢？\n我去往的景点你是否也看过？我坐过的地铁你是否也乘坐过？落在你眼角的雨滴，是否又会是我曾为你流的眼泪？\n如果不回头的话，那就慢一点忘记对方吧。\n","date":"2026-05-28T11:43:44Z","image":"/post/all_in_and_nice_fold/head.png","permalink":"/post/all_in_and_nice_fold/","title":"All In 和 Nice Fold"},{"content":"星分翼轸 “星分翼轸，地接衡庐。”出自唐代王勃的《滕王阁序》。\n古人把地上主要的封国和天上28宿做了映射，滕王阁所在的南昌在江西省，也就是古代的楚地，楚地在中国南方，对应天上的星图的南方巳火区的翼宿、轸宿之间。这就是星分翼轸的意思。————知乎/董三碗\n取“星分”为昵称用意即为：认为自己也会是天上星宿在人间的分野。\n尽管“星分”并非完全收敛的一个名词性质的词语，其含义也与我所期望的含义有所区别，但这并不妨碍我取“星分”为名。“星分”于我，可以意为我是星辰在人间的投射，也可以指代星辰的光辉笼罩于我。\n这就是星分的由来。\n关于 XingfenD 我的大学期间遇到了一位特殊的人，尽管与我并无血缘关系，但我常与其以姐弟相称。有一次她打趣般地为我起了英文名：星分弟的空耳——“芬迪”。\n在我取 Github 用户名时，本想直接起作 Xingfen，但自觉看着有些单调，考虑到上面的“取英文名”，鬼使神差般地在 Xingfen 的末尾加了一个大写D。\n这就是 github.com/XingfenD 的由来。\n关于 Fendy 源于“芬迪”的空耳英文名，由于注册某些境外网站时需要英文名，以 xingfen 汉语拼音作为英文名，显示出来时不太连贯，因此以 fendy 作为英文名。\n关于 xf 星分的首字母缩写，本人绝大多数社交媒体昵称都叫 xf.。\n关于“挽星”与“晚星” 源于“晚星”。“特别的人”在直播给朋友看时曾集思广益征集粉丝牌名。我以两人昵称中包含的“晚”、“星”拼成“晚星”，奈何粉丝牌名似乎必须三个字，并未被采纳。于是我改“晚”为“挽”得到“挽星”，用于某些平台的昵称，承载着挽起另一人，并肩同行的愿望，遗憾的是似乎总是会落空。\n关于远楒 “远楒”是从我名字的缩写出发，寻找相同声母的字所组成的词，最初为“椴远楒”——空耳为“断远思”。原意为希望自己能斩断渺不可及的思念，现在“远楒”被单取出来使用，用于表达深入的思考。具体可以参考本站关于页面。\n何以为我？ 我希望有人能愿意了解我，也常落泪于真心难寻，郁结之时常自问“何以为我？”。\n“记得来时路，方知向何生”。将我自己记录于此，不求他人认真阅读，只愿未来的我能从这里寻找到最初的样子。\n","date":"2026-05-24T23:07:01Z","permalink":"/post/about-xingfen/","title":"关于星分"},{"content":"ZIP 文件结构与解析流程核心笔记 ZIP文件基础结构图示 具体的结构和解析流程在后面会有详细介绍。\nZIP文件主要由三部分构成:\nLocal File Header: 每个压缩文件的起始位置，包含文件的元数据信息； Central Directory(CD): 压缩文件的目录，记录了所有文件的元数据信息； End of Central Directory(EOCDR): 压缩文件的结束位置，包含目录的元数据信息。 只考虑非分卷压缩的情况，EOCDR中包含指向Central Directory的偏移量，Central Directory的大小，以及CD条目的项数。\n中央目录区包含了所有文件的元数据信息，每个文件的元数据信息由一个Central Directory Entry(CD Entry)表示。每个CD项中含有指向对应Local File Header的偏移量。\n本地文件头区包含了每个压缩文件的元数据信息，每个文件的元数据信息由一个Local File Header(LFH)表示。文件的实际数据(压缩或未压缩)紧跟在其对应的LFH后面。\nZIP文件解析流程 本地文件头区已经包含了每个压缩文件的元数据信息，理论上仅通过读取LFH就能解压出压缩包内的所有文件，因此在解析ZIP文件时就产生了两种解析方式。\n标准解析模式: 先读取整个Central Directory，然后根据CD项中的偏移量，随机访问到对应文件的LFH，从而获取文件的元数据信息。 流式解析模式: 从Local File Header开始，顺序解析每个压缩文件的元数据信息，直到无法读取有效的LFH签名为止。 目前大多数ZIP解析器默认采用标准解析模式，忽略掉没有被CD条目指向的LFH。\n标准解析模式 从EOCDR中获取指向Central Directory的偏移量，然后根据CD项中的偏移量，随机访问到对应文件的LFH，从而获取文件的元数据信息。\nsequenceDiagram participant Parser as 解析器 participant File as ZIP文件 participant LFH as 本地文件头 participant CD as 中央目录 participant EOCDR as 中央目录结束记录 Parser-\u003e\u003eFile: 从文件末尾查找EOCDR签名 File--\u003e\u003eParser: 返回EOCDR位置 Parser-\u003e\u003eEOCDR: 读取EOCDR内容 EOCDR--\u003e\u003eParser: 返回EOCDR数据 Parser-\u003e\u003eParser: 提取Central Directory偏移量 Parser-\u003e\u003eFile: 跳转到Central Directory位置 loop 读取所有CD项 Parser-\u003e\u003eCD: 读取一个CentralDirectoryHeader CD--\u003e\u003eParser: 返回CD项数据 Parser-\u003e\u003eParser: 保存CD项 end loop 根据CD项读取所有LFH Parser-\u003e\u003eCD: 从CD项获取Local File Header偏移量 CD--\u003e\u003eParser: 返回偏移量 Parser-\u003e\u003eFile: 跳转到对应的Local File Header位置 Parser-\u003e\u003eLFH: 读取LocalFileHeader LFH--\u003e\u003eParser: 返回LFH数据 Parser-\u003e\u003eParser: 保存LFH end流式解析模式 从Local File Header开始，顺序解析每个压缩文件的元数据信息，直到无法读取有效的LFH签名为止。\nsequenceDiagram participant Parser as 解析器 participant File as ZIP文件 participant LFH as 本地文件头 Parser-\u003e\u003eFile: 将文件指针定位到文件开头 loop 顺序读取Local File Header Parser-\u003e\u003eLFH: 尝试读取LocalFileHeader alt 成功读取LFH签名 LFH--\u003e\u003eParser: 返回LocalFileHeader数据 Parser-\u003e\u003eParser: 保存LFH Parser-\u003e\u003eFile: 继续读取下一个LFH else 未找到有效的LFH签名或读取失败 File--\u003e\u003eParser: 返回读取失败 Parser-\u003e\u003eParser: 停止解析 end end手工篡改ZIP进行实验 zip_analyze目录为一个Zip解析器实例，可以使用两种解析模式读取未加密的Zip文件结构。另外该目录下包含两个Zip文件，分别为zip_demo.zip和zip_demo_hacked.zip。两者的区别在于zip_demo_hacked.zip在zip_demo.zip的基础上，在LFH区添加了一个额外的文件hacked.txt对应的LFH及其数据。具体如下图所示：\n使用zip_analyze解析zip_demo_hacked.zip文件，在标准解析模式下，zip_analyze忽略掉了仅在LFH中存在的hacked.txt文件；而在流式解析模式下，zip_analyze能够成功解析出hacked.txt文件的信息。\nZip文件具体分析 根据官方文档, Zip文件的结构如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [local file header 1] [encryption header 1] [file data 1] [data descriptor 1] . . . [local file header n] [encryption header n] [file data n] [data descriptor n] [archive decryption header] [archive extra data record] [central directory header 1] . . . [central directory header n] [zip64 end of central directory record] [zip64 end of central directory locator] [end of central directory record] 本地文件头 - Local File Header 1 2 3 4 5 6 7 8 9 10 11 12 13 uint32_t signature; uint16_t version_needed; uint16_t general_bit_flag; uint16_t compression_method; uint16_t last_mod_time; uint16_t last_mod_date; uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; uint16_t filename_length; uint16_t extra_field_length; std::string filename; std::unique_ptr\u0026lt;uint8_t[]\u0026gt; extra_field; 本地文件头的extra_field没有固定的内容，不同的压缩器可能会在extra_field中添加不同的信息。但是在extra_field中添加信息时通常要遵循统一的结构: 标签 - 长度 - 数据。\nTag（2 字节）：标识该扩展块的类型（如操作系统相关信息、压缩算法扩展等）。 Length（2 字节）：表示后续数据的长度。 Data：具体的扩展数据，格式由 Tag 定义。 具体标签值和含义的映射可以参考官方文档的4.5.2节，其中列举了由PKWARE定义的标签值和含义。\ngeneral_bit_flag 加密头 - Encryption Header 是否存在: 仅当general_bit_flag的第0位为1时，才存在加密头。\n加密头的具体长度与结构由general_bit_flag的第6位确定：\n当第六位为0时，加密头格式为传统PKWARE加密格式，长度为12字节； 当第六位为1时，加密头格式强加密格式，长度为可变，至少30字节。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 IVSize 2 bytes - 初始化向量大小 IVData IVSize - 初始化向量数据 Size 4 bytes - 剩余解密头数据大小 Format 2 bytes - 格式定义 (当前必须为3) AlgID 2 bytes - 加密算法标识符 Bitlen 2 bytes - 密钥长度 (32-448 bits) Flags 2 bytes - 处理标志 ErdSize 2 bytes - 加密随机数据大小 ErdData ErdSize - 加密的随机数据 Reserved1 4 bytes - 证书处理保留字段 Reserved2 (var) - 证书处理保留字段 VSize 2 bytes - 密码验证数据大小 VData VSize-4 - 密码验证数据 (加密) VCRC32 4 bytes - 密码验证数据的CRC32 (加密) 校验密码是否正确即是通过加密头内的部分字段进行的。\n数据描述符 - Data Descriptor 用于在流式压缩场景下，将压缩数据的CRC-32、压缩大小和未压缩大小等信息从文件数据中分离出来。\n由于lfh在文件数据之前，因此在流式压缩场景下，无法提前确定lfh中的crc32, compressed_size和uncompressed_size字段。此时，需要在文件数据之后添加一个数据描述符，用于存储这些信息。\n1 2 3 4 uint32_t signature; /* 0x08074b50 */ uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; 归档解密头 - Archive Decryption Header 结构与加密头相同，不同的是加密头是在每个文件数据之前，而归档解密头是在所有文件数据之后。归档解密头的位置由Zip64 End of Central Directory Record中的Start of Central Directory字段指定。\n使用归档解密头可以支持加密整个中央目录结构，保护所有文件的元数据。\n只在Zip64格式下才会存在归档解密头。\n归档解密头的结构与Encryption Header完全相同。\n归档额外数据记录 - Archive Extra Data Record Archive Extra Data Record 主要用于存储与中央目录加密相关的额外信息，特别是：\n数字证书信息：存储 PKCS#7 证书存储、X.509 证书 ID 和签名等 加密相关数据：存储加密接收者证书列表等 其结构定义如下：\n1 2 3 uint32_t signature; /* 0x08074b50 */ uint32_t extra_field_length; std::unique_ptr\u0026lt;uint8_t[]\u0026gt; extra_field; 中央目录头 - Central Directory Header 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 uint32_t signature; uint16_t version_made_by; uint16_t version_needed; uint16_t general_bit_flag; uint16_t compression_method; uint16_t last_mod_time; uint16_t last_mod_date; uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; uint16_t filename_length; uint16_t extra_field_length; uint16_t file_comment_length; uint16_t disk_number_start; uint16_t internal_attr; uint32_t external_attr; uint32_t local_header_offset; std::string filename; std::unique_ptr\u0026lt;uint8_t[]\u0026gt; extra_field; std::string file_comment; 中央目录头与本地文件头之间存在许多冗余字段，原则上相对应的一对中央目录头和本地文件头，表示含义相同的字段值应该是相同的。\n相关流程 Zip64解析流程 sequenceDiagram participant Parser AS 解析器 participant EOCDR AS End of Central Directory participant ZIP64Locator AS ZIP64 Locator participant ZIP64EOCDR AS ZIP64 End of Central Directory participant 中央目录 AS Central Directory participant CDH AS Central Directory Header participant LFH AS Local File Header participant 文件数据 AS File Data participant DD AS Data Descriptor Note over Parser: 初始化阶段 Parser-\u003e\u003eEOCDR: 1. 读取End of Central Directory Record EOCDR-\u003e\u003eParser: 返回EOCDR数据 Parser-\u003e\u003eParser: 2. 检查ZIP64格式标志 alt 检测到ZIP64标志 Parser-\u003e\u003eZIP64Locator: 3. 读取ZIP64 Locator ZIP64Locator-\u003e\u003eParser: 返回ZIP64 EOCDR位置 Parser-\u003e\u003eZIP64EOCDR: 4. 读取ZIP64 EOCDR ZIP64EOCDR-\u003e\u003eParser: 返回完整中央目录信息 end Parser-\u003e\u003e中央目录: 5. 定位中央目录 Parser-\u003e\u003eParser: 6. 初始化文件计数器 = 0 Note over Parser: 循环处理所有文件 loop 对于每个文件条目 Parser-\u003e\u003eParser: 7. 文件计数器 += 1 Parser-\u003e\u003eCDH: 8. 读取Central Directory Header CDH-\u003e\u003eParser: 返回文件元数据 Parser-\u003e\u003eParser: 9. 检查是否需要ZIP64扩展 alt 需要ZIP64扩展 Parser-\u003e\u003eCDH: 10. 提取ZIP64扩展信息 CDH-\u003e\u003eParser: 返回8字节大小和偏移量 end Note over Parser: 处理Local File Header Parser-\u003e\u003eLFH: 11. 根据CDH定位LFH LFH-\u003e\u003eParser: 返回Local File Header数据 Parser-\u003e\u003eParser: 12. 验证LFH与CDH一致性 Note over Parser: 处理文件数据 Parser-\u003e\u003e文件数据: 13. 读取压缩文件数据 文件数据-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 14. 解压/解密文件数据 alt 存在Data Descriptor Parser-\u003e\u003eDD: 15. 读取Data Descriptor DD-\u003e\u003eParser: 返回实际CRC和大小 Parser-\u003e\u003eParser: 16. 验证数据完整性 end Parser-\u003e\u003eParser: 17. 存储文件信息 Parser-\u003e\u003eParser: 18. 检查是否还有更多文件 end Note over Parser: 完成阶段 Parser-\u003e\u003eParser: 19. 生成完整文件列表 Parser-\u003e\u003eParser: 20. 验证所有文件完整性 Parser-\u003e\u003eParser: 21. 返回解析结果启用中央目录加密时的解析流程 sequenceDiagram participant Parser AS 解析器 participant User AS 用户 participant EOCD AS End of Central Directory participant ZIP64Locator AS ZIP64 Locator participant ZIP64EOCD AS ZIP64 End of Central Directory participant ArchiveDH AS Archive Decryption Header participant ArchiveExtra AS Archive Extra Data Record participant EncryptedCD AS Encrypted Central Directory participant CDH AS Central Directory Header participant LFH AS Local File Header participant FileData AS File Data Note over Parser: 阶段1: 定位和读取目录结构 Parser-\u003e\u003eEOCD: 1. 读取End of Central Directory EOCD-\u003e\u003eParser: 返回EOCD数据 Parser-\u003e\u003eParser: 2. 检测ZIP64格式标志 Parser-\u003e\u003eZIP64Locator: 3. 读取ZIP64 Locator ZIP64Locator-\u003e\u003eParser: 返回ZIP64 EOCD位置 Parser-\u003e\u003eZIP64EOCD: 4. 读取ZIP64 EOCD ZIP64EOCD-\u003e\u003eParser: 返回完整目录信息(含加密标志) Note over Parser: 阶段2: 检测中央目录加密 Parser-\u003e\u003eParser: 5. 检查general purpose bit flag 13 alt 中央目录已加密 Parser-\u003e\u003eUser: 6. 请求密码或证书 User-\u003e\u003eParser: 7. 提供密码/证书 Parser-\u003e\u003eArchiveDH: 8. 读取Archive Decryption Header ArchiveDH-\u003e\u003eParser: 返回加密的解密头 Parser-\u003e\u003eParser: 9. 使用密码/证书解密ArchiveDH ArchiveDH-\u003e\u003eParser: 返回解密后的解密头数据 Note over Parser: 阶段3: 解密中央目录 Parser-\u003e\u003eEncryptedCD: 10. 读取加密的中央目录 EncryptedCD-\u003e\u003eParser: 返回加密的中央目录数据 Parser-\u003e\u003eParser: 11. 使用ArchiveDH中的密钥解密中央目录 EncryptedCD-\u003e\u003eParser: 返回解密后的中央目录 Note over Parser: 阶段4: 处理Archive Extra Data Parser-\u003e\u003eArchiveExtra: 12. 读取Archive Extra Data Record ArchiveExtra-\u003e\u003eParser: 返回额外数据(证书等) else 中央目录未加密 Parser-\u003e\u003eEncryptedCD: 8. 直接读取中央目录 EncryptedCD-\u003e\u003eParser: 返回未加密的中央目录 end Note over Parser: 阶段5: 解析中央目录 Parser-\u003e\u003eCDH: 13. 解析中央目录条目 CDH-\u003e\u003eParser: 返回文件元数据 Parser-\u003e\u003eParser: 14. 检查是否需要ZIP64扩展 Note over Parser: 阶段6: 解压文件数据 loop 对于每个文件 Parser-\u003e\u003eLFH: 15. 根据CDH定位LFH LFH-\u003e\u003eParser: 返回Local File Header Parser-\u003e\u003eParser: 16. 验证LFH(注意掩码字段) Parser-\u003e\u003eFileData: 17. 读取文件数据 FileData-\u003e\u003eParser: 返回压缩/加密的文件数据 Parser-\u003e\u003eParser: 18. 解密/解压文件数据 Parser-\u003e\u003eParser: 19. 验证文件完整性 Parser-\u003e\u003eParser: 20. 输出解压后的文件 end Note over Parser: 阶段7: 完成解压 Parser-\u003e\u003eUser: 21. 显示解压完成信息校验密码是否正确 传统PKWARE加密格式校验密码 sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 读取Local File Header ZIP-\u003e\u003eParser: 返回LFH数据 (包含加密标志) Parser-\u003e\u003eZIP: 读取12字节加密头 ZIP-\u003e\u003eParser: 返回加密头数据 Parser-\u003e\u003eParser: 使用密码初始化加密密钥 Parser-\u003e\u003eParser: 解密12字节加密头 Parser-\u003e\u003eZIP: 读取文件CRC值 ZIP-\u003e\u003eParser: 返回文件CRC Parser-\u003e\u003eParser: 提取解密后加密头的最后1/2字节 Parser-\u003e\u003eParser: 比较是否等于文件CRC的高位字节 alt 密码验证成功 Parser-\u003e\u003eUser: 密码验证成功 Parser-\u003e\u003eZIP: 继续读取文件数据 ZIP-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 解密文件数据 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败 Parser-\u003e\u003eUser: 提示重新输入密码 end强加密 sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 读取Local File Header ZIP-\u003e\u003eParser: 返回LFH数据 (包含强加密标志) Parser-\u003e\u003eZIP: 读取强加密解密头 ZIP-\u003e\u003eParser: 返回解密头数据 (IVSize, IVData, AlgID, VSize等) Parser-\u003e\u003eParser: 解析解密头明文字段 Parser-\u003e\u003eParser: 使用密码和IVData生成主会话密钥 Parser-\u003e\u003eZIP: 读取ErdData字段 ZIP-\u003e\u003eParser: 返回ErdData Parser-\u003e\u003eParser: 解密ErdData获取随机数据 Parser-\u003e\u003eParser: 生成文件会话密钥 Parser-\u003e\u003eZIP: 读取VData和VCRC32字段 ZIP-\u003e\u003eParser: 返回VData和VCRC32 Parser-\u003e\u003eParser: 使用会话密钥解密VData和VCRC32 Parser-\u003e\u003eParser: 计算解密后VData的CRC32 Parser-\u003e\u003eParser: 比较计算的CRC32与解密的VCRC32 alt 密码验证成功 Parser-\u003e\u003eUser: 密码验证成功 Parser-\u003e\u003eZIP: 读取文件数据 ZIP-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 解密文件数据 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败 Parser-\u003e\u003eUser: 提示重新输入密码 end启用中央目录加密 sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 Parser-\u003e\u003eZIP: 读取EOCD和ZIP64记录 ZIP-\u003e\u003eParser: 返回记录数据 (包含中央目录加密标志) Parser-\u003e\u003eUser: 检测到中央目录加密，请提供密码 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 根据ZIP64记录定位Archive Decryption Header ZIP-\u003e\u003eParser: 返回Archive Decryption Header Parser-\u003e\u003eParser: 解析解密头明文字段 Parser-\u003e\u003eParser: 使用密码生成主会话密钥 Parser-\u003e\u003eZIP: 读取ErdData字段 ZIP-\u003e\u003eParser: 返回ErdData Parser-\u003e\u003eParser: 解密ErdData获取随机数据 Parser-\u003e\u003eParser: 生成中央目录解密密钥 Parser-\u003e\u003eZIP: 读取VData和VCRC32字段 ZIP-\u003e\u003eParser: 返回VData和VCRC32 Parser-\u003e\u003eParser: 解密VData和VCRC32 Parser-\u003e\u003eParser: 验证VData的CRC32 alt 密码验证成功 Parser-\u003e\u003eZIP: 读取加密的中央目录 ZIP-\u003e\u003eParser: 返回加密的中央目录数据 Parser-\u003e\u003eParser: 解密中央目录 Parser-\u003e\u003eParser: 解析中央目录获取文件列表 Parser-\u003e\u003eUser: 显示文件列表 User-\u003e\u003eParser: 选择要解压的文件 Parser-\u003e\u003eParser: 继续解压选中的文件 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败，无法解密中央目录 Parser-\u003e\u003eUser: 无法显示文件列表，请重新输入密码 end","date":"2026-04-24T02:10:00+08:00","permalink":"/post/zip-structure-notes/","title":"ZIP 文件结构与解析流程核心笔记"},{"content":"汇编语言预备知识 指令简介与总览 汇编指令 指令格式 功能 备注 lea lea reg, addr 主要用于将一个内存地址加载到寄存器中，\n而不是将该地址处的数据加载到寄存器。 如在printf函数中，将格式字符串的地址加载到指定寄存器中供printf函数读取。 inc inc reg 寄存器自增1 变量加一 mov mov reg, imm 将立即数赋值到寄存器中 imm意为立即数(immediate number) imul 指令格式见详解 用于执行带符号整数乘法。 它可以接受一个、两个或三个操作数，具体取决于你想要执行的乘法类型。 mul mul source 用于执行无符号整数乘法。 mul 指令只接受一个操作数，这个操作数可以是寄存器或内存地址。根据操作数的大小（8位、16位、32位或64位），乘积会被存储在特定的寄存器组合中。 shl/shr shl/shr dest, cnt 逻辑左/右移 shr用于将寄存器或内存中的二进制数值向右移动指定的位数。这个操作会将每个位都向右移动，并在左侧补0。对于无符号数来说，这相当于除以2的幂次；而对于有符号数，它仅当被移位的数据视为无符号时有效，因为逻辑右移不会复制符号位。 sar sar dest, count 算术左/右移 sar指令用于将指定的二进制数按算术右移的方式移动指定的位数。算术右移是指在右移时，最高位（符号位）保持不变，低位移出，高位补上符号位的值。这通常用于有符号数的除以2的幂次操作。 cdq cdq 将32位寄存器eax中的有符号数符号扩展为64位，结果存储在由edx:eax组成的64位寄存器中 它会把eax的第31位（符号位）复制到edx的每一位。如果eax的符号位是0（即正数），则edx会被设置为0；如果符号位是1（即负数），则edx会被设置为全1。\n通常在执行32位有符号除法（如idiv指令）之前使用，以确保被除数的符号位正确扩展到高位寄存器，从而保证除法运算的正确性。 cqo cqo 将64位寄存器rax中的有符号数符号扩展为128位，结果存储在由rdx:rax组成的128位寄存器中 它会把rax的第63位（符号位）复制到rdx的每一位。如果rax的符号位是0，则rdx被设置为0；如果符号位是1，则rdx被设置为全1。\n在64位模式下，通常在执行64位有符号除法之前使用，确保被除数的符号位正确扩展到高位寄存器。 imul - 带符号整数乘法 一个操作数 imul src ​\t这种形式假设隐含的目标操作数是累加器（对于16位操作为AX，对于32位操作为EAX，对于64位操作为RAX）。该指令将累加器与source相乘，并将结果存储回累加器和其对应的双倍宽度寄存器（即对于16位操作为DX:AX，对于32位操作为EDX:EAX，对于64位操作为RDX:RAX）。\n两个操作数 imul dest, source ​\t这种形式将source与dest相乘，并将结果存储在dest中。dest可以是一个寄存器或内存位置，而source可以是一个寄存器、内存位置或立即数。注意，结果被截断到目标操作数的大小。\n三个操作数 imul dest, source1, source2 ​\t这种形式将source1与source2相乘，并将结果存储在dest中。source1和source2可以是寄存器、内存位置或立即数，但dest必须是寄存器。结果同样被截断到目标操作数的大小。\nmul - 无符号整数乘法 mul的功能 ​\tMUL是一个单操作数指令，它隐式地使用AL（8位）、AX（16位）、EAX（32位）或RAX（64位）作为其中一个乘数，具体取决于操作数的大小和处理器模式。\n​\t对于字节乘法（8位），结果被存储在AX寄存器中；对于字乘法（16位），结果被存储在DX:AX寄存器对中；对于双字乘法（32位），结果被存储在EDX:EAX寄存器对中；对于四字乘法（64位），结果被存储在RDX:RAX寄存器对中。\nmul的示例 ; 8 mov al, 0x05 ; AL = 5 mov bl, 0x03 ; BL = 3 mul bl ; AX := AL * BL ; AL = 5 * 3 = 15 (0x0F), AH = 0 ; 16 mov ax, 0x0123 ; AX = 291 mov bx, 0x0004 ; BX = 4 mul bx ; DX:AX := AX * BX ; AX = 291 * 4 = 1164 (0x048C), DX = 0 ; 32 mov eax, 0x12345678 ; EAX = 305419896 mov ebx, 0x00000002 ; EBX = 2 mul ebx ; EDX:EAX := EAX * EBX ; EAX = 305419896 * 2 = 610839792 (0x2468ACF0), EDX = 0 scanf与printf函数 ​\tscanf会使用多个寄存器：rcx，rdx，r8等等，均存储输入项的地址，其中rcx为格式化字符串的地址。 ​\tprintf使用的寄存器与scanf类似。\n逆向分析程序1 ​\t使用IDA打开reverse_basic.exe，反汇编结果如下：\n; int __fastcall main(int argc, const char **argv, const char **envp) main proc near var_18= dword ptr -18h var_14= dword ptr -14h var_10= qword ptr -10h arg_0= qword ptr 8 arg_8= qword ptr 10h arg_10= qword ptr 18h ; __unwind { // __GSHandlerCheck mov [rsp+arg_0], rbx mov [rsp+arg_8], rbp mov [rsp+arg_10], rsi push rdi sub rsp, 30h mov rax, cs:__security_cookie xor rax, rsp mov [rsp+38h+var_10], rax lea r8, [rsp+38h+var_18] lea rdx, [rsp+38h+var_14] lea rcx, aDD ; \u0026#34;%d %d\u0026#34; call sub_140001080 mov ecx, [rsp+38h+var_18] mov eax, 38E38E39h add ecx, [rsp+38h+var_14] imul r8d, ecx, 7 lea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea ebx, ds:0[r8*8] lea edi, [rbx+rbx*8] mul edi mov esi, edx mov edx, r8d shr esi, 1 mov ebp, esi shr ebp, 3 call sub_140001020 mov edx, ebx lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 mov edx, edi lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 mov edx, esi lea rcx, aU ; \u0026#34;%u\\n\u0026#34; call sub_140001020 mov edx, ebp lea rcx, aU ; \u0026#34;%u\\n\u0026#34; call sub_140001020 mov eax, 24924925h lea rcx, aU ; \u0026#34;%u\\n\u0026#34; mul ebp sub ebp, edx shr ebp, 1 add edx, ebp shr edx, 2 call sub_140001020 xor eax, eax mov rcx, [rsp+38h+var_10] xor rcx, rsp ; StackCookie call __security_check_cookie mov rbx, [rsp+38h+arg_0] mov rbp, [rsp+38h+arg_8] mov rsi, [rsp+38h+arg_10] add rsp, 30h pop rdi retn ; } // starts at 1400010E0 main endp ​\t可以发现该程序的结构大致是，进行一些运算之后，集中打印运算结果。其中sub_140001020即为prinf函数，sub_140001080为scanf函数。\nlea r8, [rsp+38h+var_18] lea rdx, [rsp+38h+var_14] lea rcx, aDD ; \u0026#34;%d %d\u0026#34; call sub_140001080 ​\t因此此程序会读取两个四字节数据写入var_14和var_18。\nmov ecx, [rsp+38h+var_18] mov eax, 38E38E39h add ecx, [rsp+38h+var_14] imul r8d, ecx, 7 ​\t分析接下来的指令，程序将读取的两个数相加之后乘以7，将结果存到r8d中。\nlea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea ebx, ds:0[r8*8] lea edi, [rbx+rbx*8] mul edi mov esi, edx mov edx, r8d shr esi, 1 mov ebp, esi shr ebp, 3 call sub_140001020 ​\tlea ebx, ds:0[r8*8]通过地址计算的方式计算了r8 * 8的值存入ebx中，然后类似地计算rbx * 9存入edi中。\n​\t然后经过其他无关运算后，调用printf将r8d(乘以7的结果)打印出来（先将r8d赋给edx）。\nmov edx, ebx lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 mov edx, edi lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 ​\t这一段打印出ebx和edi。\nmov edx, esi lea rcx, aU ; \u0026#34;%u\\n\u0026#34; call sub_140001020 mov edx, ebp lea rcx, aU ; \u0026#34;%u\\n\u0026#34; call sub_140001020 ​\t根据这两个打印可以知道，在上面的运算中，esi和ebp被存入了运算结果，让我们再次分析上面被我们忽视的部分指令：\nmov eax, 38E38E39h ... lea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea ebx, ds:0[r8*8] lea edi, [rbx+rbx*8] mul edi mov esi, edx mov edx, r8d shr esi, 1 mov ebp, esi shr ebp, 3 call sub_140001020 ​\tmul edi的作用：会计算edi * eax，将结果存入edx: eax中，eax被赋值为38E38E39h，是编译器将除以9运算进行优化使用的魔数。因此esi为edi / 9。然后将esi存入ebp后右移3位。\nmov eax, 24924925h lea rcx, aU ; \u0026#34;%u\\n\u0026#34; mul ebp sub ebp, edx shr ebp, 1 add edx, ebp shr edx, 2 call sub_140001020 ​\t24924925h是除以7的魔数，因此这里计算ebp除以7后打印出来。\n综上，我们可以复原这个可执行文件的原c文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;stdio.h\u0026gt; int main(int argc, char *argv[]) { int a, b, c; scanf(\u0026#34;%d %d\u0026#34;, a, b); c = a + b; int n1 = c * 7; int n2 = n1 * 8; int n3 = n2 * 9; unsigned int n4 = n3 / 9; unsigned int n5 = n4 / 8; unsigned int n6 = n5 / 7; printf(\u0026#34;%d\\n\u0026#34;, n1); printf(\u0026#34;%d\\n\u0026#34;, n2); printf(\u0026#34;%d\\n\u0026#34;, n3); printf(\u0026#34;%u\\n\u0026#34;, n4); printf(\u0026#34;%u\\n\u0026#34;, n5); printf(\u0026#34;%u\\n\u0026#34;, n6); return 0; } 逆向分析程序2 ; int __fastcall main(int argc, const char **argv, const char **envp) main proc near var_18= dword ptr -18h var_14= dword ptr -14h var_10= qword ptr -10h arg_0= qword ptr 8 ; __unwind { // __GSHandlerCheck mov [rsp+arg_0], rbx push rdi sub rsp, 30h mov rax, cs:__security_cookie xor rax, rsp mov [rsp+38h+var_10], rax lea r8, [rsp+38h+var_14] lea rdx, [rsp+38h+var_18] lea rcx, aDD ; \u0026#34;%d %d\u0026#34; call sub_140001080 mov eax, [rsp+38h+var_14] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; mov edi, [rsp+38h+var_18] lea ebx, [rdi+rax] imul edi, eax lea edx, [rdi+rbx] call sub_140001020 mov eax, 66666667h imul [rsp+38h+var_18] sar edx, 1 mov ecx, edx shr ecx, 1Fh add edx, ecx lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 mov r8d, [rsp+38h+var_14] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea edx, [r8+r8*4] call sub_140001020 sub ebx, [rsp+38h+var_18] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; mov edx, ebx call sub_140001020 mov eax, edi lea rcx, Format ; \u0026#34;%d\\n\u0026#34; cdq idiv [rsp+38h+var_14] mov edx, eax call sub_140001020 xor eax, eax mov rcx, [rsp+38h+var_10] xor rcx, rsp ; StackCookie call __security_check_cookie mov rbx, [rsp+38h+arg_0] add rsp, 30h pop rdi retn ; } // starts at 1400010E0 main endp lea r8, [rsp+38h+var_14] lea rdx, [rsp+38h+var_18] lea rcx, aDD ; \u0026#34;%d %d\u0026#34; call sub_140001080 ​\t容易知道，这里使用scanf函数读入两个数据存入var_18， var_14中。\nmov eax, [rsp+38h+var_14] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; mov edi, [rsp+38h+var_18] lea ebx, [rdi+rax] imul edi, eax lea edx, [rdi+rbx] call sub_140001020 ​\t这段首先将var_14和var_18分别存入eax和edi中。计算ebx = rdi + rax，计算edi = edi * eax， 计算edx = rdi + rbx。即为计算edx = var_14 + var_18 + var_14 * var_18然后打印出来。\nmov eax, 66666667h imul [rsp+38h+var_18] sar edx, 1 mov ecx, edx shr ecx, 1Fh add edx, ecx lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 ​\t这里是使用魔数66666667h优化除法运算。实际上计算的是var_18 / 5。\nmov r8d, [rsp+38h+var_14] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea edx, [r8+r8*4] call sub_140001020 ​\t容易知道这里计算的是var_14 * 5，使用var_14 * 4 + var_14优化。\nsub ebx, [rsp+38h+var_18] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; mov edx, ebx call sub_140001020 mov eax, edi lea rcx, Format ; \u0026#34;%d\\n\u0026#34; cdq idiv [rsp+38h+var_14] mov edx, eax call sub_140001020 ​\tebx在之前存储着var_18 + var_14的结果，因此这里计算ebx - var_18后打印。\n​\tedi存储着var_18 * var_14的结果，这里计算var_18 * var14 / var_14后打印。\n源码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include \u0026lt;stdio.h\u0026gt; int main() { int var_18, var_14; scanf(\u0026#34;%d %d\u0026#34;, \u0026amp;var_18, var_14); int a = var_18 + var_14; int b = var_18 * var_14; printf(\u0026#34;%d\\n\u0026#34;, a + b); printf(\u0026#34;%d\\n\u0026#34;, var_18 / 5); printf(\u0026#34;%d\\n\u0026#34;, var_14 * 5); printf(\u0026#34;%d\\n\u0026#34;, a - var_18); printf(\u0026#34;%d\\n\u0026#34;, b / var_14); return 0; } ","date":"2025-03-15T06:27:52Z","permalink":"/post/reverse-program-course-week4/","title":"【武大软件逆向课程/第四周】加减乘除运算的逆向特征"},{"content":"作业目标 逆向分析扫雷程序winmine.exe，并编写程序对其进行批量插旗\n更换机器指令表.exe内的显示字体\n扫雷 定位绘制雷区的代码位置 使用Ollydbg打开winmin.exe。\n在当前模块中右键-\u0026gt;查找-\u0026gt;当前模块中的名称（标签），在弹出的窗口中寻找名称为Bitblt的函数。这里可以直接右键-\u0026gt;在每个参考上设置断点，但是我的Ollydbg会报错。因此采用后续的方法。\n选中Bitblt项后，按回车键Enter，在弹出的窗口中再次按回车Enter。\n执行上述操作后弹出的窗口即为Bitblt的源码，在源码首行按F2或者右键设置断点。\n设置断点后按F9或功能按钮运行程序，程序会暂停在断点处。此时按Ctrl + F9或者功能按钮执行到返回，程序会运行到函数Bitblt调用处的下一行。\n观察上下文程序可以发现是类似于一个循环的结构，其中esi寄存器为循环变量。\n定位雷区数据地址 在这一节内需要观察不同操作下内存区域的变化。\n在Ollydbg中可以通过单击任意数据来刷新数据窗口中的内容。\n如果想要重新绘制扫雷窗口，需要最小化后恢复窗口。\n观察内存读取部分，代码中只有两句可能是对于数组的读取。分别为：\n1 2 mov al, byte ptr [ebx+esi] push dword ptr [eax*4+1005A20] 这里可以记录一下Bitblt调用处的地址1002706，然后取消所有断点重新运行程序，将游戏切换到高级模式，随机点击一个地块。这里为了便于定位雷区数据位置，我们多次开局点击地块，直到仅连锁翻开一个地块（这样对雷区数据的影响较小，数据特征更明显）。\n然后将在数据窗口中右键-\u0026gt;转到-\u0026gt;表达式或者按Ctrl + G，输入内存读取部分的常量地址1005A20跳转到该地址。观察数据窗口内该地址处的数据。\n发现并不符合雷区数据的特征（至少应该出现大量相同的两种数据）。实际上作为绘制雷区的函数，对数组下标的访问应该具有循环变量，也就是esi寄存器应该参与内存地址的计算，而这条指令并不符合。\n因此选择下面的指令来寻找雷区数据所在的内存地址。\n1 mov al, byte ptr [ebx+esi] 在这条指令上打断点后最小化扫雷窗口，再恢复窗口，查看ebx寄存器的值，发现是内存地址1005360，在数据窗口中跟踪该地址。\n该地址处的内存数据如下。可以大致推测，未翻开的雷区和未翻开的空白对应数据为0x0F或0x8F。\n再次重开游戏，翻开第一个地块以定位数据区域首个元素的位置，如下图所示。\n对比可知1005361为首的地址存放雷区数据。\n分析地块状态对应的内存值 非雷区地块 从上一节的最后一个图可以得知，翻开的2地块对应的内存值为0x42。推测翻开的n对应的内存值为0x4n。\n翻开相邻地块进行验证，可知推测成立，同时也得知未翻开的非雷区对应的内存值为0x0F。\n雷区地块 再次观察内存区域，在9个地块后，有一个地块的内存值为不同的0x8F，翻开该地块后再次观察内存区域变化。\n可知未翻开的雷为0x8F，翻开的雷为0x8A，踩中的雷为0xCC。\n插旗地块与存疑地块 重开游戏并刷新数据窗口。在任意0x8E和0x0F对应的地块上右键以插旗后，刷新数据窗口，观察对应数据变化。扫雷游戏中还有一种标记，在插旗的地块上再次右键可以变为问号地块，这里称为存疑地块，与本次任务并不相关，但是这里还是给出其对应的内存值。\n观察上图可知，插旗地块的0x?F被替换为0x?E，存疑地块的0x?F被替换为0x?D。\n至此，所有地块状态对应的内存值均被我们分析出来。\n简单总结一下，地块状态对应的内存值如下：\n地块状态 内存值 地块状态 内存值 未翻开的空地 0x0F 插旗的空地 0x0E 翻开的空地 0x4n(n为要显示的数字) 存疑的空地 0x0D 未翻开的地雷 0x8F 插旗的地雷 0x8E 翻开的地雷 0x8A 存疑的地雷 0x8D 踩中的地雷 0xCC 此外，按Ctrl + ↓调整数据窗口首列数据从0x01偏移开始展示，可以发现在高级模式下30 * 16的布局下，每30个地块的数据后都有两个0x10，用于标注此行结束，在后面编写程序时，应当注意处理这个间隔区域。\n编写程序以批量插旗地雷 程序如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 #include \u0026lt;windows.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;tchar.h\u0026gt; #include \u0026lt;tlhelp32.h\u0026gt; #define GET_OFFSET(ROW, COL) ((ROW) * 32 + (COL)) void print_sign(BYTE val) { if (val == 0x0F || val == 0x8F) { printf(\u0026#34;--\u0026#34;); } else if (val == 0x0E || val == 0x8E) { printf(\u0026#34;||\u0026#34;); } else if (val == 0x0D || val == 0x8D) { printf(\u0026#34;??\u0026#34;); } else if (val == 0x40) { printf(\u0026#34;..\u0026#34;); } else if (val == 0x8A) { printf(\u0026#34;**\u0026#34;); } else if (val == 0xCC) { printf(\u0026#34;XX\u0026#34;); } else if (val == 0x10) { } else { printf(\u0026#34;%01X\u0026#34;, val \u0026amp; 0x0F); printf(\u0026#34;%01X\u0026#34;, val \u0026amp; 0x0F); } printf(\u0026#34; \u0026#34;); } int main() { DWORD pid = 0; // Target process ID HANDLE hProcess = NULL; LPVOID targetAddress = (LPVOID)0x1005361; // Target memory address BYTE readBuffer[512] = {0}; // BYTE writeData[4] = {0xAA, 0xBB, 0xCC, 0xDD}; // Example data to write SIZE_T bytesRead = 0; // SIZE_T bytesWritten = 0; // Create a snapshot of all processes HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot == INVALID_HANDLE_VALUE) { printf(\u0026#34;Failed to create process snapshot, error code: %d\\n\u0026#34;, GetLastError()); return 1; } PROCESSENTRY32 pe = {sizeof(PROCESSENTRY32)}; if (Process32First(hSnapshot, \u0026amp;pe)) { do { if (_tcsicmp(pe.szExeFile, _T(\u0026#34;winmine.exe\u0026#34;)) == 0) { pid = pe.th32ProcessID; printf(\u0026#34;Process found: winmine.exe, PID: %u\\n\u0026#34;, pid); break; } } while (Process32Next(hSnapshot, \u0026amp;pe)); } else { printf(\u0026#34;Failed to retrieve process information, error code: %d\\n\u0026#34;, GetLastError()); } CloseHandle(hSnapshot); if (pid == 0) { printf(\u0026#34;Process winmine.exe not found\\n\u0026#34;); return 1; } // Open the target process with required permissions for memory operations hProcess = OpenProcess( PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid ); if (hProcess == NULL) { printf(\u0026#34;Failed to open process, error code: %d\\n\u0026#34;, GetLastError()); return 1; } // Read memory content if (!ReadProcessMemory( hProcess, targetAddress, readBuffer, sizeof(readBuffer), \u0026amp;bytesRead )) { printf(\u0026#34;Failed to read memory, error code: %d\\n\u0026#34;, GetLastError()); CloseHandle(hProcess); return 1; } printf(\u0026#34;Read %d bytes of data:\\n\u0026#34;, bytesRead); for (int i = 0; i \u0026lt; 16; i++) { for (int j = 0; j \u0026lt; 32; j++) { print_sign(readBuffer[GET_OFFSET(i, j)]); } printf(\u0026#34;\\n\u0026#34;); } printf(\u0026#34;Start flaging all the mines..\\n\u0026#34;); // Write to memory int count = 0; for (int i = 0; i \u0026lt; 16; i++) { for (int j = 0; j \u0026lt; 32; j++) { BYTE flag_mine = 0x8E; if (readBuffer[GET_OFFSET(i, j)] == 0x8F) { WriteProcessMemory( hProcess, targetAddress + GET_OFFSET(i, j), \u0026amp;flag_mine, sizeof(flag_mine), NULL ); count++; } } } printf(\u0026#34;Finish! the count of mines: %d\\n\u0026#34;, count); CloseHandle(hProcess); // system(\u0026#34;pause\u0026#34;); return 0; } 这段程序将会在打印雷区当前状态后，将所有0x8F修改为0x8E，即将所有的未翻开的地雷插上旗子，用户应当在新一局游戏内翻开任意地块后运行此程序，然后最小化再恢复窗口以重新绘制雷区。\n打印当前状态过程中，未翻开的所有地块将记为--，插旗地块记为||，翻开的空地记为..，翻开的数字地块记为该数字，翻开的地雷记为**，踩中的地雷记为XX。\n实验结果 运行结果如下图所示：\n刷新窗口后成功插旗。\n降低雷数为20后重新运行，便于验证结果正确性，手动翻开所有未插旗地块，中间笑脸图标带上墨镜（表示游戏胜利），可见结果正确。\n更换机器指令表.exe显示字体 定位控制字体显示的WinAPI 使用Ollydbg打开机器指令表.exe。\n在API文档及网络资料中查询其内涉及的API。\nAPI名称 功能 GetModuleHandle 检索指定模块的模块句柄。模块必须已由调用进程加载。 GetCommandLine 检索当前进程的命令行字符串。 ExitProcess 结束调用进程及其所有线程。 GetSystemMetrics 用于获取关于显示器、鼠标、键盘等系统参数的信息。 观察发现在程序有效的程序段中涉及的三个WinAPI均与字体显示无关。还剩下一个call 004031，推测其很可能控制字体的显示。其上的四个push可能包含参数，尝试修改为push 0D 观察程序变化。修改两个push后运行程序并无明显变化。可见这两个参数并不控制该程序的字体显示。\n在call 00401031处设断点并单步步入，依次检索遇见的到的WinAPI功能。\nAPI名称 功能 CreateWindowEx 用于创建一个窗口（包括各种控件如按钮、文本框等）。 SetWindowLong 更改指定窗口的属性。该函数还将指定偏移量处的32位(长)值设置为额外的窗口内存。 GetStockObject 用于获取预定义的图形对象（如画笔、画刷和字体）的句柄。 其中GetStockObject涉及字体的显示，在4010C6处调用，其上一条指令push 0B很可能控制字体的显示。\n将push 0B修改为push 0E后继续运行程序，可见字体明显发生变化。\n将修改保存到文件 选中被修改的指令，在指令窗口中右键-\u0026gt;复制到可执行文件-\u0026gt;选择，在弹出的新窗口中选中被修改的指令，右键-\u0026gt;保存文件。将新可执行文件命名为机器指令表2.exe。\n双击运行修改前后的机器指令表，对比两者的字体差异。\n","date":"2025-02-19T17:43:52+08:00","permalink":"/post/reverse-program-course-week1/","title":"【武大软件逆向课程/第一周】扫雷逆向分析批量插旗及机器指令表.exe文件字体更换"},{"content":"计划 添加widget，展示当前博文的tags 右侧边栏的tag-cloud和categories不能处理hidden博文 tag-cloud识别hidden博文 把layouts/partials/widget/tag-cloud.html的渲染tag-cloud的逻辑进行如下修改：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;div class=\u0026#34;tagCloud-tags\u0026#34;\u0026gt; - {{ range first $limit $context.Site.Taxonomies.tags.ByCount }} - \u0026lt;a href=\u0026#34;{{ .Page.RelPermalink }}\u0026#34; class=\u0026#34;font_size_{{ .Count }}\u0026#34;\u0026gt; - {{ .Page.Title }} - \u0026lt;/a\u0026gt; + {{- $cateTaxonomy := $context.Site.GetPage \u0026#34;tags\u0026#34; -}} + {{- $cateTerms := $cateTaxonomy.Pages -}} + {{ range first $limit $cateTerms }} + {{- $pages := .Pages -}} + {{- $filteredPages := where $pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} + {{ if $filteredPages }} + \u0026lt;a href=\u0026#34;{{ .RelPermalink }}\u0026#34; class=\u0026#34;font_size_{{ len $filteredPages}}\u0026#34;\u0026gt; + {{ .Title }} + \u0026lt;/a\u0026gt; + {{ end }} + {{ end }} \u0026lt;/div\u0026gt; 如另一篇博文中的方法，从$context.Site中获取所有tags然后过滤掉hidden == true的博文后，判断$filteredPages中是否还有元素，如果有则渲染这个tag。\n添加一种展示当前博文tags的Widget 添加模板文件 在layouts/partials/widget文件夹下新建文件cur-tags.html，添加如下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {{- $context := .Context -}} {{ if .Context.Page.Params.Tags }} \u0026lt;section class=\u0026#34;widget curTags\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;widget-icon\u0026#34;\u0026gt; {{ partial \u0026#34;helper/icon\u0026#34; \u0026#34;tag\u0026#34; }} \u0026lt;/div\u0026gt; \u0026lt;h2 class=\u0026#34;widget-title section-title\u0026#34;\u0026gt;{{ T \u0026#34;widget.curTags.title\u0026#34; }}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;curTags-tags\u0026#34;\u0026gt; {{ range (.Context.Page.GetTerms \u0026#34;tags\u0026#34;) }} \u0026lt;a href=\u0026#34;{{ .RelPermalink }}\u0026#34;\u0026gt; {{ .LinkTitle }} \u0026lt;/a\u0026gt; {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/section\u0026gt; {{ end }} 首先判断当前文章是否有tag，然后根据其他widget的模板修改成current tags，使用GetTerms方法获得当前文章的tags，作为\u0026lt;a\u0026gt;标签的参数。\n注意\u0026lt;h2\u0026gt;标签中的T函数，用于在修改页面语言时批量替换文本，我们需要在对应文件中添加widget.curTags.title命名空间的值。\n添加i18n替换文本 在i18n/en.yaml和i18n/zh-cn.yaml中添加相关内容，在widget域下添加内容：\n1 2 3 4 widget: + curTags: + title: + other: Tags of current blog 添加scss样式表 在assets/scss/partials/widgets.scss中添加curTags的样式。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* Current tags widget */ .curTags { .curTags-tags { display: flex; flex-wrap: wrap; gap: 10px; a { background: var(--card-background); box-shadow: var(--shadow-l1); border-radius: var(--tag-border-radius); padding: 8px 20px; color: var(--card-text-color-main); font-size: 1.4rem; transition: box-shadow 0.3s ease; \u0026amp;:hover { box-shadow: var(--shadow-l2); } } } } 自此，current tags控件的开发已经成功，在stack/config.yaml中添加配置项即可。\n1 2 3 4 params: widgets: page: + - type: cur-tags ","date":"2025-02-12T16:33:24+08:00","image":"/post/blogbuild2-widgets/blogbuild2.png","permalink":"/post/blogbuild2-widgets/","title":"Widgets修复与开发"},{"content":"Archive页介绍 archive页面展示了当前博文的所有类型，在此之下依据年份时序展示所有博文。\n改造计划:\n添加对以tag分类的支持\n支持在archive页展示tag模块\n修改tag\u0026ndash;tile的大小\n修改tag\u0026ndash;tile的排列方式\n为tag分类的每一个tile添加不同颜色\n修复在archive页面的category tile展示异常\n修复category详细页的文章数量异常\n添加archive页面对应配置项\n使用开发者工具检查archive页面元素 发现category分类的元素为：\n1 2 3 4 5 6 7 \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;类别 - Categories\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;article-list--tile\u0026#34;\u0026gt; \u0026lt;article\u0026gt; \u0026lt;/article\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 显然\u0026lt;div class=\u0026quot;article-list--tile\u0026quot;\u0026gt;的class属性命名并不合理，因为在archive首页展示的category列表的每一个tile并不是一篇article，因此应当修改为\u0026lt;div class=\u0026quot;category-list--tile\u0026quot;\u0026gt;。\n在文件themes/hugo-theme-stack/layouts/_default/archives.html中将\u0026lt;div class=\u0026quot;article-list--tile\u0026quot;\u0026gt;修改为\u0026lt;div class=\u0026quot;category-list--tile\u0026quot;\u0026gt;。\n同时将下列文件中的选择器article-list--tile修改为category-list--tile。在scss文件中建议保留原有的选择器，新增修改名称后的选择器。\nthemes/hugo-theme-stack/assets/scss/partials/article.scss themes/hugo-theme-stack/assets/scss/partials/layout/list.scss /home/fendy/Project/BlogSrc/themes/hugo-theme-stack/assets/ts/main.ts 修复category tile展示异常 本质上是没有添加对hidden文章的判断逻辑，在themes/hugo-theme-stack/layouts/_default/archives.html中添加筛选逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;类别 - {{ $cateTaxonomy.Title }}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;category-list--tile\u0026#34;\u0026gt; {{ range $cateTerms }} {{- $pages := .Pages -}} {{- $filteredPages := where $pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} {{ if $filteredPages }} {{ partial \u0026#34;article-list/tile\u0026#34; (dict \u0026#34;context\u0026#34; . \u0026#34;size\u0026#34; \u0026#34;250x150\u0026#34; \u0026#34;Type\u0026#34; \u0026#34;taxonomy\u0026#34;) }} {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 添加对tag分类的支持 支持在archive页展示tag模块 在themes/hugo-theme-stack/layouts/_default/archives.html中，类别模块的下方添加如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {{- $tagTaxonomy := $.Site.GetPage \u0026#34;taxonomyTerm\u0026#34; \u0026#34;tags\u0026#34; -}} {{- $tagTerms := $tagTaxonomy.Pages -}} {{ if $tagTerms }} \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;标签 - {{ $tagTaxonomy.Title }}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;tag-list--tile\u0026#34;\u0026gt; {{ range $tagTerms }} {{/* 获取当前标签下的所有页面 */}} {{- $pages := .Pages -}} {{/* 过滤掉 hidden 为 true 的页面 */}} {{- $filteredPages := where $pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} {{ if $filteredPages }} {{ partial \u0026#34;article-list/tile\u0026#34; (dict \u0026#34;context\u0026#34; . \u0026#34;Type\u0026#34; \u0026#34;tag\u0026#34; \u0026#34;pages\u0026#34; $filteredPages) }} {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {{ end }} 添加tag的样式并修改tag\u0026ndash;tile的大小 在themes/hugo-theme-stack/assets/scss/partials/article.scss中添加:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 .tag-list--tile { article { border-radius: var(--card-border-radius); overflow: hidden; position: relative; height: 350px; width: 250px; box-shadow: var(--shadow-l1); transition: box-shadow 0.3s ease; background-color: var(--card-background); \u0026amp;:hover { box-shadow: var(--shadow-l2); } .article-details { border-radius: var(--card-border-radius); position: relative; height: 100%; width: 100%; display: flex; flex-direction: column; justify-content: flex-end; z-index: 2; padding: 15px; @include respond(sm) { padding: 20px; } } .article-title { font-size: 2rem; font-weight: 500; color: var(--card-text-color-main); @include respond(sm) { font-size: 2.2rem; } } } } 在themes/hugo-theme-stack/assets/scss/partials/layout/list.scss中添加:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .tag-list--tile { display: flex; article { width: 120px; height: 30px; margin-right: 20px; flex-shrink: 0; .article-title { margin: 0; font-size: 1.8rem; } .article-details { padding-bottom: 5px; text-align: center; } } } 添加archives页面对应配置项 设计配置项 由于目前archives页面有三个板块：Categories，Tags，Dates。考虑添加如下配置项（标注出的为默认配置，不进行配置也会以下面代码块给定的配置渲染）：\n1 2 3 4 5 params: archives: showCategories: true showTags: false showDates: true 修改对应源码读取配置项 修改layouts/_default/archives.html文件，将{{ if $term }}判断语句添加一个与项，同时修改变量名。最终修改后结果如下：\n1 {{ if and (eq (.Site.Params.archives.showCategories | default true) true) (gt (len $cateTerms) 0) }} 同理，在渲染tags和dates板块的部分使用如下语句对包裹：\n1 2 3 4 5 6 7 8 {{ if and (eq (.Site.Params.archives.showTags | default false) true) (gt (len $tagTerms) 0) }} {{ /* 渲染Tags板块 */ }} {{ end }} {{ if eq (.Site.Params.archives.showDates | default true) true }} {{ /* 渲染Dates板块*/ }} {{ end }} 修复category详细页的文章数量异常问题 在layouts/_default/list.html文件中进行如下修改：\n1 2 3 - \u0026lt;h3 class=\u0026#34;section-count\u0026#34;\u0026gt;{{ T \u0026#34;list.page\u0026#34; (len .Pages) }}\u0026lt;/h3\u0026gt; + {{- $filteredPages := where .Pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} + \u0026lt;h3 class=\u0026#34;section-count\u0026#34;\u0026gt;{{ T \u0026#34;list.page\u0026#34; (len $filteredPages) }}\u0026lt;/h3\u0026gt; 把.Pages的元素个数替换为筛选后的元素个数。\n新增分类方式：collections 设计配置项 在layouts/_default/archives.html中添加配置项showCollections。\n1 2 3 4 5 6 params: archives: showCategories: true + showCollections: true showTags: false showDates: true 修改archive.html渲染collections列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;!-- Collections Section --\u0026gt; {{- $collectionTaxonomy := $.Site.GetPage \u0026#34;collections\u0026#34; -}} {{- $collectionTerms := $collectionTaxonomy.Pages -}} {{ if and (eq (.Site.Params.archives.showCollections | default false) true) (gt (len $collectionTerms) 0) }} \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;合集 - {{ $collectionTaxonomy.Title }}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;category-list--tile\u0026#34;\u0026gt; {{ range $collectionTerms }} {{- $pages := .Pages -}} {{- $filteredPages := where $pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} {{ if $filteredPages }} {{ partial \u0026#34;article-list/tile\u0026#34; (dict \u0026#34;context\u0026#34; . \u0026#34;size\u0026#34; \u0026#34;250x150\u0026#34; \u0026#34;Type\u0026#34; \u0026#34;taxonomy\u0026#34;) }} {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {{ end }} 在文件layouts/_default/archives.html中需要的位置添加如上代码。这篇博客是在categories和tags中间添加。\n修改配置文件 修改hugo.toml添加如下配置项。\n1 2 3 4 [taxonomies] category = \u0026#39;categories\u0026#39; tag = \u0026#39;tags\u0026#39; collection = \u0026#39;collections\u0026#39; 新建模板文件 在content文件夹下新建文件夹collections，在collections文件夹下如category一般新建分类项。如新建子目录及文件test-collection/_index.md，并在子目录中添加如下内容：\n1 2 3 4 --- title: \u0026#34;测试用\u0026#34; description: \u0026#34;test\u0026#34; --- 在这之后就可以在博文的front-matter中添加如下内容，以将这篇博文归类到一个合集中：\n1 collections = \u0026#34;test-collection\u0026#34; 效果展示 新建合集rvs-enr-crs选择一篇博文添加collections = \u0026quot;rvs-env-crs\u0026quot;。\n可见成功生效。\n","date":"2025-02-02T22:46:55+08:00","image":"/post/blogbuild1-archive/blogbuild1.png","permalink":"/post/blogbuild1-archive/","title":"Archive页改造"},{"content":"　此博客搭建过程参考的资料在\u0026quot;关于\u0026quot;页面可详细了解。本文具体步骤也可能会给出具体参考的内容。\n不能正确渲染主题 - 2025.01.28 此博客采用源码-博客页分离的结构搭建。\n源码为hugo个人博客的源码，设为github私有仓库，通过git action，在每次push之后，通过hugo -D命令云端编译生成public文件夹中的内容之后将public文件夹内的内容推送到GitHub - XingfenD/blog-by-hugo仓库中。\n在搭建阶段，我将themes子文件夹下的第三方主题仓库设置为该主项目的子模块，导致在上传到私有仓库后，themes文件夹中没有实际的主题文件。最终导致无法编译出所选主题的页面。\n我在github action中添加步骤Print Directory以打印路径信息，最终定位此问题并修复，完整的githu action如下。\n最终博客项目的结构为三个仓库：\nHugo源码仓库(通过gitignore忽略public, resources, themes文件夹)\n主题仓库(Hugo源码仓库的public文件夹)\nGithub Page仓库(Hugo源码仓库的public文件夹)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 name: deploy # 代码提交到main分支时触发github action on: push: branches: - main jobs: deploy: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#34;latest\u0026#34; extended: true - name: Print Directory run: tree -L 3 -d - name: Clone Theme run: git clone https://github.com/XingfenD/hugo-theme-custom.git themes - name: Git Configuration run: | git config --global core.quotePath false git config --global core.autocrlf false git config --global core.safecrlf true git config --global core.ignorecase false - name: Build Web run: hugo -D - name: Print Directory run: tree -L 3 -d - name: Deploy Web uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.ACTION_ACCESS_TOKEN }} external_repository: XingfenD/xingfen-star.github.io publish_branch: main publish_dir: public commit_message: auto deploy 主题配置后代码块显示异常 - 2025.02.02 使用hugo-theme-stack主题后\n发现在白天模式下的代码块颜色异常，如下图所示：\n在通过修改hugo-theme-stack主题的scss文件后成功定位异常原因：\nhugo编译md文件后生成的博文html文件中的codeblock为如下结构:\n1 2 3 4 5 6 7 8 \u0026lt;div class=\u0026#34;highlight\u0026#34;\u0026gt; \u0026lt;pre style=\u0026#34;\u0026#34;\u0026gt; \u0026lt;code class=\u0026#34;language-markdown\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;line\u0026#34;\u0026gt; ... \u0026lt;/code\u0026gt; \u0026lt;/pre\u0026gt; \u0026lt;/div\u0026gt; 图中所示的深色异常颜色为\u0026lt;pre\u0026gt;标签的style属性控制，因此导致全局scss文件失效。\n阅读官方文档后，在hugo.toml中添加配置：\n1 markup.highlight.noClasses=false 问题得以修复。\n修改配置文件以激活右侧边栏 - 2025.02.12 阅读Stack主题官方文档右侧边栏配置项可以了解如何修改配置文件以激活右边栏。\n在config.yaml文件中修改配置项[params.widgets]:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 params: widgets: homepage: - type: search - type: categories params: limit: 10 - type: tag-cloud params: limit: 10 page: - type: toc - type: categories params: limit: 5 - type: tag-cloud params: limit: 5 配置生效后右侧边栏效果如下图:\n不能正确渲染abbr, sub等特殊html标签 - 2025.02.12 查阅在线资料修复Hugo静态生成器中忽略原始HTML的方法后，在hugo.toml中添加配置项：\n1 markup.goldmark.renderer.unsafe=true 成功渲染，效果如图。\n","date":"2025-02-02T17:21:58+08:00","image":"/post/hugo-githubio/facebook_cover_photo_2.png","permalink":"/post/hugo-githubio/","title":"hugo+GithubIO自建博客踩坑实录"},{"content":"getopt.h简介 getopt.h 是C语言中一个用于处理命令行参数的内嵌库。可以通过以下两个函数来调用其功能：\ngetopt()\ngetopt_long()\n函数 getopt() getopt() 用于解析仅包含短选项的指令，例如 -g、-o、-f。\ngetopt() 示例程序 使用 getopt() 的参数处理框架如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;getopt.h\u0026gt; int main(int argc, char *argv[]) { int opt; /* parse the command line input */ while ((opt = getopt(argc, argv, \u0026#34;f:o:kh\u0026#34;)) != -1) { switch (opt) { case \u0026#39;f\u0026#39;: printf(\u0026#34;Arg -f: %s\\n\u0026#34;, optarg); break; case \u0026#39;o\u0026#39;: printf(\u0026#34;Arg -o: %s\\n\u0026#34;, optarg); break; case \u0026#39;k\u0026#39;: printf(\u0026#34;Arg -k do not need arguments:%s\\n\u0026#34;, optarg); break; case \u0026#39;h\u0026#39;: printf(\u0026#34;Arg -k do not need arguments:%s\\n\u0026#34;, optarg); break; case \u0026#39;?\u0026#39;: // An invalid option was found or a required argument is missing. if (optopt == \u0026#39;f\u0026#39; || optopt == \u0026#39;o\u0026#39;) { printf(\u0026#34;Option -%c requires an argument.\\n\u0026#34;, optopt); } else { printf(\u0026#34;Unknown option -%c.\\n\u0026#34;, optopt); } break; default: printf(\u0026#34;Unexpected situation.\\n\u0026#34;); break; } } /* extra arguments handling */ if (optind \u0026lt; argc) { printf(\u0026#34;Remaining arguments: \u0026#34;); for (; optind \u0026lt; argc; optind++) { printf(\u0026#34;%s \u0026#34;, argv[optind]); } } /* optind \u0026lt; argc */ } 编译并运行如下指令：./test_short.out -v yuanshen -f arg:f -o arg:o -k arg:k -h arg:h\n1 2 3 4 5 6 7 8 \u0026gt; ./test_short.out -v yuanshen -f arg:f -o arg:o -k arg:k -h arg:h ./test_short.out: invalid option -- \u0026#39;v\u0026#39; Invalid option: v Arg -f: arg:f Arg -o: arg:o Arg -k do not need arguments:(null) Arg -k do not need arguments:(null) Remaining arguments: yuanshen arg:k arg:h % getopt()详细说明 全局变量 全局变量 用途 optopt (int) 遇到未知选项或缺少必要参数时设置，存储导致错误的选项字符。 optarg (char*) 存储当前选项的参数值。当解析需要参数的选项（例如 -f file.txt 中的 file.txt）时，optarg 指向该字符串。 optind (int) 在解析命令行参数过程中，追踪位置，具体指示下一个要处理的参数的索引。 以下代码段会打印所有未被 getopt() 识别的参数（忽略这些参数的输入顺序）：\n1 2 3 4 5 6 7 /* extra arguments handling */ if (optind \u0026lt; argc) { printf(\u0026#34;Remaining arguments: \u0026#34;); for (; optind \u0026lt; argc; optind++) { printf(\u0026#34;%s \u0026#34;, argv[optind]); } } 模式字符串 模式字符串的定义是 const char *__shortopts，在示例程序中，其实例为 \u0026quot;f:o:kh\u0026quot;。\n顾名思义，模式字符串定义了 getopt() 和 getopt_long() 函数中将被识别的短选项。冒号 : 跟在短选项字符后表示该选项需要一个参数。\n以 \u0026quot;f:o:kh\u0026quot; 为例，此模式字符串接收4个选项。选项 -f 和 -o 需要输入参数，而选项 -k 和 -h 不需要。\n注意：如果 -f 后跟一个选项，该选项会被视为参数。\n函数 getopt_long() getopt_long() 用于解析既包含短选项（如 -g、-o、-f）又包含长选项（如 --version、--input）的指令。\n函数 getopt_long 识别一个结构体数组 struct option，该数组定义了长选项的信息。\nstruct option 的原型及相关介绍如下：\n1 2 3 4 5 6 struct option { const char *name; /* 长选项的名称 */ int has_arg; /* 是否需要参数 */ int *flag; /* 如果用户提供了此选项，*flag 将被赋值为 @val */ int val; /* 将长选项与短选项绑定，或赋值给 @*flag */ }; 字段 has_arg 有两个有效值，即宏 no_argument 和 required_argument。\n如果字段 flag 未提供，则字段 val 会被视为与当前长选项绑定的短选项。\n如果提供了字段 flag，当用户提供当前长选项时，字段 val 将被赋值给 *flag。\n用实例详细说明 以下是一个使用 struct option 数组和模式字符串的实例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /* the corresponding pattern string */ const char *pattern = \u0026#34;suR:\u0026#34;; struct option long_options[] = { /* long option only */ /* with flag set - 1 */ { \u0026#34;onlyset1\u0026#34;, no_argument, \u0026amp;onlyset1, 1 }, /* long option only */ /* with flag set - 2 */ { \u0026#34;onlyset2\u0026#34;, no_argument, \u0026amp;onlyset1, 2 }, /* long option only */ /* with flag set - 3 */ { \u0026#34;onlyset3\u0026#34;, no_argument, \u0026amp;onlyset2, 3 }, /* long option only ( bind failed ) */ /* with flag set, with val bind */ { \u0026#34;setbind\u0026#34;, no_argument, \u0026amp;setbind_flag, \u0026#39;s\u0026#39; }, /* short and long option */ /* without flag set, with val bind */ { \u0026#34;unsetbind\u0026#34;, no_argument, NULL, \u0026#39;u\u0026#39; }, /* short and long option */ /* require argument */ { \u0026#34;RequireArg\u0026#34;, required_argument, NULL, \u0026#39;R\u0026#39; }, }; 考虑到短选项和长选项，当提到某个功能的option时，可能会有以下三种情况：\n仅有短选项 仅有长选项 同时具有短选项和长选项 结合模式字符串和 struct option 数组，程序接收的选项如下：\n短选项 长选项 \u0026ndash;onlyset1 \u0026ndash;onlyset2 \u0026ndash;onlyset3 \u0026ndash;setbind -s -u \u0026ndash;unsetbind -R \u0026ndash;RequireArg 选项 -s 和 --setbind 并未绑定在一起，因为在与 \u0026quot;setbind\u0026quot; 对应的结构体元素中设置了字段 flag。\n参数需求冲突 当短选项和长选项中都定义了参数需求时，可能会发生冲突。\n我们通过一个实例进行说明：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* the corresponding pattern string */ const char *pattern = \u0026#34;c:d\u0026#34;; struct option long_options = { /* short and long option */ /* conflict argument require - 1 */ { \u0026#34;Conflict1\u0026#34;, no_argument, NULL, \u0026#39;c\u0026#39; }, /* short and long option */ /* conflict argument require - 2 */ { \u0026#34;Conflict2\u0026#34;, required_argument, NULL, \u0026#39;d\u0026#39;}, /* end of the array ( all zero ) */ { 0, 0, 0, 0 } }; 我们可以看到，上述程序将一个需要参数的短选项 -c 与一个不需要参数的长选项 --Conflict1 绑定在一起。\n同时，它还将一个不需要参数的短选项 -d 与一个需要参数的长选项 --Conflict2 绑定在一起。\n该程序运行正常，但关于参数的配置也会生效。\n当用户提供 -c 或 --Conflict1 时，程序会进入同一个分支，但 --Conflict1 不会读取后续的参数，而 -c 会读取。\n当用户提供 -d 或 --Conflict2 时，结果则相反。\ngetopt_long() 示例程序 以下是使用 getopt_long() 的参数处理框架：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;getopt.h\u0026gt; /* test instructions: ./test_long.out --onlyset1 --onlyset2 --onlyset3 ./test_long.out -s --setbind ./test_long.out --unsetbind --RequireArg arg:RequireArg ./test_long.out -u -R arg:R ./test_long.out -c arg:c -d arg:d ./test_long.out --Conflict1 arg:Conflict1 --Conflict2 arg:Conflict2 ./test_long.out -c arg:c --Conflict1 arg:Conflict1 ./test_long.out --Conflict1 arg:Conflict1 -c arg:c */ int main(int argc, char *argv[]) { int opt; int onlyset1 = 0; int onlyset2 = 0; int setbind_flag = 0; struct option long_options[] = { /* long option only */ /* with flag set - 1 */ { \u0026#34;onlyset1\u0026#34;, no_argument, \u0026amp;onlyset1, 1 }, /* long option only */ /* with flag set - 2 */ { \u0026#34;onlyset2\u0026#34;, no_argument, \u0026amp;onlyset1, 2 }, /* long option only */ /* with flag set - 3 */ { \u0026#34;onlyset3\u0026#34;, no_argument, \u0026amp;onlyset2, 3 }, /* long option only ( bind failed ) */ /* with flag set, with val bound */ { \u0026#34;setbind\u0026#34;, no_argument, \u0026amp;setbind_flag, \u0026#39;s\u0026#39; }, /* short and long option */ /* without flag set, with val bound */ { \u0026#34;unsetbind\u0026#34;, no_argument, NULL, \u0026#39;u\u0026#39; }, /* short and long option */ /* require argument */ { \u0026#34;RequireArg\u0026#34;, required_argument, NULL, \u0026#39;R\u0026#39; }, /* short and long option */ /* conflict argument require - 1 */ { \u0026#34;Conflict1\u0026#34;, no_argument, NULL, \u0026#39;c\u0026#39; }, /* short and long option */ /* conflict argument require - 2 */ { \u0026#34;Conflict2\u0026#34;, required_argument, NULL, \u0026#39;d\u0026#39;}, /* end of the array ( all zero ) */ { 0, 0, 0, 0 } }; int long_index = 0; while ((opt = getopt_long(argc, argv, \u0026#34;suR:c:d\u0026#34;, long_options, \u0026amp;long_index)) != -1) { switch (opt) { case 0: /* provide long option with flag set */ printf(\u0026#34;long option with flag set recognized!\\n\u0026#34;); printf(\u0026#34;long option name is %s\\n\u0026#34;, long_options[long_index].name); if (long_options[long_index].flag != NULL) { printf(\u0026#34;the flag value is %d\\n\u0026#34;, *(long_options[long_index].flag)); } break; case \u0026#39;s\u0026#39;: /* provide option -s instead of --setbind */ printf(\u0026#34;long option (short option only), with flag set, with val bound.\\n\u0026#34;); printf(\u0026#34;setbind_flag == %d\\n\u0026#34;, setbind_flag); break; case \u0026#39;u\u0026#39;: /* provide option -u and --unsetbind */ printf(\u0026#34;long option, without flag set, with val bound.\\n\u0026#34;); break; case \u0026#39;R\u0026#39;: /* provide option -R and --RequireArg */ printf(\u0026#34;long option require argument.\\n\u0026#34;); printf(\u0026#34;the argument: %s\\n\u0026#34;, optarg); break; case \u0026#39;c\u0026#39;: /* -c require an argument while --Conflict1 not */ printf(\u0026#34;conflict argument require - 1\\n\u0026#34;); printf(\u0026#34;the argument: %s\\n\u0026#34;, optarg); break; case \u0026#39;d\u0026#39;: /* --Conflict2 require an argument while -d not */ printf(\u0026#34;conflict argument require - 2\\n\u0026#34;); printf(\u0026#34;the argument: %s\\n\u0026#34;, optarg); break; case \u0026#39;?\u0026#39;: /* Unrecognized option or missing argument */ printf(\u0026#34;Unknown option or missing argument.\\n\u0026#34;); break; } printf(\u0026#34;\\n\u0026#34;); } /* Process remaining non-option arguments */ for (int index = optind; index \u0026lt; argc; index++) printf(\u0026#34;Non-option argument %s\\n\u0026#34;, argv[index]); return 0; } 运行测试指令并分析测试结果 编译运行时的测试指令如下：\n1 2 3 4 5 6 7 8 9 10 ./test_long.out --onlyset1 --onlyset2 --onlyset3 ./test_long.out -s --setbind ./test_long.out --unsetbind --RequireArg arg:RequireArg ./test_long.out -u -R arg:R ./test_long.out -c arg:c -d arg:d ./test_long.out --Conflict1 arg:Conflict1 --Conflict2 arg:Conflict2 ./test_long.out -c arg:c --Conflict1 arg:Conflict1 ./test_long.out --Conflict1 arg:Conflict1 -c arg:c 结果如下：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026gt; ./test_long.out --onlyset1 --onlyset2 --onlyset3 long option with flag set recognized! long option name is onlyset1 the flag value is 1 long option with flag set recognized! long option name is onlyset2 the flag value is 2 long option with flag set recognized! long option name is onlyset3 the flag value is 3 指令 ./test_long.out --onlyset1 --onlyset2 --onlyset3 展示了 getopt_long() 的基本功能。\n它改变了 flag 地址中的值。\n1 2 3 4 5 6 7 \u0026gt; ./test_long.out -s --setbind long option (short option only), with flag set, with val bound. setbind_flag == 0 long option with flag set recognized! long option name is setbind the flag value is 115 指令 ./test_long.out -s --setbind 展示了绑定失败（当 flag 字段被设置时）。\n--setbind 和 -s 会进入不同的分支。\n1 2 3 4 5 6 7 8 9 10 11 \u0026gt; ./test_long.out --unsetbind --RequireArg arg:RequireArg long option, without flag set, with val bound. long option require argument. the argument: arg:RequireArg \u0026gt; ./test_long.out -u -R arg:R long option, without flag set, with val bound. long option require argument. the argument: arg:R 这对比展示了绑定成功的结果（当 flag 字段未设置时）。\n--unsetbind 和 -u 被绑定在一起，进入相同的分支。\n--RequireArg 和 -R 也是一样。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026gt; ./test_long.out -c arg:c -d arg:d conflict argument require - 1 the argument: arg:c conflict argument require - 2 the argument: (null) Non-option argument arg:d \u0026gt; ./test_long.out --Conflict1 arg:Conflict1 --Conflict2 arg:Conflict2 conflict argument require - 1 the argument: (null) conflict argument require - 2 the argument: arg:Conflict2 Non-option argument arg:Conflict1 这对比展示了参数要求的冲突。\n-c 和 --Conflict1 被绑定在一起，但 -c 需要一个参数。\n-d 和 --Conflict2 被绑定在一起，但 --Conflict2 需要一个参数。\n结果显示，绑定在一起的选项会进入相同的分支。\n但是，是否读取或忽略参数取决于用户提供的选项是否需要参数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026gt; ./test_long.out -c arg:c --Conflict1 arg:Conflict1 conflict argument require - 1 the argument: arg:c conflict argument require - 1 the argument: (null) Non-option argument arg:Conflict1 \u0026gt; ./test_long.out --Conflict1 arg:Conflict1 -c arg:c conflict argument require - 1 the argument: (null) conflict argument require - 1 the argument: arg:c Non-option argument arg:Conflict1 冲突选项的另一个对比结果。\n","date":"2025-01-22T13:29:03+08:00","permalink":"/post/c-getopt_h-analysis/","title":"c语言getopt.h库解析"}]