|
![]() ![]()
![]() ![]() |

在 Linux 中国开源社区 停止运营 后,我收到了数不清的关心和支持。对此我深感荣幸和感动。然而,我注意到有一个普遍的误解,那就是大家将 Linux 中国的成功完全归功于我,这并不准确。因此,我想借助这篇对 Linux 中国文章数据集的文章,正式澄清这一点,并向在 Linux 中国成长历程中为之做出贡献的每一位贡献者表达我诚挚的敬意。
功由众筑
首先,我要对 Bestony(白宦成)表示感谢。Bestony 早在 LCTT 成立时就是一位贡献者,但他对 Linux 中国的贡献并不仅限于 LCTT 的翻译工作。实际上,Bestony 是 Linux 中国背后的商业实体的唯一合伙人,对 Linux 中国的持续运营做出了无法抹去的贡献。这不仅包括对基础设施的维护、软件的开放,还包括主持我们在 2023 年举办的几乎所有场次的 LLUG 活动。
除了 Bestony,还有许多技术专家为 Linux 中国提供了技术支持,包括 Vivz、Michael Zhang 和 lujun9972 等。例如,Vizv 编写的精妙脚本构成了我们的 GitHub CI 系统,Michael Zhang 为我们提供了自由访问的基础设施,而我们一直依赖的选题系统则是由 lujun9972 开发并运行。
其次,我要感谢所有的 LCTT 贡献者,包括我们的主要流程负责人,他们的 GitHub ID 是:
- 历任选题:DeadFire、oska874、lujun9972
- 历任校对:reinoir222、carolinewuyan、PurlingNayuki、jasminepeng、pityonline
还有我们的首席译者 geekpi,他翻译的文章数量占到了我们的总量的四分之一,而排名第二的是我(wxy),我翻译的数量仅是他的一半。
以下是其他的重要贡献者(三星以上):
- 💎:qhwdw、MjSeven、GOLinux
- 🌟🌟🌟🌟🌟:lujun9972、lkxed、ictlyh、FSSlc、alim0x、ucasFL、robsean、HankChow
- 🌟🌟🌟🌟:bestony、zpl1025、GHLandy、runningwater、strugglingyouth、bazz2、rusking、martin2011qi、pinewall、BriFuture、lxbwolf、stevenzdg988、Flowsnow、Chao-zhi、chai001125、Donkey-Hao
- 🌟🌟🌟:oska874、tinyeyeser、Vic020、KayGuoWhu、ZTinoZ、Luoxcat、amwps290、StdioA、toknow-gh、wwy-hust、su-kaiyao、gxlct008、OneNewLife、name1e5s、MikeCoder、wyangsun、JonnieWayy、heguangzhi、unigeorge、cposture、disylee、Yufei-Yan、mr-ping、hwlife、hopefully2333、vim-kakali、LuuMing、firmianay、aREversez、zhousiyu325、jessie-pang、Moelf、Cubik65536、fuowang、SteveArcher、warmfrog、chen-ni、perfiffer、hanszhao80、ypingcn、XiaotingHuang22、dianbanjiu、tomjlw、wyxplus、cycoe、mengxinayan、messon007、jrglinux、CanYellow、wenwensnow、qfzy1233、laingke、cool-summer-021、DavidChenLiang、guevaraya、Morisun029
除此以外还有另外 469 位为 LCTT 做过贡献的译者,由于篇幅有限无法一一列出他们的名字,详细名单请看: https://linux.cn/lctt-list
除 LCTT 译者外,还有很多贡献者参与了我们的其他开源项目,如中文版《代码英雄》、极客漫画、LFS 中文以及中文手册页(man)。再者,除了翻译,我们的 QQ 群、微信群管理,以及 LLUG 线下活动的组织也都有着大量的贡献者。
果归众享
之前,我们承诺会向社区开放 Linux 中国的文章数据。借助 Bestony 春节期间的努力,我们终于完成了这一工作。
归档站
当前 Linux 中国官方网站是基于 Discuz 开发的,文章内容以 HTML 格式存储。我们考虑到了标准化的重要性,所以已经将原创文章转换为 Markdown 格式。你可以根据自己的需要对这些信息进行处理,例如创建归档站或制作电子书。
当然,我们也官方提供了一个静态归档站:
- 归档站: https://archive.linux.cn/ ,虽然目前功能尚简单,但欢迎大家上传功能补丁
- 归档站源码: https://github.com/Linux-CN/archive
我们欣喜地发现,已有人利用这些数据创建了自己的归档站:
- @modiqi 麟悦(https://linuxjoy.com/ ),我个人觉得非常美观~
如果你也基于这些数据创建了自己的归档站,欢迎大家 分享 给大家访问。
电子书
我们原先希望将所有文章汇集成一本完整的电子书,但后来发现这个目标并不现实。
由于我们的原创文章数量超过万篇,仅图片内容压缩过后就超过了 2GB。因此,你可以利用我们提供的原始数据,筛选自己需要的文章制作电子书。我们也欢迎你将成品 分享 给大家。
原始数据
目前我们为大家提供了两种不同的数据:
- 标准的 Markdown 格式的数据: https://github.com/Linux-CN/archive/releases/tag/release
- Huggingface 数据集: https://huggingface.co/datasets/linux-cn/archive
对于 GitHub 下载不便的朋友,我们也提供了百度网盘的下载方式:https://pan.baidu.com/s/1i7DTuf_umPkkleHFtdmZJA?pwd=lccn 提取码: lccn
请注意,上述内容基于 CC-BY-NC 4.0 协议开放,使用数据时请遵守协议规定。
- Facebook✓
- Google+✓
- Instapaper✓
- Tweet✓

正在寻找开源的 ChatGPT 替代方案?我们已经为你挑选了最优选项,供你一览。
ChatGPT 是 OpenAI 推出的一个强大的生成式 AI 工具。你只需要以对话形式键入文本提示,它就会给出详细的回应。
虽然它并非无懈可击,但无可否认,有时它表现得就是那么得力。然而,无论你如何利用它,不可更改的事实是,它并非开源方案。
作为一种专用选项,它并非在所有方面都有优势。那么,何处可以寻找 ChatGPT 的开源替代品呢?让我为你揭晓自然开源的优秀 ChatGPT 替代方案。
📋 并非所有 ChatGPT 的替代品都有统一的运作方式。有些是为开发者设计的,让他们在其上打造属于自己的聊天机器人。还有一些则提供了供你测试使用的机器人或演示。
搜寻开源 ChatGPT 替代品的理由
依赖任何单一的服务对消费者来说都不是好事。对于 ChatGPT 来说也是如此。
除此之外,以下是我们需要寻找开源 ChatGPT 替代品的原因:
- 掌握我们的数据处理方式,实现透明化。
- 可以选择自己构建解决方案,节省了成本。
- 按需定制 ChatGPT 替代方案。
- 你不再受制于某个公司的规定。开源解决方案可以根据您的需求灵活变化。
- 对开源项目的贡献会对全球每个人产生影响。
我并不是在此表明 ChatGPT 的功能不足,或者你应当放弃使用。然而,以“我们”用户的角度来看,长期而言,更应该从开源的替代品中获得更多收益。
现在,就让我们开始探寻最佳开源的 ChatGPT 方案吧。
❌ 并非所有的选项都可用于商业用途。你需要特别留意使用某些机器人时的相关规定。
1、OpenChatKit
OpenChatKit 是由 Together 开发的全功能 ChatGPT 替代品。
该公司起初与 LAION(主导了 稳定扩散)等研究机构共同合作,构建出一个训练数据集。在撰写本文时,它搭载了 RedPajama 模型,这是目前最大的开源 AI 模型之一。
你可以试验看看它是否适合你的使用需求,并在其 GitHub 页面 进一步探索更多技术细节。
2、ChatRWKV
ChatRWKV 是由 循环神经网络(RNN) 语言模型驱动的开源替代品。
你可以在 Huggingface 发现它的演示。 GitHub 页面 列出了关于其代码库、技术细节,以及最新版本预览的所有信息。
开发者和商业公司都可以利用 ChatRWKV 构建他们的聊天机器人。
3、ColossalChat
Colossal AI 是一个开源项目,目标是帮助你克隆 AI 模型,并打造出满足你需求的 ChatGPT 类似的平台。
ColossalChat 是以此项目为基础打造的聊天机器人。然而遗憾的是,在本文撰写之时,它的演示暂未上线。
你可以在 GitHub 上探索其源代码。
4、KoboldAI
KoboldAI 是一个类似于 ChatGPT 的 AI,主要以浏览器前端的形式为写作提供辅助。虽然它具备聊天机器人模式,但它首要的设计目标是作为专为小说设计的 AI 写作助手。
提供了各种各样的模型,你可以在 Google Colab 上轻松运行它。
它支持各种模式来优化你的写作,让你有更大的发挥空间。你可以在其 GitHub 页面 中详细了解它的所有信息。
5、GPT4ALL
GPT4ALL 是一个令人感兴趣的开源项目,旨在为你提供可在任何地方运行的聊天机器人。
没错,你可以在自己的 CPU 上本地运行它,而且几乎所有其他型号的 GPU 都能支持。
你需要做的就是安装它的桌面应用程序(聊天客户端),然后就能开始使用了。如果对其制作过程感兴趣,可以查看其 GitHub 页面。
6、HuggingChat
听起来熟悉吗?嗯,Huggingface 的平台被其他 AI 模型和聊天机器人用作展示他们的演示。
HuggingChat 正是 Huggingface 打造的开源的 ChatGPT 替代品,使用了社区中最优秀的 AI 模型来实现聊天机器人的功能。
你可以 尝试一下,并在 源代码 中深入探讨更多细节。如果有特别的需求,可以选择搭配不同的后端来使用聊天应用。
7、Koala
EasyLM 的研发成果 Koala 是一款可尝试本地运行的聊天机器人,整个架构基于 LLaMA 数据集完成。
你可以参阅其 官方博客 来深入了解细节。
目前,演示版本还未上线,但是,你可以参考其 文档 在本地部署,进行试运行并测试。
8、Vicuna
Vicuna 是另一款开源聊天机器人,训练基础依然是 LLaMA。根据开发者的说法,使用 GPT-4 作为评价标准,得出的结论是 Vicuna 的聊天体验接近 ChatGPT。
这是一个你可能想要验证的有趣声明。你可以选择 Vicuna 作为语言模型,并在 Chatbot Arena 进行尝试。
想要了解更多关于它的信息,你可以查阅其 官方博文。
9、Alpaca-LoRA
Alpaca-LoRA 雄心勃勃,旨在使用低秩适应技术提供一种可在树莓派中运行的模型。
利用单个 RTX 4090 GPU,整个模型可在几小时之内完成训练。
此时,演示版本仍然未开放,但你可以在其 GitHub 页面 上寻找更多详情。
10、Dolly
Dolly 是另一个在 Databricks 机器学习平台上训练的语言模型,并已获得商业使用许可。
你可以在 GitHub 上查阅源代码,并在 Huggingface 上探究模型详情。
11、H2oGPT
H2oGPT 是专门为查询定制的,可以帮助你总结文档。在保证隐私的前提下,它允许你通过用户界面上传和查看文档。
同样,你可以像使用 ChatGPT 那样开始对话。对于 Windows 和 macOS,该项目提供了易于安装的程序。对于 Linux 系统,你可能需要通过 Docker 进行些许设置。
可以尝试该聊天机器人的 在线演示 来了解其运行情况,并在 GitHub 页面 查阅源代码。
12、Cerebras-GPT
Cerebras-GPT 呈现了通过大量参数训练的开源 GPT 类似模型。
它并没有提供聊天机器人服务。它的目标旨在提供精准且高效的开源模型供你使用。
获取模型详情,你可以访问 Hugging Face。
13、OpenAssistant
OpenAssistant 的目标,是让每一个人都能访问类似于 ChatGPT 的聊天机器人。
有一段时间,他们通过演示版本收集了用户数据。现在,由于项目已由创作者标记为完成,演示版本不再开放。
但你可以利用该项目的成果,及其 源代码,在此基础上进行扩展。
总结
面向用户和开发者,ChatGPT 的开源替代品提供了各种各样的优点。你可以选择运用一个开源的聊天机器人,或者借助开源语言模型构建一个属于自己的聊天机器人。
无论你选择哪一个,只要遵守可用语言模型的政策,你都可以自由地修改和使用它以满足你的需求。
你最爱哪个开源的 ChatGPT 替代品呢?是否还有其他优秀的开源型 ChatGPT 项目,我们错过了但你特别喜欢的?请在下方评论区告诉我们你的想法。
(题图:DA/b757432f-56a4-4493-a109-1eca6c57c1fc)
via: https://itsfoss.com/open-source-chatgpt-alternatives/
作者:Ankush Das 选题:lujun9972 译者:ChatGPT 校对:wxy
- Facebook✓
- Google+✓
- Instapaper✓
- Tweet✓
下面将引导你安装和使用 Jami,让你对其工作原理和你能从中获取的内容有更深入的理解。
不管你是为了联系亲爱的人,还是为了职业需求与同事/团队沟通,一个安全的通讯平台都是大家的需求。Jami 就是其中一款通讯平台,利用分布式网络来实现视频通话、文件分享、聊天等功能全都不经过服务器。
这个工具完全免费且开源,具有多样的功能。
然而,它的表现如何呢?用户体验会好么?使用体验能像其他已有的专有平台那么流畅么?或者,你能在手机上使用吗?
在这篇文章里,我们将仔细研究 Jami。我们试用了一下以助你作出更明智的决定。
Jami 的特性
对很多用户来说,体验的好坏取决于功能的全面性。
所以,在深入研究 Jami 之前,让我概述一下,你可以期待从它得到什么样的功能:
- 即时发送消息
- 群聊
- 视频会议
- 音频通话
- 音频和视频信息
- 屏幕共享
- 文件分享
- 增强功能的扩展
- 作为 SIP(会话启动协议)客户端
看起来,Jami 是一款在考虑分布式解决方案时值得考虑的全能选项。
听上去挺刺激的,对吧?
那我们现在就来细看一下,看看它是如何工作的,以及我们能从中获得什么。
使用 Jami 的入门步骤
在你的系统上安装 Jami 是相当简单的。
你可以直接从它的网站下载最新的官方版本,或者直接在你的包管理器中(对于 Linux)搜索它。
它可以跨平台使用,包括:Linux、Windows、macOS、安卓和 iOS。
对于安卓用户,你可以在 Play Store 或 F-Droid 中安装。至本文撰写之时,Jami 支持所有最新和最棒的操作系统版本,所以,从这个层面上看,它的维护工作做得相当好。
在安装完成之后,就开始了注册过程。不像其他一些平台,它并不需要任何私人信息,只需要你创建一个你喜欢的独特用户名。
完成之后,你只需要把这个用户名与其他用户分享,就可以开始对话了。你也可以展现出 QR 码让其他人连接你。就是这么简单。
在注册时,你可以选择加密你的帐户,以在设备上保护它,并自定义显示名。
使用 Jami 作为通讯平台
使用这类平台时,我们的首要关心点通常是发送 / 接收消息的响应速度,以及语音 / 视频通话的质量。
好消息是:使用体验快捷顺畅。不管你是要发送一个视频信息,进行一个音频通话,还是发送文本,Jami 都像你想象的一样响应灵敏。
当我们谈及 用户界面,每个人的看法可能都会不同。我发现它的用户界面简明扼要,干净利落。
然而,当你调窄应用窗口的宽度时,事情可能会有所不同。它失去了适应较小尺寸的能力,忽视了重要的元素,甚至难以顺畅地访问设置,这降低了使用体验。
不要误会,你仍可以正常导航至所有选项。然而,体验感会显得有些生硬。
当把 Jami 的窗口缩小时,我点击了“设置”,结果直接进入了“账户管理”选项,而没有给我任何下拉菜单,也没有为我打开“账户”/“一般”或“音频/视频”部分。
假设我想要进入“音频/视频”设置,我就会进入账户设置。接下来,我需要再次导航到其他设置。这并不是一个让人感到方便的体验。
如果你不会经常调小 Jami 的窗口,或者你更喜欢大一点的窗口,那么你应该不会遇到此类问题。总结一下,如果你希望所有东西都有现代化的设计风格,那它可能会有些失望。
当你第一次发送/接收文本或通话时,会被视作一次邀请。一旦你接受了邀请,用户就会出现在你的会话列表中。
发送短信,添加新朋友的体验是无缝连接的,我没有遇到任何问题。
📋 在撰写本文时进行的最新版本测试中,我无法关闭捐款消息。我答复的同事也遇到这个问题。
在信息中,你会有标记“正在输入中”的提示,你也可以回复一个特定的消息,用一个表情符号做出反应,附加文件,或发送语音或者视频消息。
这些消息都可以被编辑和删除。
上面的对话截图是一个通过选择多个用户创建的群聊。
这样,当你创建这样的群聊时,一个新的邀请会被发送到用户那里,这是好的(不是强制他们加入群聊)。如果他们接受了邀请,他们就能加入你创建的群聊。
默认情况下,创建群组的设备将会默认承载群组内的任何音频 / 视频通话。
如果你连接的账户有多个设备,你可以选择你想要作为主机的那个设备。
通话体验有些不尽人意,具体来说,音频通话体验十分出色,音频清晰。
然而,视频通话的音频方面就有些令人失望。
当然了,用直连的方式,视频质量会严重依赖网络连接,然而,和网络连接相比,我们得到的质量并没有那么好。(主机是我,网速是 200 Mbps)。
视频质量可能是由于优化不佳或网络不佳导致的,但是,双方的音频并不清晰,音量在整个通话中都在上下波动。
虽然我的一些同事能分享他们的屏幕,但是在带有 AMD Radeon 集成 GPU 的 Ubuntu 22.04 LTS 上,应用崩溃了。
默认情况下,你无法添加绿幕效果,这和一些专有的视频通话应用不同。然而,你可以安装扩展增加此类功能。
另外,为了增强你的体验,你可以调整通话设置,包括自动接听电话,将你的账户作为接入点,切换正在输入中的指示等。
总结
如果 Jami 能在现代化用户界面这方面投入更多努力,那么它应该能成为像微软 Teams、Slack 和 Zoom 这样的应用的热门选择。
当然了,因为 P2P 连接原因,视频质量可能不如其他专有选项那么稳定。但在音频通话、视频/音频信息、文字输入、文件发送等方面你应该不会有任何问题,同时享受私密的通讯体验。
总的来说,Jami 为你提供了所有基本的需求,在一个更快的体验上提供了基本的用户界面。
它可能不能给你最好的用户体验,但是考虑到它在分布式网络连接、开源应用和隐私保护方面的优势,对于注重隐私的用户来说,它仍是个极好的选择。
(题图:DA/26649e6b-a4b6-4591-a5c1-ecac982529a9)
via: https://itsfoss.com/jami/
作者:Ankush Das 选题:lujun9972 译者:译者ID 校对:校对者ID
- Facebook✓
- Google+✓
- Instapaper✓
- Tweet✓

为何选择文字用户界面(TUI)?
许多人每日都在使用终端,因此,文字用户界面(TUI)逐渐显示出其价值。它能减少用户输入命令时的误差,让终端操作更高效,提高生产力。
以我的个人使用情况为例:我每日会通过家用电脑远程连接到我使用 Linux 系统的实体 PC。所有的远程网络连接都通过私有 VPN 加密保护。然而,当我需要频繁重复输入命令进行连接时,这种经历实在令人烦躁。
于是,我创建了下面这个 Bash 函数,从而有所改进:
export REMOTE_RDP_USER="myremoteuser"
function remote_machine() {
/usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_RDP_USER /v:$1 /p:$2
}
但后来,我发现自己还是频繁地执行下面这条命令(在一行中):
remote_pass=(/bin/cat/.mypassfile) remote_machine $remote_machine $remote_pass
这太烦了。更糟糕的是,我的密码被明文存储在我的电脑上(我虽然使用了加密驱动器,但这点依然令人不安)。
因此,我决定投入一些时间,编写一个实用的脚本,从而更好地满足我的基本需求。
我需要哪些信息才能连接到远程桌面?
实际上,要连接到远程桌面,你只需提供少量信息。这些信息需要进行结构化处理,所以一个简单的 JSON 文件就能够满足要求:
{"machines": [
{
"name": "machine1.domain.com",
"description": "Personal-PC"
},
{
"name": "machine2.domain.com",
"description": "Virtual-Machine"
}
],
"remote_user": "MYUSER@DOMAIN",
"title" : "MY COMPANY RDP connection"
}
尽管在各种配置文件格式中,JSON 并非最佳选择(例如,它不支持注解),但是 Linux 提供了许多工具通过命令行方式解析 JSON 内容。其中,特别值得一提的工具就是 jq。下面我要向你展示如何利用它来提取机器列表:
/usr/bin/jq --compact-output --raw-output '.machines[]| .name' \
$HOME/.config/scripts/kodegeek_rdp.json) \
"machine1.domain.com" "machine2.domain.com"
jq
的文档可以在 这里 找到。另外,你也可以直接将你的 JSON 文件复制粘贴到 jq play,试用你的表达式,然后在你的脚本中使用这些表达式。
既然已经准备好了连接远程计算机所需的所有信息,那现在就让我们来创建一个美观实用的 TUI 吧。
Dialog 的帮助
Dialog 是那些你可能希望早些认识的、被低评估的 Linux 工具之一。你可以利用它构建出一个井然有序、简介易懂,并且完美适用于你终端的用户界面。
比如,我可以创建一个包含我喜欢的编程语言的简单的复选框列表,且默认选择 Python:
dialog --clear --checklist "Favorite programming languages:" 10 30 7\
1 Python on 2 Java off 3 Bash off 4 Perl off 5 Ruby off
我们通过这条命令向 dialog
下达了几个指令:
- 清除屏幕(所有选项都以
--
开头) - 创建一个带有标题的复选框(第一个位置参数)
- 决定窗口尺寸(高度、宽度和列表高度,共 3 个参数)
- 列表中的每条选项都由一个标签和一个值组成。
惊人的是,仅仅一行代码,就带来了简洁直观和视觉友好的选择列表。
关于 dialog
的详细文档,你可以在 这里 阅读。
整合所有元素:使用 Dialog 和 JQ 编写 TUI
我编写了一个 TUI,它使用 jq
从我的 JSON 文件中提取配置详细信息,并且使用 dialog
来组织流程。每次运行,我都会要求输入密码,并将其保存在一个临时文件中,脚本使用后便会删除这个临时文件。
这个脚本非常基础,但它更安全,也使我能够专注于更重要的任务 🙂
那么 脚本 看起来是怎样的呢?下面是代码:
#!/bin/bash
# Author Jose Vicente Nunez
# Do not use this script on a public computer. It is not secure...
# https://invisible-island.net/dialog/
# Below some constants to make it easier to handle Dialog
# return codes
: ${DIALOG_OK=0}
: ${DIALOG_CANCEL=1}
: ${DIALOG_HELP=2}
: ${DIALOG_EXTRA=3}
: ${DIALOG_ITEM_HELP=4}
: ${DIALOG_ESC=255}
# Temporary file to store sensitive data. Use a 'trap' to remove
# at the end of the script or if it gets interrupted
declare tmp_file=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file=/tmp/test$$
trap "/bin/rm -f $tmp_file" QUIT EXIT INT
/bin/chmod go-wrx ${tmp_file} > /dev/null 2>&1
:<<DOC
Extract details like title, remote user and machines using jq from the JSON file
Use a subshell for the machine list
DOC
declare TITLE=$(/usr/bin/jq --compact-output --raw-output '.title' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100
declare REMOTE_USER=$(/usr/bin/jq --compact-output --raw-output '.remote_user' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100
declare MACHINES=$(
declare tmp_file2=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file2=/tmp/test$$
# trap "/bin/rm -f $tmp_file2" 0 1 2 5 15 EXIT INT
declare -a MACHINE_INFO=$(/usr/bin/jq --compact-output --raw-output '.machines[]| join(",")' $HOME/.config/scripts/kodegeek_rdp.json > $tmp_file2)
declare -i i=0
while read line; do
declare machine=$(echo $line| /usr/bin/cut -d',' -f1)
declare desc=$(echo $line| /usr/bin/cut -d',' -f2)
declare toggle=off
if [ $i -eq 0 ]; then
toggle=on
((i=i+1))
fi
echo $machine $desc $toggle
done < $tmp_file2
/bin/cp /dev/null $tmp_file2
) || exit 100
# Create a dialog with a radio list and let the user select the
# remote machine
/usr/bin/dialog \
--clear \
--title "$TITLE" \
--radiolist "Which machine do you want to use?" 20 61 2 \
$MACHINES 2> ${tmp_file}
return_value=$?
# Handle the return codes from the machine selection in the
# previous step
export remote_machine=""
case $return_value in
$DIALOG_OK)
export remote_machine=$(/bin/cat ${tmp_file})
;;
$DIALOG_CANCEL)
echo "Cancel pressed.";;
$DIALOG_HELP)
echo "Help pressed.";;
$DIALOG_EXTRA)
echo "Extra button pressed.";;
$DIALOG_ITEM_HELP)
echo "Item-help button pressed.";;
$DIALOG_ESC)
if test -s $tmp_file ; then
/bin/rm -f $tmp_file
else
echo "ESC pressed."
fi
;;
esac
# No machine selected? No service ...
if [ -z "${remote_machine}" ]; then
/usr/bin/dialog \
--clear \
--title "Error, no machine selected?" --clear "$@" \
--msgbox "No machine was selected!. Will exit now..." 15 30
exit 100
fi
# Send 4 packets to the remote machine. I assume your network
# administration allows ICMP packets
# If there is an error show message box
/bin/ping -c 4 ${remote_machine} >/dev/null 2>&1
if [ $? -ne 0 ]; then
/usr/bin/dialog \
--clear \
--title "VPN issues or machine is off?" --clear "$@" \
--msgbox "Could not ping ${remote_machine}. Time to troubleshoot..." 15 50
exit 100
fi
# Remote machine is visible, ask for credentials and handle user
# choices (like password with a password box)
/bin/rm -f ${tmp_file}
/usr/bin/dialog \
--title "$TITLE" \
--clear \
--insecure \
--passwordbox "Please enter your Windows password for ${remote_machine}\n" 16 51 2> $tmp_file
return_value=$?
case $return_value in
$DIALOG_OK)
# We have all the information, try to connect using RDP protocol
/usr/bin/mkdir -p -v $HOME/logs
/usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_USER /v:${remote_machine} /p:$(/bin/cat ${tmp_file})| \
/usr/bin/tee $HOME/logs/$(/usr/bin/basename $0)-$remote_machine.log
;;
$DIALOG_CANCEL)
echo "Cancel pressed.";;
$DIALOG_HELP)
echo "Help pressed.";;
$DIALOG_EXTRA)
echo "Extra button pressed.";;
$DIALOG_ITEM_HELP)
echo "Item-help button pressed.";;
$DIALOG_ESC)
if test -s $tmp_file ; then
/bin/rm -f $tmp_file
else
echo "ESC pressed."
fi
;;
esac
你从代码中可以看出,dialog
预期的是位置参数,并且允许你在变量中捕获用户的回应。这实际上使其成为编写文本用户界面的 Bash 扩展。
上述的小例子只涵盖了一些部件的使用,其实还有更多的文档在 官方 dialog 网站上。
Dialog 和 JQ 是最好的选择吗?
实现这个功能可以有很多方法(如 Textual,Gnome 的 Zenity,Python 的 TKinker等)。我只是想向你展示一种高效的方式——仅用 100 行代码就完成了这项任务。
确实,它并不完美。更具体地讲,它与 Bash 的深度集成使得代码有些冗长,但仍然保持了易于调试和维护的特性。相比于反复复制粘贴长长的命令,这无疑是一个更好的选择。
最后,如果你喜欢在 Bash 中使用 jq
处理 JSON,那么你会对这个 jq 配方的精彩集合 感兴趣的。
(题图:MJ/a9b7f60a-02ec-4d3f-88ae-2321f49ac0e1)
via: https://fedoramagazine.org/writing-useful-terminal-tui-on-linux-with-dialog-and-jq/
作者:Jose Nunez 选题:lujun9972 译者:ChatGPT 校对:wxy
- Facebook✓
- Google+✓
- Instapaper✓
- Tweet✓

一份让你深入体验最新 Linux 内核编译过程的实操指南。
出于各种原因,自行编译 Linux 内核可能引起你的兴趣。这些原因可能包括但不限于:
- 测试一个比你目前的 Linux 发行版更新的内核版本
- 采用一组不同的配置选项、驱动来构建内核
- 学习者的好奇心 😃
此指南将一步步指导你如何亲自编译 Linux 内核,包括你该运行哪些命令,为什么运行这些命令以及这些命令的执行效果。本文篇幅较长,所以请做好准备!
🚧 诸如 Ubuntu 这样的发行版提供了更简单地安装主线 Linux 内核的方式。但本教程目标是从源码手动完成所有工作。此教程需要你付出时间、耐心以及丰富的 Linux 命令行使用经验。本文更注重亲身实践的体验。不管怎么说,我仍建议你在虚拟机或备用系统中尝试此冒险,而非在你的主系统上进行。
前置准备
在软件领域,构建任何事物都有两个基本要求:
- 源代码
- 构建依赖
因此,作为预备环节,我们需要下载 Linux 内核的源码压缩包,并安装一些能让我们成功构建 Linux 内核的依赖项。
Linux 版本导览
在任何时刻,Freax Linux 内核都有四种“版本”。
Linux 的这些 “版本”,按照开发流程的顺序是:
- linux-next 树: 所有准备合并到 Linux 代码库的代码首先被合并到 linux-next 树。它代表的是 Linux 内核最新也是“最不稳定”的状态。大多数 Linux 内核开发者和测试人员使用这个来提高代码质量,为 Linus Torvalds 的后续提取做准备。请谨慎使用!
- 发布候选版(RC) / 主线版: Linus 从 linux-next 树抽取代码并创建一个初始发布版本。这个初始发布版本的测试版称为 RC(发布候选)版本。一旦 RC 版本发布,Linus 只会接受对它的错误修复和性能退化相关的补丁。基础这些反馈,Linus 会每周发布一个 RC 内核,直到他对代码感到满意。RC 发行版本的标识是
-rc
后缀,后面跟一个数字。 - 稳定版: 当 Linus 觉得最新的 RC 版本已稳定时,他会发布最终的“公开”版本。稳定发布版将会维护几周时间。像 Arch Linux 和 Fedora Linux 这样的前沿 Linux 发行版会使用此类版本。我建议你在试用 linux-next 或任何 RC 版本之前,先试一试此版本。
- LTS 版本: 每年最后一个稳定版将会再维护 几年。这通常是一个较旧的版本,但它会 会积极地维护并提供安全修复。Debian 的稳定版本会使用 Linux 内核的 LTS 版版本。
若想了解更多此方面的知识,可参阅 官方文档。
本文将以当前可用的最新稳定版为例,编写此文时的 Linux 内核版本是 6.5.5。
系统准备
由于 Linux 内核使用 C 语言编写,编译 Linux 内核至少需要一个 C 编译器。你的计算机上可能还需要其他一些依赖项,现在是安装它们的时候了。
💡 这个指南主要聚焦于使用 GNU C 编译器(GCC)来编译 Linux 内核。但在未来的文章中(可能会深入介绍 Rust 的支持),我可能会介绍使用 LLVM 的 Clang 编译器作为 GCC 的替代品。
不过,请注意,MSVC 并不适用。尽管如此,我仍期待有微软的员工为此发送修补程序集。我在瞎想啥?
对于 Arch Linux 以及其衍生版本的用户,安装命令如下:
sudo pacman -S base-devel bc coreutils cpio gettext initramfs kmod libelf ncurses pahole perl python rsync tar xz
对于 Debian 以及其衍生版本的用户,安装命令如下:
sudo apt install bc binutils bison dwarves flex gcc git gnupg2 gzip libelf-dev libncurses5-dev libssl-dev make openssl pahole perl-base rsync tar xz-utils
对于 Fedora 以及其衍生版本的用户,安装命令如下:
sudo dnf install binutils ncurses-devel \
/usr/include/{libelf.h,openssl/pkcs7.h} \
/usr/bin/{bc,bison,flex,gcc,git,gpg2,gzip,make,openssl,pahole,perl,rsync,tar,xz,zstd}
下载 Linux 内核源码
请访问 kernel.org,在页面中寻找第一个 稳定 版本。你不会找不到它,因为它是最显眼的黄色方框哦 😉
通过点击黄色的方框,你就可以下载 Tar 文件。同时,也别忘了下载相匹配的 PGP 签名文件,稍后我们需要用到它来验证 Tar 文件。它的扩展名为 .tar.sign
。
校验 Tar 文件的完整性
你如何知道刚下载的 Tar 文件是否被损坏?对于个人来说,一个损坏的 Tar 文件只会浪费你的宝贵时间,如果你是在为一个组织工作,那么可能会危及到组织的安全(这时你可能还有更大的问题需要担忧,但我们并不想让所有人都产生创伤后应激障碍!)。
为了验证我们的 Tar 文件的完整性,我们需要先解压它。目前,它是使用 XZ 压缩算法压缩的。因此,我将使用 unxz
工具(其实就是 xz --decompress
的别名)来解压 .tar.xz
格式的压缩文件。
unxz --keep linux-*.tar.xz
解压完成后,我们需要获取 Linus Torvalds 和 Greg KH 使用的 GPG 公开密钥。这些密钥用于对 Tar 文件进行签名。
gpg2 --locate-keys torvalds@kernel.org gregkh@kernel.org
你应该可以得到一个与我在我的电脑上看到的类似的结果:
$ gpg2 --locate-keys torvalds@kernel.org gregkh@kernel.org
gpg: /home/pratham/.gnupg/trustdb.gpg: trustdb created
gpg: key 38DBBDC86092693E: public key "Greg Kroah-Hartman <gregkh@kernel.org>" imported
gpg: Total number processed: 1
gpg: imported: 1
gpg: key 79BE3E4300411886: public key "Linus Torvalds <torvalds@kernel.org>" imported
gpg: Total number processed: 1
gpg: imported: 1
pub rsa4096 2011-09-23 [SC]
647F28654894E3BD457199BE38DBBDC86092693E
uid [ unknown] Greg Kroah-Hartman <gregkh@kernel.org>
sub rsa4096 2011-09-23 [E]
pub rsa2048 2011-09-20 [SC]
ABAF11C65A2970B130ABE3C479BE3E4300411886
uid [ unknown] Linus Torvalds <torvalds@kernel.org>
sub rsa2048 2011-09-20 [E]
在导入 Greg 和 Linus 的密钥后,我们可以使用 --verify
标志来验证 Tar 的完整性,操作如下:
gpg2 --verify linux-*.tar.sign
如果验证成功,你应该会看到如下的输出信息:
$ gpg2 --verify linux-*.tar.sign
gpg: assuming signed data in 'linux-6.5.5.tar'
gpg: Signature made Saturday 23 September 2023 02:46:13 PM IST
gpg: using RSA key 647F28654894E3BD457199BE38DBBDC86092693E
gpg: Good signature from "Greg Kroah-Hartman <gregkh@kernel.org>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 647F 2865 4894 E3BD 4571 99BE 38DB BDC8 6092 693E
务必查看是否存在 gpg: Good signature
的提示,然后再继续!
💡 你可以忽略以下警告:
WARNING: This key is not certified with a trusted signature! There is no indication that the signature belongs to the owner.
。我们已根据 Linus 和 Greg 的邮件地址获取了公开密钥,并无需对此警告感到担忧。
解压 Tar 文件
如果你顺利的进行到这里,意味着你的 Tar 文件完整性检查已经成功完成。接下来,我们将从 Tar 文件中解压出 Linux 内核的源码。
这个步骤十分简单,只需对 Tar 文件执行 tar -xf
命令,如下:
tar -xf linux-*.tar
在这里,-x
选项表示解压,-f
选项则用来告诉 Tar 文件的文件名。
这个解压过程可能需要几分钟时间,你可以先放松,耐心等待一下。
配置 Linux 内核
Linux 内核的构建过程会查找 .config
文件。顾名思义,这是一个配置文件,用于指定 Linux 内核的所有可能的配置选项。这是必需的文件。
获取 Linux 内核的 .config
文件有两种方式:
- 使用你的 Linux 发行版的配置作为基础(推荐做法)
- 使用默认的,通用的配置
💡 也有第三种方法,也就是从零开始,手动配置每一个选项,但注意,这需要配置超过 12,000 个选项。并不推荐这种方式,因为手动配置所有选项将花费大量的时间,并且你还需要理解每个启用和禁用选项的含义。
使用发行版提供的配置
使用你的 Linux 发行版提供的配置是一个安全的选择。 如果你只是跟随这个指南测试一个不是你的发行版提供的新内核,那么这就是推荐的方式。
你的 Linux 发行版的 Linux 内核配置文件会在以下两个位置之一:
- 大多数 Linux 发行版,如 Debian 和 Fedora 及其衍生版,将会把它存在
/boot/config-$(uname -r)
。 - 一些 Linux 发行版,比如 Arch Linux 将它整合在了 Linux 内核中。所以,可以在
/proc/config.gz
找到。
💡 如果两者都有,建议使用
/proc/config.gz
。这是因为它在只读文件系统中,所以是未被篡改的。
进入含有已经解压出的 Tar 文件的目录。
cd linux-*/
接着,复制你的 Linux 发行版的配置文件:
### Debian 和 Fedora 及其衍生版:
$ cp /boot/config-"$(uname -r)" .config
### Arch Linux 及其衍生版:
$ zcat /proc/config.gz > .config
更新配置文件
一旦完成这些步骤,接下来就需要“更新”配置文件了。因为你的发行版提供的配置很可能比你正在构建的 Linux 内核版本要旧。
💡 这同样适用于像 Arch Linux 和 Fedora 这样前沿的 Linux 发行版。 它们并不会因为有新版本可用就立刻发布更新。他们会进行一些质量控制工作,这必然会花费些时间。因此,即便是你的发行版提供的最新内核,相较于你在 kernel.org 上获取的版本也会滞后几个小版本。
要更新一个已有的 .config
文件,我们使用 make
命令搭配 olddefconfig
参数。简单解释一下,这个命令的意思是使用 旧的、默认的、配置。
这将使用“旧的配置文件”(当前保存为 .config
,这是你发行版配置的一份直接副本),并检查从上一版本以来 Linux 代码库中新加的任何配置选项。如果找到任何新的、未配置 的选项,该选项的默认配置值会被使用,并会对 .config
文件进行更新。
原来的 .config
文件将被重命名为 .config.old
进行备份,并将新的更改写入至 .config
文件。
make olddefconfig
以下是我机器上的输出:
$ file .config
.config: Linux make config build file, ASCII text
$ make olddefconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/menu.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTLD scripts/kconfig/conf
.config:8593:warning: symbol value 'm' invalid for USB_FOTG210_HCD
.config:8859:warning: symbol value 'm' invalid for USB_FOTG210_UDC
#
# configuration written to .config
#
针对 Debian 及其衍生版用户
Debian 及其衍生版为内核模块使用一个签名证书。默认情况下,你的计算机并不包含这个证书。
我推荐关闭启用模块签名的选项。具体如下所示:
./scripts/config --file .config --set-str SYSTEM_TRUSTED_KEYS ''
./scripts/config --file .config --set-str SYSTEM_REVOCATION_KEYS ''
如果你不这么做,在后面你进行 Linux 内核构建时,可能会导致构建失败。要注意这点。
使用自定义配置
如果你出于学习内核开发的目的学习如何构建 Linux 内核,那你应该这样做。
🚧 请注意,偏离你的 Linux 发行版的配置可能无法在实体硬件上“正常”工作。问题可能是特定硬件无法工作、Linux 内核无法启动等。
因此,我们只建议在虚拟机中使用。
你可以通过查看 make help 的输出 来查看 所有 可用的选项,但我们主要关注三个 make
目标:
defconfig
: 默认配置。allmodconfig
: 根据当前系统状态,尽可能地把项目构建为可加载模块(而非内建)。tinyconfig
: 极简的 Linux 内核。
由于 tinyconfig
目标只会构建少数项目,构建时间将会缩短。我个人选择它的原因主要有:
- 检查我在代码/工具链中做的修改是否正确,以及代码是否可以编译。
- 在虚拟机中只进行少数选项的测试。
🚧 在为 ARM 或 RISC-V 机器构建 Linux 内核时,你可能需要 DTB(设备树的二进制文件)。使用
tinyconfig
目标将不会启用构建 DTB 的选项,你的内核很可能无法启动。当然,你可以用 QEMU 在没有任何 DTB 的情况下启动 Linux 内核。但这篇文章并不会聚焦在此。或许你可以通过评论,让我在之后的时间里覆盖这个话题 😉
除非你确切地知道自己在做什么,否则你应当使用 defconfig
目标。 以下是我在我的电脑上运行的效果:
$ make defconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/menu.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTLD scripts/kconfig/conf
*** Default configuration is based on 'defconfig'
#
# configuration written to .config
#
修改配置
无论你是使用 Linux 发行版的配置并更新它,还是使用 defconfig
目标创建新的 .config
文件,你都可能希望熟悉如何修改这个配置文件。最可靠的修改方式是使用 menuconfig
或 nconfig
目标。
这两个目标的功能是相同的,只不过提供给你的界面有所不同。这是这两者间唯一的区别。我个人更偏向于使用 menuconfig
目标,但近来我发现 nconfig
在搜索选项时似乎更具直观性,所以我逐渐转向使用它。
首先,带着 menuconfig
目标运行 make
命令:
$ make menuconfig
HOSTCC scripts/kconfig/mconf.o
HOSTCC scripts/kconfig/lxdialog/checklist.o
HOSTCC scripts/kconfig/lxdialog/inputbox.o
HOSTCC scripts/kconfig/lxdialog/menubox.o
HOSTCC scripts/kconfig/lxdialog/textbox.o
HOSTCC scripts/kconfig/lxdialog/util.o
HOSTCC scripts/kconfig/lxdialog/yesno.o
HOSTLD scripts/kconfig/mconf
在此界面,你可以根据各选项的类型来进行切换操作。
有两类可切换选项:
- 布尔状态选项:这类选项只能关闭(
[ ]
)或作为内建组件开启([*]
)。 - 三态选项:这类选项可以关闭(
< >
)、内建(<*>
),或作为可加载模块(<M>
)进行构建。
想要了解更多关于某个选项的信息,使用上/下箭头键导航至该选项,然后按 <TAB>
键,直至底部的 < Help >
选项被选中,然后按回车键进行选择。此时就会显示关于该配置选项的帮助信息。
在修改选项时请务必谨慎。
当你满意配置后,按 <TAB>
键直到底部的 < Save >
选项被选中。然后按回车键进行选择。然后再次按回车键(记住,此时不要更改文件名),就能将更新后的配置保存到 .config
文件中。
构建 Linux 内核
构建 Linux 内核实际上十分简单。然而,在开始构建之前,让我们为自定义内核构建添加一个标签。我将使用字符串 -pratham
作为标签,并利用 LOCALVERSION
变量来实施。你可以使用以下命令实现配置:
./scripts/config --file .config --set-str LOCALVERSION "-pratham"
这一命令将 .config
文件中的 CONFIG_LOCALVERSION
配置选项设为我在结尾指定的字符串,即 -pratham
。当然,你也不必非得使用我所用的名字哦 😉
LOCALVERSION
选项可用于设置一个“本地”版本,它会被附加到通常的 x.y.z
版本方案之后,并在你运行 uname -r
命令时一并显示。
由于我正在构建的是 6.5.5 版本内核,而 LOCALVERSION
字符串被设为 -pratham
,因此,对我来说,最后的版本名将会是 6.5.5-pratham
。这么做的目的是确保我所构建的自定义内核不会与发行版所提供的内核产生冲突。
接下来,我们来真正地构建内核。可以用以下的命令完成此步骤:
make -j$(nproc) 2>&1 | tee log
这对大部分(99%)用户来说已经足够了。
其中的 -j
选项用于指定并行编译任务的数量。而 nproc
命令用于返回可用处理单位(包括线程)的数量。因此,-j$(nproc)
其实意味着“使用我拥有的 CPU 线程数相同数量的并行编译任务”。
2>&1
会将 STDOUT 和 STDIN 重定向到相同的文件描述符,并通过管道传输给 tee
命令,这会将输出存储在一个名为 log
的文件,并且在控制台打印出完全相同的文本。如果你在构建时遇到错误,并希望回顾日志来检查出了什么问题,这将会十分有用。遇到那种情况,你只需要简单执行 grep Error log
命令就能找到线索。
自定义 make 目标
在 Linux 内核的源文件夹中,make
命令有一些自定义的目标可供执行各种操作。这些主要作为开发者的参考。如果你的唯一目标是安装一个比你当前发行版更新的 Linux 内核,那么你完全可以跳过这部分内容 😉
构建目标
作为一名开发者,你可能只想构建 Linux 内核,或者只想构建模块,或者只想构建设备树二进制(DTB)。在这种情况下,你可以指定一个构建目标,然后 make
命令只会构建指定的项目,而不会构建其他的。
以下是一些构建目标:
vmlinux
:纯粹的 Linux 内核。modules
:可加载模块。dtbs
:设备树二进制文件(主要用于 ARM 和 RISC-V 架构)。all
:构建所有被标记了星号*
的项目(从make help
的输出中可以查看)。
通常情况下,你并不需要指定构建目标,因为它们都已经在构建列表中。所列出的目标是在你只想要测试某一个构建目标,而不是其他目标时的情况。
依据你的 计算机架构,构建完成的 Linux 内核镜像(存放在 /boot
目录)的名称会有所不同。
对于 x86_64
,Linux 内核的默认镜像名称是 bzImage
。因此,如果你只需要构建引导所需的 Linux 内核,你可以像下面这样设定 bzImage
为目标:
### 对于 x86_64
$ make bzImage
“那么如何在我的架构上找到用来调用 make
的目标名称呢?”
有两种方法。要么你可以执行 make help
之后查找在 Architecture specific targets
下,第一个前面带有星号 *
的选项。
或者,如果你希望自动完成,你可以利用 image_name
目标得到镜像的完全路径(相对路径),选择性地添加 -s
标志来获得有用的输出。
以下是我拥有的三台电脑的输出,一台是 x86_64
,另一台是 AArch64
,还有一台是 riscv
:
### x86_64
$ make -s image_name
arch/x86/boot/bzImage
### AArch64
$ make -s image_name
arch/arm64/boot/Image.gz
### RISC-V
$ make -s image_name
arch/riscv/boot/Image.gz
现在,要只构建 Linux 内核镜像,你可以这样进行:
make $(make -s image_name | awk -F '/' '{print $4}')
清理目标
如果你需要清理构建产生的文件,你可以用以下的目标来实现你的需求:
clean
:除了.config
文件外,删除几乎所有其他内容。mrproper
:执行了make clean
的所有操作外,还会删除.config
文件。distclean
:除了执行make mrproper
的所有操作外,还会清理任何补丁文件。
安装
一旦成功编译了 Linux 内核,接下来就是启动安装一些东西的时候了。“一些 东西?” 没错,我们至少构建了两种不同的东西,如果你使用的是 ARM 或 RISC-V 架构,那就有三种。我会在以下内容中详细解释。
🚧 虽然我将告诉你不同的安装方式,尤其是关于如何改变默认安装路径的方法,但如果你不确定自己在做什么,那么我不建议你这么做! 请慎重考虑,如果你决定走自定义的路线,那你需要自己负责后果。默认设置之所以存在,是因为它们有其特殊的原因 😉
安装内核模块
Linux 内核有部分在系统启动时并非必需的。这些部分被构建为可加载模块,即在需要时才进行加载和卸载。
所以,首先需要安装这些模块。这可以通过 modules_install
目标完成。必须使用 sudo
,因为模块会被安装在 /lib/modules/<kernel_release>-<localversion>
这个需要 root
权限的路径下。
这个过程不仅会安装内核模块,还会对其进行签名,所以可能需要一些时间。好消息是你可以通过之前提到的 -j$(nproc)
选项来并行执行安装任务,这样会快一些。😉
sudo make modules_install -j$(nproc)
给开发者的提示: 你可以通过设定
INSTALL_MOD_PATH
变量来指定一个不同的路径存放 Linux 模块,而不用默认的/lib/modules/<kernel_release>-<localversion>
,具体如下:sudo make modules_install INSTALL_MOD_PATH=<path>
另一个给开发者的提示: 你可以使用
INSTALL_MOD_STRIP
变量来决定是否需要剥离模块的调试符号。如果未设定该变量,调试符号不会被剥离。当设为1
时,符号信息将会被使用--strip-debug
选项剥离,随后该选项会传递给strip
(或者在使用 Clang 的时候传递给llvm-strip
)工具。
(可选)安装 Linux 内核头文件
如果你打算使用这个内核来支持树外模块,比如 ZFS 或英伟达 DKMS,或者打算尝试自行编写模块,你可能会需要 Linux 内核提供的头文件。
可以通过以下方式使用 headers_install
目标来安装 Linux 内核头文件:
sudo make headers_install
应使用 sudo
命令,因为这些头文件会被安装到 /usr
目录。同时还会在 /usr
目录内创建子目录 include/linux
,然后将头文件安装到 /usr/include/linux
内。
给开发者的提示: 通过设定
INSTALL_HDR_PATH
变量,你可以修改 Linux 内核头文件的安装路径。
安装 DTB(只针对 ARM 和 RISC-V)
如果你使用的是 x86_64 架构,那么你可以跳过此步骤!
如果你针对 ARM 或者 RISC-V 构建了内核,那么在运行 make
的过程中,设备树的二进制文件可能已经被编译出来了。你可以通过在 arch/<machine_architecture>/boot/dts
目录查找 .dtb
文件来确认这一点。
这里提供了一个快速检查的技巧:
### 对于 AArch32
$ find arch/arm/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for ARM32 were built"
### 对于 AArch64
$ find arch/arm64/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for ARM64 were built"
### 对于 RISC-V
$ find arch/riscv/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for RISC-V were built"
如果你看到出现 DTBs for <arch> were built
的消息,那么你可以开始安装 DTB。这可以通过 dtbs_install
目标来实现。
需要使用 sudo
,因为它们会被安装在 /boot/dtb-<kernel_release>-<localversion>
中,而这个目录是由 root
所拥有的。
sudo make dtbs_install
给开发者的提示: 就像安装模块一样,你可以使用
INSTALL_DTBS_PATH
变量指定一个自定义的路径来安装设备树二进制文件。
安装 Linux 内核
最后,我们来安装 Linux 内核本身!这可以通过 install
目标来完成,就像这样:
sudo make install
在这里必须使用 sudo
,因为 Linux 内核将被安装在 /boot
目录,而这个目录不允许普通用户写入。
💡 一般来讲,
install
目标也会更新引导加载程序,但是如果它没有成功,那可能是不支持你使用的引导加载程序。如果你没有使用 GRUB 作为你的引导加载程序,请一定要阅读你引导加载程序的使用手册 😉
给开发者的提示: 并不奇怪,
INSTALL_PATH
变量被用来设定 Linux 内核的安装位置,而非默认的/boot
目录。
针对 Arch Linux 用户的说明
如果你尝试执行了 make install
命令,可能已经注意到产生了错误。错误如下:
$ sudo make install
INSTALL /boot
Cannot find LILO.
要在 Arch Linux 上实际完成 Linux 内核的安装,我们需要手动复制 Linux 内核镜像文件。别担心,如果你使用的是 Arch Linux,手动操作应该是家常便饭了。( ͡° ͜ʖ ͡°)
可以使用以下命令完成这个步骤:
sudo install -Dm644 "$(make -s image_name)" /boot/vmlinuz-<kernel_release>-<localversion>
因为我编译的是 6.5.5 版本的内核,所以我将会执行下面这条命令,你可以根据你的实际情况进行适当调整:
sudo install -Dm644 "$(make -s image_name)" /boot/vmlinuz-6.5.5-pratham
虽然不是必须的,但最好复制一份名为 System.map
的文件。既然你已经在操作了,一并也复制了 .config
文件吧 😉
sudo cp -vf System.map /boot/System.map-<kernel_release>-<localversion>
sudo cp -vf .config /boot/config-<kernel_release>-<localversion>
生成初始 RAM 磁盘
当你安装 Arch Linux 时,可能已经了解过 mkinitcpio
这个工具。现在,我们将使用它来创建初始的 RAM 磁盘。
首先,我们需要创建一个预设文件。向 /etc/mkinitcpio.d/linux-<localversion>.preset
文件中添加以下内容,根据实际需要来替换 <kernel_release>
和 <localversion>
。
ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-<kernel_release>-<localversion>"
PRESETS=('default' 'fallback')
default_image="/boot/initramfs-<kernel_release>-<localversion>.img"
fallback_options="-S autodetect"
配置完成后,执行下面的命令来生成初始 RAM 磁盘:
sudo mkinitcpio -p linux-<localversion>
我自己的电脑上得到的输出如下,你的结果应该会类似!
$ sudo mkinitcpio -p linux-pratham
==> Building image from preset: /etc/mkinitcpio.d/linux-pratham.preset: 'default'
==> Using configuration file: '/etc/mkinitcpio.conf'
-> -k /boot/vmlinuz-6.5.5-pratham -c /etc/mkinitcpio.conf -g /boot/initramfs-6.5.5-pratham.img
==> Starting build: '6.5.5-pratham'
-> Running build hook: [base]
-> Running build hook: [udev]
-> Running build hook: [autodetect]
-> Running build hook: [modconf]
-> Running build hook: [kms]
-> Running build hook: [keyboard]
==> WARNING: Possibly missing firmware for module: 'xhci_pci'
-> Running build hook: [keymap]
-> Running build hook: [consolefont]
==> WARNING: consolefont: no font found in configuration
-> Running build hook: [block]
-> Running build hook: [filesystems]
-> Running build hook: [fsck]
==> Generating module dependencies
==> Creating zstd-compressed initcpio image: '/boot/initramfs-6.5.5-pratham.img'
==> Image generation successful
==> Building image from preset: /etc/mkinitcpio.d/linux-pratham.preset: 'fallback'
==> Using configuration file: '/etc/mkinitcpio.conf'
==> WARNING: No image or UKI specified. Skipping image 'fallback'
初始 RAM 磁盘已成功生成,现在我们可以进入下一步,更新引导加载器!
更新 GRUB
一旦所有必要的文件已成功复制到其对应的位置,接下来,我们将进行 GRUB 的更新。
使用以下命令对 GRUB 引导加载器进行更新:
sudo grub-mkconfig -o /boot/grub/grub.cfg
💡 如果你使用的引导加载器不是 GRUB,请参看 Arch Wiki 中相关的引导加载器文档。
注意,更新 GRUB 并不会直接使新的内核版本设为默认启动选项。在引导时,请在启动菜单中手动选择新的内核版本。
你可以通过选择 Advanced options for Arch Linux
菜单,并在随后的菜单中选择 Arch Linux, with Linux <kernel_release>-<localversion>
来启用新版的 Linux 内核。
重启电脑
恭喜你!你已经完成了获取 Linux 内核源代码、进行配置、构建以及安装等所有步骤。现在只需要通过重启电脑并进入新构建和安装的 Linux 内核,就可以开始享受你的努力成果了。
启动时,请确保从引导加载器中选择正确的 Linux 内核版本。系统启动后,运行 uname -r
命令来确认你正在使用预期的 Linux 内核。
以下是我自己的电脑输出的内容:
$ uname -r
6.5.5-pratham
是时候开始庆祝了! 🎉
卸载操作
🚧 提示:在删除当前正在使用的内核版本之前,你应该首先切换至较旧的内核版本。
可能你的 Linux 发行版所使用的 Linux 内核版本就是你手动编译的版本,或者你自行编译了新的内核并注意到应卸载旧的内核以节省空间,于是你开始想如何才能卸载。当然,虽然我们无法简单地运行 make uninstall
命令,但这并不代表没有其他的方法!
我们清楚各个文件的安装位置,因此删除它们相对简单。
### 删除内核模块
$ rm -rf /lib/modules/<kernel_release>-<localversion>
### 删除设备树二进制文件
$ rm -rf /boot/dtb-<kernel_release>-<localversion>
### 删除 Linux 内核本身
$ rm -vf /boot/{config,System,vmlinuz}-<kernel_release>-<localversion>
总结
这个过程不是一次简单的旅程,是吧?但是现在,我们终于抵达了终点。我们一起学习了手动编译 Linux 内核的全过程,包括安装依赖、获取和验证源码、解压源码、配置 Linux 内核、构建内核以及安装内核。
如果你喜欢这个详细的步骤指南,请给我留言反馈。如果在操作过程中遇到问题,也欢迎提出,让我知道!
(题图:MJ/853481c5-87e3-42aa-8ace-e9ddfa232f75)
via: https://itsfoss.com/compile-linux-kernel/
作者:Pratham Patel 选题:lujun9972 译者:ChatGPT 校对:wxy
- Facebook✓
- Google+✓
- Instapaper✓
- Tweet✓
我们将介绍几款可在 Linux 系统中使用的白板应用程序。我相信,这些信息对你将大有帮助。请继续阅读!
一般而言,数字白板是一种大型互动显示面板的工具。比如:平板、大屏手机、触控笔记本和表面显示设备等,就是一些常见的白板设备类型。
假设教师使用了白板,你就可以使用触控笔、手写笔、手指甚至鼠标在设备屏幕上进行绘画、书写或操作元素。这意味着,你可以在白板上进行拖动、点击、删除和绘画,就如同用笔在纸上一样。
但是,要做到这一切,就需要一款软件来支持这些功能,即实现触控和显示之间的精细协调。
现在的市面上,有很多商业应用都能完成这项工作。然而,在这篇文章中,我们将会重点介绍一些自由且开源的、并且可在 Linux 系统中运行的白板应用程序。
Linux 系统白板应用程序佳选
1、Xournal++
在我们的推荐名单上,第一个就是 Xournal++。我认为,这是此列表中最好的应用程序。它十分稳定,而且已经存在了一段时间。
Xournal++ 允许你进行书写、绘画,以及其他常见的在纸上进行的各种事情。它支持手写、自定义荧光笔、橡皮擦等工具。此外,它还有其他诸多功能,例如:支持图层,支持添加外部图像,支持添加音频,具有多页特性等。
该应用程序兼容几乎所有的压敏输入设备,例如 Wacom、Huion 和 XP-Pen。我曾在触摸板笔记本上进行过测试,经过少量设置修改后,它就可以正常工作了。因此,你可以开始使用任何你手头上的触控设备。
Xournal++ 是使用 C++ 和 GTK3 进行编写的。
对于 Linux 系统,你可以按下述步骤进行安装。此程序是完全免费的,且适用于 Linux、macOS 和 Windows 系统。如果你想在移动设备上尝试,也提供了 BETA 版本。
安装步骤:
该应用程序可以通过 AppImage、Snap、Flatpak 和 deb 包进行安装。同时,也可以通过 PPA 的方式,安装至基于 Ubuntu/Debian 的系统。
特别的是,还为 Fedora、SUSE 和 Arch 提供了专用的安装包。你只需要点击下方的链接,就可以按你所需的格式,下载相应的执行程序。
更多信息:
2、OpenBoard
我们接下来想要重点介绍的是 OpenBoard。这是一个简洁易用的白板绘画应用,它不会通过过多的选项干扰你的使用体验。
该软件非常适合初学者以及在线课程的初级学生来使用进行笔记。
OpenBoard 内置了丰富的功能,包括颜色、画刷、文本、简单的绘图形状,页面支持等等。此应用基于 Qt 技术构建。
安装步骤:
此应用仅为 Ubuntu 提供独立的 deb 包。你可以通过以下链接下载。
更多信息:
3、Scrivano
Scrivano 是一款新推出的基于 Qt 技术的白板应用。其设计理念强调易用性,能在记笔记和其他绘画活动中提供高效的功能表现。
它提供了一些独特的功能,如吸附到网格、创建自定义贴纸、填充笔触、激光选项等等。
此外,你还可以在此工具中导入 PDF 文件,并利用各种功能对其进行批注。
以下是一些值得关注的功能。
你可以在我们关于 该应用官方评述 中了解更多关于 Scrivano 的信息。
你可以 对你的系统进行 Flatpak 配置以使用 Flathub,然后点击下面的按钮,通过 Flathub 进行安装。
更多信息:
4、Notelab
NoteLab 是一款具有十年历史的老牌白板应用。它是一个自由开源的应用,内含丰富的功能集,因此你可以理解这款应用的稳定性和广受欢迎的程度。
以下是其中的部分特性:
- 此应用支持所有常用的图片格式作为导出选项,例如 SVG、PNG、JPG、BMP 等。
- 提供了可配置的笔和纸的自定义选项
- 内建的内存管理器,用于指定 NoteLab 所使用的内存量。
- 不同格式的纸张可供选择,如宽格式、大学格式和图形纸。
- 含所有标准绘图工具。
- 对任何笔记部分都可以进行调整大小、移动、删除、改变颜色等多种操作。
然而,这是一个由 Java 编写并以 .jar 文件形式发布的应用,所以你需要 Java 运行环境来运行它。你可以参考我们的指南在 Linux 系统中安装 Java 或 JRE。
如何安装:
NoteLab 提供了独立的可执行 .jar 文件,可以通过下方链接从 SourceForge 下载。请记住,要运行此应用,你需要 JRE。
更多信息:
5、Rnote
我们此次要介绍的第五款应用叫做 Rnote。Rnote 是一款在触屏设备上进行手写笔记的绝佳应用程序。这款应用基于矢量图像技术,不仅便于进行绘图和注释图片,还方便对 PDF 文件进行标注。它支持本地的 .rnote 文件格式,并提供了 png、jpeg、SVG 和 PDF 的导入/导出选项。
Rnote 最酷的一项特性是它支持 Xournal++ 的文件格式(即本列表的首款应用程序),这使它成为必备的工具。
Rnote 使用 GTK4 和 Rust 进行开发,非常适合在 GNOME 桌面以及各类 Linux 系统上运行。
需要注意的是,该应用程序目前仍在开发中。
如何安装:
此应用以 Flatpak 包的形式提供。你可以使用 此指南 为你的 Linux 系统安装 Flatpak,然后点击下方的按钮进行安装。
更多信息:
6、Lorien
Lorien 是一个能让你利用多种工具创造个性化笔记的理想数字笔记本软件。Lorien 是一个基于 Godot 游戏引擎的跨平台、自由开源的“无限画布绘图/笔记”应用。这款应用十分适合于快速记取头脑风暴会议中的笔记。
其工具箱相当标准,拥有自由手绘刷、橡皮擦、直线工具和选择工具等。你可以移动或删除你的笔划部分——这些在运行时会被视为点的集合进行渲染。
如何安装:
使用 Lorien 不需要安装。你可以从下面的链接下载一个自我包含的可执行文件(下载 tar 文件)。下载后,解压文件并双击即可运行。
更多信息:
7、Rainbow Board
Rainbow Board 是一款基于 Electron 和 React 的自由开源白板应用。通常,由于性能和臃肿的问题,人们并不喜欢 Electron 应用。但考虑到我们正在列出这个分类的应用,我认为值得一提。
它提供了一个标准的画布,支持触控和手写输入。工具箱包括刷子大小、颜色、填充颜色、字体以及撤销和重做操作。你可以将你的绘制结果导出为 PNG 或 SVG 文件。
如何安装:
此应用可以作为 Snap、Flatpak 或独立的 deb 安装程序下载。你可以从下面的链接中得到它们。
更多信息:
特别推荐
在这里,我想额外提到的两款绘图应用分别是 Vectr 和 Excalidraw。这两款应用都是基于网络的白板绘图应用。它们被单独归类是因为它们并非传统意义上的桌面应用。
因此,如果你不愿安装其他应用,或者你使用的学校或工作系统中无法进行安装,你可以选择在浏览器中使用这些网络应用。下面是它们的网址。
结束语
以上是一些适用于 Linux 及其他操作系统的现代白板 绘图 应用。由于大流行病和居家工作环境的影响,你们许多人可能在使用笔和纸进行在线课程或会议的笔记记录。我相信这些软件会在你的学习和工作中提供帮助。
试一试这些软件,你肯定能找到最适合你的。对此清单如有任何评论或反馈,请在下方的留言区域告诉我。
干杯。
(题图:MJ/373801b1-50f2-4078-8961-fcea78623f2f)
via: https://www.debugpoint.com/top-whiteboard-applications-linux/
- Facebook✓
- Google+✓
- Instapaper✓
- Tweet✓

大家好!今天我和一个朋友讨论 Git 的工作原理,我们感到奇怪,Git 是如何存储你的文件的?我们知道它存储在 .git
目录中,但具体到 .git
中的哪个位置,各个版本的历史文件又被存储在哪里呢?
以这个博客为例,其文件存储在一个 Git 仓库中,其中有一个文件名为 content/post/2019-06-28-brag-doc.markdown
。这个文件在我的 .git
文件夹中具体的位置在哪里?过去的文件版本又被存储在哪里?那么,就让我们通过编写一些简短的 Python 代码来探寻答案吧。
Git 把文件存储在 .git/objects 之中
你的仓库中,每一个文件的历史版本都被储存在 .git/objects
中。比如,对于这个博客,.git/objects
包含了 2700 多个文件。
$ find .git/objects/ -type f | wc -l
2761
注意:
.git/objects
包含的信息,不仅仅是 “仓库中每一个文件的所有先前版本”,但我们暂不详细讨论这一内容。
这里是一个简短的 Python 程序(find-git-object.py),它可以帮助我们定位在 .git/objects
中的特定文件的具体位置。
import hashlib
import sys
def object_path(content):
header = f"blob {len(content)}\0"
data = header.encode() + content
sha1 = hashlib.sha1()
sha1.update(data)
digest = sha1.hexdigest()
return f".git/objects/{digest[:2]}/{digest[2:]}"
with open(sys.argv[1], "rb") as f:
print(object_path(f.read()))
此程序的主要操作如下:
- 读取文件内容
- 计算一个头部(
blob 16673\0
),并将其与文件内容合并 - 计算出文件的 sha1 校验和(此处为
e33121a9af82dd99d6d706d037204251d41d54
) - 将这个 sha1 校验和转换为路径(如
.git/objects/e3/3121a9af82dd99d6d706d037204251d41d54
)
运行的方法如下:
$ python3 find-git-object.py content/post/2019-06-28-brag-doc.markdown
.git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
术语解释:“内容寻址存储”
这种存储策略的术语为“内容寻址存储”,它指的是对象在数据库中的文件名与文件内容的哈希值相同。
内容寻址存储的有趣之处就是,假设我有两份或许多份内容完全相同的文件,在 Git 的数据库中,并不会因此占用额外空间。如果内容的哈希值是 aabbbbbbbbbbbbbbbbbbbbbbbbb
,它们都会被存储在 .git/objects/aa/bbbbbbbbbbbbbbbbbbbbb
中。
这些对象是如何进行编码的?
如果我尝试在 .git/objects
目录下查看这个文件,显示的内容似乎有一些奇怪:
$ cat .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
x^A<8D><9B>}s<E3>Ƒ<C6><EF>o|<8A>^Q<9D><EC>ju<92><E8><DD><9C><9C>*<89>j<FD>^...
这是怎么回事呢?让我们来运行 file
命令检查一下:
$ file .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
.git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54: zlib compressed data
原来,它是压缩的!我们可以编写一个小巧的 Python 程序—— decompress.py
,然后用 zlib
模块去解压这些数据:
import zlib
import sys
with open(sys.argv[1], "rb") as f:
content = f.read()
print(zlib.decompress(content).decode())
让我们来解压一下看看结果:
$ python3 decompress.py .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
blob 16673---
title: "Get your work recognized: write a brag document"
date: 2019-06-28T18:46:02Z
url: /blog/brag-documents/
categories: []
---
... the entire blog post ...
结果显示,这些数据的编码方式非常简单:首先有 blob 16673\0
标识,其后就是文件的全部内容。
这里并没有差异性数据(diff)
这里有一件我第一次知道时让我感到惊讶的事:这里并没有任何差异性数据!那个文件是该篇博客文章的第 9 个版本,但 Git 在 .git/objects
目录中存储的版本是完整文件内容,而并非与前一版本的差异。
尽管 Git 实际上有时候会以差异性数据存储文件(例如,当你运行 git gc
时,为了提升效率,它可能会将多个不同的文件封装成 “打包文件”),但在我个人经验中,我从未需要关注这个细节,所以我们不在此深入讨论。然而,关于这种格式如何工作,Aditya Mukerjee 有篇优秀的文章 《拆解 Git 的打包文件》。
博客文章的旧版本在哪?
你可能会好奇:如果在我修复了一些错别字之前,这篇博文已经存在了 8 个版本,那它们在 .git/objects
目录中的位置是哪里?我们如何找到它们呢?
首先,我们来使用 git log
命令来查找改动过这个文件的每一个提交:
$ git log --oneline content/post/2019-06-28-brag-doc.markdown
c6d4db2d
423cd76a
7e91d7d0
f105905a
b6d23643
998a46dd
67a26b04
d9999f17
026c0f52
72442b67
然后,我们选择一个之前的提交,比如 026c0f52
。提交也被存储在 .git/objects
中,我们可以尝试在那里找到它。但是失败了!因为 ls .git/objects/02/6c*
没有显示任何内容!如果有人告诉你,“我们知道有时 Git 会打包对象来节省空间,我们并不需过多关心它”,但现在,我们需要去面对这个问题了。
那就让我们去解决它吧。
让我们开始解包一些对象
现在我们需要从打包文件中解包出一些对象。我在 Stack Overflow 上查找了一下,看起来我们可以这样进行操作:
$ mv .git/objects/pack/pack-adeb3c14576443e593a3161e7e1b202faba73f54.pack .
$ git unpack-objects < pack-adeb3c14576443e593a3161e7e1b202faba73f54.pack
这种直接对库进行手术式的做法让人有些紧张,但如果我误操作了,我还可以从 Github 上重新克隆这个库,所以我并不太担心。
解包所有的对象文件后,我们得到了更多的对象:大约有 20000 个,而不是原来的大约 2700 个。看起来很酷。
find .git/objects/ -type f | wc -l
20138
我们回头再看看提交
现在我们可以继续看看我们的提交 026c0f52
。我们之前说过 .git/objects
中并不都是文件,其中一部分是提交!为了弄清楚我们的旧文章 content/post/2019-06-28-brag-doc.markdown
是在哪里被保存的,我们需要深入查看这个提交。
首先,我们需要在 .git/objects
中查看这个提交。
查看提交的第一步:找到提交
经过解包后,我们现在可以在 .git/objects/02/6c0f5208c5ea10608afc9252c4a56c1ac1d7e4
中找到提交 026c0f52
,我们可以用下面的方法去查看它:
$ python3 decompress.py .git/objects/02/6c0f5208c5ea10608afc9252c4a56c1ac1d7e4
commit 211tree 01832a9109ab738dac78ee4e95024c74b9b71c27
parent 72442b67590ae1fcbfe05883a351d822454e3826
author Julia Evans <julia@jvns.ca> 1561998673 -0400
committer Julia Evans <julia@jvns.ca> 1561998673 -0400
brag doc
我们也可以用 git cat-file -p 026c0f52
命令来获取相同的信息,这个命令能起到相同的作用,但是它在格式化数据时做得更好一些。(-p
选项意味着它能够以更友好的方式进行格式化)
查看提交的第二步:找到树
这个提交包含一个树。树是什么呢?让我们看一下。树的 ID 是 01832a9109ab738dac78ee4e95024c74b9b71c27
,我们可以使用先前的 decompress.py
脚本查看这个 Git 对象,尽管我不得不移除 .decode()
才能避免脚本崩溃。
$ python3 decompress.py .git/objects/01/832a9109ab738dac78ee4e95024c74b9b71c27
这个输出的格式有些难以阅读。主要的问题在于,该提交的哈希(\xc3\xf7$8\x9b\x8dO\x19/\x18\xb7}|\xc7\xce\x8e…
)是原始字节,而没有进行十六进制的编码,因此我们看到 \xc3\xf7$8\x9b\x8d
而非 c3f76024389b8d
。我打算切换至 git cat-file -p
命令,它能以更友好的方式显示数据,我不想自己编写一个解析器。
$ git cat-file -p 01832a9109ab738dac78ee4e95024c74b9b71c27
100644 blob c3f76024389b8d4f192f18b77d7cc7ce8e3a68ad .gitignore
100644 blob 7ebaecb311a05e1ca9a43f1eb90f1c6647960bc1 README.md
100644 blob 0f21dc9bf1a73afc89634bac586271384e24b2c9 Rakefile
100644 blob 00b9d54abd71119737d33ee5d29d81ebdcea5a37 config.yaml
040000 tree 61ad34108a327a163cdd66fa1a86342dcef4518e content <-- 这是我们接下来的目标
040000 tree 6d8543e9eeba67748ded7b5f88b781016200db6f layouts
100644 blob 22a321a88157293c81e4ddcfef4844c6c698c26f mystery.rb
040000 tree 8157dc84a37fca4cb13e1257f37a7dd35cfe391e scripts
040000 tree 84fe9c4cb9cef83e78e90a7fbf33a9a799d7be60 static
040000 tree 34fd3aa2625ba784bced4a95db6154806ae1d9ee themes
这是我在这次提交时库的根目录中所有的文件。看起来我曾经不小心提交了一个名为 mystery.rb
的文件,后来我删除了它。
我们的文件在 content
目录中,接下来让我们看看那个树:61ad34108a327a163cdd66fa1a86342dcef4518e
查看提交的第三步:又一棵树
$ git cat-file -p 61ad34108a327a163cdd66fa1a86342dcef4518e
040000 tree 1168078878f9d500ea4e7462a9cd29cbdf4f9a56 about
100644 blob e06d03f28d58982a5b8282a61c4d3cd5ca793005 newsletter.markdown
040000 tree 1f94b8103ca9b6714614614ed79254feb1d9676c post <-- 我们接下来的目标!
100644 blob 2d7d22581e64ef9077455d834d18c209a8f05302 profiler-project.markdown
040000 tree 06bd3cee1ed46cf403d9d5a201232af5697527bb projects
040000 tree 65e9357973f0cc60bedaa511489a9c2eeab73c29 talks
040000 tree 8a9d561d536b955209def58f5255fc7fe9523efd zines
还未结束……
查看提交的第四步:更多的树……
我们要寻找的文件位于 post/
目录,因此我们需要进一步探索:
$ git cat-file -p 1f94b8103ca9b6714614614ed79254feb1d9676c
.... 省略了大量行 ...
100644 blob 170da7b0e607c4fd6fb4e921d76307397ab89c1e 2019-02-17-organizing-this-blog-into-categories.markdown
100644 blob 7d4f27e9804e3dc80ab3a3912b4f1c890c4d2432 2019-03-15-new-zine--bite-size-networking-.markdown
100644 blob 0d1b9fbc7896e47da6166e9386347f9ff58856aa 2019-03-26-what-are-monoidal-categories.markdown
100644 blob d6949755c3dadbc6fcbdd20cc0d919809d754e56 2019-06-23-a-few-debugging-resources.markdown
100644 blob 3105bdd067f7db16436d2ea85463755c8a772046 2019-06-28-brag-doc.markdown <-- 我们找到了!!!
在此,2019-06-28-brag-doc.markdown
之所以位于列表最后,是因为在发布时它是最新的博文。
查看提交的第五步:我们终于找到它!
经过努力,我们找到了博文历史版本所在的对象文件!太棒了!它的哈希值是 3105bdd067f7db16436d2ea85463755c8a772046
,因此它位于 git/objects/31/05bdd067f7db16436d2ea85463755c8a772046
。
我们可以使用 decompress.py
来查看它:
$ python3 decompress.py .git/objects/31/05bdd067f7db16436d2ea85463755c8a772046 | head
blob 15924---
title: "Get your work recognized: write a brag document"
date: 2019-06-28T18:46:02Z
url: /blog/brag-documents/
categories: []
---
... 文件的剩余部分在此 ...
这就是博文的旧版本!如果我执行命令 git checkout 026c0f52 content/post/2019-06-28-brag-doc.markdown
或者 git restore --source 026c0f52 content/post/2019-06-28-brag-doc.markdown
,我就会获取到这个版本。
这样遍历树就是 git log 的运行机制
我们刚刚经历的整个过程(找到提交、逐层遍历目录树、搜索所需文件名)看似繁琐,但实际上当我们执行 git log content/post/2019-06-28-brag-doc.markdown
时,背后就是这样在运行。它需要逐个检查你历史记录中的每一个提交,在每个提交中核查 content/post/2019-06-28-brag-doc.markdown
的版本(例如在这个案例中为 3105bdd067f7db16436d2ea85463755c8a772046
),并查看它是否自上一提交以来有所改变。
这就是为什么有时 git log FILENAME
会执行的有些缓慢 —— 我的这个仓库中有 3000 个提交,它需要对每个提交做大量的工作,来判断该文件是否在该提交中发生过变化。
我有多少个历史版本的文件?
目前,我在我的博客仓库中跟踪了 1530 个文件:
$ git ls-files | wc -l
1530
但历史文件有多少呢?我们可以列出 .git/objects
中所有的内容,看看有多少对象文件:
$ find .git/objects/ -type f | grep -v pack | awk -F/ '{print $3 $4}' | wc -l
20135
但并不是所有这些都代表过去版本的文件 —— 正如我们之前所见,许多都是提交和目录树。不过,我们可以编写一个小小的 Python 脚本 find-blobs.py
,遍历所有对象并检查是否以 blob
开头:
import zlib
import sys
for line in sys.stdin:
line = line.strip()
filename = f".git/objects/{line[0:2]}/{line[2:]}"
with open(filename, "rb") as f:
contents = zlib.decompress(f.read())
if contents.startswith(b"blob"):
print(line)
$ find .git/objects/ -type f | grep -v pack | awk -F/ '{print $3 $4}' | python3 find-blobs.py | wc -l
6713
于是,看起来在我的 Git 仓库中存放的旧文件版本有 6713 - 1530 = 5183
个,Git 会为我保存这些文件,以备我想着要恢复它们时使用。太好了!
就这些啦!
在 这个 gist 中附上了全部的此篇文章所用代码,其实没多少。
我以为我已经对 Git 的工作方式了如指掌,但我以前从未真正涉及过打包文件,所以这次探索很有趣。我也很少思考当我让 git log
跟踪一个文件的历史时,它实际上有多大的工作量,因此也很开心能深入研究这个。
作为一个有趣的后续:我提交这篇博文后,Git 就警告我仓库中的对象太多(我猜 20,000 太多了!),并运行 git gc
将它们全部压缩成打包文件。所以现在我的 .git/objects
目录已经被压缩得十分小了:
$ find .git/objects/ -type f | wc -l
14
(题图:MJ/319a396c-6f3f-4891-b051-261312c8ea9a)
via: https://jvns.ca/blog/2023/09/14/in-a-git-repository--where-do-your-files-live-/
作者:Julia Evans 选题:lujun9972 译者:ChatGPT 校对:wxy
- Facebook✓
- Google+✓
- Instapaper✓
- Tweet✓

今天我在想 —— 当你在 Linux 上运行一个简单的 “Hello World” Python 程序时,发生了什么,就像下面这个?
print("hello world")
这就是在命令行下的情况:
$ python3 hello.py
hello world
但是在幕后,实际上有更多的事情在发生。我将描述一些发生的情况,并且(更重要的是)解释一些你可以用来查看幕后情况的工具。我们将用 readelf
、strace
、ldd
、debugfs
、/proc
、ltrace
、dd
和 stat
。我不会讨论任何只针对 Python 的部分 —— 只研究一下当你运行任何动态链接的可执行文件时发生的事情。
0、在执行 execve 之前
要启动 Python 解释器,很多步骤都需要先行完成。那么,我们究竟在运行哪一个可执行文件呢?它在何处呢?
1、解析 python3 hello.py
Shell 将 python3 hello.py
解析成一条命令和一组参数:python3
和 ['hello.py']
。
在此过程中,可能会进行一些如全局扩展等操作。举例来说,如果你执行 python3 *.py
,Shell 会将其扩展到 python3 hello.py
。
2、确认 python3 的完整路径
现在,我们了解到需要执行 python3
。但是,这个二进制文件的完整路径是什么呢?解决办法是使用一个名为 PATH
的特殊环境变量。
自行验证:在你的 Shell 中执行 echo $PATH
。对我来说,它的输出如下:
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
当执行一个命令时,Shell 将会依序在 PATH
列表中的每个目录里搜索匹配的文件。
对于 fish
(我的 Shell),你可以在 这里 查看路径解析的逻辑。它使用 stat
系统调用去检验是否存在文件。
自行验证:执行 strace -e stat bash
,然后运行像 python3
这样的命令。你应该会看到如下输出:
stat("/usr/local/sbin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory)
stat("/usr/sbin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory)
stat("/usr/bin/python3", {st_mode=S_IFREG|0755, st_size=5479736, ...}) = 0
你可以观察到,一旦在 /usr/bin/python3
找到了二进制文件,搜索就会立即终止:它不会继续去 /sbin
或 /bin
中查找。
对 execvp 的补充说明
如果你想要不用自己重新实现,而运行和 Shell 同样的 PATH
搜索逻辑,你可以使用 libc 函数 execvp
(或其它一些函数名中含有 p
的 exec*
函数)。
3、stat 的背后运作机制
你可能在思考,Julia,stat
到底做了什么?当你的操作系统要打开一个文件时,主要分为两个步骤:
- 它将 文件名 映射到一个包含该文件元数据的 inode
- 它利用这个 inode 来获取文件的实际内容
stat
系统调用只是返回文件的 inode 内容 —— 它并不读取任何的文件内容。好处在于这样做速度非常快。接下来让我们一起来快速了解一下 inode。(在 Dmitry Mazin 的这篇精彩文章 《磁盘就是一堆比特》中有更多的详细内容)
$ stat /usr/bin/python3
File: /usr/bin/python3 -> python3.9
Size: 9 Blocks: 0 IO Block: 4096 symbolic link
Device: fe01h/65025d Inode: 6206 Links: 1
Access: (0777/lrwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2023-08-03 14:17:28.890364214 +0000
Modify: 2021-04-05 12:00:48.000000000 +0000
Change: 2021-06-22 04:22:50.936969560 +0000
Birth: 2021-06-22 04:22:50.924969237 +0000
自行验证:我们来实际查看一下硬盘上 inode 的确切位置。
首先,我们需要找出硬盘的设备名称:
$ df
...
tmpfs 100016 604 99412 1% /run
/dev/vda1 25630792 14488736 10062712 60% /
...
看起来它是 /dev/vda1
。接着,让我们寻找 /usr/bin/python3
的 inode 在我们硬盘上的确切位置(在 debugfs 提示符下输入 imap
命令):
$ sudo debugfs /dev/vda1
debugfs 1.46.2 (28-Feb-2021)
debugfs: imap /usr/bin/python3
Inode 6206 is part of block group 0
located at block 658, offset 0x0d00
我不清楚 debugfs
是如何确定文件名对应的 inode 的位置,但我们暂时不需要深入研究这个。
现在,我们需要计算硬盘中 “块 658,偏移量 0x0d00” 处是多少个字节,这个大的字节数组就是你的硬盘。每个块有 4096 个字节,所以我们需要到 4096 * 658 + 0x0d00
字节。使用计算器可以得到,这个值是 2698496
。
$ sudo dd if=/dev/vda1 bs=1 skip=2698496 count=256 2>/dev/null | hexdump -C
00000000 ff a1 00 00 09 00 00 00 f8 b6 cb 64 9a 65 d1 60 |...........d.e.`|
00000010 f0 fb 6a 60 00 00 00 00 00 00 01 00 00 00 00 00 |..j`............|
00000020 00 00 00 00 01 00 00 00 70 79 74 68 6f 6e 33 2e |........python3.|
00000030 39 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |9...............|
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000060 00 00 00 00 12 4a 95 8c 00 00 00 00 00 00 00 00 |.....J..........|
00000070 00 00 00 00 00 00 00 00 00 00 00 00 2d cb 00 00 |............-...|
00000080 20 00 bd e7 60 15 64 df 00 00 00 00 d8 84 47 d4 | ...`.d.......G.|
00000090 9a 65 d1 60 54 a4 87 dc 00 00 00 00 00 00 00 00 |.e.`T...........|
000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
好极了!我们找到了 inode!你可以在里面看到 python3
,这是一个很好的迹象。我们并不打算深入了解所有这些,但是 Linux 内核的 ext4 inode 结构 指出,前 16 位是 “模式”,即权限。所以现在我们将看一下 ffa1
如何对应到文件权限。
ffa1
对应的数字是0xa1ff
,或者 41471(因为 x86 是小端表示)- 41471 用八进制表示就是
0120777
- 这有些奇怪 - 那个文件的权限肯定可以是
777
,但前三位是什么呢?我以前没见过这些!你可以在 inode 手册页 中找到012
的含义(向下滚动到“文件类型和模式”)。这里有一个小的表格说012
表示 “符号链接”。
我们查看一下这个文件,确实是一个权限为 777
的符号链接:
$ ls -l /usr/bin/python3
lrwxrwxrwx 1 root root 9 Apr 5 2021 /usr/bin/python3 -> python3.9
它确实是!耶,我们正确地解码了它。
4、准备复刻
我们尚未准备好启动 python3
。首先,Shell 需要创建一个新的子进程来进行运行。在 Unix 上,新的进程启动的方式有些特殊 - 首先进程克隆自己,然后运行 execve
,这会将克隆的进程替换为新的进程。
自行验证: 运行 strace -e clone bash
,然后运行 python3
。你应该会看到类似下面的输出:
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f03788f1a10) = 3708100
3708100
是新进程的 PID,这是 Shell 进程的子进程。
这里有些工具可以查看进程的相关信息:
pstree
会展示你的系统中所有进程的树状图cat /proc/PID/stat
会显示一些关于该进程的信息。你可以在man proc
中找到这个文件的内容说明。例如,第四个字段是父进程的PID。
新进程的继承
新的进程(即将变为 python3
的)从 Shell 中继承了很多内容。例如,它继承了:
- 环境变量:你可以通过
cat /proc/PID/environ | tr '\0' '\n'
查看 - 标准输出和标准错误的文件描述符:通过
ls -l /proc/PID/fd
查看 - 工作目录(也就是当前目录)
- 命名空间和控制组(如果它在一个容器内)
- 运行它的用户以及群组
- 还有可能是我此刻未能列举出来的更多东西
5、Shell 调用 execve
现在我们准备好启动 Python 解释器了!
自行验证:运行 strace -f -e execve bash
,接着运行 python3
。其中的 -f
参数非常重要,因为我们想要跟踪任何可能产生的子进程。你应该可以看到如下的输出:
[pid 3708381] execve("/usr/bin/python3", ["python3"], 0x560397748300 /* 21 vars */) = 0
第一个参数是这个二进制文件,而第二个参数是命令行参数列表。这些命令行参数被放置在程序内存的特定位置,以便在运行时可以访问。
那么,execve
内部到底发生了什么呢?
6、获取该二进制文件的内容
我们首先需要打开 python3
的二进制文件并读取其内容。直到目前为止,我们只使用了 stat
系统调用来获取其元数据,但现在我们需要获取它的内容。
让我们再次查看 stat
的输出:
$ stat /usr/bin/python3
File: /usr/bin/python3 -> python3.9
Size: 9 Blocks: 0 IO Block: 4096 symbolic link
Device: fe01h/65025d Inode: 6206 Links: 1
...
该文件在磁盘上占用 0 个块的空间。这是因为符号链接(python3.9
)的内容实际上是存储在 inode 自身中:在下面显示你可以看到(来自上述 inode 的二进制内容,以 hexdump
格式分为两行输出)。
00000020 00 00 00 00 01 00 00 00 70 79 74 68 6f 6e 33 2e |........python3.|
00000030 39 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |9...............|
因此,我们将需要打开 /usr/bin/python3.9
。所有这些操作都在内核内部进行,所以你并不会看到其他的系统调用。
每个文件都由硬盘上的一系列的 块 构成。我知道我系统中的每个块是 4096 字节,所以一个文件的最小大小是 4096 字节 —— 甚至如果文件只有 5 字节,它在磁盘上仍然占用 4KB。
自行验证:我们可以通过 debugfs
找到块号,如下所示:(再次说明,我从 Dmitry Mazin 的《磁盘就是一堆比特》文章中得知这些步骤)。
$ debugfs /dev/vda1
debugfs: blocks /usr/bin/python3.9
145408 145409 145410 145411 145412 145413 145414 145415 145416 145417 145418 145419 145420 145421 145422 145423 145424 145425 145426 145427 145428 145429 145430 145431 145432 145433 145434 145435 145436 145437
接下来,我们可以使用 dd
来读取文件的第一个块。我们将块大小设定为 4096 字节,跳过 145408
个块,然后读取 1 个块。
$ dd if=/dev/vda1 bs=4096 skip=145408 count=1 2>/dev/null | hexdump -C | head
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 c0 a5 5e 00 00 00 00 00 |..>.......^.....|
00000020 40 00 00 00 00 00 00 00 b8 95 53 00 00 00 00 00 |@.........S.....|
00000030 00 00 00 00 40 00 38 00 0b 00 40 00 1e 00 1d 00 |....@.8...@.....|
00000040 06 00 00 00 04 00 00 00 40 00 00 00 00 00 00 00 |........@.......|
00000050 40 00 40 00 00 00 00 00 40 00 40 00 00 00 00 00 |@.@.....@.@.....|
00000060 68 02 00 00 00 00 00 00 68 02 00 00 00 00 00 00 |h.......h.......|
00000070 08 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 |................|
00000080 a8 02 00 00 00 00 00 00 a8 02 40 00 00 00 00 00 |..........@.....|
00000090 a8 02 40 00 00 00 00 00 1c 00 00 00 00 00 00 00 |..@.............|
你会发现,这样我们得到的输出结果与直接使用 cat
读取文件所获得的结果完全一致。
$ cat /usr/bin/python3.9 | hexdump -C | head
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 c0 a5 5e 00 00 00 00 00 |..>.......^.....|
00000020 40 00 00 00 00 00 00 00 b8 95 53 00 00 00 00 00 |@.........S.....|
00000030 00 00 00 00 40 00 38 00 0b 00 40 00 1e 00 1d 00 |....@.8...@.....|
00000040 06 00 00 00 04 00 00 00 40 00 00 00 00 00 00 00 |........@.......|
00000050 40 00 40 00 00 00 00 00 40 00 40 00 00 00 00 00 |@.@.....@.@.....|
00000060 68 02 00 00 00 00 00 00 68 02 00 00 00 00 00 00 |h.......h.......|
00000070 08 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 |................|
00000080 a8 02 00 00 00 00 00 00 a8 02 40 00 00 00 00 00 |..........@.....|
00000090 a8 02 40 00 00 00 00 00 1c 00 00 00 00 00 00 00 |..@.............|
关于魔术数字的额外说明
这个文件以 ELF
开头,这是一个被称为“魔术数字”的标识符,它是一种字节序列,告诉我们这是一个 ELF 文件。在 Linux 上,ELF 是二进制文件的格式。
不同的文件格式有不同的魔术数字。例如,gzip 的魔数是 1f8b
。文件开头的魔术数字就是 file blah.gz
如何识别出它是一个 gzip 文件的方式。
我认为 file
命令使用了各种启发式方法来确定文件的类型,而其中,魔术数字是一个重要的特征。
7、寻找解释器
我们来解析这个 ELF 文件,看看里面都有什么内容。
自行验证:运行 readelf -a /usr/bin/python3.9
。我得到的结果是这样的(但是我删减了大量的内容):
$ readelf -a /usr/bin/python3.9
ELF Header:
Class: ELF64
Machine: Advanced Micro Devices X86-64
...
-> Entry point address: 0x5ea5c0
...
Program Headers:
Type Offset VirtAddr PhysAddr
INTERP 0x00000000000002a8 0x00000000004002a8 0x00000000004002a8
0x000000000000001c 0x000000000000001c R 0x1
-> [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
...
-> 1238: 00000000005ea5c0 43 FUNC GLOBAL DEFAULT 13 _start
从这段内容中,我理解到:
- 请求内核运行
/lib64/ld-linux-x86-64.so.2
来启动这个程序。这就是所谓的动态链接器,我们将在随后的部分对其进行讨论。 - 该程序制定了一个入口点(位于
0x5ea5c0
),那里是这个程序代码开始的地方。
接下来,让我们一起来聊聊动态链接器。
8、动态链接
好的!我们已从磁盘读取了字节数据,并启动了这个“解释器”。那么,接下来会发生什么呢?如果你执行 strace -o out.strace python3
,你会在 execve
系统调用之后观察到一系列的信息:
execve("/usr/bin/python3", ["python3"], 0x560af13472f0 /* 21 vars */) = 0
brk(NULL) = 0xfcc000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=32091, ...}) = 0
mmap(NULL, 32091, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f718a1e3000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 l\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=149520, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f718a1e1000
...
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
这些内容初看可能让人望而生畏,但我希望你能重点关注这一部分:openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0" ...
。这里正在打开一个被称为 pthread
的 C 语言线程库,运行 Python 解释器时需要这个库。
自行验证:如果你想知道一个二进制文件在运行时需要加载哪些库,你可以使用 ldd
命令。下面展示的是我运行后的效果:
$ ldd /usr/bin/python3.9
linux-vdso.so.1 (0x00007ffc2aad7000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f2fd6554000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f2fd654e000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f2fd6549000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f2fd6405000)
libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f2fd63d6000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f2fd63b9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2fd61e3000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2fd6580000)
你可以看到,第一个列出的库就是 /lib/x86_64-linux-gnu/libpthread.so.0
,这就是它被第一个加载的原因。
关于 LD_LIBRARY_PATH
说实话,我关于动态链接的理解还有些模糊,以下是我所了解的一些内容:
- 动态链接发生在用户空间,我的系统上的动态链接器位于
/lib64/ld-linux-x86-64.so.2
. 如果你缺少动态链接器,可能会遇到一些奇怪的问题,比如这种 奇怪的“文件未找到”错误 - 动态链接器使用
LD_LIBRARY_PATH
环境变量来查找库 - 动态链接器也会使用
LD_PRELOAD
环境变量来覆盖你想要的任何动态链接函数(你可以使用它来进行 有趣的魔改,或者使用像 jemalloc 这样的替代品来替换默认内存分配器) strace
的输出中有一些mprotect
,因为安全原因将库代码标记为只读- 在 Mac 上,不是使用
LD_LIBRARY_PATH
(Linux),而是DYLD_LIBRARY_PATH
你可能会有疑问,如果动态链接发生在用户空间,我们为什么没有看到大量的 stat
系统调用在 LD_LIBRARY_PATH
中搜索这些库,就像 Bash 在 PATH
中搜索那样?
这是因为 ld
在 /etc/ld.so.cache
中有一个缓存,因此所有之前已经找到的库都会被记录在这里。你可以在 strace
的输出中看到它正在打开缓存 - openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
。
在 完整的 strace 输出 中,我仍然对动态链接之后出现的一些系统调用感到困惑(什么是 prlimit64
?本地环境的内容是如何介入的?gconv-modules.cache
是什么?rt_sigaction
做了什么?arch_prctl
是什么?以及 set_tid_address
和 set_robust_list
是什么?)。尽管如此,我觉得已经有了一个不错的开头。
旁注:ldd 实际上是一个简单的 Shell 脚本!
在 Mastodon 上,有人 指出,ldd
实际上是一个 Shell 脚本,它设置了 LD_TRACE_LOADED_OBJECTS=1
环境变量,然后启动程序。因此,你也可以通过以下方式实现相同的功能:
$ LD_TRACE_LOADED_OBJECTS=1 python3
linux-vdso.so.1 (0x00007ffe13b0a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f01a5a47000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f01a5a41000)
libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f2fd6549000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f2fd6405000)
libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f2fd63d6000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f2fd63b9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2fd61e3000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2fd6580000)
事实上,ld
也是一个可以直接运行的二进制文件,所以你也可以通过 /lib64/ld-linux-x86-64.so.2 --list /usr/bin/python3.9
来达到相同的效果。
关于 init 和 fini
让我们来谈谈这行 strace
输出中的内容:
set_tid_address(0x7f58880dca10) = 3709103
这似乎与线程有关,我认为这可能是因为 pthread
库(以及所有其他动态加载的库)在加载时得以运行初始化代码。在库加载时运行的代码位于 init
区域(或者也可能是 .ctors
区域)。
自行验证:让我们使用 readelf
来看看这个:
$ readelf -a /lib/x86_64-linux-gnu/libpthread.so.0
...
[10] .rela.plt RELA 00000000000051f0 000051f0
00000000000007f8 0000000000000018 AI 4 26 8
[11] .init PROGBITS 0000000000006000 00006000
000000000000000e 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000006010 00006010
0000000000000560 0000000000000010 AX 0 0 16
...
这个库没有 .ctors
区域,只有一个 .init
。但是,.init
区域都有些什么内容呢?我们可以使用 objdump
来反汇编这段代码:
$ objdump -d /lib/x86_64-linux-gnu/libpthread.so.0
Disassembly of section .init:
0000000000006000 <_init>:
6000: 48 83 ec 08 sub $0x8,%rsp
6004: e8 57 08 00 00 callq 6860 <__pthread_initialize_minimal>
6009: 48 83 c4 08 add $0x8,%rsp
600d: c3
所以它在调用 __pthread_initialize_minimal
。我在 glibc 中找到了 这个函数的代码,尽管我不得不找到一个较早版本的 glibc,因为在更近的版本中,libpthread 不再是一个独立的库。
我不确定这个 set_tid_address
系统调用是否实际上来自 __pthread_initialize_minimal
,但至少我们知道了库可以通过 .init
区域在启动时运行代码。
这里有一份关于 .init 区域的 elf 手册的笔记:
$ man elf
.init
这个区域保存着对进程初始化代码有贡献的可执行指令。当程序开始运行时,系统会安排在调用主程序入口点之前执行该区域中的代码。
在 ELF 文件中也有一个在结束时运行的 .fini
区域,以及其他可以存在的区域 .ctors
/ .dtors
(构造器和析构器)。
好的,关于动态链接就说这么多。
9、转到 _start
在动态链接完成后,我们进入到 Python 解释器中的 _start
。然后,它将执行所有正常的 Python 解析器会做的事情。
我不打算深入讨论这个,因为我在这里关心的是关于如何在 Linux 上运行二进制文件的一般性知识,而不是特别针对 Python 解释器。
10、写入字符串
不过,我们仍然需要打印出 “hello world”。在底层,Python 的 print
函数调用了 libc 中的某个函数。但是,它调用了哪一个呢?让我们来找出答案!
自行验证:运行 ltrace -o out python3 hello.py
:
$ ltrace -o out python3 hello.py
$ grep hello out
write(1, "hello world\n", 12) = 12
看起来它确实在调用 write
函数。
我必须承认,我对 ltrace
总是有一些疑虑 —— 与我深信不疑的 strace
不同,我总是不完全确定 ltrace
是否准确地报告了库调用。但在这个情况下,它似乎有效。并且,如果我们查阅 cpython 的源代码,它似乎在一些地方确实调用了 write()
函数,所以我倾向于相信这个结果。
什么是 libc?
我们刚刚提到,Python 调用了 libc 中的 write
函数。那么,libc 是什么呢?它是 C 的标准库,负责许多基本操作,例如:
- 用
malloc
分配内存 - 文件 I/O(打开/关闭文件)
- 执行程序(像我们之前提到的
execvp
) - 使用
getaddrinfo
查找 DNS 记录 - 使用
pthread
管理线程
在 Linux 上,程序不一定需要使用 libc(例如 Go 就广为人知地未使用它,而是直接调用了 Linux 系统调用),但是我常用的大多数其他编程语言(如 node、Python、Ruby、Rust)都使用了 libc。我不确定 Java 是否也使用了。
你能通过在你的二进制文件上执行 ldd
命令,检查你是否正在使用 libc:如果你看到了 libc.so.6
这样的信息,那么你就在使用 libc。
为什么 libc 重要?
你也许在思考 —— 为何重要的是 Python 调用 libc 的 write
函数,然后 libc 再调用 write
系统调用?为何我要着重提及 libc
是调用过程的一环?
我认为,在这个案例中,这并不真的很重要(根据我所知,libc 的 write
函数与 write
系统调用的映射相当直接)。
然而,存在不同的 libc 实现,有时它们的行为会有所不同。两个主要的实现是 glibc(GNU libc)和 musl libc。
例如,直到最近,musl 的 getaddrinfo
并不支持 TCP DNS,这是一篇关于这个问题引发的错误的博客文章。
关于 stdout 和终端的小插曲
在我们的程序中,stdout(1
文件描述符)是一个终端。你可以在终端上做一些有趣的事情!例如:
- 在终端中运行
ls -l /proc/self/fd/1
。我得到了/dev/pts/2
的结果。 - 在另一个终端窗口中,运行
echo hello > /dev/pts/2
。 - 返回到原始终端窗口。你应会看到
hello
被打印出来了!
暂时就到这儿吧!
希望通过上文,你对 hello world
是如何打印出来的有了更深的了解!我暂时不再添加更多的细节,因为这篇文章已经足够长了,但显然还有更多的细节可以探讨,如果大家能提供更多的细节,我可能会添加更多的内容。如果你有关于我在这里没提到的程序内部调用过程的任何工具推荐,我会特别高兴。
我很期待看到一份 Mac 版的解析
我对 Mac OS 的一个懊恼是,我不知道如何在这个级别上解读我的系统——当我打印 “hello world”,我无法像在 Linux 上那样,窥视背后的运作机制。我很希望看到一个深度的解析。
我所知道的一些在 Mac 下的对应工具:
ldd
->otool -L
readelf
->otool
- 有人说你可以在 Mac 上使用
dtruss
或dtrace
来代替strace
,但我尚未有足够的勇气关闭系统完整性保护来让它工作。 strace
->sc_usage
似乎能够收集关于系统调用使用情况的统计信息,fs_usage
则可以收集文件使用情况的信息。
延伸阅读
一些附加的链接:
- 快速教程:如何在 Linux 上创建超小型 ELF 可执行文件
- 在 FreeBSD 上探索 “hello world”
- [微观视角下的 Windows 中 “Hello World”][23A]
- 来自 LWN 的文章:如何运行程序 (以及第二部分)详尽介绍了
execve
的内部机制 - Lexi Mattick 的文章,赋予 CPU “你” 的存在
- 从零开始在 6502 上实现 “Hello, World” (来自 Ben Eater 的视频)
(题图:MJ/b87ed0a2-80d6-49cd-b2bf-1ef822485e3f)
via: https://jvns.ca/blog/2023/08/03/behind--hello-world/
作者:Julia Evans 选题:lujun9972 译者:ChatGPT 校对:wxy
- Facebook✓
- Google+✓
- Instapaper✓
- Tweet✓