<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; Ubuntu</title>
	<atom:link href="http://tonybai.com/tag/ubuntu/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Wed, 08 Apr 2026 00:17:11 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Rust 的“跨越鸿沟”时刻：Ubuntu 全面拥抱 Rust 意味着什么？</title>
		<link>https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace/</link>
		<comments>https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace/#comments</comments>
		<pubDate>Wed, 25 Feb 2026 00:27:50 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AdoptionRate]]></category>
		<category><![CDATA[BatteriesIncluded]]></category>
		<category><![CDATA[Canonical]]></category>
		<category><![CDATA[CommunityCulture]]></category>
		<category><![CDATA[CrossingTheChasm]]></category>
		<category><![CDATA[EarlyAdopters]]></category>
		<category><![CDATA[EarlyMajority]]></category>
		<category><![CDATA[Empathy]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[IndustryStandard]]></category>
		<category><![CDATA[infrastructure]]></category>
		<category><![CDATA[MemorySafety]]></category>
		<category><![CDATA[NikoMatsakis]]></category>
		<category><![CDATA[OpenSourceBusinessModel]]></category>
		<category><![CDATA[OpenSourceInvestment]]></category>
		<category><![CDATA[Pragmatism]]></category>
		<category><![CDATA[ReferenceCustomers]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[SoftwareEngineering]]></category>
		<category><![CDATA[standardlibrary]]></category>
		<category><![CDATA[TechnologyLifeCycle]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[内存安全]]></category>
		<category><![CDATA[内置电池]]></category>
		<category><![CDATA[务实主义]]></category>
		<category><![CDATA[同理心]]></category>
		<category><![CDATA[基础设施]]></category>
		<category><![CDATA[工业标准]]></category>
		<category><![CDATA[开源商业模式]]></category>
		<category><![CDATA[开源投资]]></category>
		<category><![CDATA[技术生命周期]]></category>
		<category><![CDATA[早期大众]]></category>
		<category><![CDATA[早期采用者]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[标杆客户]]></category>
		<category><![CDATA[社区文化]]></category>
		<category><![CDATA[跨越鸿沟]]></category>
		<category><![CDATA[软件工程]]></category>
		<category><![CDATA[采用率]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5946</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace 大家好，我是Tony Bai。 在技术世界里，一门编程语言的成功往往分为两个阶段：第一阶段是赢得“极客”和“先驱者”的狂热追捧；第二阶段则是说服那些保守、务实的“早期大众”将其投入到枯燥却庞大的企业级生产中。这两个阶段之间，横亘着一条深不见底的“鸿沟”。 2026 年初，Rust 核心团队成员、语言设计的灵魂人物 Niko Matsakis 在参加完 Rust Nation 大会后，发表了一篇引人深思的文章——《What it means that Ubuntu is using Rust》。在这篇随笔中，Niko 借由 Canonical（Ubuntu 的母公司）全面拥抱 Rust 这一标志性事件，极其坦诚地剖析了 Rust 当前在行业接纳生命周期中所处的位置、面临的阵痛，以及为了走向真正的“工业标准”，Rust 社区必须在技术和心理上做出的巨大改变。 本文将深度解读 Niko 的这篇文章，带你透视 Rust 在“后狂热时代”的商业化演进路线、标准库之争、开源商业模式，以及为何“同理心”成了这门硬核语言最大的护城河。 无处不在的“鸿沟”——Rust 到底走到哪了？ 如果你熟悉硅谷营销大师杰弗里·摩尔（Geoffrey Moore）的经典理论《跨越鸿沟》（Crossing the Chasm），就会知道任何一项高科技产品在市场推广时，都会经历创新者（Innovators）、早期采用者（Early Adopters）、早期大众（Early Majority）、后期大众（Late Majority）和落后者（Laggards）五个阶段。而在“早期采用者”与“早期大众”之间，存在着一个巨大的断层，这就是“鸿沟”。 Rust 跨过这条鸿沟了吗？ Niko 给出的答案是：这取决于你问的是谁。 在某些互联网巨头（大厂）中：答案是“已经跨越了一大半”。比如在亚马逊云（AWS）这样对性能和资源有着极致苛求的地方，Rust 已经被牢牢确立为构建大规模数据平面（Data Planes）和资源感知代理（Agents）的“正确选择”。它甚至正在向设备端和机器人领域的底层代码渗透。 在普通企业应用中：依然存在一种根深蒂固的刻板印象——“Rust 是给 S3（亚马逊云存储）那些穿西装打领带的高级工程师用的，对于我们普通的 CRUD（增删改查）业务来说，完全是杀鸡用牛刀。” [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/rust-crossing-the-chasm-ubuntu-embrace-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace">本文永久链接</a> &#8211; https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace</p>
<p>大家好，我是Tony Bai。</p>
<p>在技术世界里，一门编程语言的成功往往分为两个阶段：第一阶段是赢得“极客”和“先驱者”的狂热追捧；第二阶段则是说服那些保守、务实的“早期大众”将其投入到枯燥却庞大的企业级生产中。这两个阶段之间，横亘着一条深不见底的“鸿沟”。</p>
<p>2026 年初，Rust 核心团队成员、语言设计的灵魂人物 Niko Matsakis 在参加完 Rust Nation 大会后，发表了一篇引人深思的文章——《<a href="https://smallcultfollowing.com/babysteps/blog/2026/02/23/ubuntu-rustnation/">What it means that Ubuntu is using Rust</a>》。在这篇随笔中，Niko 借由 Canonical（Ubuntu 的母公司）全面拥抱 Rust 这一标志性事件，极其坦诚地剖析了 Rust 当前在行业接纳生命周期中所处的位置、面临的阵痛，以及为了走向真正的“工业标准”，Rust 社区必须在技术和心理上做出的巨大改变。</p>
<p>本文将深度解读 Niko 的这篇文章，带你透视 Rust 在“后狂热时代”的商业化演进路线、标准库之争、开源商业模式，以及为何“同理心”成了这门硬核语言最大的护城河。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/system-programming-in-go-pr.png" alt="" /></p>
<h2>无处不在的“鸿沟”——Rust 到底走到哪了？</h2>
<p>如果你熟悉硅谷营销大师杰弗里·摩尔（Geoffrey Moore）的经典理论《跨越鸿沟》（<em>Crossing the Chasm</em>），就会知道任何一项高科技产品在市场推广时，都会经历创新者（Innovators）、早期采用者（Early Adopters）、早期大众（Early Majority）、后期大众（Late Majority）和落后者（Laggards）五个阶段。而在“早期采用者”与“早期大众”之间，存在着一个巨大的断层，这就是“鸿沟”。</p>
<p><strong>Rust 跨过这条鸿沟了吗？</strong></p>
<p>Niko 给出的答案是：<strong>这取决于你问的是谁。</strong></p>
<ul>
<li>在某些互联网巨头（大厂）中：答案是“已经跨越了一大半”。比如在亚马逊云（AWS）这样对性能和资源有着极致苛求的地方，Rust 已经被牢牢确立为构建大规模数据平面（Data Planes）和资源感知代理（Agents）的“正确选择”。它甚至正在向设备端和机器人领域的底层代码渗透。</li>
<li>在普通企业应用中：依然存在一种根深蒂固的刻板印象——“Rust 是给 S3（亚马逊云存储）那些穿西装打领带的高级工程师用的，对于我们普通的 CRUD（增删改查）业务来说，完全是杀鸡用牛刀。”</li>
<li>在安全关键软件（Safety Critical Software）领域：比如汽车的转向柱控制系统或航空航天系统，Rust 依然在艰难地寻找立足点。大多数传统工业巨头仍处于“观望”状态，他们希望让早期采用者先去铺路、踩坑。</li>
</ul>
<p>这揭示了一个残酷的现实：<strong>技术上的优越性并不等同于市场上的普遍接受度</strong>。</p>
<p>当技术走向“早期大众”时，受众的心态发生了根本性变化。</p>
<p>早期采用者买的是“变革”，他们愿意容忍不成熟的生态，只为获得降维打击的竞争优势；而“早期大众”买的是“生产力提升”，他们极度厌恶风险，追求的是业务连续性——他们想要的是进化，而不是革命。</p>
<h2>寻找“标杆客户”——Ubuntu 搭建的跨越之桥</h2>
<p>如何说服那些极度厌恶风险的“早期大众”尝试新事物？唯一的答案是：<strong>让他们看到与他们相似的成功案例。</strong></p>
<p>这就是为什么 Canonical（Ubuntu 背后的公司）的入局对 Rust 生态具有决定性的历史意义。在 Rust Nation 大会上，Canonical 的工程副总裁 Jon Seager 发表了题为《在 Ubuntu 中大规模采用 Rust》的闭幕演讲。这场演讲完美诠释了什么是“既有远见，又极其务实”。</p>
<p>Canonical 明确表示，他们已将公司内部开发的语言收敛为一个极小的集合：Python、C/C++ 和 Go。</p>
<p>而现在，Rust 被正式引入，并被确立为编写新底层基础工具的首选语言，逐步取代 C、C++ 以及部分 Python 的使用场景。</p>
<p>更令人振奋的是，Canonical 不仅仅是自己“用”，他们还在“反哺”生态，充当桥梁。</p>
<p>Jon Seager 谈到了 Ubuntu 作为操作系统发行版的责任——通过支持内存安全的基础设施库来“向前支付（Pay it forward）”。Canonical 正在提供财务和声誉上的双重支持：<br />
1.  赞助 Trifecta Tech 基金会开发 sudo-rs 和 ntpd-rs（用 Rust 重写关键的系统组件）。<br />
2.  赞助 uutils 组织开发 Rust 版的 coreutils（Linux 核心命令集）。</p>
<p><strong>为什么说 Ubuntu 是完美的“标杆客户”？</strong></p>
<p>在 Linux 用户态领域，Ubuntu 的体量和权威性毋庸置疑。当 Ubuntu 愿意承担尝试新事物的风险，并证明“用 Rust 重写 sudo 是可行的且更安全的”时，这种示范效应是巨大的。</p>
<p>那些“早期大众”企业看到这一幕时会想：“如果连 Ubuntu 这样对稳定性要求极其变态的操作系统底层都在用 Rust，那我们的业务系统用 Rust 应该也是安全的。”</p>
<p>这正是《跨越鸿沟》中破局的核心策略：利用标杆客户的背书，提供能无缝融入现有工作流的“即插即用”方案，从而最小化系统的不连续性。</p>
<h2>成长的阵痛——为了壮大，Rust 必须改变“人设”</h2>
<p>当目标受众从追求极致的“极客”变成追求稳定的“务实派”时，Rust 面临着一种极其尴尬的转型痛点。</p>
<p>Niko 在文中引用了《跨越鸿沟》里的一段话：</p>
<blockquote>
<p>“在任何两个采用群体之间的过渡通常都是极度令人尴尬的，因为你必须在你对旧策略感到最舒服的时候采用新策略……当务实派想听到‘行业标准（Industry Standard）’时，科技公司可能还在向他们推销‘最先进的技术（State-of-the-art）’。”</p>
</blockquote>
<p>这精准地命中了 Rust 当下的软肋。</p>
<p>在过去的十年里，Rust 社区的营销口号是“零成本抽象”、“无畏并发”、“最先进的内存安全所有权模型”。这套说辞成功吸引了早期的系统工程师。</p>
<p>但如今，当 Rust 走向大众时，普通开发者更关心的是：“有没有现成的库？”、“编译能不能快点？”和“能不能开箱即用？”</p>
<p><strong>核心冲突爆发点：标准库的规模之争。</strong></p>
<p>在闭门晚宴上，Canonical 的 Jon Seager 提出了一个极具挑衅性的观点：Rust 需要重新审视其维持“极小标准库（Small Standard Library）”的政策。</p>
<p>长期以来，Rust 奉行“标准库只包含最核心的类型和原语，其余全部交给社区（Crates.io）”的哲学。比如，Rust 的标准库里甚至没有随机数生成、正则表达式或 HTTP 客户端。这种设计在早期非常受极客欢迎，因为它保证了核心库的轻量级，并允许社区自由竞争出最好的第三方库（如 serde、tokio）。</p>
<p>但对于“早期大众”来说，这简直是个噩梦。他们不明白为什么解析一个 JSON 或发起一个 HTTP 请求都需要在数以万计的第三方包中去筛选、评估安全性、担心供应链投毒。他们想要的是像 Go 语言或 Python 那样“内置电池（Batteries Included）”的开箱即用体验。</p>
<p>实际上，Rust 社区在 2016 年曾推出过一个名为“Rust 平台（Rust Platform）”的提案，试图官方“钦定”一批高质量的第三方包作为“扩展标准库”。但当时遭到了早期采用者的强烈抵制，理由是“直接改 Cargo.toml 很容易，没必要官方下场干预”。</p>
<p>Niko 反思道：当年早期采用者讨厌的东西，恰恰可能是如今“早期大众”最渴望的东西。</p>
<p>Rust 必须面对现实：过去引导其成功的信条，正在阻碍其向更广阔的市场迈进。</p>
<p>Niko 透露，他正在构思一个名为“电池包（Battery packs）”的新项目，试图在不搞庞大标准库的前提下，为企业级用户提供一种官方背书的、开箱即用的库集合方案。这标志着 Rust 正在从“追求它能成为什么样（What it could be）”向“承认它实际是什么样（What it actually is）”的务实转变。</p>
<h2>商业与开源的闭环——如何将“采用率”转化为“真金白银”？</h2>
<p>任何一门编程语言生态的长远发展，都离不开雄厚的资金支持。随着 Rust 采用率的爆炸式增长，对 Rust 开源项目和生态系统的维护压力也与日俱增。钱从哪来？</p>
<p>Niko 分享了几个关于开源投资的深刻洞见，这不仅适用于 Rust，对所有开源项目（包括 Go、Node.js 生态）都有极大的启发。</p>
<p><strong>洞见一：投资不一定只是“砸钱”，更是“下场共建”。</strong></p>
<p>对于像 Canonical 这样的纯粹开源组织，最宝贵的投资是“建立深度的组织间关系”。</p>
<p>在“Rust for Linux”项目中，早期都是 Rust 核心维护者在帮 Linux 内核开发者修 Bug。但随着时间推移，现在越来越多的 Linux 内核开发者开始自己动手修复 Rust 编译器或工具链的问题，而 Rust 维护者则退居幕后扮演导师的角色。这种“授人以渔”的贡献，比单纯的捐款更有价值。</p>
<p><strong>洞见二：钱往往在公司“采用 Rust 之前”到来，而不是之后。</strong></p>
<p>我们通常认为，企业是在大量使用某个开源软件后，出于反哺或维护自身利益的目的才会掏钱赞助。</p>
<p>但 Niko 观察到了一个完全不同的趋势：<strong>更容易获取的资金，来自于那些“正在考虑但尚未采用” Rust 的公司。</strong></p>
<p>在这些公司内部，通常有一批“早期采用者”（内推者），他们试图说服保守的公司管理层采用 Rust。为了促成此事，他们往往需要拿着一份“准入条件清单”——比如，Rust 必须支持某种特定的芯片架构，或者必须具备某个安全认证组件。</p>
<p>更关键的是，这些内推者手里往往握有预算。为了让这门技术顺利落地公司，他们愿意花钱去填补 Rust 生态中的这些空白。</p>
<p>Rust 基金会的 Alexandru Radovici 证实了这一点：许多对安全性要求极高的公司，手里攥着钱想帮 Rust 补齐短板，却“不知道该怎么花这笔钱”。Canonical 赞助 sudo-rs 本质上也是一样的——他们是在花钱扫除阻碍 Ubuntu 更大规模采用 Rust 的障碍。</p>
<p>开源社区需要建立一种机制，精准对接这些带着预算的“潜在采用者”，将他们的痛点转化为开源项目的开发资金。</p>
<h2>社区的终极考验——同理心是最大的护城河</h2>
<p>在文章的最后，Niko 抛出了一个直击灵魂的观点，这不仅是给 Rust 社区的警钟，也是所有程序员的必修课：</p>
<blockquote>
<p>“如果我们在其中表现得太像‘中学生（Middle School）’，那开源跨越鸿沟的愿景就会彻底破灭。”</p>
</blockquote>
<p>什么是“中学生”行为？</p>
<p>当你深度参与一个开源社区时，你会觉得这里充满阳光，欢迎所有人。但对于外部的“早期大众”来说，开源社区往往看起来像一个充满小圈子、潜规则和“口口相传的规矩（Oral traditions）”的排外组织。</p>
<p>一个企业级的保守开发者，带着一个务实的业务问题来到社区提问。他可能只是用错了一个术语，或者没有遵循某种隐形的“社区政治正确”，结果就遭到了一群激进贡献者的群嘲、冷嘲热讽，甚至因为提出不同的设计理念而被强硬关闭 Issue。</p>
<p>这位企业开发者根本分不清哪些是喷子，哪些代表官方立场。他只会觉得：“这个语言的社区太有毒了，我们公司还是用 Java 吧。”</p>
<p><strong>只需要一次粗鲁的回复，就能彻底赶走一个潜在的企业级标杆客户。</strong></p>
<p>Niko 强调，帮助 Rust 最终取得成功的，绝不是更快的编译速度或更完美的类型系统，而是“开源中的同理心”。</p>
<p>“早期大众”并不想参与编程语言的“宗教战争”，他们不关心“纯粹性”，他们只是想按时下班，安全地把产品发布出去。Rust 社区必须学会倾听这群人的声音，理解他们的价值观，用温和、包容和同理心去服务他们，而不是用技术傲慢去居高临下地教训他们。</p>
<h2>小结：语言的进化，更是心智的成熟</h2>
<p>从 2015 年发布 1.0 版本至今，Rust 用了十余年的时间，证明了自己在技术和理论上的卓越。如今，借由 Ubuntu 这样的重磅标杆客户的背书，它正式站在了跨越主流企业级市场鸿沟的跳板上。</p>
<p>Niko Matsakis 的这篇文章，不仅是对 Rust 现状的一份清醒诊断，更是对整个技术生态演进规律的深刻洞察，也非常值得其他主流编程语言的掌舵者和社区学习借鉴。</p>
<p>无论是标准库的扩展、商业投资机制的完善，还是社区同理心的建设，都表明 Rust 正在经历一场脱胎换骨的“成年礼”。它正在从一个由极客驱动的“炫酷玩具”，蜕变为一个能够承载人类核心数字基础设施的“工业巨兽”。</p>
<p>也许属于 Rust 的激荡时代，才刚刚开始。</p>
<p>资料链接：https://smallcultfollowing.com/babysteps/blog/2026/02/23/ubuntu-rustnation/</p>
<hr />
<p><strong>你认为 Rust 该“扩充”标准库吗？</strong></p>
<p>Rust 坚持“极小标准库”让极客疯狂，却让企业用户头大。你是支持 Go 这种“内置电池”的开箱即用，还是支持 Rust 这种“社区竞争”的极简主义？你在项目中是否也曾因为 Rust 缺乏某个基础库（如随机数、正则）而感到沮丧？</p>
<p>欢迎在评论区分享你的看法！</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>2025年最佳机器人Linux操作系统——顶级发行版与最新进展！</title>
		<link>https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025/</link>
		<comments>https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025/#comments</comments>
		<pubDate>Sun, 17 Aug 2025 14:49:28 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[DDS]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[Fedora]]></category>
		<category><![CDATA[Gazebo]]></category>
		<category><![CDATA[Humble]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[LIDAR]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LTS]]></category>
		<category><![CDATA[MoveIt!]]></category>
		<category><![CDATA[NASA]]></category>
		<category><![CDATA[NVIDIA]]></category>
		<category><![CDATA[OpenCV]]></category>
		<category><![CDATA[OpenEmbedded]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[PyTorch]]></category>
		<category><![CDATA[reddit]]></category>
		<category><![CDATA[ROS]]></category>
		<category><![CDATA[ROS2]]></category>
		<category><![CDATA[Stackoverflow]]></category>
		<category><![CDATA[TensorFlow]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[UbuntuCore]]></category>
		<category><![CDATA[Yocto]]></category>
		<category><![CDATA[人工智能]]></category>
		<category><![CDATA[仿真]]></category>
		<category><![CDATA[具身智能]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[嵌入式]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[机器人]]></category>
		<category><![CDATA[机器人操作系统]]></category>
		<category><![CDATA[树莓派]]></category>
		<category><![CDATA[波士顿动力]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5048</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025 大家好，我是Tony Bai。 如果你正投身于机器人技术领域，选择正确的操作系统至关重要。随着人工智能、自动化和机器学习的进步，机器人正变得前所未有的复杂。在为这些智能机器提供动力方面，Linux凭借其开源的灵活性、稳定性以及对机器人框架的广泛支持，仍然是首选。 在本文中，我们将探讨2025年最佳的机器人Linux操作系统，帮助你为你的项目找到完美的发行版——无论你是从事工业自动化、人工智能驱动的机器人技术，还是业余爱好者的创作。我们还将介绍专注于机器人的Linux发行版的最新发展，让你保持领先。 1. Ubuntu机器人操作系统 机器人技术正以前所未有的速度发展，改变着医疗、自动化、制造乃至太空探索等行业。任何机器人系统的基础都是其操作系统，它决定了系统的效率、安全性和性能。 Ubuntu机器人操作系统 截至2025年，Ubuntu已成为机器人领域的最佳Linux操作系统。凭借其与机器人操作系统（ROS）的无缝集成、优化的实时性能以及对AI驱动机器人技术的扩展支持，Ubuntu成为开发者、研究人员和行业的首选。 为什么Ubuntu是机器人领域的最佳Linux操作系统 Ubuntu在机器人领域的主导地位并非偶然——它建立在多年的持续发展和强大的社区支持之上。以下是Ubuntu脱颖而出的一些关键原因： 1. 与ROS（机器人操作系统）的无缝集成 ROS已成为使用最广泛的机器人中间件，提供了一系列工具和库，帮助开发者构建复杂的机器人应用程序。由于ROS最初就是为Ubuntu设计的，因此集成非常无缝。 ROS 2与Ubuntu：到2025年，Ubuntu为ROS 2提供了内置支持，ROS 2提供了实时功能、安全增强和对多机器人系统更好的支持。 预装ROS软件包：Ubuntu通过预配置的软件包简化了ROS的安装，为开发者节省了大量时间。 强大的开发者社区：由于Ubuntu是机器人领域使用最多的操作系统，因此有庞大的支持网络可用于故障排除、教程和协作。 2. 针对嵌入式和边缘设备进行优化 并非所有机器人系统都是大型工业机器——许多现代机器人是需要轻量级和高效软件的小型嵌入式设备。Ubuntu Core是Ubuntu的最小化版本，专为边缘计算和嵌入式机器人技术而优化。 基于事务的更新：Ubuntu Core提供自动、故障安全的更新，确保机器人系统保持最新状态，而不会有破坏功能的风险。 注重安全的设计：Ubuntu Core包含内置的安全功能，如应用程序沙箱和验证启动机制，这对于在敏感环境中运行的机器人至关重要。 低系统资源占用：凭借其轻量级的特性，Ubuntu Core能在小型机器人硬件上高效运行，包括树莓派（Raspberry Pi）、NVIDIA Jetson和定制AI板卡。 3. 安全性与长期维护 安全性是机器人技术中的一个主要问题，尤其是在医疗和国防等行业。Ubuntu背后的公司Canonical提供扩展安全维护（ESM），确保基于Ubuntu的机器人系统获得长期的安全更新。 定期安全补丁：这可以防止可能被黑客利用的漏洞，使Ubuntu成为机器人项目最安全的选择之一。 行业采用：许多航空航天、汽车和工业自动化公司因其安全优先的方法而信任Ubuntu。 4. 硬件兼容性与行业采用 Ubuntu支持广泛的硬件，从AI驱动的机械臂到自动驾驶无人机。无论你是在开发工业机器人还是个人助理机器人，Ubuntu都为大量的传感器、执行器和计算单元提供驱动程序、库和支持。 可与流行的硬件平台配合使用，例如： NVIDIA Jetson AI驱动的机器人套件 树莓派（用于小型机器人项目） Intel RealSense（用于3D深度感应机器人） 定制的基于ARM的机器人系统 因为Ubuntu是一个开源操作系统，制造商也可以为其特定的机器人应用定制内核并进行优化。 Ubuntu机器人技术的最新发展（2025年） 过去一年，Ubuntu的机器人技术生态系统取得了显著进步。以下是2025年一些最激动人心的更新： 1. [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025">本文永久链接</a> &#8211; https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025</p>
<p>大家好，我是Tony Bai。</p>
<p>如果你正投身于机器人技术领域，选择正确的操作系统至关重要。随着人工智能、自动化和机器学习的进步，机器人正变得前所未有的复杂。在为这些智能机器提供动力方面，Linux凭借其开源的灵活性、稳定性以及对机器人框架的广泛支持，仍然是首选。</p>
<p>在本文中，我们将探讨2025年最佳的机器人Linux操作系统，帮助你为你的项目找到完美的发行版——无论你是从事工业自动化、人工智能驱动的机器人技术，还是业余爱好者的创作。我们还将介绍专注于机器人的Linux发行版的最新发展，让你保持领先。</p>
<h2>1. Ubuntu机器人操作系统</h2>
<p>机器人技术正以前所未有的速度发展，改变着医疗、自动化、制造乃至太空探索等行业。任何机器人系统的基础都是其操作系统，它决定了系统的效率、安全性和性能。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-2.png" alt="" /><br />
<center>Ubuntu机器人操作系统</center></p>
<p>截至2025年，Ubuntu已成为机器人领域的最佳Linux操作系统。凭借其与机器人操作系统（ROS）的无缝集成、优化的实时性能以及对AI驱动机器人技术的扩展支持，Ubuntu成为开发者、研究人员和行业的首选。</p>
<h3>为什么Ubuntu是机器人领域的最佳Linux操作系统</h3>
<p>Ubuntu在机器人领域的主导地位并非偶然——它建立在多年的持续发展和强大的社区支持之上。以下是Ubuntu脱颖而出的一些关键原因：</p>
<p><strong>1. 与ROS（机器人操作系统）的无缝集成</strong></p>
<p>ROS已成为使用最广泛的机器人中间件，提供了一系列工具和库，帮助开发者构建复杂的机器人应用程序。由于ROS最初就是为Ubuntu设计的，因此集成非常无缝。</p>
<ul>
<li>ROS 2与Ubuntu：到2025年，Ubuntu为ROS 2提供了内置支持，ROS 2提供了实时功能、安全增强和对多机器人系统更好的支持。</li>
<li>预装ROS软件包：Ubuntu通过预配置的软件包简化了ROS的安装，为开发者节省了大量时间。</li>
<li>强大的开发者社区：由于Ubuntu是机器人领域使用最多的操作系统，因此有庞大的支持网络可用于故障排除、教程和协作。</li>
</ul>
<p><strong>2. 针对嵌入式和边缘设备进行优化</strong></p>
<p>并非所有机器人系统都是大型工业机器——许多现代机器人是需要轻量级和高效软件的小型嵌入式设备。Ubuntu Core是Ubuntu的最小化版本，专为边缘计算和嵌入式机器人技术而优化。</p>
<ul>
<li>基于事务的更新：Ubuntu Core提供自动、故障安全的更新，确保机器人系统保持最新状态，而不会有破坏功能的风险。</li>
<li>注重安全的设计：Ubuntu Core包含内置的安全功能，如应用程序沙箱和验证启动机制，这对于在敏感环境中运行的机器人至关重要。</li>
<li>低系统资源占用：凭借其轻量级的特性，Ubuntu Core能在小型机器人硬件上高效运行，包括树莓派（Raspberry Pi）、NVIDIA Jetson和定制AI板卡。</li>
</ul>
<p><strong>3. 安全性与长期维护</strong></p>
<p>安全性是机器人技术中的一个主要问题，尤其是在医疗和国防等行业。Ubuntu背后的公司Canonical提供扩展安全维护（ESM），确保基于Ubuntu的机器人系统获得长期的安全更新。</p>
<ul>
<li>定期安全补丁：这可以防止可能被黑客利用的漏洞，使Ubuntu成为机器人项目最安全的选择之一。</li>
<li>行业采用：许多航空航天、汽车和工业自动化公司因其安全优先的方法而信任Ubuntu。</li>
</ul>
<p><strong>4. 硬件兼容性与行业采用</strong></p>
<p>Ubuntu支持广泛的硬件，从AI驱动的机械臂到自动驾驶无人机。无论你是在开发工业机器人还是个人助理机器人，Ubuntu都为大量的传感器、执行器和计算单元提供驱动程序、库和支持。</p>
<p>可与流行的硬件平台配合使用，例如：</p>
<ul>
<li>NVIDIA Jetson AI驱动的机器人套件</li>
<li>树莓派（用于小型机器人项目）</li>
<li>Intel RealSense（用于3D深度感应机器人）</li>
<li>定制的基于ARM的机器人系统</li>
</ul>
<p>因为Ubuntu是一个开源操作系统，制造商也可以为其特定的机器人应用定制内核并进行优化。</p>
<h3>Ubuntu机器人技术的最新发展（2025年）</h3>
<p>过去一年，Ubuntu的机器人技术生态系统取得了显著进步。以下是2025年一些最激动人心的更新：</p>
<p><strong>1. 针对机器人技术的实时内核增强</strong></p>
<p>实时性能在机器人技术中至关重要，微秒之差可能决定机器人是平稳运行还是彻底失败。2025年，Ubuntu引入了改进的实时内核支持，确保机器人应用满足低延迟处理要求。</p>
<ul>
<li>更快的响应时间：改进后的内核确保机器人的运动和决策能够无延迟地发生。</li>
<li>为多任务机器人提供更好的调度：对于同时执行多项操作的工业机器人非常有用。</li>
<li>增强的稳定性：减少机器人功能中的意外崩溃和延迟。</li>
</ul>
<p><strong>2. AI与机器学习集成</strong></p>
<p>现代机器人依赖于AI驱动的决策，Ubuntu已采取重要措施来优化机器人在机器学习方面的能力。</p>
<ul>
<li>内置的AI库，如TensorFlow、PyTorch和OpenCV，都为Ubuntu进行了预配置。</li>
<li>ROS 2现在包含了基于AI的运动规划和计算机视觉改进。</li>
<li>边缘AI支持：机器人可以在本地处理AI任务，而不是依赖云计算，从而减少延迟并改善实时决策。</li>
</ul>
<p><strong>3. 扩展对机器人硬件的支持</strong></p>
<p>Ubuntu已扩大其硬件支持范围，包括更多的工业机械臂、自动驾驶车辆和人形机器人。开发者现在可以将Ubuntu用于更广泛的机器人组件，包括：</p>
<ul>
<li>用于自动驾驶机器人的LIDAR传感器</li>
<li>用于云连接机器人的5G连接支持</li>
<li>用于基于感知的机器人的高级摄像头和深度感应模块</li>
</ul>
<p>通过这种扩展的兼容性，Ubuntu可以加快机器人应用程序的原型设计和部署。</p>
<p><strong>机器人社区对Ubuntu的评价</strong></p>
<p>机器人社区因其可靠性、灵活性和强大的开发者生态系统而广泛接受<a href="https://ubuntu.com/robotics">Ubuntu</a>。</p>
<ul>
<li>许多机器人专家认为精通Linux是必备技能，因为大多数机器人工具都是为Ubuntu构建的。</li>
<li>在Reddit和Stack Overflow等论坛的讨论中，经常强调Ubuntu相比其他操作系统选项提供了更好的支持、库和长期稳定性。</li>
<li>NASA、特斯拉和波士顿动力等公司都使用Ubuntu进行机器人研究和开发。</li>
</ul>
<p><strong>Ubuntu是机器人技术的未来</strong></p>
<p>凭借以下优势，Ubuntu已在2025年牢固确立了其作为最佳机器人Linux操作系统的地位：</p>
<ul>
<li>无缝的ROS 2集成</li>
<li>支持实时计算</li>
<li>AI和机器学习优化</li>
<li>增强的安全性和长期维护</li>
<li>广泛的行业采用</li>
</ul>
<p>无论你是在构建自动驾驶无人机、工业机器人，还是以研究为中心的AI驱动机器人系统，Ubuntu都为成功提供了最佳基础。</p>
<p>如果你计划进入机器人领域，学习Ubuntu、ROS和AI驱动的机器人开发是你能做出的最明智的决定。</p>
<h2>2. Debian机器人操作系统</h2>
<p>在快速发展的机器人世界中，选择正确的操作系统可以决定一个项目的成败。机器人工程师、研究人员和爱好者需要一个不仅稳定可靠，而且配备最新工具和库以支持开发的操作系统。在2025年，Debian机器人操作系统已成为机器人领域最佳的基于Linux的操作系统，提供了无与伦比的稳定性、灵活性和尖端软件支持的组合。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-3.png" alt="" /><br />
<center>Debian机器人操作系统</center></p>
<h3>为什么选择Debian用于机器人技术？</h3>
<p>Debian长期以来以其对自由和开源软件的承诺而闻名，这使其成为机器人开发者的一个有吸引力的选择。与专有系统不同，Debian确保了对庞大工具库的无限制访问，允许开发者在没有许可限制的情况下进行实验、创新和协作。</p>
<p>以下是Debian在2025年成为机器人领域首选Linux发行版的原因：</p>
<ul>
<li>稳定性和可靠性：Debian以其严格的测试过程而闻名。每个稳定版本都经过广泛审查，确保机器人应用程序平稳、一致地运行。</li>
<li>全面的软件包仓库：Debian维护着最大的软件仓库之一，其中包括数千个专门为机器人应用设计的软件包。</li>
<li>社区支持：一个强大而活跃的Debian社区为持续的改进、错误修复和功能增强做出贡献，使机器人开发者更容易解决问题和改进他们的项目。</li>
<li>安全性和性能：Debian增强的安全功能确保机器人系统免受潜在威胁，这在工业自动化和自主系统等关键应用中尤为重要。</li>
</ul>
<p><strong>与ROS的无缝集成</strong></p>
<p>机器人操作系统（ROS）是现代机器人开发的支柱。它提供了必要的工具、库和驱动程序，帮助开发者高效地创建复杂的机器人应用程序。Debian与ROS的深度集成确保了无缝的开发体验，允许用户在没有兼容性问题的情况下利用ROS的功能。</p>
<p>Debian的包管理系统使安装ROS变得简单直接。Debian科学团队积极维护一个专门用于机器人相关软件包的仓库，确保用户始终能访问到最新版本的基本工具。</p>
<p>对于那些从事高级机器人系统开发的开发者来说，Debian对ROS 2（ROS的下一代版本）的支持确保了与更新框架的兼容性、增强的实时性能和改进的安全功能。</p>
<h3>Debian机器人技术的最新发展</h3>
<p>Debian机器人技术在2025年持续发展，取得了显著进步。以下是一些最新的更新：</p>
<p><strong>1. 扩展的机器人软件包仓库</strong></p>
<p>Debian科学团队一直在积极扩展机器人软件包仓库。此次更新包括了流行工具的新的和改进的版本，例如：</p>
<ul>
<li>Gazebo – 一款强大的仿真工具，用于在虚拟环境中测试机器人应用。</li>
<li>MoveIt! – 一个广泛用于机械臂和操纵器的运动规划框架。</li>
<li>OpenCV – 这个计算机视觉库的最新版本现已针对机器人应用中的更佳性能进行了优化。</li>
<li>Navigation Stack – 升级的模块，用于改进自主机器人的路径规划和避障功能。</li>
</ul>
<p>通过这些更新，开发者无需安装第三方仓库即可访问最前沿的工具。</p>
<p><strong>2. 实时内核支持</strong></p>
<p>实时处理对于机器人技术至关重要，精确的计时和快速的响应率是必不可少的。Debian现在正式支持实时Linux内核（RT-PREEMPT），允许开发者以最小的延迟运行对时间敏感的机器人应用程序。</p>
<p>这项更新对于工业机器人、机器人手术和自主无人机尤其有益，因为在这些领域，即使是毫秒级的延迟也可能导致严重问题。</p>
<p><strong>3. 增强的安全功能</strong></p>
<p>随着机器人更多地融入工业和智能环境，安全风险也随之增加。作为回应，Debian为机器人系统引入了先进的安全功能，包括：</p>
<ul>
<li>强制访问控制（MAC） – 强制执行严格的安全策略，以防止对机器人系统的未授权访问。</li>
<li>安全启动支持 – 确保只有经过验证和信任的软件才能在机器人硬件上运行。</li>
<li>自动安全更新 – 实时保护机器人应用免受漏洞和新兴威胁的侵害。</li>
</ul>
<p>凭借这些增强功能，Debian机器人操作系统现在成为依赖机器人进行自动化、医疗和国防的行业的一个更安全的选择。</p>
<p><strong>社区与支持</strong></p>
<p>Debian最大的优势之一是其社区驱动的开发模式。与专有机器人软件不同，Debian受益于全球数千名开发者和研究人员对其改进的贡献。Debian科学邮件列表、论坛和Git仓库是宝贵的资源，用户可以在这些地方讨论问题、分享解决方案和协作项目。</p>
<p>Debian科学团队还确保Debian机器人操作系统与最新的技术进步保持同步，使初学者和专家都能更容易地开始机器人开发。</p>
<h3>为什么在2025年选择Debian机器人操作系统？</h3>
<p><a href="https://wiki.debian.org/DebianScience/Robotics/ROS">Debian机器人操作系统</a>不仅仅是一个操作系统；它是一个生态系统，使开发者、研究人员和企业能够充满信心地构建先进的机器人系统。从其无缝的ROS集成和实时内核支持，到其强大的安全功能和广泛的软件包仓库，Debian为2025年的机器人开发提供了一切所需。</p>
<p>无论你是从事自主机器人、工业自动化还是AI驱动的机器人应用，Debian机器人操作系统都提供了一个稳定、安全和强大的基础，以构建机器人技术的未来。</p>
<p>你在项目中使用Debian机器人操作系统吗？在下面的评论中分享你的想法和经验吧！</p>
<h2>3. 基于ROS的发行版 (ROS 2)</h2>
<p>机器人操作系统（ROS）一直是机器人行业的变革者，为开发机器人应用程序提供了一个强大而灵活的框架。多年来，ROS 2已发展成为致力于尖端机器人解决方案的开发者、研究人员和公司的首选。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-4.png" alt="" /><br />
<center>基于ROS的发行版 (ROS 2)</center></p>
<p>当我们进入2025年，ROS 2发行版已经成熟，提供了改进的实时能力、增强的安全性和更广泛的兼容性。如果你正在寻找最佳的机器人Linux操作系统，本指南将带你了解最新的ROS 2发行版、它们的特性以及运行它们的理想Linux发行版。</p>
<h3>理解ROS 2发行版</h3>
<p>ROS 2发行版是ROS 2框架的定期发布版本，包含了最新的改进、安全补丁和功能升级。</p>
<p>每个发行版都有一个定义的生命周期，通常每两年提供一次长期支持（LTS），而非LTS版本则作为实验性功能的测试平台。选择正确的ROS 2发行版取决于项目稳定性要求、硬件兼容性和功能需求等因素。</p>
<p><strong>2025年的关键ROS 2发行版</strong></p>
<p><strong>1. Jazzy Jalisco (LTS) – 2024年5月23日发布</strong></p>
<p>最新的LTS版本<a href="https://docs.ros.org/en/rolling/Releases/Release-Jazzy-Jalisco.html">Jazzy Jalisco</a>，将获得五年的支持，使其成为工业应用和长期项目的最佳选择。它引入了：</p>
<ul>
<li>针对时间敏感机器人操作的先进实时能力</li>
<li>通过加密通信和认证功能增强的安全性</li>
<li>扩展了对不同硬件平台的兼容性</li>
<li>更好的中间件支持，以提高性能和可伸缩性</li>
</ul>
<p><strong>2. Iron Irwini (非LTS) – 2023年5月23日发布</strong></p>
<p>虽然<a href="https://docs.ros.org/en/iron/Releases/Release-Iron-Irwini.html">Iron Irwini</a>不是LTS版本（支持期1.5年），但它充当了新创新的试验场。希望尝试尖端机器人功能的开发者可以从中受益：</p>
<ul>
<li>更快的开发周期和频繁的更新</li>
<li>实验性的中间件改进</li>
<li>提前接触可能包含在未来LTS版本中的功能</li>
</ul>
<p><strong>3. Humble Hawksbill (LTS) – 2022年5月23日发布</strong></p>
<p><a href="https://docs.ros.org/en/foxy/Releases/Release-Humble-Hawksbill.html">Humble Hawksbill</a>在2025年仍然是一个受欢迎的选择，因为它将获得支持直到2027年。它在以下方面发挥了关键作用：</p>
<ul>
<li>改进中间件通信协议</li>
<li>改进工具和调试能力</li>
<li>在基于ARM的平台上有更好的性能</li>
</ul>
<p>对于在Humble上启动的项目，迁移到Jazzy Jalisco可以确保长期稳定性。</p>
<h3>ROS 2的最新发展（2025年）</h3>
<p>ROS 2持续发展，为机器人技术生态系统带来了几项关键改进：</p>
<p><strong>1. 实时支持</strong></p>
<p>凭借改进的实时调度，ROS 2现在可以处理更复杂的机器人任务，并具有确定性的性能。</p>
<p><strong>2. 安全性增强</strong></p>
<p>通过安全的通信协议和更好的认证机制，ROS 2现在比以往任何时候都更安全，解决了工业机器人和自动驾驶汽车中的安全问题。</p>
<p><strong>3. 跨平台兼容性</strong></p>
<p>虽然Ubuntu仍然是主要的操作系统，但ROS 2已将其支持扩展到Debian、Fedora、Windows甚至macOS。</p>
<p><strong>4. 更好的中间件性能</strong></p>
<p>DDS（数据分发服务）中的中间件改进增强了大型机器人系统中的延迟、可靠性和可伸缩性。</p>
<p>随着机器人技术的不断进步，ROS 2仍然是行业领先的框架，其中Jazzy Jalisco（LTS）是2025年的首选。</p>
<p>对于ROS 2的最佳Linux操作系统，Ubuntu 22.04 LTS作为最稳定和得到最广泛支持的选项脱颖而出。然而，开发者也可以灵活选择Debian、Fedora和Arch Linux。</p>
<p>随着在实时性能、安全性和跨平台支持方面的持续改进，ROS 2正在塑造2025年及以后机器人技术的未来。保持对最新发展的了解，可以确保你的机器人项目保持未来竞争力。</p>
<h2>4. Fedora机器人操作系统</h2>
<p>在不断发展的机器人世界中，选择正确的操作系统对于无缝开发和部署至关重要。截至2025年，Fedora机器人操作系统凭借其专门的工具、强大的社区支持和对开源原则的承诺，已成为机器人领域最强大的Linux发行版之一。无论你是尝试自主机器人的业余爱好者，还是开发工业自动化解决方案的专业人士，Fedora机器人操作系统都提供了一个量身定制的综合平台，以满足你的需求。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-5.png" alt="" /><br />
<center>Fedora机器人操作系统</center></p>
<h3>为什么Fedora机器人操作系统脱颖而出</h3>
<p>Fedora机器人操作系统是Fedora项目的一个专门分支，专为机器人专家设计。它提供了一套精心策划的软件包，涵盖了机器人技术的各个方面，从仿真到硬件接口。以下是Fedora机器人操作系统在2025年广受欢迎的关键原因：</p>
<p><strong>1. 全面的软件套件</strong></p>
<p>Fedora机器人操作系统包含一套广泛的预装软件包，使开发者可以轻松上手，而无需花费数小时设置环境。Fedora机器人操作系统中的一些核心工具包括：</p>
<ul>
<li>Gazebo – 一款强大的3D机器人模拟器，使开发者能够在虚拟环境中测试机器人应用程序。</li>
<li>OpenCV – 广泛用于图像处理和机器学习任务的计算机视觉库。</li>
<li>Arduino IDE – 用于编程微控制器的流行开发环境。</li>
<li>Player/Stage – 在学术界和研究中广泛使用的仿真工具。</li>
<li>Gazebo, V-REP, and Webots – 先进的机器人仿真软件，用于训练AI模型和在虚拟环境中测试算法。</li>
</ul>
<p><strong>2. 与ROS（机器人操作系统）的无缝集成</strong></p>
<p>Fedora机器人操作系统的最大优势之一是其与ROS的无缝集成，ROS是使用最广泛的机器人软件框架。ROS提供了一些基本服务，例如：</p>
<ul>
<li>硬件抽象 – 使控制传感器、电机和执行器变得更加容易。</li>
<li>底层设备控制 – 提供对机器人硬件组件的直接访问。</li>
<li>进程间通信 – 促进不同机器人模块和进程之间的无缝通信。</li>
</ul>
<p>Fedora机器人操作系统预配置了最新版本的ROS 2，确保与尖端的机器人应用兼容。这种集成使开发者能够利用广泛的ROS生态系统，包括库、驱动程序和可视化工具。</p>
<p><strong>3. 强大的社区支持</strong></p>
<p>Fedora机器人操作系统得益于一个由开发者、研究人员和机器人爱好者组成的活跃社区。Fedora机器人特别兴趣小组（SIG）致力于确保Fedora用户能够获得最新的机器人软件和更新。该小组积极维护Fedora的机器人软件包，提供教程，并帮助用户解决问题。</p>
<h3>Fedora机器人技术的最新发展</h3>
<p>Fedora机器人团队一直积极地将该领域的最新进展融入其中。2025年一些最显著的更新包括：</p>
<p><strong>1. 增强的仿真工具</strong></p>
<p>仿真在机器人开发中起着至关重要的作用，它允许开发者在物理机器人上部署算法之前进行测试。Fedora机器人操作系统通过集成以下内容显著改善了其仿真能力：</p>
<ul>
<li>Ignition Gazebo – 一款提供高保真物理和传感器仿真的高级模拟器。</li>
<li>AI驱动的仿真环境 – 支持基于机器学习的仿真，机器人可以在其中学习并适应环境。</li>
</ul>
<p><strong>2. 改进的硬件支持</strong></p>
<p>随着机器人硬件的迅速扩展，Fedora机器人操作系统已包括对以下内容的支持：</p>
<ul>
<li>新的机器人传感器和执行器 – 确保软件和硬件组件之间的无缝通信。</li>
<li>树莓派和Jetson Nano优化 – Fedora机器人操作系统现在在低功耗硬件上运行更高效，非常适合DIY机器人项目。</li>
<li>扩展的驱动程序支持 – Fedora机器人操作系统现在包括用于机械臂、激光雷达传感器和人形机器人的额外驱动程序。</li>
</ul>
<p><strong>3. 教育资源和教程</strong></p>
<p>了解到机器人技术对初学者可能具有挑战性，Fedora机器人操作系统在教育资源上投入了大量资金。这些资源包括：</p>
<ul>
<li>分步教程 – 涵盖从设置开发环境到编程机器人运动的所有内容。</li>
<li>交互式学习模块 – 用户可以在虚拟训练环境中练习为不同的机器人任务编写代码。</li>
<li>在线社区论坛和黑客马拉松 – 为开发者提供协作、学习和分享见解的空间。</li>
</ul>
<p><strong>为什么开发者更喜欢Fedora机器人操作系统而非其他Linux发行版</strong></p>
<p>机器人社区经常争论用于开发的最佳操作系统。虽然像Ubuntu和Debian这样的其他Linux发行版被广泛使用，但Fedora机器人操作系统具有明显的优势：</p>
<ul>
<li>最新的内核和软件包 – Fedora以跟上最新技术而闻名，确保开发者能够访问尖端功能。</li>
<li>为性能和安全优化 – Fedora的安全特性使其成为工业和研究应用的首选。</li>
<li>使用DNF实现无缝包管理 – Fedora的包管理系统效率高，减少了在其他发行版中经常遇到的依赖问题。</li>
</ul>
<p>此外，基于Linux的操作系统通常比Windows更受机器人开发者的青睐，因为它们提供：</p>
<ul>
<li>更好地控制操作系统功能 – 直接访问系统资源。</li>
<li>更简便的依赖管理 – 简化了机器人库的安装。</li>
<li>开源的灵活性 – 可根据项目需求进行完全定制。</li>
</ul>
<p><a href="https://fedoraproject.org/wiki/Robotics">Fedora机器人操作系统</a>无疑是2025年最佳的机器人Linux发行版之一。凭借其广泛的软件套件、强大的ROS集成、改进的硬件支持和活跃的社区，它为机器人专家开发、测试和部署他们的项目提供了一个理想的环境。</p>
<p>随着机器人技术的不断发展，Fedora机器人操作系统仍然致力于走在创新的前沿，使其成为有抱负的和专业的机器人专家的首选。如果你正在寻找一个强大、可靠且面向未来的机器人Linux操作系统，Fedora机器人操作系统是完美的选择。</p>
<h2>5. 用于机器人的OpenEmbedded Linux (Yocto)</h2>
<p>机器人领域正以前所未有的速度发展，人工智能、自动化和边缘计算的进步推动了对强大且可定制的操作系统的需求。在2025年，由Yocto项目驱动的OpenEmbedded Linux，作为机器人领域最佳的基于Linux的操作系统之一脱颖而出。它提供灵活性、可扩展性和优化性能的能力，使其成为从事机器人应用的开发者的首选。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-6.png" alt="" /><br />
<center>用于机器人的OpenEmbedded Linux (Yocto)</center></p>
<p>如果你参与机器人开发——无论是在工业自动化、自动驾驶汽车、无人机还是AI驱动的机器人系统中——了解OpenEmbedded Linux和Yocto的能力将至关重要。在这篇博文中，我们将深入探讨OpenEmbedded Linux（Yocto）如何成为机器人的理想操作系统，探索其最新发展，并讨论其对机器人行业的影响。</p>
<h3>什么是OpenEmbedded Linux和Yocto项目？</h3>
<p><strong>OpenEmbedded Linux</strong></p>
<p>OpenEmbedded是一个开源的构建框架和交叉编译环境，专为创建针对嵌入式设备的Linux发行版而设计。与Ubuntu或Fedora等通用Linux发行版不同，OpenEmbedded允许开发者专门为他们的硬件和应用需求定制和优化他们的Linux构建。</p>
<p><strong>Yocto项目</strong></p>
<p>Yocto项目由Linux基金会于2010年发起，是一个与OpenEmbedded协同工作的合作项目，旨在简化和标准化为嵌入式和物联网设备定制Linux发行版的开发。以BitBake为核心构建系统，Yocto项目为开发者提供工具、模板和最佳实践，以创建最小化、高效且针对硬件优化的基于Linux的操作系统。</p>
<p>对于机器人开发者来说，OpenEmbedded和Yocto项目的结合使他们能够创建为机器人应用量身定制的轻量、快速且针对特定硬件的Linux发行版。</p>
<h3>为什么OpenEmbedded Linux (Yocto)是2025年机器人的理想选择</h3>
<p><strong>高度定制化与模块化</strong></p>
<ul>
<li>与传统的Linux发行版（预装了软件和功能）不同，OpenEmbedded Linux让开发者可以构建一个只包含其机器人系统所需内容的发行版。</li>
<li>这种模块化的方法确保了一个优化且轻量级的操作系统，从而提升性能。</li>
</ul>
<p><strong>硬件抽象与兼容性</strong></p>
<ul>
<li>机器人项目通常涉及各种各样的硬件组件，从传感器和执行器到专用处理器和AI加速器。</li>
<li>OpenEmbedded的基于层的结构使开发者能够创建板级支持包（BSP），从而可以轻松地与不同的硬件架构集成。</li>
</ul>
<p><strong>长期支持与安全性</strong></p>
<ul>
<li>Yocto项目定期发布带有安全补丁的LTS（长期支持）版本，使其成为机器人应用的一个安全稳定的选择。</li>
<li>安全性是机器人技术中的一个主要问题，尤其是在自主系统和工业自动化中，而OpenEmbedded Linux提供了安全启动、内核加固和访问控制策略等功能。</li>
</ul>
<p><strong>更好的资源效率</strong></p>
<ul>
<li>机器人应用通常在低功耗和资源受限的硬件上运行。</li>
<li>OpenEmbedded Linux允许开发者创建极简的Linux构建，减少系统开销并最大化效率。</li>
</ul>
<p><strong>强大的社区与行业采用</strong></p>
<ul>
<li>Yocto项目得到了嵌入式Linux社区和英特尔、高通、恩智浦和德州仪器等主要行业参与者的强力支持。</li>
<li>这意味着为机器人开发者提供了持续的改进、广泛的文档和长期的可靠性。</li>
</ul>
<h3>OpenEmbedded Linux在机器人技术领域的最新发展（2025年）</h3>
<p><strong>1. 上游Linux对机器人硬件的支持</strong></p>
<p>在2025年，像Linaro和高通这样的公司通过将对高通机器人RB5等机器人平台的支持上游化，为OpenEmbedded Linux做出了重大贡献。这一发展确保了下一代机器人系统更好的兼容性、实时处理和AI集成。</p>
<p><strong>2. 改进的培训与学习资源</strong></p>
<p>随着基于Yocto的Linux系统的日益普及，Bootlin和Yocto项目社区等组织推出了新的培训项目、研讨会和在线课程。这些资源使开发者更容易为机器人项目学习、实施和优化OpenEmbedded Linux。</p>
<p><strong>3. 扩展的AI与机器学习能力</strong></p>
<p>OpenEmbedded Linux在集成AI和机器学习框架（如TensorFlow Lite和ROS 2（机器人操作系统））方面取得了重大改进。这使得机器人系统能够执行边缘AI推理、实时决策和高级自动化。</p>
<p><strong>4. 全行业采用与标准化</strong></p>
<p>许多机器人公司和研究机构已转向使用基于Yocto的Linux发行版作为其嵌入式机器人平台。这一转变正在帮助创建一个更加标准化的软件生态系统，减少碎片化并改善机器人设备间的兼容性。</p>
<h3>OpenEmbedded Linux在机器人技术中的应用</h3>
<p><strong>工业自动化</strong></p>
<p>OpenEmbedded Linux正在为需要高性能计算、实时处理和强大安全功能的新一代自动化制造机器人提供动力。</p>
<p><strong>自动驾驶汽车与无人机</strong></p>
<p>机器人公司正在使用基于Yocto的Linux来开发自主无人机和自动驾驶汽车，确保低延迟通信和AI驱动的导航。</p>
<p><strong>医疗机器人</strong></p>
<p>医疗机器人，如手术机器人和康复设备，受益于OpenEmbedded Linux提供安全、实时和稳定操作系统环境的能力。</p>
<p><strong>AI驱动的家庭与服务机器人</strong></p>
<p>智能助手、配送机器人和其他AI驱动的机器人解决方案利用OpenEmbedded Linux进行定制化的AI模型和实时的语音/图像处理。</p>
<h3>OpenEmbedded Linux是2025年最佳的机器人操作系统吗？</h3>
<p>随着机器人行业的不断扩大，对可定制、轻量级和高性能操作系统的需求比以往任何时候都更加关键。由Yocto项目驱动的OpenEmbedded Linux无疑是2025年机器人领域最佳的Linux操作系统。</p>
<p>其提供针对特定硬件的优化、实时处理、安全性和AI集成的能力，使其成为全球机器人专家、工程师和开发者的首选。随着持续的进步和行业采用，OpenEmbedded Linux必将在未来几年塑造机器人技术的未来。</p>
<p>如果你正在开发一个机器人项目，并且需要一个可扩展且高效的Linux操作系统，那么OpenEmbedded Linux (Yocto)是2025年的最佳选择。</p>
<p><strong>下一步是什么？</strong></p>
<p>探索OpenEmbedded Linux：<a href="https://www.openembedded.org/wiki/Main_Page">https://www.openembedded.org/wiki/Main&#95;Page</a></p>
<p>了解更多关于Yocto项目的信息：<a href="https://techrefreshing.com/best-linux-os-for-robotics-in-2025/www.yoctoproject.org">www.yoctoproject.org</a></p>
<p>开始开发：<a href="https://docs.yoctoproject.org/">Yocto文档</a></p>
<p>你在机器人项目中使用OpenEmbedded Linux吗？在下面的评论中分享你的想法和经验吧！</p>
<h2>结论</h2>
<p>在2025年选择最佳的机器人Linux操作系统取决于你的具体需求。如果你需要一个支持良好、对初学者友好的选项，Ubuntu机器人操作系统是你的不二之选。对于稳定性和长期项目，Debian机器人操作系统是一个绝佳的选择。那些从事AI驱动或实验性机器人技术的人应该考虑Fedora机器人操作系统，而嵌入式系统开发者可以依赖基于Yocto的Linux发行版。随着ROS 2、AI和实时内核优化的不断进步，Linux仍然是塑造机器人技术未来的首选操作系统。</p>
<h2><strong>免责声明</strong></h2>
<p>本文中的信息基于截至2025年的最新可用更新。2025年最佳的机器人Linux操作系统可能因特定的硬件、软件更新和项目要求而异。在选择操作系统之前，请务必验证其与你的机器人框架的兼容性。本文仅供参考，不构成专业建议。</p>
<p>本文翻译自文章《<a href="https://techrefreshing.com/best-linux-os-for-robotics-in-2025/">Best Linux OS for Robotics in 2025</a>》- https://techrefreshing.com/best-linux-os-for-robotics-in-2025/</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go安全版图再添利器：OpenPubkey SSH开源，用SSO彻底改变SSH认证</title>
		<link>https://tonybai.com/2025/03/31/openpubkey-ssh-open-source/</link>
		<comments>https://tonybai.com/2025/03/31/openpubkey-ssh-open-source/#comments</comments>
		<pubDate>Sun, 30 Mar 2025 22:05:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AuthorizedKeysCommand]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[bastionzero]]></category>
		<category><![CDATA[cloudflare]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[IdP]]></category>
		<category><![CDATA[key]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[login]]></category>
		<category><![CDATA[Microsoft]]></category>
		<category><![CDATA[OIDC]]></category>
		<category><![CDATA[OP]]></category>
		<category><![CDATA[OpenIDConnect]]></category>
		<category><![CDATA[openpubkey]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[opkssh]]></category>
		<category><![CDATA[pop]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[sshd]]></category>
		<category><![CDATA[sshd_config]]></category>
		<category><![CDATA[SSO]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[公钥]]></category>
		<category><![CDATA[密钥]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[私钥]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4532</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/03/31/openpubkey-ssh-open-source 对于许多开发者和运维工程师而言，管理SSH密钥是一项繁琐且易出错的任务。正如SSH发明者、芬兰计算机科学家Tatu Ylonen所指出的，许多组织中过时授权密钥的数量甚至远超员工人数，这带来了巨大的安全隐患。现在，一个基于Go语言生态的创新项目——OpenPubkey SSH (OPKSSH)，旨在彻底改变这一现状。近日，随着Cloudflare将OPKSSH代码捐赠给Linux基金会下的OpenPubkey项目并将其开源，开发者们终于可以拥抱一种更便捷、更安全的SSH认证方式：使用熟悉的单点登录(SSO)系统。本文将简要介绍OPKSSH项目及其技术基石OpenPubkey技术。 1. 核心看点：OPKSSH 开源与价值解读 OPKSSH (OpenPubkey SSH) 是一个巧妙的工具，它将OpenID Connect (OIDC) 等现代SSO技术与SSH协议集成起来，其核心目标是消除手动管理和配置SSH公私钥的需求，同时不引入除身份提供商(IdP)之外的任何新的可信第三方。 此前，虽然底层的OpenPubkey协议已于2023年成为Linux基金会的开源项目，但OPKSSH作为BastionZero（现已被Cloudflare收购）的产品，一直是闭源的。Cloudflare的此次捐赠，使得整个OpenPubkey技术栈的关键应用层实现也完全开放，这对于Go社区和整个基础设施安全领域都是一个重要进展。 2. OPKSSH解决了什么痛点？ 通常，我们在进行远程服务器管理和运维操作时会使用SSH免密登录，即通过生成SSH密钥对并将公钥复制到远程服务器来实现。但这种传统方式的SSH密钥管理存在诸多问题： 密钥分发与轮换困难：需要手动将公钥部署到目标服务器，密钥泄露或员工离职后的吊销流程复杂。 长期密钥风险：长期存在的私钥增加了泄露风险，一旦泄露，影响范围广。 可见性差：难以清晰追踪谁拥有对哪些服务器的访问权限，公钥本身缺乏身份信息。 这些问题常常困扰企业的IT运维团队和安全管理人员，他们需要确保访问控制的安全性和可管理性，同时降低操作复杂性和人力成本。 那如何解决这些问题呢？OPKSSH带来了新的解决方案。 3. OPKSSH如何解决这些问题？ OPKSSH基于OpenPubkey协议，带来了革命性的改进： 使用临时性密钥(Ephemeral Keys)提升安全性 OPKSSH使用按需生成的临时SSH密钥对取代长期密钥。用户通过SSO登录后，OPKSSH自动生成有效期较短（默认为24小时，可配置）的密钥。这大大缩短了密钥泄露的风险窗口。 通过单点登录(SSO Login)增强易用性 用户只需运行opkssh login，通过熟悉的IdP (如Google, Azure AD等) 进行SSO认证，即可自动获取所需的SSH密钥。无需手动生成、复制或管理私钥文件，即可在任何安装了opkssh的机器上进行SSH连接。 通过Identity-based Auth提升可见性与简化管理 授权不再基于难以管理的公钥列表（比如~/.ssh/known_hosts），而是基于易于理解和审计的用户身份（如Email地址）。管理员只需在服务器配置中指定允许访问的电子邮件地址列表即可。 到这里你可能会问：这么好用的OPKSSH是如何工作的呢？别急，我们下面就来介绍一下OPKSSH的工作原理。 4. OPKSSH的工作原理 Cloudflare的文章中有一个很好的介绍Opkssh工作原理的例子和示意图，这里也借用过来： 如图所示，当用户alice@example.com使用OPKSSH登录服务器，这个过程大致如下： 用户本地执行命令opkssh login触发OIDC流程，用户向IdP认证。 OpenPubkey协议介入，在OIDC流程中巧妙地将用户临时生成的公钥与用户的身份信息绑定，生成一个PK Token(本质上是一个增强的ID Token，包含了公钥信息并由IdP签名)。 OPKSSH将此PK Token打包进一个临时的SSH [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/03/31/openpubkey-ssh-open-source">本文永久链接</a> &#8211; https://tonybai.com/2025/03/31/openpubkey-ssh-open-source</p>
<p>对于许多开发者和运维工程师而言，管理SSH密钥是一项繁琐且易出错的任务。正如<a href="https://ylonen.org/index.html">SSH发明者、芬兰计算机科学家Tatu Ylonen</a>所指出的，许多组织中过时授权密钥的数量甚至远超员工人数，这带来了巨大的安全隐患。现在，一个基于Go语言生态的创新项目——<a href="https://github.com/openpubkey/opkssh">OpenPubkey SSH (OPKSSH)</a>，旨在彻底改变这一现状。近日，随着Cloudflare将<a href="https://github.com/openpubkey/opkssh">OPKSSH代码</a>捐赠给Linux基金会下的<a href="https://github.com/openpubkey">OpenPubkey项目</a>并将其开源，开发者们终于可以拥抱一种更便捷、更安全的SSH认证方式：使用熟悉的单点登录(SSO)系统。本文将简要介绍OPKSSH项目及其技术基石OpenPubkey技术。</p>
<h2>1. 核心看点：OPKSSH 开源与价值解读</h2>
<p>OPKSSH (OpenPubkey SSH) 是一个巧妙的工具，它将<a href="https://tonybai.com/2023/12/22/understand-oidc-by-example/">OpenID Connect (OIDC)</a> 等现代SSO技术与<a href="https://www.ssh.com/academy/ssh">SSH协议</a>集成起来，其核心目标是<strong>消除手动管理和配置SSH公私钥的需求</strong>，同时<strong>不引入除身份提供商(IdP)之外的任何新的可信第三方</strong>。</p>
<p>此前，虽然底层的<a href="https://github.com/openpubkey/openpubkey">OpenPubkey协议已于2023年成为Linux基金会的开源项目</a>，但OPKSSH作为BastionZero（现已被Cloudflare收购）的产品，一直是闭源的。Cloudflare的此次捐赠，使得整个OpenPubkey技术栈的关键应用层实现也完全开放，这对于Go社区和整个基础设施安全领域都是一个重要进展。</p>
<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-2.jpeg" alt="" /></p>
<h2>2. OPKSSH解决了什么痛点？</h2>
<p>通常，我们在进行远程服务器管理和运维操作时会使用SSH免密登录，即通过生成SSH密钥对并将公钥复制到远程服务器来实现。但这种传统方式的SSH密钥管理存在诸多问题：</p>
<ul>
<li><strong>密钥分发与轮换困难</strong>：需要手动将公钥部署到目标服务器，密钥泄露或员工离职后的吊销流程复杂。</li>
<li><strong>长期密钥风险</strong>：长期存在的私钥增加了泄露风险，一旦泄露，影响范围广。</li>
<li><strong>可见性差</strong>：难以清晰追踪谁拥有对哪些服务器的访问权限，公钥本身缺乏身份信息。</li>
</ul>
<p>这些问题常常困扰企业的IT运维团队和安全管理人员，他们需要确保访问控制的安全性和可管理性，同时降低操作复杂性和人力成本。</p>
<p>那如何解决这些问题呢？OPKSSH带来了新的解决方案。</p>
<h2>3. OPKSSH如何解决这些问题？</h2>
<p>OPKSSH基于<a href="https://www.bastionzero.com/openpubkey-faq">OpenPubkey协议</a>，带来了革命性的改进：</p>
<ul>
<li><strong>使用临时性密钥(Ephemeral Keys)提升安全性</strong></li>
</ul>
<p>OPKSSH使用<strong>按需生成的临时SSH密钥对</strong>取代长期密钥。用户通过SSO登录后，OPKSSH自动生成有效期较短（默认为24小时，可配置）的密钥。这大大缩短了密钥泄露的风险窗口。</p>
<ul>
<li><strong>通过单点登录(SSO Login)增强易用性</strong></li>
</ul>
<p>用户只需运行opkssh login，通过熟悉的IdP (如Google, Azure AD等) 进行SSO认证，即可自动获取所需的SSH密钥。无需手动生成、复制或管理私钥文件，即可在任何安装了opkssh的机器上进行SSH连接。</p>
<ul>
<li><strong>通过Identity-based Auth提升可见性与简化管理</strong></li>
</ul>
<p>授权不再基于难以管理的公钥列表（比如~/.ssh/known_hosts），而是基于易于理解和审计的用户身份（如Email地址）。管理员只需在服务器配置中指定允许访问的电子邮件地址列表即可。</p>
<p>到这里你可能会问：这么好用的OPKSSH是如何工作的呢？别急，我们下面就来介绍一下OPKSSH的工作原理。</p>
<h2>4. OPKSSH的工作原理</h2>
<p>Cloudflare的文章中有一个很好的介绍Opkssh工作原理的例子和示意图，这里也借用过来：</p>
<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-5.png" alt="" /></p>
<p>如图所示，当用户alice@example.com使用OPKSSH登录服务器，这个过程大致如下：</p>
<ul>
<li>用户本地执行命令opkssh login触发OIDC流程，用户向IdP认证。</li>
<li>OpenPubkey协议介入，在OIDC流程中巧妙地将用户<strong>临时生成的公钥</strong>与用户的身份信息绑定，生成一个<strong>PK Token</strong>(本质上是一个增强的ID Token，包含了公钥信息并由IdP签名)。</li>
<li>OPKSSH将此PK Token打包进一个临时的SSH 公钥文件（利用SSH证书的扩展字段）。</li>
<li>当用户发起SSH连接时，这个特殊的公钥文件被发送到服务器。</li>
<li>服务器配置了AuthorizedKeysCommand指令，调用opkssh verify(OpenPubkey验证器)。</li>
<li>验证器检查PK Token的有效性（签名、有效期、颁发者），提取公钥和用户身份(Email)，并根据服务器配置判断该用户是否有权访问。</li>
</ul>
<p>关键在于，这一切<strong>无需修改现有的SSH客户端或服务器软件本身</strong>，仅需在服务器端sshd_config中添加两行配置即可启用，这个我们在本文后面会详细说明。</p>
<p>OPKSSH的魔力源于其底层的<strong>OpenPubkey</strong>协议。OpenPubkey本身是一个基于Go语言实现的Linux基金会项目 (<a href="https://github.com/openpubkey/openpubkey">github.com/openpubkey/openpubkey</a>)。</p>
<p>OpenPubkey的核心创新在于，它通过一种<strong>客户端修改</strong>的方式，将用户持有的公钥(PKu)与OIDC的ID Token进行了加密绑定，而<strong>无需 OIDC 提供商(OP)作任何修改</strong>。这是通过巧妙利用OIDC流程中的nonce参数实现的。客户端不再生成完全随机的nonce，而是生成一个包含其公钥等信息的<strong>客户端实例声明(cic)</strong>，并将cic的哈希值作为nonce发送给OP。OP在签发ID Token时会包含这个nonce。这样，最终得到的PK Token就同时承载了OP 对用户身份的认证以及用户对其公钥的所有权声明（通过客户端的额外签名防止身份误绑定攻击）。</p>
<p>这一机制将OIDC的认证模型从<strong>持有者认证(Bearer Authentication)</strong> 升级到了<strong>持有证明(Proof-of-Possession, PoP)</strong>。在Bearer模型下，任何窃取到ID Token的人都可以冒充用户；而在PoP模型下，用户需要证明自己持有与PK Token中公钥对应的私钥，从而有效抵御<strong>令牌重放(Token Replay)</strong> 和<strong>令牌泄露(Token Export)</strong> 攻击，安全性显著提高。</p>
<p>OpenPubkey的设计还考虑了可扩展性，例如引入MFA-Cosigner概念，可以进一步增强安全性，甚至在OP本身被攻陷的情况下也能提供保护。关于OpenPubkey协议设计的详细内容，可以参见参考资料中OpenPubkey的论文，这里就不赘述了。</p>
<p>了解了原理之后，下面我们来实际验证一下opkssh通过IdP实现SSO一键登录服务器的效果。</p>
<h2>5. 使用opkssh实现免密登录服务器</h2>
<p>这次验证的环境是这样的：</p>
<ul>
<li>客户端：macOS</li>
<li>服务端：Ubuntu 22.04.1 LTS </li>
<li>IdP：microsoft (注：国内访问microsoft的服务器成功率高)</li>
</ul>
<p>我们先来看看客户端的操作步骤：</p>
<h3>5.1 opkssh在客户端的操作</h3>
<p>首先在客户端安装opkssh，你可以选择直接下载编译好的opkssh二进制文件：</p>
<pre><code>$curl -L https://github.com/openpubkey/opkssh/releases/latest/download/opkssh-osx-amd64 -o opkssh; chmod +x opkssh
</code></pre>
<p>由于opkssh是纯Go实现的，如果你本地有Go工具链，也可以选择通过源码安装(在国内，可能选择源码安装的速度更快)：</p>
<pre><code>$go install github.com/openpubkey/opkssh@latest
</code></pre>
<p>安装完成后，我们就来进行客户端的IdP认证。输入下面命令：</p>
<pre><code>$opkssh login
INFO[0000] Opening browser to http://127.0.0.1:59638/chooser
</code></pre>
<p>该命令会打开本地浏览器，并展示下面页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-3.png" alt="" /></p>
<p>截止到目前，opkssh支持选择Google、Microsoft或Gitlab作为IdP，这里我们选择<strong>Sign in with Microsoft</strong>。</p>
<p>之后浏览器将跳转到下面页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/openpubkey-ssh-open-source-4.png" alt="" /></p>
<p>这里使用我的Microsoft账号进行身份认证，点击“接受”，即完成认证，之后你可以关闭页面！</p>
<p>而命令行也会提示下面信息：</p>
<pre><code>INFO[0002] listening on http://127.0.0.1:3000/
INFO[0002] press ctrl+c to stop
Writing opk ssh public key to /Users/tonybai/.ssh/id_ed25519.pub and corresponding secret key to /Users/tonybai/.ssh/id_ed25519Keys generated for identity
Email, sub, issuer, audience:
bigwhite.cn@hotmail.com AAAAAAAAAAAAAAAAAAAAAP5YMhbf2Ufl_eI1PdK12VE https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 096ce0a3-5e72-4da8-9c86-12924b294a01
</code></pre>
<p>接下来，我们再来看看服务端要进行的操作与配置。</p>
<h3>5.2 opkssh在服务端的操作</h3>
<p>在要登录的服务器端安装opkssh，由于安装后还要进行一些设置，我建议直接采用opkssh项目提供的安装脚本进行安装：</p>
<pre><code>$ wget -qO- "https://raw.githubusercontent.com/openpubkey/opkssh/main/scripts/install-linux.sh" | sudo bash
Detected OS is debian
Created group: opksshuser
Created user: opksshuser with group: opksshuser
Downloading version latest of opkssh from https://github.com/openpubkey/opkssh/releases/latest/download/opkssh-linux-amd64...
opkssh                           100%[========================================================&gt;]  16.01M  83.4MB/s    in 0.2s
Installed opkssh to /usr/local/bin/opkssh
Configuring opkssh:
  Creating sudoers file at /etc/sudoers.d/opkssh...
  Adding sudoers rule for opksshuser...
Installation successful! Run 'opkssh' to use it.
</code></pre>
<p>之后我们需要修改一下服务端的sshd server的配置。SSH服务器支持一个名为AuthorizedKeysCommand的配置参数，该参数允许我们使用自定义程序来确定SSH公钥是否被授权。因此，我们通过对/etc/ssh/sshd_config文件进行以下两行更改，将SSH服务器的配置文件更改为使用OpenPubkey验证程序而不是SSH默认的验证程序：</p>
<pre><code>AuthorizedKeysCommand /usr/local/bin/opkssh verify %u %k %t
AuthorizedKeysCommandUser opksshuser
</code></pre>
<p>然后通过opkssh添加授权的用户，这些用户登录后将具备root用户权限：</p>
<pre><code>$opkssh add root bigwhite.cn@hotmail.com microsoft
Successfully added new policy to /etc/opk/auth_id
</code></pre>
<p>最后重启一下sshd服务：</p>
<pre><code>$systemctl daemon-reload
$systemctl status sshd
</code></pre>
<h3>5.3 ssh登录验证</h3>
<blockquote>
<p>注：为了避免使用之前的ssh免密登录，可以在服务端将.ssh/authorized_keys中的公钥删除！</p>
</blockquote>
<p>服务端的opkssh命令行被sshd服务调用进行客户端验证时，会在/var/log/opkssh.log中打印相关日志，这也是opkssh起到作用的一个间接证明。</p>
<p>我在客户端依然以原先的ssh登录命令尝试登录服务器：</p>
<pre><code>$ssh root@&lt;your_server_ip&gt;
</code></pre>
<p>我们在服务端opkssh.log中可以看到下面一些输出：</p>
<pre><code>2025/03/29 02:57:43 /usr/local/bin/opkssh verify root AAAAKGVjZHNhLXNoYTItbDUQAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSXO9YZhMPnGkYfnwpFu/HeX29s7q0l4lK5qCgvaeaWh3zBSidDh49Nirsu5Iwh7YVRkKMa5q+hhnJEFAh7FL5LAAAAZAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAASQAAACEAqD5msj3BsQhlpszOJHBoIcmK3Ex/BwyNWKHgp6labScAAAAgULO5naYi9xOmzrShcGiVIprRbdSvdWltioSVKu63h6Y= ecdsa-sha2-nistp256-cert-v01@openssh.com
2025/03/29 02:57:43 Providers loaded:  https://accounts.google.com 206584157355-7cbe4s640tvm7naoludob4ut1emii7sf.apps.googleusercontent.com 24h
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 096ce0a3-5e72-4da8-9c86-12924b294a01 24h
https://gitlab.com 8d8b7024572c7fd501f64374dec6bba37096783dfcd792b3988104be08cb6923 24h

2025/03/29 02:57:44 warning: failed to load user policy: failed to read user policy file /root/.opk/auth_id: error reading root home policy using command /usr/bin/sudo -n /usr/local/bin/opkssh readhome root got output Failed to read user's home policy file: failed to open /root/.opk/auth_id, open /root/.opk/auth_id: no such file or directory
 and err exit status 1
2025/03/29 02:57:44 successfully verified
</code></pre>
<p>之后，我就成功登录到服务器上了！</p>
<h2>6.小结</h2>
<p>OPKSSH 的开源是 OpenPubkey 项目和 Go 安全生态的重要里程碑。它不仅提供了一个解决 SSH 密钥管理难题的实用方案，也展示了 Go 语言在构建安全、可靠的基础设施工具方面的强大能力。</p>
<p>我们鼓励对安全、身份认证和 Go 开发感兴趣的开发者们：</p>
<ul>
<li><strong>试用 OPKSSH</strong>: 在你的开发或测试环境中体验 SSO 登录 SSH 的便捷。</li>
<li><strong>关注 OpenPubkey 项目</strong>: Star GitHub 仓库，了解最新动态。</li>
<li><strong>参与社区贡献</strong>: 通过 Pull Request、Issue 反馈、参与讨论等方式为项目贡献力量。可以在 OpenSSF Slack 的 <code>#openpubkey</code> 频道找到社区成员，或参加每月一次的社区会议。</li>
</ul>
<p>随着 OPKSSH 的加入和持续发展，我们期待 OpenPubkey 能够在更多场景下发挥价值，例如代码签名 (Sigstore 集成)、端到端加密通信等，进一步丰富和巩固 Go 语言在云原生和安全领域的基础设施地位。</p>
<h2>7. 参考资料</h2>
<ul>
<li><a href="https://github.com/openpubkey/opkssh/">OPKSSH项目</a> &#8211; https://github.com/openpubkey/opkssh</li>
<li><a href="https://eprint.iacr.org/2023/296">Paper: OpenPubkey: Augmenting OpenID Connect with User held Signing Keys</a> &#8211; https://eprint.iacr.org/2023/296</li>
<li><a href="https://github.com/openpubkey/openpubkey/">OpenPubkey项目</a> &#8211; https://github.com/openpubkey/openpubkey/</li>
<li><a href="https://blog.cloudflare.com/open-sourcing-openpubkey-ssh-opkssh-integrating-single-sign-on-with-ssh">Open-sourcing OpenPubkey SSH (OPKSSH): integrating single sign-on with SSH</a> &#8211; https://blog.cloudflare.com/open-sourcing-openpubkey-ssh-opkssh-integrating-single-sign-on-with-ssh</li>
<li><a href="https://www.docker.com/blog/how-to-use-openpubkey-to-solve-key-management-via-sso">How to Use OpenPubkey to Solve Key Management via SSO</a> &#8211; https://www.docker.com/blog/how-to-use-openpubkey-to-solve-key-management-via-sso/</li>
<li><a href="https://www.bastionzero.com/openpubkey-faq">Open Pubkey Frequently Asked Questions</a> &#8211; https://www.bastionzero.com/openpubkey-faq</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/03/31/openpubkey-ssh-open-source/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go，14周年[译]</title>
		<link>https://tonybai.com/2023/11/11/go-opensource-14-years/</link>
		<comments>https://tonybai.com/2023/11/11/go-opensource-14-years/#comments</comments>
		<pubDate>Fri, 10 Nov 2023 22:05:01 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[coverage-profile]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[go1.22]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[gopls]]></category>
		<category><![CDATA[govulncheck]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[loop]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[PGO]]></category>
		<category><![CDATA[profile]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[slog]]></category>
		<category><![CDATA[typeparameter]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vscode]]></category>
		<category><![CDATA[wasi]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[WebAssembly]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[兼容性]]></category>
		<category><![CDATA[向前兼容]]></category>
		<category><![CDATA[向后兼容]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[工具链]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[测试覆盖率]]></category>
		<category><![CDATA[漏洞]]></category>
		<category><![CDATA[类型参数]]></category>
		<category><![CDATA[结构化日志]]></category>
		<category><![CDATA[编译器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4042</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/11/11/go-opensource-14-years 国内的双十一购物狂欢已没有了当年的那种热闹与喧嚣，但大洋彼岸的Go团队却始终保持稳中有增的开发和语言演进节奏。今晨Go核心团队的Russ Cox代表Go语言项目团队在Go官博上发表了《Fourteen Years of Go》的博文，纪念Go语言开源14周年，并对2023年以来Go语言的演进进行了归纳总结，并对Go在其第15个年头将要做的改进给予了很高的期望。这里对博文做简单翻译，供大家参考。 今天，我们欢庆Go语言开源发布十四周年！Go在过去一年中取得了巨大的进步，发布了两个功能特性丰富的版本，并达成了其他一些重要的里程碑。 我们在2月发布了Go 1.20，在8月发布了Go 1.21，在这两个版本中，我们更多地关注实现改进而不是新语言特性。 我们在Go 1.20版本中发布了Profile-guided optimization(PGO)功能的预览版，并在Go 1.21中正式发布了该功能，它允许Go编译器读取程序的Profile，然后花更多时间对程序中运行最频繁的部分进行优化。在Go 1.21中，启用PGO后，工作负载的CPU使用率通常可以提高2%到7%。关于PGO的介绍请参阅“Go 1.21中的Profile-guided optimization”，对PGO的全面说明请参阅“PGO用户指南”。 Go从Go 1.2版本开始就支持在go test期间收集覆盖率profile数据。Go 1.20版本增加了对go build构建的二进制文件收集测试覆盖率profile数据的支持，这样你就可以在集成测试期间收集测试覆盖率数据，详情请参阅“Go集成测试的代码覆盖率”。 兼容性一直是Go的重要组成部分，我们最初对兼容性的承诺始于“Go 1和Go程序的未来”这篇文章。针对那些可能会给现有程序造成破坏但又必须要修正的重要错误，Go 1.21版本通过扩展GODEBUG的约定用法进一步改进了兼容性。请参阅博文“后向兼容性，Go 1.21和Go 2”了解概况，详情请参阅文档“Go、后向兼容性和GODEBUG”。 Go 1.21还发布了对内置工具链管理的支持，允许你像改变其他依赖的版本一样轻松地改变特定模块(module)中使用的Go工具链版本。请参阅博文“Go 1.21中的向前兼容性和工具链管理”，更多详情请参阅文档“Go工具链”。 另一个在工具链方面的重要成就是将磁盘索引集成到gopls(Go语言服务器)。这将gopls的启动延迟和内存使用缩短了3-5倍。“扩展gopls以适应不断增长的Go生态系统”一文解释了其中的技术细节。你可以通过运行以下命令确保运行最新的gopls: $go install golang.org/x/tools/gopls@latest Go 1.21引入了新的cmp、maps和slices包 —— Go的第一个泛型标准库 —— 以及扩展了可比较类型(comparable)的集合。详情请参阅博文“所有可比较的类型”。 总体而言，我们继续完善泛型，并通过会议演讲和撰写博文来解释重要细节。今年两篇值得关注的博文是“分解类型参数”和“关于类型推断你一直想知道的事情 —— 以及更多”。 Go 1.21中另一个重要的新包是log/slog，它为标准库添加了结构化日志的官方API。请参阅“使用slog实现结构化日志”了解概况。 在对WebAssembly(Wasm)的移植方面，Go 1.21增加了在WebAssembly System Interface(WASI) preview1版本上运行的支持。WASI preview1是一种新的“操作系统”接口，支持大多数服务器端的Wasm环境。详情请参阅“Go对WASI的支持”一文。 在安全方面，我们将继续确保Go在帮助开发人员了解其依赖关系和漏洞方面处于领先地位，7月发布的Govulncheck 1.0正是这样的例子。如果你使用VS [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-opensource-14-years-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/11/11/go-opensource-14-years">本文永久链接</a> &#8211; https://tonybai.com/2023/11/11/go-opensource-14-years</p>
<p>国内的双十一购物狂欢已没有了当年的那种热闹与喧嚣，但大洋彼岸的Go团队却始终保持稳中有增的开发和语言演进节奏。今晨Go核心团队的<a href="https://swtch.com/~rsc/">Russ Cox</a>代表Go语言项目团队在Go官博上发表了<a href="https://go.dev/blog/14years">《Fourteen Years of Go》</a>的博文，纪念<a href="https://opensource.googleblog.com/2009/11/hey-ho-lets-go.html">Go语言开源14周年</a>，并对2023年以来Go语言的演进进行了归纳总结，并对Go在其第15个年头将要做的改进给予了很高的期望。这里对博文做简单翻译，供大家参考。</p>
<hr />
<p>今天，我们欢庆<a href="https://opensource.googleblog.com/2009/11/hey-ho-lets-go.html">Go语言开源发布十四周年</a>！Go在过去一年中取得了巨大的进步，发布了两个功能特性丰富的版本，并达成了其他一些重要的里程碑。</p>
<p>我们在2月发布了<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20/">Go 1.20</a>，在8月发布了<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21/">Go 1.21</a>，在这两个版本中，我们更多地关注实现改进而不是新语言特性。</p>
<p>我们在<a href="https://go.dev/blog/pgo-preview">Go 1.20版本中发布了Profile-guided optimization(PGO)功能的预览版</a>，并<a href="https://go.dev/blog/pgo">在Go 1.21中正式发布了该功能</a>，它允许Go编译器读取程序的Profile，然后花更多时间对程序中运行最频繁的部分进行优化。在Go 1.21中，启用PGO后，工作负载的CPU使用率通常可以提高2%到7%。关于PGO的介绍请参阅“<a href="https://go.dev/blog/pgo">Go 1.21中的Profile-guided optimization</a>”，对PGO的全面说明请参阅“<a href="https://go.dev/doc/pgo">PGO用户指南</a>”。</p>
<p>Go<a href="https://go.dev/blog/cover">从Go 1.2版本开始</a>就支持在go test期间收集覆盖率profile数据。Go 1.20版本增加了对go build构建的二进制文件收集测试覆盖率profile数据的支持，这样你就可以在集成测试期间收集测试覆盖率数据，详情请参阅“<a href="https://go.dev/blog/integration-test-coverage">Go集成测试的代码覆盖率</a>”。</p>
<p><a href="https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/">兼容性一直是Go的重要组成部分</a>，我们最初对兼容性的承诺始于“<a href="https://go.dev/doc/go1compat">Go 1和Go程序的未来</a>”这篇文章。针对那些可能会给现有程序造成破坏但又必须要修正的重要错误，Go 1.21版本通过扩展GODEBUG的约定用法进一步改进了兼容性。请参阅博文“<a href="https://go.dev/blog/compat">后向兼容性，Go 1.21和Go 2</a>”了解概况，详情请参阅文档“<a href="https://go.dev/doc/godebug">Go、后向兼容性和GODEBUG</a>”。</p>
<p>Go 1.21还发布了对内置工具链管理的支持，允许你像改变其他依赖的版本一样轻松地改变特定模块(module)中使用的Go工具链版本。请参阅博文“<a href="https://go.dev/blog/toolchain">Go 1.21中的向前兼容性和工具链管理</a>”，更多详情请参阅文档“<a href="https://go.dev/doc/toolchain">Go工具链</a>”。</p>
<p>另一个在工具链方面的重要成就是将磁盘索引集成到gopls(Go语言服务器)。这将gopls的启动延迟和内存使用缩短了3-5倍。“<a href="https://go.dev/blog/gopls-scalability">扩展gopls以适应不断增长的Go生态系统</a>”一文解释了其中的技术细节。你可以通过运行以下命令确保运行最新的gopls:</p>
<pre><code>$go install golang.org/x/tools/gopls@latest
</code></pre>
<p>Go 1.21引入了新的<a href="https://go.dev/pkg/cmp/">cmp</a>、<a href="https://go.dev/pkg/maps/">maps</a>和<a href="https://go.dev/pkg/slices/">slices</a>包 —— Go的第一个泛型标准库 —— 以及扩展了可比较类型(comparable)的集合。详情请参阅博文“<a href="https://go.dev/blog/comparable">所有可比较的类型</a>”。</p>
<p>总体而言，我们继续完善泛型，并通过会议演讲和撰写博文来解释重要细节。今年两篇值得关注的博文是“<a href="https://go.dev/blog/deconstructing-type-parameters">分解类型参数</a>”和“<a href="https://go.dev/blog/type-inference">关于类型推断你一直想知道的事情 —— 以及更多</a>”。</p>
<p>Go 1.21中另一个重要的新包是<a href="https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go">log/slog</a>，它为标准库添加了<a href="https://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration/">结构化日志</a>的官方API。请参阅“<a href="https://go.dev/blog/slog">使用slog实现结构化日志</a>”了解概况。</p>
<p>在对WebAssembly(Wasm)的移植方面，Go 1.21增加了在WebAssembly System Interface(WASI) preview1版本上运行的支持。WASI preview1是一种新的“操作系统”接口，支持大多数服务器端的Wasm环境。详情请参阅“<a href="https://go.dev/blog/wasi">Go对WASI的支持</a>”一文。</p>
<p>在安全方面，我们将继续确保Go在帮助开发人员了解其依赖关系和漏洞方面处于领先地位，7月发布的<a href="https://tonybai.com/2022/09/10/an-intro-of-govulncheck">Govulncheck 1.0</a>正是这样的例子。如果你使用VS Code，可以通过Go扩展直接在编辑器中运行govulncheck。请参阅<a href="https://go.dev/doc/tutorial/govulncheck-ide">govulncheck IDE教程</a>了解如何开始使用govulncheck。如果你使用GitHub，可以使用<a href="https://github.com/marketplace/actions/golang-govulncheck-action">GitHub Action for govulncheck</a>将运行govulncheck作为CI/CD流程的一部分。有关检查依赖项漏洞问题的更多信息，请参阅今年的Google I/O大会的演讲“<a href="https://www.youtube.com/watch?v=HSt6FhsPT8c&amp;ab_channel=TheGoProgrammingLanguage">使用Go和Google构建更安全的应用程序</a>”。</p>
<p>另一个重要的安全里程碑是Go 1.21的高度可重现的工具链构建。详情请参阅“<a href="https://go.dev/blog/rebuild">完全可重现的经验证的Go工具链</a>”，包括在没有使用任何Linux工具的情况下在Mac上重现Ubuntu Linux Go工具链的演示。</p>
<p>这是非常繁忙的一年!</p>
<p>在Go的第15个年头，我们将继续努力使Go成为最佳的大规模软件工程环境。我们特别兴奋的一个变化是重新定义for循环中”:=”的语义，以消除意外别名bug的可能性。详情请参阅“<a href="https://go.dev/blog/loopvar-preview">在Go 1.22中修复For循环</a>”，其中包括在Go 1.21中对此更改的预览版的说明。</p>
<p>感谢!</p>
<p>Go项目一直远不止我们在Google的Go小组。感谢所有贡献者和Go社区中的每一个人，使得今天的Go成为可能。我们衷心祝愿大家在未来一年中一切顺利。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/11/11/go-opensource-14-years/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>让reviewdog支持gitlab-push-commit，守住代码质量下限</title>
		<link>https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor/</link>
		<comments>https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor/#comments</comments>
		<pubDate>Thu, 08 Sep 2022 13:37:30 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.gitlab-ci.yml]]></category>
		<category><![CDATA[.reviewdog.yml]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[diff]]></category>
		<category><![CDATA[fortran]]></category>
		<category><![CDATA[fuzzing]]></category>
		<category><![CDATA[gerrit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[git-commit]]></category>
		<category><![CDATA[git-diff]]></category>
		<category><![CDATA[git-push]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[gitlab]]></category>
		<category><![CDATA[gitlab-runner]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golangci-lint]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[pre-commit-hook]]></category>
		<category><![CDATA[reviewdog]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[代码评审]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[同行评审]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[程序设计语言]]></category>
		<category><![CDATA[编程]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[集成测试]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3654</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor 一. 代码质量保证的手段 从世界上首款计算机高级程序设计语言Fortran自上世纪50年代诞生以来，编程这个行当已经走过了近70年。虽然年头已不少，但不可否认的一点是：软件生产依然无法像硬件那样标准化，同一个小功能，N个程序员的有N种实现方法。 那么如何保证生产出的软件的质量符合我们的要求呢？不同领域的程序员都在进行着努力，比如：做编译器的让编译器更加严格，努力将内存安全问题彻底消除(如Rust)；做工具链的为程序员提供了内置于语言的各种单测、集成测试、接口测试、fuzzing test等工具(如Go工具链)，让程序员可以更容易地对自己所写的代码进行全方位的测试，以期找出更多的代码中的潜在问题&#8230; 当然，还有一种主观的代码质量保证方法目前依旧是主流，它就是是同行的代码评审(code review, cr)。 代码评审的方法主要有两种，一种是大家坐到一个会议室中，对某个人的某段代码“发表大论”；另外一种则是利用像gerrit这样的工具，在线对其他人的某次提交的代码或某PR的代码进行“评头论足”。 不过无论哪种，最初的时候大家都会细无巨细地从语法层面看到代码结构设计，再到业务逻辑层面，但这样做的弊端也是很显而易见，那就是效率低下，不聚焦(focus)。 于是人们想到了：能否利用工具来尽可能地发现语法层面的问题，这样代码评审时，人类专家便可以聚焦代码结构设计与业务逻辑层面的问题，分工明确后，效率自然提升(如下图)： 注：目前绝大多数工具链仅能自动帮助程序员解决语法层面的问题。将来，随着工具的日益强大，工具可以不断升级关注层次，逐渐进化到具备发现代码结构设计问题，甚至可以发现业务层面逻辑问题的能力。 于是就有了reviewdog这样的可以调用各种linter工具对代码进行自动扫描并将问题以comment的形式自动提交的代码仓库的工具。 到这里很多朋友会问，即便让工具来关注语法层面的问题，为何要用reviewdog这样的工具，git的pre-commit hook、git server hooks、利用Make等工具做开发阶段检查等手段也能检查代码中的语法问题，它们不再香了吗？ 下面简单看看这些方法的“问题”(我们假设大家都已经在使用git作为代码版本管理工具)： git pre-commit-hook git pre-commit hook是一个客户端的git hook，它是放在开发人员本地代码copy中的.git/hooks目录下的钩子，当开发人员在本地执行git commit时会被唤起执行。pre-commot hook的问题就在于我们没法在中心代码仓库对pre-commit hook的脚本内容做统一管理和维护。这个更适合开发人员根据自己的喜好、代码素养在自己的开发环境下部署。 此外，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，pre-commit hook就不在生效。 利用Make等工具做本地检查 利用make工具，我们可以在本地build代码之前对代码做lint等各种静态检查，但和pre-commit-hook一样，虽然Makefile可以提交代码仓库，但真正用于检查代码的工具依旧是在开发人员本地，难于对工具版本，设定的检查规则进行统一管理维护，可能导致不同开发人员环境有不一致的情况。另外同样的情况，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，Make工具依赖的代码检查工具可能并不存在，检查环节就无法有效实施。 git server hooks git支持server hooks，gitlab自12.8版本也开始支持server hooks(替换之前的custom hooks)。 Git server支持以下钩子： pre-receive post-receive update 我倒是没有深研究过这些server hooks是否能满足我们的功能要求，但就git server hooks的部署特点就决定了，它不适合，因为它要在gitlab的server上执行，这就意味着我们需要的所有静态代码检查工具都要部署和配置在与gitlab server同一个环境中，这耦合性太强，根本不便于我们对这些静态代码检查工具的管理与日常维护。 而像reviewdog这样的工具将与ci工具(比如gitlab-ci)集成，运行在slave/worker/runner的机器上，而这些机器上的环境便很容易统一的定制与管理。 好了，下面进入reviewdog时间！ 注：我们以代码仓库为gitlab为例，我曾做过小调查，目前企业内部基本都在使用gitlab搭建私有git仓库，除了那些自实现code仓库平台的大厂。 二. [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor">本文永久链接</a> &#8211; https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor</p>
<h3>一. 代码质量保证的手段</h3>
<p>从世界上首款计算机高级程序设计语言<a href="https://fortran-lang.org/en/">Fortran</a>自上世纪50年代诞生以来，编程这个行当已经走过了近70年。虽然年头已不少，但不可否认的一点是：<strong>软件生产依然无法像硬件那样标准化，同一个小功能，N个程序员的有N种实现方法</strong>。</p>
<p>那么如何保证生产出的软件的质量符合我们的要求呢？不同领域的程序员都在进行着努力，比如：做编译器的让编译器更加严格，努力将内存安全问题彻底消除(如<a href="https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together">Rust</a>)；做工具链的为程序员提供了内置于语言的各种单测、集成测试、接口测试、fuzzing test等工具(如Go工具链)，让程序员可以更容易地对自己所写的代码进行全方位的测试，以期找出更多的代码中的潜在问题&#8230;</p>
<p>当然，还有一种主观的代码质量保证方法目前依旧是主流，它就是是<a href="https://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/"><strong>同行的代码评审(code review, cr)</strong></a>。</p>
<p>代码评审的方法主要有两种，一种是大家坐到一个会议室中，对某个人的某段代码“发表大论”；另外一种则是利用像<a href="https://www.gerritcodereview.com">gerrit</a>这样的工具，在线对其他人的某次提交的代码或某PR的代码进行“评头论足”。</p>
<p>不过无论哪种，最初的时候大家都会细无巨细地从语法层面看到代码结构设计，再到业务逻辑层面，但这样做的弊端也是很显而易见，那就是<strong>效率低下，不聚焦(focus)</strong>。</p>
<p>于是人们想到了：能否利用工具来尽可能地发现语法层面的问题，这样代码评审时，人类专家便可以聚焦代码结构设计与业务逻辑层面的问题，分工明确后，效率自然提升(如下图)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-2.png" alt="" /></p>
<blockquote>
<p>注：目前绝大多数工具链仅能自动帮助程序员解决语法层面的问题。将来，随着工具的日益强大，工具可以不断升级关注层次，逐渐进化到具备发现代码结构设计问题，甚至可以发现业务层面逻辑问题的能力。</p>
</blockquote>
<p>于是就有了<a href="https://github.com/reviewdog/reviewdog">reviewdog</a>这样的可以调用各种linter工具对代码进行自动扫描并将问题以comment的形式自动提交的代码仓库的工具。</p>
<p>到这里很多朋友会问，即便让工具来关注语法层面的问题，为何要用reviewdog这样的工具，git的pre-commit hook、git server hooks、利用Make等工具做开发阶段检查等手段也能检查代码中的语法问题，它们不再香了吗？</p>
<p>下面简单看看这些方法的“问题”(我们假设大家都已经在使用git作为代码版本管理工具)：</p>
<ul>
<li>git pre-commit-hook  </li>
</ul>
<p>git pre-commit hook是一个客户端的git hook，它是放在开发人员本地代码copy中的.git/hooks目录下的钩子，当开发人员在本地执行git commit时会被唤起执行。pre-commot hook的问题就在于我们没法在中心代码仓库对pre-commit hook的脚本内容做统一管理和维护。这个更适合开发人员根据自己的喜好、代码素养在自己的开发环境下部署。</p>
<p>此外，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，pre-commit hook就不在生效。</p>
<ul>
<li>利用Make等工具做本地检查</li>
</ul>
<p>利用make工具，我们可以在本地build代码之前对代码做lint等各种静态检查，但和pre-commit-hook一样，虽然Makefile可以提交代码仓库，但真正用于检查代码的工具依旧是在开发人员本地，难于对工具版本，设定的检查规则进行统一管理维护，可能导致不同开发人员环境有不一致的情况。另外同样的情况，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，Make工具依赖的代码检查工具可能并不存在，检查环节就无法有效实施。</p>
<ul>
<li>git server hooks</li>
</ul>
<p>git支持<a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#_server_side_hooks">server hooks</a>，<a href="https://docs.gitlab.com/ee/administration/server_hooks.html">gitlab自12.8版本也开始支持server hooks</a>(替换之前的custom hooks)。</p>
<p>Git server支持以下钩子：</p>
<ul>
<li>pre-receive</li>
<li>post-receive</li>
<li>update</li>
</ul>
<p>我倒是没有深研究过这些server hooks是否能满足我们的功能要求，但就git server hooks的部署特点就决定了，它不适合，因为它要在gitlab的server上执行，这就意味着我们需要的所有静态代码检查工具都要部署和配置在与gitlab server同一个环境中，这耦合性太强，根本不便于我们对这些静态代码检查工具的管理与日常维护。</p>
<p>而像reviewdog这样的工具将与ci工具(比如gitlab-ci)集成，运行在slave/worker/runner的机器上，而这些机器上的环境便很容易统一的定制与管理。</p>
<p>好了，下面进入reviewdog时间！</p>
<blockquote>
<p>注：我们以代码仓库为gitlab为例，我曾做过小调查，目前企业内部基本都在使用gitlab搭建私有git仓库，除了那些自实现code仓库平台的大厂。</p>
</blockquote>
<h3>二. reviewdog是什么</h3>
<p>reviewdog是一个什么样的工具呢？我们来看看下面这幅示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-3.png" alt="" /></p>
<p>我们看到，这是一幅基于gitlab的ci执行流程图，在这个流程中，reviewdog运行在gitlab-runner节点，也就是负责真正执行ci job的节点上。每当开发人员执行一次git push，将commit同步到代码仓库，一次ci job将被触发，在承载该ci job的gitlab-runner节点上，reviewdog被唤起，它做了三件事：</p>
<ul>
<li>调用静态代码检查工具对最新pull下来的代码进行检查；</li>
<li>将代码检查结果(第几行有问题)与commit diff的结果进行比对，得到交集(即commit diff中变更(add和update)的代码行与代码检查结果的行一致的，放入交集中)；</li>
<li>将交集中代码检查结果信息以gitlab commit comment的形式post到gitlab仓库中</li>
</ul>
<p>这样开发人员就可以通过commit页面看到这些comments，并应对这些comment，必要情况下，会修复这些问题。</p>
<p>我们看到reviewdog和其他工具相比，最大的不同就是可以找出commit diff与lint结果中的交集，并与代码仓库交互，将这些交集中的结果以comments的形式放入commit页面，<strong>就像同行代码评审时，同行直接在你的commit页面添加comment一样</strong>。</p>
<p>然而当前版本的reviewdog还不支持直接在gitlab-push-commit上做检查与提交comment，可能是这样的场景较为少见，因为目前开源项目更多采用基于pr(pull request)的工作流，所以reviewdog内置了诸如github-pr-check、github-pr-review、gitlab-mr-commit等工作流的代码review。而像我们使用的基于gitlab-push-commit可能并不多见（当然我们内部使用这种也是有特定上下文的）。</p>
<p>那么如何让reviewdog支持gitlab-push-commit，即对push动作中的commit进行静态代码检查并将结果以comment的形式放入commit页面呢？我们只能<a href="https://github.com/bigwhite/reviewdog">fork reviewdog项目</a>，并在<a href="https://github.com/bigwhite/reviewdog">fork后的项目</a>中自行添加对gitlab-push-commit模式的支持。</p>
<h3>三. 改造reviewdog以支持gitlab-push-commit模式</h3>
<p>reviewdog就是一个命令行工具，通常就是一次性执行，因此它的代码结构较为清晰。我们可以简单围绕它支持的几种reporter模式来搞清楚如何增加对gitlab-push-commit模式的支持。</p>
<p>这里说明一下gitlab-push-commit模式的含义，首先该模式适用于开发人员通过git push推送代码到gitlab时触发的ci job。在该ci job中，reviewdog会运行配置的静态代码分析工具(比如golangci-lint等)对最新的代码进行扫描，并得到问题集合；然后获取最新的commit的sha值(CI_COMMIT_SHA)以及push之前的latest commit的sha值(CI_COMMIT_BEFORE_SHA)，并比较这两个版本间的diff。最后通过文件名与行号将问题集合与diff集合中的“交集”找出来，并将结果以comment形式通过gitlab client api提交到的此次push的最新的那个commit的页面。</p>
<p>目前该模式尚存在一个“瑕疵”，那就是如果一个push中有多个commit，那么gitlab-push-commit模式不会针对每个commit做diff和comment，而只是会用push中的latest commit与push之前的最新commit做比较。</p>
<p>定义清除gitlab-push-commit模式含义后，我们就可以“照葫芦画瓢”的为reviewdog增加该模式的支持了！</p>
<p>在main.go中，我们主要是在run函数中增加一个reporter case分支：</p>
<pre><code>// https://github.com/bigwhite/reviewdog/blob/master/cmd/reviewdog/main.go
func run(r io.Reader, w io.Writer, opt *option) error {
... ...

case "gitlab-push-commit":
    build, cli, err := gitlabBuildWithClient(opt.reporter)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] gitlabBuildWithClient ok\n")

    gc, err := gitlabservice.NewGitLabPushCommitsCommenter(cli, build.Owner, build.Repo, build.SHA)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\n")

    cs = reviewdog.MultiCommentService(gc, cs)
    ds, err = gitlabservice.NewGitLabPushCommitsDiff(cli, build.Owner, build.Repo, build.SHA, build.BeforeSHA)
    if err != nil {
        return err
    }
    log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\n")
... ...

}
</code></pre>
<p>在这个case中，我们主要是为后面的project.Run或reviewdog.Run方法准备gitlab client对象、PushCommitsCommenter对象(位于service/gitlab/gitlab_push_commits.go中)、PushCommitsDiff对象(位于service/gitlab/gitlab_push_commits_diff.go中)等。</p>
<p>gitlab_push_commits.go和gitlab_push_commits_diff.go是新增的两个go源文件，也是参考了同目录下的gitlab_mr_commit.go和gitlab_mr_diff.go改写而成的。具体代码这里就不列出来了，大家有兴趣可以自行阅读。</p>
<h3>四. 部署gitlab-runner验证新版reviewdog</h3>
<p>下面我们就来验证一下上述改造后的reviewdog。</p>
<h4>1. 安装gitlab-runner</h4>
<p>我们先在gitlab上建立一个实验项目，然后为该项目配置ci。如果你的gitlab还没有注册gitlab-runner，可以按下面步骤安装和注册runner节点(可以在顶层group下面建立，这样runner可以在group内共享：settings => CI/CD => Runners => Show runner installation instructions 有部署runner的详细命令说明)：</p>
<pre><code>//假设我们有一个ubuntu 20.04的主机，我们可以按下面命令安装和注册一个gitlab-runner：

sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# Give it permissions to execute
sudo chmod +x /usr/local/bin/gitlab-runner

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

# 注册该runner
sudo gitlab-runner register --url http://{gitlab-server-ip-addr}/ --registration-token {registration token}
</code></pre>
<p>上面命令会在/etc/gitlab-runner下面建立一个runner自用配置文件：config.toml：</p>
<pre><code>//  /etc/gitlab-runner/config.toml

concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "runner for ard group"
  url = "http://gitlab_ip_addr/"
  id = 1
  token = "{registration token}"
  token_obtained_at = 2022-09-01T11:03:43Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "shell"
  shell = "bash"
  environment = ["PATH=/home/tonybai/.bin/go1.18/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"]
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
</code></pre>
<p>这里我选择了shell executor，即基于主机shell执行ci job中的命令。runners下的environment可以设置shell的环境变量，这里的设置将覆盖对应账号(比如gitlab-runner)下的环境变量值。</p>
<p>gitlab-runner部署成功后，我们在group的runners下面便可以看到下面的available runners：</p>
<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-4.png" alt="" /></p>
<blockquote>
<p>注：在创建runner时，我为该runner设置了两个tag：ard和ci。</p>
<p>注：确保runner执行的命令在主机的PATH下面可以找到。</p>
</blockquote>
<h4>2. 创建personal access token</h4>
<p>reviewdog需要通过gitlab client API访问gitlab仓库获取信息并提交comments，这就需要我们为runner执行的命令提供access token。</p>
<p>gitlab有多种access token，比如：personal access token、project access token等。我们创建personal access token，我也测试过project access token，使用<a href="https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html">project access token</a>可以成功提交comment，但是notify mail十有八九无法发送出来。</p>
<p>access token要保存好，因为它只显示一次。</p>
<p>我们将personal access token配置到实验项目的variable中(Settings => CI/CD => variables)，variable的key为REVIEWDOG_GITLAB_API_TOKEN，值为刚刚创建的token。</p>
<p>后续每次CI job执行，该variable会作为预定义的环境变量对job生效。我们的reviewdog便可以使用该token访问gitlab。</p>
<h4>3. 配置实验项目的ci pipeline</h4>
<p>我们可以通过代码的形式配置实验项目的ci pipeline，我们在项目根目录下建立.gitlab-ci.yml文件，其内容如下：</p>
<pre><code>// .gitlab-ci.yml

build-job:
  tags:
      - ard
  stage: build
  script:
    - export CI_REPO_OWNER=ard/incubators
    - export CI_REPO_NAME=learn-gitlab
    - reviewdog -reporter=gitlab-push-commit
  only:
    - master
    - pushes
</code></pre>
<p>.gitlab-ci.yml的具体字段含义可以参考gitlab文档。在这个配置中，值得注意的有几点：</p>
<ul>
<li>使用tags关联runner(这里用ard这个tag)；</li>
<li>script部分是job具体执行的命令列表，这里先设置CI_REPO_OWNER和CI_REPO_NAME两个环境变量，供reviewdog使用；然后执行reviewdog；</li>
<li>only部分描述仅针对master分支的push事件触发ci job。</li>
</ul>
<h4>4. 配置.reviewdog.yml</h4>
<p>最后，我们来配置一下适合实验项目的reviewdog的配置文件。我们同样在项目根目录下建立.reviewdog.yml文件，其内容如下：</p>
<pre><code>runner:
  golangci:
    cmd: golangci-lint run --max-same-issues=0 --out-format=line-number ./...
    errorformat:
      - '%E%f:%l:%c: %m'
      - '%E%f:%l: %m'
      - '%C%.%#'
    level: warning
</code></pre>
<p>在这里我们看到，我们使用golangci-lint这个静态检查工具对实验项目的代码进行检查。这里的&#8211;max-same-issues=0的含义是不限制相同错误的数量。至于.reviewdog.yml的具体格式，reviewdog项目自身的<a href="https://github.com/bigwhite/reviewdog/blob/master/.reviewdog.yml">.reviewdog.yml</a>很具参考价值，大家需要时可以仔细研究。</p>
<h4>5. 推送代码并验证reviewdog的执行结果</h4>
<p>我们可以故意在代码中写下有问题的一些代码，这些问题要保证可以被golangci-lint工具扫描出来，比如：</p>
<pre><code>package main

type Foo struct {
    A int
    B string
    C bool
}

func Demo1() error {
    return nil
}

func Demo2() error {
    return nil
}

func Demo3() error {
    return nil
}

func main() {
    f := &amp;Foo{1, "tony", false}
    _ = f
    Demo2()
    Demo1()
    Demo3()
}
</code></pre>
<p>这里并没有对Demo函数调用进行错误处理，golangci-lint中的errcheck可以检测出这个问题。提交并push这些代码到仓库，稍等片刻，我们便可收到notify mail，打开commit页面，便会看到下面这样的commit comments：</p>
<p><img src="https://tonybai.com/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-5.png" alt="" /></p>
<p>看到这样的结果，说明reviewdog按预期工作了！</p>
<h3>五. 小结</h3>
<p>本文介绍了如何基于reviewdog对push提交的commit进行静态代码检查并像一个“同行”一样在commit中提交评论的方法。</p>
<p>这样做的目的就是希望通过工具提升代码评审的效率，同时也守住代码质量的下限。</p>
<p>就像本文开始所说的那样，随着检查工具能力的增强，这样的基于reviewdog自动检查代码的方案在保证代码质量方面还可以继续提升。</p>
<p>Go开源了go/ast等工具链，有能力的童鞋可以基于go/ast自行开发具有“特定目的”的检查工具并集成到reviewdog中，这将使得检查更有针对性和有效性。</p>
<p>本文涉及源码在<a href="https://github.com/bigwhite/reviewdog">这里</a>下载 &#8211; https://github.com/bigwhite/reviewdog/</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Go语言实现eBPF程序内核态与用户态的双向数据交换</title>
		<link>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/</link>
		<comments>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/#comments</comments>
		<pubDate>Mon, 25 Jul 2022 13:26:19 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpf2go]]></category>
		<category><![CDATA[bpfObjects]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BPF_MAP_TYPE_HASH]]></category>
		<category><![CDATA[BTF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CO-RE]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[falco]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-generate]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[isovalent]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katran]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[libbpf-bootstrap]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[llvm-objdump]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[pixie]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[readelf]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[submodule]]></category>
		<category><![CDATA[Thoughtworks]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[火焰图]]></category>
		<category><![CDATA[符号表]]></category>
		<category><![CDATA[系统调用]]></category>
		<category><![CDATA[网络]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3629</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go 在之前的两篇文章中，无论是使用C语言开发eBPF程序，还是使用Go开发的eBPF程序，都是hello world级别的，可能有用，但谈不上十分实用。 通常来说，一个实用的eBPF程序，它的内核态部分与用户态部分是有数据交换的，有了这种数据交换，eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性，eBPF MAP是绕不过去的机制。 在这一篇有关eBPF程序开发的文章中，我们就来看看如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换。 一. why BPF MAP？ 永远不要忘记BPF字节码是运行于OS内核态的代码，这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据，通常仅能通过系统调用陷入内核态来实现。因此，在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。 那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢？用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢？ Linux内核BPF开发者于是就引入了BPF MAP机制。BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道。同时由于bpf map存储在内核分配的内存空间，处于内核态，可以被运行于在内核态的多个BPF程序所共享，同样可以作为多个BPF程序交换和共享数据的机制。 二. BPF MAP不是狭义的map数据结构 BPF MAP究竟是什么呢？它不是我们狭义理解的哈希映射表的数据结构，而是一种通用数据结构，可以存储不同类型数据的通用数据结构。用著名内核BPF开发者Andrii Nakryiko的话来说，MAP就是BPF中代表抽象数据容器(abstract data container)的一个概念。 截至目前，内核BPF支持的MAP类型已经有20+种，下面是libbpf中bpf.h中列出的当前支持的MAP类型： // libbpf/include/uapi/linux/bpf.h enum bpf_map_type { BPF_MAP_TYPE_UNSPEC, BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_PROG_ARRAY, BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_PERCPU_ARRAY, BPF_MAP_TYPE_STACK_TRACE, BPF_MAP_TYPE_CGROUP_ARRAY, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH, BPF_MAP_TYPE_LPM_TRIE, BPF_MAP_TYPE_ARRAY_OF_MAPS, BPF_MAP_TYPE_HASH_OF_MAPS, BPF_MAP_TYPE_DEVMAP, BPF_MAP_TYPE_SOCKMAP, BPF_MAP_TYPE_CPUMAP, BPF_MAP_TYPE_XSKMAP, BPF_MAP_TYPE_SOCKHASH, BPF_MAP_TYPE_CGROUP_STORAGE, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY, BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go">本文永久链接</a> &#8211; https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go</p>
<p>在之前的两篇文章中，无论是<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">使用C语言开发eBPF程序</a>，还是<a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/">使用Go开发的eBPF程序</a>，都是hello world级别的，可能有用，但谈不上十分实用。</p>
<p>通常来说，一个实用的eBPF程序，它的内核态部分与用户态部分是有数据交换的，有了这种数据交换，eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性，<strong>eBPF MAP是绕不过去的机制</strong>。</p>
<p>在这一篇有关eBPF程序开发的文章中，我们就来看看<strong>如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换</strong>。</p>
<h3>一. why BPF MAP？</h3>
<p>永远不要忘记BPF字节码是运行于OS内核态的代码，这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据，通常仅能通过系统调用陷入内核态来实现。因此，在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。</p>
<p>那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢？用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢？</p>
<p>Linux内核BPF开发者于是就引入了<a href="https://www.kernel.org/doc/html/latest/bpf/maps.html">BPF MAP机制</a>。<strong>BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道</strong>。同时由于bpf map存储在内核分配的内存空间，处于内核态，可以被运行于在内核态的多个BPF程序所共享，同样可以作为多个BPF程序交换和共享数据的机制。</p>
<h3>二. BPF MAP不是狭义的map数据结构</h3>
<p>BPF MAP究竟是什么呢？它不是我们狭义理解的哈希映射表的数据结构，而是<a href="https://man7.org/linux/man-pages/man2/bpf.2.html">一种通用数据结构，可以存储不同类型数据的通用数据结构</a>。用著名内核BPF开发者<a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">Andrii Nakryiko</a>的话来说，<strong>MAP就是BPF中代表<a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">抽象数据容器(abstract data container)</a>的一个概念</strong>。</p>
<p>截至目前，内核BPF支持的MAP类型已经有20+种，下面是libbpf中bpf.h中列出的当前支持的MAP类型：</p>
<pre><code>// libbpf/include/uapi/linux/bpf.h
enum bpf_map_type {
    BPF_MAP_TYPE_UNSPEC,
    BPF_MAP_TYPE_HASH,
    BPF_MAP_TYPE_ARRAY,
    BPF_MAP_TYPE_PROG_ARRAY,
    BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    BPF_MAP_TYPE_PERCPU_HASH,
    BPF_MAP_TYPE_PERCPU_ARRAY,
    BPF_MAP_TYPE_STACK_TRACE,
    BPF_MAP_TYPE_CGROUP_ARRAY,
    BPF_MAP_TYPE_LRU_HASH,
    BPF_MAP_TYPE_LRU_PERCPU_HASH,
    BPF_MAP_TYPE_LPM_TRIE,
    BPF_MAP_TYPE_ARRAY_OF_MAPS,
    BPF_MAP_TYPE_HASH_OF_MAPS,
    BPF_MAP_TYPE_DEVMAP,
    BPF_MAP_TYPE_SOCKMAP,
    BPF_MAP_TYPE_CPUMAP,
    BPF_MAP_TYPE_XSKMAP,
    BPF_MAP_TYPE_SOCKHASH,
    BPF_MAP_TYPE_CGROUP_STORAGE,
    BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
    BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
    BPF_MAP_TYPE_QUEUE,
    BPF_MAP_TYPE_STACK,
    BPF_MAP_TYPE_SK_STORAGE,
    BPF_MAP_TYPE_DEVMAP_HASH,
    BPF_MAP_TYPE_STRUCT_OPS,
    BPF_MAP_TYPE_RINGBUF,
    BPF_MAP_TYPE_INODE_STORAGE,
    BPF_MAP_TYPE_TASK_STORAGE,
    BPF_MAP_TYPE_BLOOM_FILTER,
};
</code></pre>
<p>这里数据结构类型众多，但不是本文的重点，我们不一一介绍了。其中的BPF_MAP_TYPE_HASH类型是BPF支持的第一种MAP数据结构，这个类型可以理解为我们日常接触的hash映射表，通过键值对的形式索引数据。在后续的例子中我们将使用这种类型的MAP。</p>
<p>那么BPF MAP是如何可以在内核态与用户态共享数据的？原理是什么呢？</p>
<p>从<a href="https://man7.org/linux/man-pages/man2/bpf.2.html">bpf这个系统调用的说明</a>中，我们能找到端倪。下面是bpf系统调用的函数原型：</p>
<pre><code>// https://man7.org/linux/man-pages/man2/bpf.2.html

#include &lt;linux/bpf.h&gt;

int bpf(int cmd, union bpf_attr *attr, unsigned int size);
</code></pre>
<p>从bpf的原型来看，似乎比较简单。但bpf其实是一个“富调用”，即不止能干一件事，通过cmd传入的值不同，它可以围绕BPF完成很多事情。最主要的功能是加载bpf程序(cmd=BPF_PROG_LOAD)，其次是围绕MAP的一系列操作，包括创建MAP(cmd=BPF_MAP_CREATE)、MAP元素查询(cmd=BPF_MAP_LOOKUP_ELEM)、MAP元素值更新(cmd=BPF_MAP_UPDATE_ELEM)等。</p>
<p>当cmd=BPF_MAP_CREATE时，即bpf执行创建MAP的操作后，bpf调用会返回一个文件描述符fd，<strong>通过该fd后续可以操作新创建的MAP</strong>。通过fd访问map，这个<strong>很unix</strong>！</p>
<p>当然这么底层的系统调用，一般BPF用户态开发人员无需接触到，像libbpf就包装了一系列的map操作函数，这些函数不会暴露map fd给用户，简化了使用方法，提升了使用体验。</p>
<p>下面我们先来看一下如何用C语言实现基于map的BPF用户态与内核态的数据交换。</p>
<h3>三. 使用C基于libbpf使用map的示例</h3>
<p>这个示例改造自<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld">helloworld示例</a>。原helloworld示例在execve这个系统调用被调用时输出一条内核日志(在/sys/kernel/debug/tracing/trace_pipe中可以查看到)，用户态程序并没有与内核态程序做任何数据交换。</p>
<p>在这个新示例(execve_counter)中，我们依然跟踪系统调用execve，不同的是我们对execve进行调用计数，并将技术存储在BPF MAP中。而用户态部分程序则读取该MAP中的计数并定时输出计数值。</p>
<p>我们先来看看BPF内核态部分的源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter/execve_counter.bpf.c

#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;

typedef __u64 u64;
typedef char stringkey[64];

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 128);
    //__type(key, stringkey);
    stringkey* key;
    __type(value, u64);
} execve_counter SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
  stringkey key = "execve_counter";
  u64 *v = NULL;
  v = bpf_map_lookup_elem(&amp;execve_counter, &amp;key);
  if (v != NULL) {
    *v += 1;
  }
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
</code></pre>
<p>和helloworld示例不同，我们在新示例中定义了一个map结构execve_counter，通过SEC宏将其标记为BPF MAP变量。</p>
<p>这个map结构有四个字段：</p>
<ul>
<li>type: 使用的BPF MAP类型(参见前面的bpf_map_type枚举类型)，这里我们使用BPF_MAP_TYPE_HASH，即一个hash散列表结构；</li>
<li>max_entries：map内的key-value对的最大数量；</li>
<li>key: 指向key内存空间的指针。这里我们自定义了一个类型stringkey(char[64])来表示每个key元素的类型；</li>
<li>value: 指向value内存空间的指针，这里value元素的类型为u64，一个64位整型。</li>
</ul>
<p>内核态函数bpf_prog的实现也比较简单：在上面的map中查询”execve_counter”这个key，如果查到了，则将得到的value指针指向的内存中的值加1。</p>
<p>我们再来看看execve_counter这个示例的用户态部分的程序源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve_counter/execve_counter.c

#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include &lt;linux/bpf.h&gt;
#include "execve_counter.skel.h"

typedef __u64 u64;
typedef char stringkey[64];

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct execve_counter_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = execve_counter_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }

    /* Load &amp; verify BPF programs */
    err = execve_counter_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* init the counter */
    stringkey key = "execve_counter";
    u64 v = 0;
    err = bpf_map__update_elem(skel-&gt;maps.execve_counter, &amp;key, sizeof(key), &amp;v, sizeof(v), BPF_ANY);
    if (err != 0) {
        fprintf(stderr, "Failed to init the counter, %d\n", err);
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = execve_counter_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    for (;;) {
            // read counter value from map
            err = bpf_map__lookup_elem(skel-&gt;maps.execve_counter, &amp;key, sizeof(key), &amp;v, sizeof(v), BPF_ANY);
            if (err != 0) {
               fprintf(stderr, "Lookup key from map error: %d\n", err);
               goto cleanup;
            } else {
               printf("execve_counter is %llu\n", v);
            }

            sleep(5);
    }

cleanup:
    execve_counter_bpf__destroy(skel);
    return -err;
}
</code></pre>
<p>map是在execve_counter_bpf__load中完成的创建，跟踪代码你会发现(参考libbpf源码)，最终会调用bpf系统调用创建map。</p>
<p>和helloworld示例不同的是，我们在attach handler之前，先使用libbpf封装的bpf_map__update_elem初始化了bpf map中的key(初始化为0，如果没有这一步，第一次bpf程序执行时，会提示找不到key)。</p>
<p>然后attach handler后，我们在一个循环中每隔5s通过bpf_map__lookup_elem查询一下key=”execve_counter”的值并输出到控制台。</p>
<p>用户态程序之所以可以直接使用map，是因为bpftool基于execve_counter.bpf.c生成的execve_counter.skel.h中包含了map的各种信息。</p>
<p>接下来我们执行make编译一下这个ebpf程序，然后执行并观察输出：</p>
<pre><code>$sudo ./execve_counter
libbpf: loading object 'execve_counter_bpf' from buffer
libbpf: elf: section(3) tracepoint/syscalls/sys_enter_execve, size 192, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 24 insns (192 bytes)
libbpf: elf: section(4) .reltracepoint/syscalls/sys_enter_execve, size 16, link 22, flags 0, type=9
libbpf: elf: section(5) .rodata, size 64, link 0, flags 2, type=1
libbpf: elf: section(6) .maps, size 32, link 0, flags 3, type=1
libbpf: elf: section(7) license, size 13, link 0, flags 3, type=1
libbpf: license of execve_counter_bpf is Dual BSD/GPL
libbpf: elf: section(13) .BTF, size 898, link 0, flags 0, type=1
libbpf: elf: section(15) .BTF.ext, size 176, link 0, flags 0, type=1
libbpf: elf: section(22) .symtab, size 744, link 1, flags 0, type=2
libbpf: looking for externs among 31 symbols...
libbpf: collected 0 externs total
libbpf: map 'execve_counter': at sec_idx 6, offset 0.
libbpf: map 'execve_counter': found type = 1.
libbpf: map 'execve_counter': found key [9], sz = 64.
libbpf: map 'execve_counter': found value [13], sz = 8.
libbpf: map 'execve_counter': found max_entries = 128.
libbpf: map 'execve_c.rodata' (global data): at sec_idx 5, offset 0, flags 480.
libbpf: map 1 is "execve_c.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(3) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #15 against 'execve_counter'
libbpf: prog 'bpf_prog': found map 0 (execve_counter, sec 6, off 0) for insn #15
libbpf: map 'execve_counter': created successfully, fd=4
libbpf: map 'execve_c.rodata': created successfully, fd=5
execve_counter is 0
execve_counter is 0
execve_counter is 9
execve_counter is 23
... ...
</code></pre>
<blockquote>
<p>注：如果不知道如何编译execve_counter这个示例，请先移步<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>了解其构建原理。</p>
</blockquote>
<p>bpftool工具提供了查看map的特性，我们可以通过它查看示例创建的map：</p>
<pre><code>$sudo bpftool map
114: hash  name execve_counter  flags 0x0
    key 64B  value 8B  max_entries 128  memlock 20480B
    btf_id 120
116: array  name execve_c.rodata  flags 0x80
    key 4B  value 64B  max_entries 1  memlock 4096B
    frozen
</code></pre>
<p>我们还可以dump一下整个map：</p>
<pre><code>$sudo bpftool map dump id 114
[{
        "key": "execve_counter",
        "value": 23
    }
]
</code></pre>
<p>我们看到，整个map中就一个键值对(key=”execve_counter”)，其值与示例的用户态部分程序输出的一致。</p>
<p>好了，有了C示例作为基础，我们再来看看如何基于Go来实现这个示例。</p>
<h3>四. 使用Go基于cilium/ebpf实现execve-counter示例</h3>
<p>使用Go开发BPF用户态部分程序要容易的多，cilium/ebpf提供了的包用起来很简单。如果还不知道如何用Go开发ebpf用户态部分的套路，请先移步<a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go">《使用Go语言开发eBPF程序》</a>一文了解一下。</p>
<p>Go语言示例的必不可少的原料是execve_counter.bpf.c，这个C源码文件与上面的execve_counter示例中的execve_counter.bpf.c的唯一差别就是include的头文件改成了common.h：</p>
<pre><code>$diff execve_counter.bpf.c ../execve-counter/execve_counter.bpf.c
1,2c1,2
&lt;
&lt; #include "common.h"
---
&gt; #include &lt;linux/bpf.h&gt;
&gt; #include &lt;bpf/bpf_helpers.h&gt;
</code></pre>
<p>基于原料execve_counter.bpf.c，bpf2go工具会生成用户态部分所需的Go源码，比如：bpfObject中包含的bpf map实例：</p>
<pre><code>// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
    ExecveCounter *ebpf.Map `ebpf:"execve_counter"`
}
</code></pre>
<p>最后，我们在main包main函数中直接使用这些生成的与bpf objects相关的Go函数即可，下面是main.go部分源码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf execve_counter.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&amp;objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    // init the map element
    var key [64]byte
    copy(key[:], []byte("execve_counter"))
    var val int64 = 0
    if err := objs.bpfMaps.ExecveCounter.Put(key, val); err != nil {
        log.Fatalf("init map key error: %s", err)
    }

    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case &lt;-ticker.C:
            if err := objs.bpfMaps.ExecveCounter.Lookup(key, &amp;val); err != nil {
                log.Fatalf("reading map error: %s", err)
            }
            log.Printf("execve_counter: %d\n", val)

        case &lt;-stopper:
            // Wait for a signal and close the perf reader,
            // which will interrupt rd.Read() and make the program exit.
            log.Println("Received signal, exiting program..")
            return
        }
    }
}
</code></pre>
<p>在main函数，我们通过objs.bpfMaps.ExecveCounter直接访问map实例，并通过其Put和Lookup方法可以直接操作map。这里要注意的是key的类型必须与execve_counter.bpf.c中的key类型(char[64])保持内存布局一致，不能直接用string类型，否则会在执行时报下面错误：</p>
<pre><code>init map key error: can't marshal key: string doesn't marshal to 64 bytes
</code></pre>
<p>编译和执行execve-counter-go和helloworld-go别无二致：</p>
<pre><code>$make
$go run -exec sudo main.go bpf_bpfel.go

2022/07/17 16:59:52 execve_counter: 0
2022/07/17 16:59:57 execve_counter: 14
^C2022/07/17 16:59:59 Received signal, exiting program..
</code></pre>
<h3>五. 小结</h3>
<p>本文介绍了eBPF内核态部分与用户态部分进行数据交换的主要方法：BPF MAP机制。这里的MAP不是狭义的一种hash散列表，而是一个抽象数据结构容器，目前支持二十几种数据结构，大家可以根据自己的需求挑选适当的结构（可查询手册了解各种数据结构的特点)。</p>
<p>MAP本质上也是由bpf系统调用创建的，bpf程序只需要声明map的key、value、type等组成信息即可。用户态可以通过bpf系统调用返回的fd操作map，libbpf和cilium/ebpf等封装了对fd的操作，这样简化了API的使用。</p>
<p>内核中map的update操作不是原子的，因此当有多个bpf程序并发访问一个map时，需要同步操作。bpf提供了bpf_spin_lock来实现对map操作的同步。我们可以在value类型中加入bpf_spin_lock来同步对value的修改，就像下面的例子(例子来自<a href="https://book.douban.com/subject/33398015/">《Linux Observability with BPF》</a>一书)：</p>
<pre><code>struct concurrent_element {
    struct bpf_spin_lock semaphore;
    int count;
}

struct bpf_map_def SEC("maps") concurrent_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(int),
    .value_size = sizeof(struct concurrent_element),
    .max_entries = 100,
};

int bpf_program(struct pt_regs *ctx) {
      intkey=0;
      struct concurrent_element init_value = {};
      struct concurrent_element *read_value;
      bpf_map_create_elem(&amp;concurrent_map, &amp;key, &amp;init_value, BPF_NOEXIST);
      read_value = bpf_map_lookup_elem(&amp;concurrent_map, &amp;key);
      bpf_spin_lock(&amp;read_value-&gt;semaphore);
      read_value-&gt;count += 100;
      bpf_spin_unlock(&amp;read_value-&gt;semaphore);
}
</code></pre>
<p>本文涉及代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples">这里</a>下载。</p>
<h3>六. 参考资料</h3>
<ul>
<li><a href="https://www.ebpf.top/post/map_internal/">《揭秘BPF map前生今世》</a> &#8211; https://www.ebpf.top/post/map_internal/</li>
<li><a href="https://mp.weixin.qq.com/s/Is84xGHFExE1BPkbPpKjwg">《边缘网络eBPF超能力：eBPF map原理与性能解析》</a> &#8211; https://mp.weixin.qq.com/s/Is84xGHFExE1BPkbPpKjwg</li>
<li><a href="https://man7.org/linux/man-pages/man2/bpf.2.html">bpf系统调用说明</a> &#8211; https://man7.org/linux/man-pages/man2/bpf.2.html</li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/maps.html">官方bpf map参考手册</a> &#8211; https://www.kernel.org/doc/html/latest/bpf/maps.html</li>
<li><a href="https://www.mankier.com/8/bpftool">bpftool参考手册</a> &#8211; https://www.mankier.com/8/bpftool</li>
<li><a href="https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps">《Building BPF applications with libbpf-bootstrap》</a> &#8211; https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>使用Go语言开发eBPF程序</title>
		<link>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/</link>
		<comments>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/#comments</comments>
		<pubDate>Tue, 19 Jul 2022 13:11:17 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpf2go]]></category>
		<category><![CDATA[bpfObjects]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BTF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CO-RE]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[falco]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-generate]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[isovalent]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katran]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[libbpf-bootstrap]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[llvm-objdump]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[pixie]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[readelf]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[submodule]]></category>
		<category><![CDATA[Thoughtworks]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[火焰图]]></category>
		<category><![CDATA[符号表]]></category>
		<category><![CDATA[网络]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3625</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/19/develop-ebpf-program-in-go 在前面的《使用C语言从头开发一个Hello World级别的eBPF程序》一文中，我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础，因为到目前为止，无论eBPF程序的用户态部分用什么语言开发，运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来，其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了，Go语言也不例外。 在Go社区中，目前最为活跃的用于开发eBPF用户态部分的Go eBPF包莫过于cilium项目开源的cilium/ebpf，cilium项目背后的Isovalent公司也是eBPF技术在云原生领域应用的主要推手之一。 本文我们就来说说基于cilium/ebpf开发eBPF程序的套路！ 一. 探索cilium/ebpf项目示例 cilium/ebpf项目借鉴了libbpf-boostrap的思路，通过代码生成与bpf程序内嵌的方式构建eBPF程序用户态部分。为了搞清楚基于cilium/ebpf开发ebpf程序的套路，我们先来探索一下cilium/ebpf项目提供的示例代码。 我们首先来下载和看看ebpf的示例的结构。 下载cilium/ebpf项目 $ git clone https://github.com/cilium/ebpf.git Cloning into 'ebpf'... remote: Enumerating objects: 7054, done. remote: Counting objects: 100% (183/183), done. remote: Compressing objects: 100% (112/112), done. remote: Total 7054 (delta 91), reused 124 (delta 69), pack-reused 6871 Receiving objects: 100% (7054/7054), 10.91 MiB &#124; [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/develop-ebpf-program-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/19/develop-ebpf-program-in-go">本文永久链接</a> &#8211; https://tonybai.com/2022/07/19/develop-ebpf-program-in-go</p>
<p>在前面的<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中，我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础，因为到目前为止，无论eBPF程序的用户态部分用什么语言开发，运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来，其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了，Go语言也不例外。</p>
<p>在Go社区中，目前最为活跃的用于开发eBPF用户态部分的Go eBPF包莫过于cilium项目开源的<a href="https://github.com/cilium/ebpf/">cilium/ebpf</a>，cilium项目背后的<a href="https://isovalent.com/">Isovalent公司</a>也是eBPF技术在云原生领域应用的主要推手之一。</p>
<p>本文我们就来说说<strong>基于cilium/ebpf开发eBPF程序的套路</strong>！</p>
<h3>一. 探索cilium/ebpf项目示例</h3>
<p>cilium/ebpf项目借鉴了<a href="https://github.com/libbpf/libbpf-bootstrap">libbpf-boostrap</a>的思路，通过代码生成与bpf程序内嵌的方式构建eBPF程序用户态部分。为了搞清楚基于cilium/ebpf开发ebpf程序的套路，我们先来探索一下cilium/ebpf项目提供的示例代码。</p>
<p>我们首先来下载和看看ebpf的示例的结构。</p>
<ul>
<li>下载cilium/ebpf项目</li>
</ul>
<pre><code>$ git clone https://github.com/cilium/ebpf.git
Cloning into 'ebpf'...
remote: Enumerating objects: 7054, done.
remote: Counting objects: 100% (183/183), done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 7054 (delta 91), reused 124 (delta 69), pack-reused 6871
Receiving objects: 100% (7054/7054), 10.91 MiB | 265.00 KiB/s, done.
Resolving deltas: 100% (4871/4871), done.
</code></pre>
<ul>
<li>探索ebpf项目示例代码结构</li>
</ul>
<p>ebpf示例在examples目录下，我们以tracepoint_in_c为例看看其组织形式：</p>
<pre><code>$tree tracepoint_in_c
tracepoint_in_c
├── bpf_bpfeb.go
├── bpf_bpfeb.o
├── bpf_bpfel.go
├── bpf_bpfel.o
├── main.go
└── tracepoint.c

0 directories, 6 files
</code></pre>
<p>根据经验判断，这里面的tracepoint.c对应的是ebpf程序内核态部分，而main.go和bpf_bpfel.go/bpf_bpfeb.go则是ebpf程序用户态部分，至于bpf_bpfeb.o/bpf_bpfel.o应该是某种中间目标文件。通过readelf -a bpf_bpfeb.o查看该中间文件：</p>
<pre><code>$readelf -a bpf_bpfeb.o
ELF Header:
  Magic:   7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, big endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1968 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 1
... ...

</code></pre>
<p>我们看到这是一个内含linux bpf字节码的elf文件(Machine: Linux BPF)。</p>
<p>阅读了cilium/ebpf的相关文档，我搞明白了这几个文件的关系，用下面示意图呈现给大家：</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-ebpf-program-in-go-2.png" alt="" /></p>
<p>ebpf程序的源码文件(比如图中tracepoint.c)经过bpf2go(cilium/ebpf提供的一个代码生成工具)被编译(bpf2go调用clang)为ebpf字节码文件bpf_bpfeb.o(大端)和bpf_bpfel.o(小端)，然后bpf2go会基于ebpf字节码文件生成bpf_bpfeb.go或bpf_bpfel.go，ebpf程序的字节码会以二进制数据的形式内嵌到这两个go源文件中，以bpf_bpfel.go为例，我们可以在其代码中找到下面内容(利用<a href="https://tonybai.com/2021/02/25/some-changes-in-go-1-16">go:embed特性</a>)：</p>
<pre><code>//go:embed bpf_bpfel.o
var _BpfBytes []byte
</code></pre>
<p>main.go则是ebpf程序用户态部分的主程序，将main.go与bpf_bpfeb.go或bpf_bpfel.go之一一起编译就形成了ebpf程序。</p>
<p>有了对cilium/ebpf项目示例的初步探索后，我们来构建ebpf示例代码。</p>
<h3>二. 构建ebpf示例代码</h3>
<p>cilium/ebpf提供了便利的构建脚本，我们只需在ebpf/examples下面执行”make -C ..”即可进行示例代码的构建。</p>
<p>make构建过程会基于quay.io/cilium/ebpf-builder镜像启动构建容器，不过在国内的童鞋需要像下面一样对Makefile内容做一丁点修改，增加GOPROXY环境变量，否则wall外的go module无法拉取：</p>
<pre><code>$git diff ../Makefile
diff --git a/Makefile b/Makefile
index 3a1da88..d7b1712 100644
--- a/Makefile
+++ b/Makefile
@@ -48,6 +48,7 @@ container-all:
        ${CONTAINER_ENGINE} run --rm ${CONTAINER_RUN_ARGS} \
                -v "${REPODIR}":/ebpf -w /ebpf --env MAKEFLAGS \
                --env CFLAGS="-fdebug-prefix-map=/ebpf=." \
+               --env GOPROXY="https://goproxy.io" \
                --env HOME="/tmp" \
                "${IMAGE}:${VERSION}" \
                $(MAKE) all

</code></pre>
<p>这之后再执行构建就会顺利得到我们所要的结果：</p>
<pre><code>$ cd examples
$ make -C ..
make: Entering directory '/root/go/src/github.com/cilium/ebpf'
docker run --rm  --user "0:0" \
    -v "/root/go/src/github.com/cilium/ebpf":/ebpf -w /ebpf --env MAKEFLAGS \
    --env CFLAGS="-fdebug-prefix-map=/ebpf=." \
    --env GOPROXY="https://goproxy.io" \
    --env HOME="/tmp" \
    "quay.io/cilium/ebpf-builder:1648566014" \
    make all
make: Entering directory '/ebpf'
find . -type f -name "*.c" | xargs clang-format -i
go generate ./cmd/bpf2go/test
go: downloading golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34
Compiled /ebpf/cmd/bpf2go/test/test_bpfel.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfel.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfel.go
Compiled /ebpf/cmd/bpf2go/test/test_bpfeb.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfeb.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfeb.go
go generate ./internal/sys
enum AdjRoomMode
enum AttachType
enum Cmd
enum FunctionId
enum HdrStartOff
enum LinkType
enum MapType
enum ProgType
enum RetCode
enum SkAction
enum StackBuildIdStatus
enum StatsType
enum XdpAction
struct BtfInfo
... ...
attr ProgRun
attr RawTracepointOpen
cd examples/ &amp;&amp; go generate ./...
go: downloading github.com/cilium/ebpf v0.8.2-0.20220424153111-6da9518107a8
go: downloading golang.org/x/sys v0.0.0-20211001092434-39dca1131b70
Compiled /ebpf/examples/cgroup_skb/bpf_bpfel.o
Stripped /ebpf/examples/cgroup_skb/bpf_bpfel.o
Wrote /ebpf/examples/cgroup_skb/bpf_bpfel.go
Compiled /ebpf/examples/cgroup_skb/bpf_bpfeb.o
Stripped /ebpf/examples/cgroup_skb/bpf_bpfeb.o
Wrote /ebpf/examples/cgroup_skb/bpf_bpfeb.go
Compiled /ebpf/examples/fentry/bpf_bpfeb.o
Stripped /ebpf/examples/fentry/bpf_bpfeb.o
Wrote /ebpf/examples/fentry/bpf_bpfeb.go
Compiled /ebpf/examples/fentry/bpf_bpfel.o
Stripped /ebpf/examples/fentry/bpf_bpfel.o
Wrote /ebpf/examples/fentry/bpf_bpfel.go
Compiled /ebpf/examples/kprobe/bpf_bpfel.o
Stripped /ebpf/examples/kprobe/bpf_bpfel.o
Wrote /ebpf/examples/kprobe/bpf_bpfel.go
Stripped /ebpf/examples/uretprobe/bpf_bpfel_x86.o
... ...
Wrote /ebpf/examples/uretprobe/bpf_bpfel_x86.go
ln -srf testdata/loader-clang-14-el.elf testdata/loader-el.elf
ln -srf testdata/loader-clang-14-eb.elf testdata/loader-eb.elf
make: Leaving directory '/ebpf'
make: Leaving directory '/root/go/src/github.com/cilium/ebpf'
</code></pre>
<p>以uretprobe下面的ebpf为例，我们运行一下：</p>
<pre><code>$go run -exec sudo uretprobe/*.go
2022/06/05 18:23:23 Listening for events..
</code></pre>
<p>打开一个新的terminal，然后在用户home目录下执行vi .bashrc。在上面的uretprobe程序的执行窗口我们能看到：</p>
<pre><code>2022/06/05 18:24:34 Listening for events..
2022/06/05 18:24:42 /bin/bash:readline return value: vi .bashrc
</code></pre>
<p>这就表明uretprobe下面的ebpf程序如预期地执行了。</p>
<h3>三. 使用cilium/ebpf为前文的Hello World eBPF程序开发用户态部分</h3>
<p>有了对cilium/ebpf示例程序的初步了解，下面我们就来为前面的<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中的那个helloworld ebpf程序开发用户态部分。</p>
<p>回顾一下那个hello world ebpf程序的C源码：</p>
<pre><code>// github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld-go/helloworld.bpf.c
#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
</code></pre>
<p>当这个ebpf程序被加载到内核中后，每当execve这个系统调用被执行，该ebpf程序都会被调用一次，我们就会在/sys/kernel/debug/tracing/trace_pipe中看到对应的日志输出。</p>
<h4>1. 使用bpf2go将ebpf核心态程序转换为Go代码</h4>
<p>根据我们在前面探索cilium/ebpf示例程序时所得到的“套路”，我们接下来第一个要做的就是将helloworld.bpf.c转换为Go代码文件，这一转换过程不可缺少的工具就是cilium/ebpf提供的bpf2go工具，我们先来安装一下该工具：</p>
<pre><code>$go install github.com/cilium/ebpf/cmd/bpf2go@latest
</code></pre>
<p>接下来，我们可以直接使用bpf2go工具将helloworld.ebpf.c转换为对应的go源文件：</p>
<pre><code>$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/test/ebpf/libbpf/include/uapi -I /usr/local/bpf/include -idirafter /usr/local/include -idirafter /usr/lib/llvm-10/lib/clang/10.0.0/include -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include

Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<p>不过这里有一个问题，那就是bpf2go命令行后面的一系列提供给clang编译器的头文件引用路径参考了<a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">《使用C语言从头开发一个Hello World级别的eBPF程序》</a>一文中的Makefile。如果按照这些头文件路径来引用，虽然bpf2go转换可以成功，但是我们需要依赖并安装libbpf这个库，这显然不是我们想要的。</p>
<p>cilium/ebpf在examples中提供了一个headers目录，这个目录中包含了开发ebpf程序用户态部分所需的所有头文件，我们使用它作为我们的头文件引用路径。不过要想基于这个headers目录构建ebpf，我们需要将helloworld.bpf.c中的原头文件include语句由：</p>
<pre><code>#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
</code></pre>
<p>改为：</p>
<pre><code>#include "common.h"
</code></pre>
<p>接下来我们再来执行bpf2go工具进行转换：</p>
<pre><code>$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/go/src/github.com/cilium/ebpf/examples/headers

Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<p>我们看到bpf2go顺利生成ebpf字节码与对应的Go源文件。</p>
<h4>2. 构建helloworld ebpf程序用户态部分</h4>
<p>下面是参考cilium/ebpf示例而构建的helloword ebpf程序用户态部分的main.go源码：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go
package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
)

func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&amp;objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    //SEC("tracepoint/syscalls/sys_enter_execve")
    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    log.Printf("Successfully started! Please run \"sudo cat /sys/kernel/debug/tracing/trace_pipe\" to see output of the BPF programs\n")

    // Wait for a signal and close the perf reader,
    // which will interrupt rd.Read() and make the program exit.
    &lt;-stopper
    log.Println("Received signal, exiting program..")
}
</code></pre>
<p>我们知道一个ebpf程序有几个关键组成：</p>
<ul>
<li>ebpf程序数据</li>
<li>map：用于用户态与内核态的数据交互</li>
<li>挂接点(attach point)</li>
</ul>
<p>根据<a href="https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md">cilium/ebpf架构</a>的说明，ebpf包将前两部分抽象为了一个数据结构bpfObjects：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go

// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
    bpfPrograms
    bpfMaps
}
</code></pre>
<p>我们看到，main函数通过生成的loadBpfObjects函数将ebpf程序加载到内核，并填充bpfObjects结构，一旦加载bpf程序成功，后续我们便可以使用bpfObjects结构中的字段来完成其余操作，比如通过link包的函数将bpf程序与目标挂节点对接在一起(如文中的link.Tracepoint函数），这样挂接后，bpf才能在对应的事件发生后被回调执行。</p>
<p>下面编译执行一下该helloworld示例：</p>
<pre><code>$go run -exec sudo main.go bpf_bpfel.go
[sudo] password for tonybai:
2022/06/05 14:12:40 Successfully started! Please run "sudo cat /sys/kernel/debug/tracing/trace_pipe" to see output of the BPF programs
</code></pre>
<p>之后新打开一个窗口，执行sudo cat /sys/kernel/debug/tracing/trace_pipe，当execve被调用时，我们就能看到类似下面的日志输出：</p>
<pre><code>&lt;...&gt;-551077  [000] .... 6062226.208943: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551077  [000] .... 6062226.209098: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551079  [007] .... 6062226.215421: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-551079  [007] .... 6062226.215578: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-554756  [007] .... 6063476.785212: 0: invoke bpf_prog: Hello, World!
&lt;...&gt;-554756  [007] .... 6063476.785378: 0: invoke bpf_prog: Hello, World!
</code></pre>
<h4>3. 使用go generate来驱动bpf2go的转换</h4>
<p>在生成代码方面，Go工具链原生提供了go generate工具，cilium/ebpf的examples中也是利用go generate来驱动bpf2go将bpf程序转换为Go源文件的，这里我们也来做一下改造。</p>
<p>首先我们在main.go的main函数上面增加一行go:generate指示语句：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf helloworld.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal,  1)
    ... ...
}
</code></pre>
<p>这样当我们显式执行go generate语句时，go generate会扫描到该指示语句，并执行后面的命令。这里使用了几个变量，变量是定义在Makefile中的。当然如果你不想使用Makefile，也可以将变量替换为相应的值。这里我们使用Makefile，下面是Makefile的内容：</p>
<pre><code>// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/Makefile

CLANG ?= clang-10
CFLAGS ?= -O2 -g -Wall -Werror

LIBEBPF_TOP = /home/tonybai/go/src/github.com/cilium/ebpf
EXAMPLES_HEADERS = $(LIBEBPF_TOP)/examples/headers

all: generate

generate: export BPF_CLANG=$(CLANG)
generate: export BPF_CFLAGS=$(CFLAGS)
generate: export BPF_HEADERS=$(EXAMPLES_HEADERS)
generate:
    go generate ./...
</code></pre>
<p>有了该Makefile后，我们执行make命令便可以执行bpf2go对bpf程序的转换：</p>
<pre><code>$make
go generate ./...
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go
</code></pre>
<h3>四. 小结</h3>
<p>本文我们讲解了如何基于cilium/ebpf包来开发ebpf的用户态部分。</p>
<p>ebpf借鉴了libbpf的思路，通过生成代码与数据内嵌的方式来构建ebpf的用户态部分。</p>
<p>ebpf提供了bpf2go工具，可以将bpf的C源码转换为相应的go源码。</p>
<p>ebpf将bpf程序抽象为bpfObjects，通过生成的loadBpfObjects完成bpf程序加载到内核的过程，然后利用ebpf库提供的诸如link之类的包实现ebpf与内核事件的关联。</p>
<p>ebpf包的玩法还有很多，这一篇仅仅是为了打好基础，在后续文章中，我们还会针对各种类型的bpf程序做进一步学习和说明。</p>
<p>本文代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld-go">这里</a>下载。</p>
<h3>无. 参考资料</h3>
<ul>
<li><a href="https://www.ebpf.top/post/ebpf_go/">使用Go语言管理和分发ebpf程序</a> &#8211; https://www.ebpf.top/post/ebpf_go/</li>
<li><a href="https://lpc.events/event/4/contributions/449/attachments/239/529/A_pure_Go_eBPF_library.pdf">A Pure Go eBPF library</a> &#8211; https://lpc.events/event/4/contributions/449/attachments/239/529/A_pure_Go_eBPF_library.pdf</li>
<li><a href="https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md">cilium ebpf library architecture</a> &#8211; https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Go基于国密算法实现双向认证</title>
		<link>https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm/</link>
		<comments>https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm/#comments</comments>
		<pubDate>Sun, 17 Jul 2022 13:45:57 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[CA]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[CipherSuite]]></category>
		<category><![CDATA[Client]]></category>
		<category><![CDATA[config]]></category>
		<category><![CDATA[CSR]]></category>
		<category><![CDATA[DH]]></category>
		<category><![CDATA[DHE]]></category>
		<category><![CDATA[ECDHE]]></category>
		<category><![CDATA[ESDSA]]></category>
		<category><![CDATA[gmsm]]></category>
		<category><![CDATA[gmssl]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[handshake]]></category>
		<category><![CDATA[hash]]></category>
		<category><![CDATA[MAC]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[mkcert]]></category>
		<category><![CDATA[openssl]]></category>
		<category><![CDATA[PKI]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[Server]]></category>
		<category><![CDATA[SHA-1]]></category>
		<category><![CDATA[SHA256]]></category>
		<category><![CDATA[SM2]]></category>
		<category><![CDATA[SM3]]></category>
		<category><![CDATA[SM4]]></category>
		<category><![CDATA[SM9]]></category>
		<category><![CDATA[SSL]]></category>
		<category><![CDATA[TLCP]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[TLS1.2]]></category>
		<category><![CDATA[TLS1.3]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[x509]]></category>
		<category><![CDATA[传输层]]></category>
		<category><![CDATA[公钥]]></category>
		<category><![CDATA[加密]]></category>
		<category><![CDATA[双向认证]]></category>
		<category><![CDATA[商密]]></category>
		<category><![CDATA[国密]]></category>
		<category><![CDATA[密码套件]]></category>
		<category><![CDATA[对称加密]]></category>
		<category><![CDATA[握手]]></category>
		<category><![CDATA[数字证书]]></category>
		<category><![CDATA[椭圆曲线算法]]></category>
		<category><![CDATA[私钥]]></category>
		<category><![CDATA[签名]]></category>
		<category><![CDATA[非对称加密]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3617</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm 国内做2B(to Biz)或2G(to Gov)产品和解决方案的企业都绕不过国密算法，越来越多的国内甲方在采购需求中包含了基于国密算法的认证、签名、加密等需求。对于国内的车联网平台来说，支持基于国密的双向认证也是大势所趋。在这篇文章中，我就来说说如何基于国密算法实现双向认证，即使用国密算法的安全传输层双向认证。 一. 简要回顾基于TLS的双向认证 在《Go语言精进之路》第2册的第51条中，我详细介绍了TLS的建连握手与双向认证过程，并对非对称加密与公钥证书的原理做了系统全面的讲解。为了让大家更好地理解后面的内容，这里简单回顾一下基于TLS的双向认证。 TLS，全称Transport Layer Security，即安全传输层。其前身为SSL（Secure Socket Layer）。TLS是建构在TCP传输层之上和应用层之下的、为应用层提供端到端安全连接和传输服务的虚拟协议层。 应用层基于TLS的通信都是加密的(如上图所示)，保证了传输数据的安全，即便被窃听，攻击者也无法拿到明文数据(密钥够长，加密算法强度够强的前提下)。对于应用开发者而言，重点在于TLS连接的建立过程，连接一旦建立，后续的加解密传输过程就很容易了。 TLS连接的建立过程称为TLS握手(handshake)，握手的过程见下图(适用于TLS 1.2)： 上图引自《Go语言精进之路》第2册的图51-5 关于握手的各个步骤的详细说明，大家可以参考《Go语言精进之路》第2册的第51条中的内容，这里不赘述。 从图中我们可以看到：TLS连接的建立过程需要数字证书的参与，而数字证书主要用于对通信双方的身份进行验证以及参与双方会话密钥的协商与生成。一般情况下，客户端会校验服务端的公钥证书，服务端不会校验客户端公钥证书。但在一些安全级别较高的系统中，服务端也会要求校验客户端的公钥证书(TLS握手阶段，服务端向客户端发送CertificateRequest请求)。 下面我们就来看一个基于TLS的双向认证的实例。 二. 基于TLS双向认证的示例 我们先来看看示例开发与执行的环境并创建相关的数字证书。 1. 环境与数字证书 我们在Ubuntu 20.04.3 LTS环境使用Go 1.18版本开发和执行该示例。示例是一个echo server，即将client端发来的数据重新发回client端，下面是示意图： 开发基于TLS的应用离不开数字证书，因此在开发程序之前，我们先来生成server与client所用的各类公钥数字证书。 在开发和测试环境，我们可以使用自签发的公钥数字证书。我们可以先生成自用的CA私钥与证书，然后利用该CA签发我们所需的服务端和客户端的公钥证书。制作证书最著名的工具是openssl，不过openssh使用起来较为复杂，这些年一些开发者体验更好的工具也逐渐成熟，比如由Go项目前安全负责人Filippo Valsorda开源的mkcert就是一个不错的工具，本文就使用这个工具建立CA并签发制作各类证书。 我们先来安装mkcert工具： $go install filippo.io/mkcert@latest go: downloading filippo.io/mkcert v1.4.4 go: downloading golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 go: downloading software.sslmate.com/src/go-pkcs12 v0.2.0 go: downloading golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 接下来，生成并安装local [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm">本文永久链接</a> &#8211; https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm</p>
<p>国内做2B(to Biz)或2G(to Gov)产品和解决方案的企业都绕不过<strong>国密算法</strong>，越来越多的国内甲方在采购需求中包含了基于国密算法的认证、签名、加密等需求。对于国内的车联网平台来说，支持基于国密的双向认证也是大势所趋。在这篇文章中，我就来说说如何基于国密算法实现双向认证，即<strong>使用国密算法的安全传输层双向认证</strong>。</p>
<hr />
<h3>一. 简要回顾基于TLS的双向认证</h3>
<p>在<a href="https://item.jd.com/13694000.html">《Go语言精进之路》</a>第2册的第51条中，我详细介绍了TLS的建连握手与双向认证过程，并对非对称加密与公钥证书的原理做了系统全面的讲解。为了让大家更好地理解后面的内容，这里简单回顾一下<strong>基于TLS的双向认证</strong>。</p>
<p>TLS，全称Transport Layer Security，即<strong>安全传输层</strong>。其前身为SSL（Secure Socket Layer）。TLS是建构在TCP传输层之上和应用层之下的、<strong>为应用层提供端到端安全连接和传输服务的虚拟协议层</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-3.png" alt="" /></p>
<p>应用层基于TLS的通信都是加密的(如上图所示)，保证了传输数据的安全，即便被窃听，攻击者也无法拿到明文数据(密钥够长，加密算法强度够强的前提下)。对于应用开发者而言，重点在于TLS连接的建立过程，连接一旦建立，后续的加解密传输过程就很容易了。</p>
<p>TLS连接的建立过程称为<strong>TLS握手(handshake)</strong>，握手的过程见下图(适用于TLS 1.2)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-4.png" alt="" /><br />
<center>上图引自《Go语言精进之路》第2册的图51-5</center></p>
<p>关于握手的各个步骤的详细说明，大家可以参考<a href="https://item.jd.com/13694000.html">《Go语言精进之路》第2册</a>的第51条中的内容，这里不赘述。</p>
<p>从图中我们可以看到：TLS连接的建立过程<strong>需要数字证书的参与</strong>，而数字证书主要用于对通信双方的身份进行验证以及参与双方会话密钥的协商与生成。一般情况下，客户端会校验服务端的公钥证书，服务端不会校验客户端公钥证书。但在一些安全级别较高的系统中，服务端也会要求校验客户端的公钥证书(TLS握手阶段，服务端向客户端发送CertificateRequest请求)。</p>
<p>下面我们就来看一个基于TLS的双向认证的实例。</p>
<h3>二. 基于TLS双向认证的示例</h3>
<p>我们先来看看示例开发与执行的环境并创建相关的数字证书。</p>
<h4>1. 环境与数字证书</h4>
<p>我们在Ubuntu 20.04.3 LTS环境使用<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go 1.18版本</a>开发和执行该示例。示例是一个echo server，即将client端发来的数据重新发回client端，下面是示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-5.png" alt="" /></p>
<p>开发基于TLS的应用离不开数字证书，因此在开发程序之前，我们先来生成server与client所用的各类公钥数字证书。</p>
<p>在开发和测试环境，我们可以使用<strong>自签发的公钥数字证书</strong>。我们可以先生成<strong>自用的CA私钥与证书</strong>，然后利用该CA签发我们所需的服务端和客户端的公钥证书。制作证书最著名的工具是<a href="https://www.openssl.org">openssl</a>，不过openssh使用起来较为复杂，这些年一些开发者体验更好的工具也逐渐成熟，比如由Go项目前安全负责人<a href="https://filippo.io/">Filippo Valsorda</a>开源的<a href="https://github.com/FiloSottile/mkcert">mkcert</a>就是一个不错的工具，本文就使用这个工具建立CA并签发制作各类证书。</p>
<p>我们先来安装mkcert工具：</p>
<pre><code>$go install filippo.io/mkcert@latest
go: downloading filippo.io/mkcert v1.4.4
go: downloading golang.org/x/net v0.0.0-20220421235706-1d1ef9303861
go: downloading software.sslmate.com/src/go-pkcs12 v0.2.0
go: downloading golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
</code></pre>
<p>接下来，生成并安装local CA根证书：</p>
<pre><code>$mkcert -install
Created a new local CA:
The local CA is now installed in the system trust store!
</code></pre>
<p>从mkcert的输出来看，CA私钥和证书被安装到所谓system trust store中。这个system trust store在不同平台上的位置不同。在linux上有如下几个位置：</p>
<pre><code>// github.com/FiloSottile/mkcert/truststore_linux.go 

func init() {
    switch {
    case binaryExists("apt"):
        CertutilInstallHelp = "apt install libnss3-tools"
    case binaryExists("yum"):
        CertutilInstallHelp = "yum install nss-tools"
    case binaryExists("zypper"):
        CertutilInstallHelp = "zypper install mozilla-nss-tools"
    }
    if pathExists("/etc/pki/ca-trust/source/anchors/") {
        SystemTrustFilename = "/etc/pki/ca-trust/source/anchors/%s.pem"
        SystemTrustCommand = []string{"update-ca-trust", "extract"}
    } else if pathExists("/usr/local/share/ca-certificates/") {
        SystemTrustFilename = "/usr/local/share/ca-certificates/%s.crt"
        SystemTrustCommand = []string{"update-ca-certificates"}
    } else if pathExists("/etc/ca-certificates/trust-source/anchors/") {
        SystemTrustFilename = "/etc/ca-certificates/trust-source/anchors/%s.crt"
        SystemTrustCommand = []string{"trust", "extract-compat"}
    } else if pathExists("/usr/share/pki/trust/anchors") {
        SystemTrustFilename = "/usr/share/pki/trust/anchors/%s.pem"
        SystemTrustCommand = []string{"update-ca-certificates"}
    }
}
</code></pre>
<p>在我的ubuntu 20.04环境中，CA的公钥证书被<strong>安装(install)</strong>在/usr/local/share/ca-certificates下面了：</p>
<pre><code>$ls -l /usr/local/share/ca-certificates/
total 4
-rw-r--r-- 1 root root 1631 Jul  3 16:22 mkcert_development_CA_333807542491031300702675758897110223851.crt
</code></pre>
<p>生成的CA私钥在哪里呢？我们可以通过-CAROOT参数获得该位置：</p>
<pre><code>$mkcert -CAROOT
/home/tonybai/.local/share/mkcert

$ls -l /home/tonybai/.local/share/mkcert
total 8
-r-------- 1 tonybai tonybai 2484 Jul  3 16:22 rootCA-key.pem
-rw-r--r-- 1 tonybai tonybai 1631 Jul  3 16:22 rootCA.pem
</code></pre>
<p>这里的rootCA.pem与系统信任区中的mkcert_development_CA_333807542491031300702675758897110223851.crt与rootCA.pem内容是一样的。后者是mkcert将rootCA.pem安装到系统可信store后的结果。通过mkcert -uninstall可以删除/usr/local/share/ca-certificates下面的CA公钥证书。但/home/tonybai/.local/share/mkcert下的CA私钥与证书不会被删除。后续若再执行mkcert install，CA证书不会重新生成，现有的rootCA.pem还会被install到/usr/local/share/ca-certificates下面。</p>
<p>接下来我们分别server端和client端的私钥与证书。</p>
<p>server端key和cert：</p>
<pre><code>$mkcert -key-file key.pem -cert-file cert.pem example.com 

Created a new certificate valid for the following names
 - "example.com"

The certificate is at "cert.pem" and the key at "key.pem"

It will expire on 3 October 2024
</code></pre>
<p>我们可以通过下面命令查看证书内容：</p>
<pre><code>$openssl x509 -in cert.pem -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            fc:cc:96:17:55:2d:70:e8:67:3e:b2:25:a9:b8:a3:80
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul  7 09:05:09 2022 GMT
            Not After : Oct  7 09:05:09 2024 GMT
        Subject: O = mkcert development certificate, OU = tonybai@tonybai
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:a6:d1:00:f7:da:03:d0:06:17:cb:ee:b4:99:30:
                    20:66:d0:78:b0:94:67:0b:7a:37:d2:76:21:71:9a:
                    7a:17:d6:44:0a:7b:f1:24:71:2f:ed:b5:67:66:a1:
                    1f:b0:e6:3b:18:66:de:f4:83:78:9a:bc:f5:ae:88:
                    23:a1:f9:7d:7c:3e:7f:a8:f9:9f:54:d0:68:48:b9:
                    d0:56:10:a0:84:0b:cf:a8:bc:b8:74:3f:3c:27:db:
                    ff:28:1d:63:e8:79:a6:93:44:a8:14:43:53:bf:e8:
                    ca:ee:bf:4c:63:f7:23:51:e6:a2:8d:0b:9a:7d:95:
                    2e:bc:37:ae:6d:ea:9e:0e:e6:e0:c5:8e:07:0c:d4:
                    9b:50:30:de:31:c9:97:ee:ac:7e:33:ab:0d:6f:87:
                    f3:70:2b:22:26:8d:a8:95:8e:1f:0e:b7:61:71:e8:
                    36:06:a7:f4:d8:d2:f6:89:12:26:fd:7e:6b:19:a2:
                    2a:4c:d7:cb:7e:09:fc:65:86:be:b6:c2:0b:fb:b5:
                    d8:63:07:aa:ba:59:ab:fc:34:0d:4a:d1:93:dd:62:
                    b0:3a:cd:e1:21:79:13:e4:f4:45:00:f7:10:a1:bc:
                    c7:51:38:84:c4:75:22:5e:5f:a9:11:07:34:16:9f:
                    ad:c7:94:af:57:30:17:77:49:14:6e:16:ff:d8:00:
                    78:11
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Authority Key Identifier:
                keyid:A8:C4:06:2D:2C:25:71:EC:08:C8:1A:92:9A:F2:52:87:22:6E:85:2D

            X509v3 Subject Alternative Name:
                DNS:example.com
    Signature Algorithm: sha256WithRSAEncryption
         be:6e:90:60:bd:43:b9:3a:09:14:c2:44:22:88:a6:af:e5:22:
         d3:97:19:64:8b:59:5d:60:33:36:01:a1:4e:01:eb:7e:5c:6a:
         48:c4:04:a6:0a:e4:91:95:db:5a:2c:c8:e9:93:fa:37:34:6d:
         81:d1:96:ed:5b:67:ae:27:e3:d3:ea:ee:5c:74:0f:6e:f1:48:
         72:d2:75:85:a1:70:0f:a0:9a:73:7a:ca:b8:7b:92:46:27:73:
         e5:f8:ec:72:f8:fc:ac:5f:22:68:0c:d6:8c:20:5b:93:e1:52:
         17:79:57:71:33:5b:98:05:11:8a:cb:d4:3c:b2:24:4b:7b:c5:
         32:8f:ae:1f:a5:af:9d:3a:9b:bb:fc:46:8a:d6:48:39:86:de:
         f3:f7:54:03:45:8d:bd:40:91:26:d2:29:0a:c4:91:cf:b2:5c:
         41:d5:66:24:02:6d:60:22:ea:78:0d:b0:66:80:b9:5d:03:27:
         09:c7:aa:61:1b:ee:e4:08:21:7e:be:bb:13:8a:fb:d8:9e:24:
         5f:5b:a2:4a:d5:db:be:a2:84:74:03:fb:04:37:d0:b3:c4:b7:
         4e:3e:31:a7:2d:5d:62:bd:aa:68:3c:84:d9:32:cb:f2:93:7a:
         3a:8a:2b:c3:81:76:f0:b5:f5:3c:d4:69:8d:5e:f8:39:74:88:
         2b:56:7f:2b:4c:f9:39:2a:f2:4d:15:75:a1:f3:62:ee:57:ce:
         f7:33:c7:cc:a6:97:25:f0:66:bf:5d:5b:c2:d7:d3:ee:20:be:
         c3:5f:fb:9a:50:59:b8:e7:ea:d2:4c:35:9d:48:3f:93:63:96:
         3c:52:dd:b8:d6:ba:1f:30:18:2e:c4:3d:3a:03:66:e1:a3:48:
         6e:a0:5d:b0:0b:65:d4:40:9e:da:5c:36:b1:ac:6b:9e:1f:01:
         69:8a:92:63:7d:27:79:42:bd:d4:f5:e2:d3:bf:8e:97:2f:57:
         ae:0b:f8:c1:b1:35:47:d0:4e:77:b0:e7:88:69:4b:44:dc:01:
         6e:6e:4d:87:e2:71
</code></pre>
<p>接下来，我们再生成client端的key和cert。client端的cert专门用来提供给server端进行证书验证的，我们需要向mkcert传入-client选项生成client端证书：</p>
<pre><code>$mkcert -client -key-file client-key.pem -cert-file client-cert.pem client1

Created a new certificate valid for the following names
 - "client1"

The certificate is at "client-cert.pem" and the key at "client-key.pem"

It will expire on 3 October 2024
</code></pre>
<p>同样，我们可以通过下面命令查看客户端证书的内容：</p>
<pre><code>$openssl x509 -in client-cert.pem -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            62:59:40:5c:e7:5a:61:74:73:bf:08:b0:d9:a7:d4:a1
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul  7 09:11:27 2022 GMT
            Not After : Oct  7 09:11:27 2024 GMT
        Subject: O = mkcert development certificate, OU = tonybai@tonybai
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:e5:25:c6:a1:c9:e2:5f:64:72:bd:ed:fc:24:fa:
                    12:8d:9c:30:52:8d:d8:5a:e7:f4:0c:b5:d5:0a:ef:
                    06:26:e3:06:54:54:cc:72:77:4e:22:cd:22:04:c0:
                    08:2e:94:2d:0f:cc:e8:9f:b9:c5:f4:13:8e:d1:f4:
                    bb:64:9d:1a:74:1b:e3:8c:95:2c:18:44:ec:e7:2c:
                    ec:0c:19:0f:e1:e6:1a:22:e7:3e:a6:1b:35:6e:05:
                    5f:c3:04:3f:1a:0f:c4:55:6f:ff:15:a0:a0:de:44:
                    5c:2d:3d:0b:dc:8a:01:ca:d2:2a:71:9d:b7:3a:d2:
                    10:9f:79:76:e0:a7:14:aa:d8:f0:90:bd:7c:4d:2d:
                    45:e6:16:ab:1d:03:7f:d8:97:4f:4d:41:13:76:72:
                    35:f2:41:b7:f1:3b:a8:42:d4:79:39:fd:f6:8d:10:
                    d1:54:06:60:6a:79:04:6c:6f:05:37:9c:4e:e7:ba:
                    9d:87:e8:05:65:9a:22:56:91:cb:03:bd:89:42:16:
                    66:92:bf:df:50:27:f2:81:89:c0:c5:46:f7:01:e8:
                    80:d0:4d:2e:ae:7f:5a:e9:fa:69:f3:50:c4:58:48:
                    dc:b5:20:13:01:3a:ac:fd:a8:69:2d:20:a9:55:cc:
                    90:4a:f1:f7:3f:9e:3b:7a:cb:77:c7:d2:c4:2b:4f:
                    4c:09
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Client Authentication, TLS Web Server Authentication
            X509v3 Authority Key Identifier:
                keyid:A8:C4:06:2D:2C:25:71:EC:08:C8:1A:92:9A:F2:52:87:22:6E:85:2D

            X509v3 Subject Alternative Name:
                DNS:client1
    Signature Algorithm: sha256WithRSAEncryption
         a6:68:a8:b3:cf:8c:8c:f6:03:56:68:e4:d3:02:cd:ec:8d:fa:
         7f:73:56:c2:91:fa:d8:65:82:a7:f5:d9:8b:32:2a:3b:f9:59:
         71:0c:f8:d3:b6:d3:b3:11:99:f6:f6:d1:ab:d9:1e:fc:bd:f5:
         71:d9:35:4e:0e:fb:f2:f9:65:12:f2:1d:26:77:7d:eb:2c:52:
         80:2c:05:64:0f:99:35:83:31:b0:eb:71:85:04:48:d6:f6:29:
         92:81:f5:22:ee:77:8b:3d:e8:66:6a:5f:59:69:73:15:bb:69:
         46:e9:df:8c:7c:1d:28:b5:71:ed:2e:ca:8e:d3:08:da:85:b4:
         6c:26:89:85:16:c3:9a:e4:45:ef:3d:16:a2:32:45:70:e5:7e:
         82:e1:55:32:e7:1a:63:6b:56:8f:11:70:53:6f:71:d8:e0:76:
         bc:af:bd:dc:53:d6:fb:f0:b6:29:5f:3b:3f:dd:5c:58:b4:f0:
         d2:bb:63:d6:7f:b6:5f:29:ac:43:fa:56:f6:38:a4:03:6e:f3:
         b6:0d:e3:94:4c:0e:de:28:0c:63:27:94:5c:c8:15:78:c1:3b:
         a3:9f:f3:7f:d8:79:c1:ee:23:da:42:ef:25:40:a1:b9:e4:54:
         c4:d0:6b:81:b8:c1:b6:78:aa:d9:25:31:25:fe:5c:a8:d4:46:
         61:38:2e:6e:ba:34:b6:21:cb:66:47:9e:4f:ca:e2:6a:6a:06:
         60:d4:cb:fd:e6:a2:d5:d3:44:40:f1:f9:a9:0d:38:47:a4:20:
         1a:59:4f:14:ab:ab:e9:20:53:91:1b:0e:57:7b:2e:72:d6:1c:
         73:37:d3:17:f6:65:75:ef:27:19:ee:32:2d:ac:ca:46:c4:aa:
         ea:60:d8:6c:fa:62:ad:d4:34:f5:f9:57:48:8f:c0:b3:30:0e:
         13:ec:69:7b:52:97:d6:f5:fa:16:bb:38:c6:03:2f:1a:21:6e:
         bb:69:2a:74:dc:3c:71:3e:af:91:dd:28:86:ca:c8:3b:58:29:
         07:3b:5c:67:3d:31
</code></pre>
<p>我们看到：client-cert.pem与cert.pem在“X509v3 Extended Key Usage”一项有差别，client-pert.pem除了包含TLS Web Server Authentication，还增加了TLS Web Client Authentication。</p>
<h4>2. echoserver与echoclient</h4>
<p>Go标准库提供了tls的基本实现，支持tls1.2和1.3版本。下面是echoserver的主要源码：</p>
<pre><code>// github.com/bigwhite/experiments/gmssl-examples/tls/server/server.go
func main() {
    cert, err := tls.LoadX509KeyPair("./certs/cert.pem", "./certs/key.pem")
    if err != nil {
        fmt.Println("load x509 keypair error:", err)
        return
    }
    cfg := &amp;tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
    }
    listener, err := tls.Listen("tcp", ":18000", cfg)
    if err != nil {
        fmt.Println("listen error:", err)
        return
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("accept error:", err)
            return
        }
        fmt.Println("accept connection:", conn.RemoteAddr())
        go func() {
            for {
                // echo "request"
                var b = make([]byte, 16)
                _, err := conn.Read(b)
                if err != nil {
                    fmt.Println("connection read error:", err)
                    conn.Close()
                    return
                }

                fmt.Println(string(b))
                _, err = conn.Write(b)
                if err != nil {
                    fmt.Println("connection write error:", err)
                    return
                }
            }
        }()
    }
}

</code></pre>
<p>我们看到基于tls的echoserver与一个普通的tcp server的代码差别不多，最主要就是在创建listener时传入了一个tls.Config结构，这个结构中有tls握手(handshake)所需要的全部信息，包括server端使用的私钥与证书(通过LoadX509KeyPair加载)以及对client端进行证书校验的标志(ClientAuth:   tls.RequireAndVerifyClientCert)。一旦连接建立，握手成功，后续的数据读写都和基于tcp连接的普通服务端程序无异。</p>
<p>下面是echoclient的主要源码：</p>
<pre><code>// github.com/bigwhite/experiments/gmssl-examples/tls/client/client.go

func main() {
    cert, err := tls.LoadX509KeyPair("./certs/client-cert.pem", "./certs/client-key.pem")
    if err != nil {
        fmt.Println("load x509 keypair error:", err)
        return
    }

    conn, err := tls.Dial("tcp", "example.com:18000", &amp;tls.Config{
        Certificates: []tls.Certificate{cert},
    })
    if err != nil {
        fmt.Println("failed to connect: " + err.Error())
        return
    }
    defer conn.Close()

    fmt.Println("connect ok")
    for i := 0; i &lt; 100; i++ {
        _, err := conn.Write([]byte("hello, gm"))
        if err != nil {
            fmt.Println("conn write error:", err)
            return
        }

        var b = make([]byte, 16)
        _, err = conn.Read(b)
        if err != nil {
            fmt.Println("conn read error:", err)
            return
        }
        fmt.Println(string(b))
        time.Sleep(time.Second)
    }
}
</code></pre>
<p>client端的代码更为简单一些，只需load client端使用的私钥与证书，然后传给tls.Config实例。tls.Dial使用该Config实例便可以顺利连接echoserver。</p>
<h4>3. 用于验证对方证书的CA证书</h4>
<p>在上面两个程序中都没有提到CA证书，那么server端和client端用什么去验证对方的公钥证书呢？其实依旧是用mkcert创建的CA证书去验证，只不过由于mkcert将CA证书安装到了操作系统trust store路径中，程序可以在系统CA证书中自动找到用来验证client和server两端公钥证书的CA证书，因此无需在程序中显式加载特定CA证书。</p>
<p>如果我们执行mkcert -uninstall，那么client程序在与server作tls handshake时就会报如下错误：</p>
<pre><code>// client程序的输出日志：
failed to connect: x509: certificate signed by unknown authority

// server程序的输出日志：
accept connection: 127.0.0.1:56734
connection read error: remote error: tls: bad certificate
</code></pre>
<h3>三. 密码算法在TLS握手以及后续通信过程中的作用</h3>
<p>在TLS握手阶段，密码算法起到了关键作用。那在握手的每个阶段都在使用什么算法呢？我们看看下面使用curl命令访问https站点的输出：</p>
<pre><code>$curl -v https://baidu.com
*   Trying 220.181.38.148:443...
* TCP_NODELAY set
* Connected to baidu.com (220.181.38.148) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
... ...
</code></pre>
<p>在这段内容中，我们看到这样一行输出：</p>
<pre><code>SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
</code></pre>
<p>这行后面的<strong>ECDHE-RSA-AES128-GCM-SHA256</strong>就是在握手过程中以及后续通信阶段会使用到的算法。这样的一串称为<strong>密码套件(cipher suite)</strong>，在SSL协议时代被称为<a href="https://datatracker.ietf.org/doc/html/draft-hickman-netscape-ssl-00#appendix-C.4">cipher kinds</a>。</p>
<p>密码套件一般由多个用途不同的密码算法名称组合而成(套件中的算法都是要配合使用的，单独使用没法保证信息安全传输)。下面是openssl-1.1.1f支持的密码套件列表：</p>
<pre><code>$openssl ciphers -V | column -t
0x13,0x02  -  TLS_AES_256_GCM_SHA384         TLSv1.3  Kx=any       Au=any    Enc=AESGCM(256)             Mac=AEAD
0x13,0x03  -  TLS_CHACHA20_POLY1305_SHA256   TLSv1.3  Kx=any       Au=any    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0x13,0x01  -  TLS_AES_128_GCM_SHA256         TLSv1.3  Kx=any       Au=any    Enc=AESGCM(128)             Mac=AEAD
0xC0,0x2C  -  ECDHE-ECDSA-AES256-GCM-SHA384  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AESGCM(256)             Mac=AEAD
0xC0,0x30  -  ECDHE-RSA-AES256-GCM-SHA384    TLSv1.2  Kx=ECDH      Au=RSA    Enc=AESGCM(256)             Mac=AEAD
0x00,0x9F  -  DHE-RSA-AES256-GCM-SHA384      TLSv1.2  Kx=DH        Au=RSA    Enc=AESGCM(256)             Mac=AEAD
0xCC,0xA9  -  ECDHE-ECDSA-CHACHA20-POLY1305  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xCC,0xA8  -  ECDHE-RSA-CHACHA20-POLY1305    TLSv1.2  Kx=ECDH      Au=RSA    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xCC,0xAA  -  DHE-RSA-CHACHA20-POLY1305      TLSv1.2  Kx=DH        Au=RSA    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xC0,0x2B  -  ECDHE-ECDSA-AES128-GCM-SHA256  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AESGCM(128)             Mac=AEAD
0xC0,0x2F  -  ECDHE-RSA-AES128-GCM-SHA256    TLSv1.2  Kx=ECDH      Au=RSA    Enc=AESGCM(128)             Mac=AEAD
0x00,0x9E  -  DHE-RSA-AES128-GCM-SHA256      TLSv1.2  Kx=DH        Au=RSA    Enc=AESGCM(128)             Mac=AEAD
0xC0,0x24  -  ECDHE-ECDSA-AES256-SHA384      TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AES(256)                Mac=SHA384
... ...
</code></pre>
<p>我们看看上面输出的后四列的含义：</p>
<ul>
<li>第四列（Kx）</li>
</ul>
<p>Kx代表key exchange，这一列是密钥交换算法，常见的密钥交换算法包括DH(Diffie-Hellman)、DHE(Diffie-Hellman Ephemeral)、ECDHE(在DHE算法的基础上利用了ECC椭圆曲线特性)等。在tls握手阶段，密钥交换算法用于在不安全的通道上协商会话加密(对称加密)算法密钥。</p>
<ul>
<li>第五列（Au)</li>
</ul>
<p>Au代表authentication，这一列是身份认证算法，通常是非对称加密算法，比如：RSA、ECDSA等。该算法用于服务端与客户端相互验证对方的公钥数字证书时。</p>
<ul>
<li>第六列（Enc）</li>
</ul>
<p>Enc代表对称加密算法，比如：AES、CHACHA20等，对称加密算法在tls握手后用于对客户端与服务端交互的数据进行加解密，它的加解密性能要比非对称加密算法快上很多。</p>
<ul>
<li>第七列（Mac）</li>
</ul>
<p>Mac代表Message Authentication Code，消息认证码算法，本质上是一个hash函数，用于计算数据的摘要值，是常用的用于保证消息数据完整性的工具。常见的算法有SHA1、SHA256等。</p>
<p>有了这些知识，我们再回到前面的<strong>ECDHE-RSA-AES128-GCM-SHA256</strong>，我们可以知道这个密码套件使用ECDHE作为密钥交换算法，使用RSA作为服务器认证算法（非对称加密），使用AES128-GCM作为对称加密算法，使用SHA256作为消息认证码算法。</p>
<blockquote>
<p>注：TLS 1.3版本的握手过程已经修改，仅需对称加密和Mac算法参与，因此TLS 1.3的密码套件格式已经变化。在TLS 1.3中，密码套件仅用于协商对称加密和MAC算法。对应的，我们看到上面OpenSSL输出的TLSv1.3版本的密码套件(如TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256等)的Kx和Au都是any。换句话说：TLSv1.2和TLSv1.3版本的密码套件并不兼容，不能混用(TLS v1.3的密码套件不能用在TLS v1.2版本中，反之亦然)。</p>
</blockquote>
<p>Go标准库(Go 1.18.3)内置支持的cipher suite如下：</p>
<pre><code>// $GOROOT/src/crypto/tls/cipher_suites.go
func CipherSuites() []*CipherSuite {
    return []*CipherSuite{
        {TLS_RSA_WITH_AES_128_CBC_SHA, "TLS_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_RSA_WITH_AES_256_CBC_SHA, "TLS_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_RSA_WITH_AES_128_GCM_SHA256, "TLS_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_RSA_WITH_AES_256_GCM_SHA384, "TLS_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},

        {TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", supportedOnlyTLS13, false},
        {TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384", supportedOnlyTLS13, false},
        {TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256", supportedOnlyTLS13, false},

        {TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
    }
}
</code></pre>
<blockquote>
<p>每个密码套件具有唯一标识值(value)，这些值在https://www.iana.org/assignments/tls-parameters/tls-parameters.xml中有标准参考。</p>
</blockquote>
<p>我们看到这些密码套件中的算法都是一些耳熟能详的国际标准密码算法，但并没有看到我们国家的国密的影子？我们国家的国密算法都有哪些？是否可以作为TLS握手过程使用的密码套件的一部分呢？如何基于国密算法实现一个安全传输层呢？我们接下来就正式进入国密算法(前面的铺垫有些长^_^)。</p>
<h3>四. 国家商用密码(国密)介绍</h3>
<p>密码算法是最基础、最重要的密码技术。国家密码管理局近十年来，先后发布了祖冲之序列密码算法、SM2~SM9等商用密码系列（SM系列）算法，构成了包含序列密码算法、对称密码算法、非对称密码算法、密码杂凑算法和标识密码算法等在内的完整、自主国产密码算法体系。2019年10月26日，第十三届全国人民代表大会常务委员会第十四次会议表决通过了《中华人民共和国密码法》，并于2020年1月1日起施行。这些算法目前也已经成为ISO/IEC相关国际标准了(只是在国外的应用还<strong>极少</strong>)。</p>
<p>国密是<strong>国家商用密码标准</strong>的简称。商用密码是指对不涉及国家秘密内容的信息进行加密保护或者安全认证。公民、法人和其他组织可以依法使用商用密码保护网络与信息安全。所有的国密相关标准都可以在<a href="http://www.gmbz.org.cn/main/bzlb.html">这个站点查询</a>到：http://www.gmbz.org.cn/main/bzlb.html。国密算法用SM(“商密”二字的拼音头字母组合)作为前缀标识，比如上面提到的SM2、SM3、SM4、SM9等等。</p>
<p>我们较为熟悉的是像RSA、AES这样的国际标准常用密码算法。初次看到SM2、SM3等算法名字的童鞋可能会有点懵，这些算法是什么密码算法，用在哪里？SM系列密码算法有很多，我们不能一一说明，我们重点来看看与安全传输层建立与认证相关的算法：</p>
<ul>
<li>SM2：椭圆曲线公钥密码算法</li>
</ul>
<p>SM2是用在公钥基础设施（PKI）领域的椭圆曲线公钥密码算法，与大名鼎鼎的RSA算法一样，是一种用于<strong>非对称加密</strong>的算法。该算法包括3部分：数字签名算法、密钥协商算法和加密/解密算法。该算法推荐使用素域为256比特的椭圆曲线。与RSA公钥密码算法相比，SM2椭圆曲线公钥密码算法具有安全性高、密钥短、速度快等优势。256比特的SM2椭圆曲线公钥密码算法密码强度已超过RSA-2048。SM2椭圆曲线公钥密码算法使用的密钥长度通常为192～256比特，而RSA公钥密码算法通常需要1024～2048比特。在同等安全强度下，SM2椭圆曲线公钥密码算法在用私钥签名时，速度远超RSA公钥密码算法。</p>
<p>SM2椭圆曲线公钥密码算法广泛应用于电子政务、移动办公、电子商务、移动支付、电子证书等领域。在公钥基础设施（PKI）领域，基于SM2椭圆曲线公钥密码算法的数字证书应用最具有代表性。</p>
<ul>
<li>SM3：密码杂凑算法</li>
</ul>
<p>SM3实质就是一种<strong>密码散列函数</strong>标准，再简单地说就是Hash函数。和我们熟悉的Hash散列算法SHA-1、<strong>SHA256</strong>等一样，SM3也主要用于数字签名及验证、消息认证码(MAC)生成及验证、随机数生成等领域。</p>
<p>SM3密码杂凑算法消息分组长度为512比特，输出摘要长度为256比特。SM3密码杂凑算法在MD（Message Digest）结构的基础上，新增了16步全异或操作、消息双字介入、加速雪崩效应的P置换等多种设计技术，能够有效避免高概率的局部碰撞，有效抵抗强碰撞性的差分攻击、弱碰撞性的线性攻击和比特追踪攻击等密码攻击方法。SM3密码杂凑算法能够有效抵抗目前已知的攻击方法，具有较高的安全冗余，在安全级别上与SHA256相当。</p>
<ul>
<li>SM4：分组密码算法</li>
</ul>
<p>SM4分组密码算法广泛应用于有<strong>对称加解密</strong>需求的应用系统和产品，与我们熟知的AES对称加密算法具有相同用途。</p>
<p>SM4算法的分组长度为128比特，密钥长度为128比特，加密算法和密钥扩展算法都采用32轮非线性迭代结构，解密算法与加密算法相同，只是轮密钥的使用顺序相反，解密轮密钥是加密轮密钥的逆序。轮变换使用的模块包括异或运算、8比特输入8比特输出的S盒，以及一个32比特输入的线性置换。</p>
<p>在密码指标性能方面，SM4分组密码算法的S盒设计已达到欧美分组密码标准算法的水平，具有较高的安全特性。线性置换的分支数达到了最优，可以抵抗差分攻击、线性攻击、代数攻击等。它具有算法速度快、实现效率高、安全性好等优点，主要用于保护数据的机密性。</p>
<p>除了密码算法之外，国家密码管理局还颁布一系列<strong>周边标准</strong>，比如基于国密的SSL传输层安全协议，以下简称为<strong>国密SSL</strong>。</p>
<p>最初的国密SSL是作为密码行业标准存在的，没有独立的协议标准定义，而是定义在SSL LPN产品的技术规范里，即<a href="www.gmbz.org.cn/main/viewfile/20180110021416665180.html">《GM/T 0024-2014 SSL VPN技术规范》</a>。</p>
<p>后来，国密SSL从密码行业标准上升到了独立的国家标准，也就是《GB/T 38636-2020 信息安全技术 传输层密码协议(TLCP)》，新版标准基本兼容《GM/T 0024-2014 SSL VPN技术规范》，主要变化是增加了GCM的密码套件：ECC_SM4_GCM_SM3和ECDHE_SM4_GCM_SM3以及去掉了行标《GM/T 0024-2014》中的涉及SM1和RSA的密码套件。</p>
<p>国密SSL是参考TLS 1.1制定的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-6.png" alt="" /></p>
<p>但“遗憾”的是<strong>国密SSL与TLS协议并不兼容</strong>，这就意味着现有的各个编程语言实现的TLS实现在不经改造的前提下，是无法支持国密SSL握手过程的。</p>
<p>此外，前面提到的TLS握手涉及到的证书都是RSA证书(前面证书内容中Public Key Algorithm: rsaEncryption)，即用RSA算法生成的公钥，用RSA算法的CA私钥签过名的证书。另一端在验证证书时，也要用RSA算法公钥(CA公钥)验证证书。如果我们要支持基于SM2算法的证书体系，需要CA、参与通信的两端都要支持SM2算法。而如今支持SM2的CA少之又少。并且，从前面我们看到的Go标准库TLS实现内置的密码套件列表来看，<strong>我们也没有看到SM2等国密算法实现的踪影</strong>。</p>
<p>不得不说，这是当前支持国密的一个“尴尬”。</p>
<p>那么我们要如何支持国密证书以及国密SSL呢？我们继续向下看。</p>
<h3>五. 基于国密证书的tls身份认证</h3>
<h4>1. 使用openssl生成国密证书并验证是否可以成功进行tls握手</h4>
<p>openssl是加解密领域“风向标”，openssl在1.1.1版本中加入对SM系列算法的支持：</p>
<p><img src="https://tonybai.com/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-2.png" alt="" /></p>
<p>大家可以通过下面命令查看你的openssl是否支持SM2椭圆曲线公钥密码算法：</p>
<pre><code>$openssl ecparam -list_curves | grep SM2
  SM2       : SM2 curve over a 256 bit prime field
</code></pre>
<p>如果支持，我们就可以利用该算法制作国密证书了(openssl-sm2/certs下面)。</p>
<ul>
<li>使用SM2创建server端私钥</li>
</ul>
<pre><code>$openssl ecparam -genkey -name SM2 -out server-sm2.key
</code></pre>
<ul>
<li>创建server csr </li>
</ul>
<pre><code>$openssl req -new -out server-sm2.csr -key server-sm2.key
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
</code></pre>
<p>查看该csr：</p>
<pre><code>$openssl req -in server-sm2.csr -noout -text
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:5b:3f:7e:c7:36:43:9c:22:cf:68:34:73:7f:c2:
                    11:23:05:2d:e5:34:5f:29:30:11:c5:c4:f1:df:e2:
                    97:9d:5c:eb:6c:29:3e:d0:e3:a2:d4:6c:67:e4:4f:
                    42:90:70:a2:dc:db:a6:b4:fd:5d:53:b6:53:8e:fd:
                    a8:37:aa:5e:4b
                ASN1 OID: SM2
        Attributes:
            a0:00
    Signature Algorithm: ecdsa-with-SHA256
         30:45:02:21:00:be:4b:31:93:fb:6a:74:2f:0a:0d:8d:69:08:
         d1:ad:bf:b2:e8:02:c1:76:c5:50:01:f2:f9:c8:1e:6f:1f:4f:
         9b:02:20:2c:43:16:5f:a4:4b:fb:2d:26:13:04:e0:ef:27:d1:
         84:69:41:71:9a:aa:e8:29:1d:98:f8:0c:df:be:52:c6:9d
</code></pre>
<p>可以看到：</p>
<pre><code>Public Key Algorithm: id-ecPublicKey
ASN1 OID: SM2
</code></pre>
<p>sm2算法是id-ecPublicKey算法的别名(alias)：</p>
<pre><code>$openssl list -public-key-algorithms

... ...
Name: sm2
    Alias for: id-ecPublicKey
</code></pre>
<ul>
<li>使用mkcert创建的ca来签发证书</li>
</ul>
<p>该ca是使用RSA算法创建的。我们用它签发sm2证书。</p>
<pre><code>$openssl x509 -req -in server-sm2.csr -CA ~/.local/share/mkcert/rootCA.pem -CAkey ~/.local/share/mkcert/rootCA-key.pem -CAcreateserial -out server-sm2-signed-by-rsa-ca.pem -days 5000
Signature ok
subject=C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
Getting CA Private Key
</code></pre>
<p>查看生成的server端证书：</p>
<pre><code>$openssl x509 -in server-sm2-signed-by-rsa-ca.pem -noout -text
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            5c:9c:ac:2f:03:8e:4e:72:fd:41:8a:c5:eb:8e:d4:c0:fc:0f:8a:4b
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul 11 06:23:28 2022 GMT
            Not After : Mar 19 06:23:28 2036 GMT
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:5b:3f:7e:c7:36:43:9c:22:cf:68:34:73:7f:c2:
                    11:23:05:2d:e5:34:5f:29:30:11:c5:c4:f1:df:e2:
                    97:9d:5c:eb:6c:29:3e:d0:e3:a2:d4:6c:67:e4:4f:
                    42:90:70:a2:dc:db:a6:b4:fd:5d:53:b6:53:8e:fd:
                    a8:37:aa:5e:4b
                ASN1 OID: SM2
    Signature Algorithm: sha256WithRSAEncryption
         2b:67:c0:12:41:ad:da:2a:2f:9f:89:81:f1:ef:4a:4b:6d:66:
         e8:93:62:e0:68:d4:5b:0e:8a:83:2b:4d:77:36:d1:8e:f2:d6:
         92:b0:7f:db:12:78:49:ac:c4:80:2b:ca:c8:70:91:c3:2f:31:
         8d:5d:97:27:60:77:95:e6:61:7c:62:c4:f5:0c:ce:90:43:7d:
         0c:f6:4e:8d:62:f3:67:08:4b:7e:5e:ad:0b:11:13:13:30:ec:
         d2:fc:78:ae:77:ca:97:f1:eb:fd:a3:5d:0f:58:70:a0:b3:2a:
         6e:91:eb:81:37:6f:54:a9:56:9b:11:3c:4e:63:0b:a2:d7:d6:
         36:b4:7f:d2:90:c3:15:ab:9b:bf:86:98:bb:9a:1c:64:71:3b:
         92:4c:aa:89:d1:8b:03:35:34:ad:64:66:83:bc:0d:5f:38:ba:
         a0:07:82:92:1b:44:ef:72:c2:36:eb:38:84:ac:a1:d3:44:17:
         a8:7b:d5:64:f6:55:05:5f:3a:3b:b5:eb:1a:66:51:33:7a:76:
         ce:e3:cc:82:04:f2:28:70:90:3a:57:a5:db:32:08:47:f1:4d:
         81:33:87:dd:b6:dc:4f:4f:49:59:e2:ac:71:a4:2f:7e:08:14:
         b0:cd:96:2d:fb:3d:b8:f2:c5:db:de:b9:0c:fe:91:15:fb:b1:
         2e:df:23:6f:3e:26:2c:66:db:5e:e2:f6:f3:1f:23:2c:5c:70:
         1d:d1:2b:b2:6e:ae:87:c6:cd:53:44:23:b0:1d:8d:08:40:3c:
         02:87:81:1d:65:04:2a:b8:c6:f5:59:28:6a:ea:22:95:d3:e2:
         24:93:9e:6c:d6:d7:0a:25:5b:4e:4a:cf:43:4c:71:e2:1a:bf:
         26:de:27:14:38:ea:69:9c:a9:bf:12:3a:5b:65:33:4e:83:87:
         81:5e:85:2a:e3:62:c7:5d:0e:15:e7:35:06:35:45:69:db:0b:
         aa:c6:45:e4:74:93:aa:45:e8:6f:22:11:15:14:f1:5a:4e:0a:
         34:e2:74:eb:44:32

</code></pre>
<p>使用openssl的s_server和s_client命令验证是否可以握手成功：</p>
<pre><code>$openssl s_server -tls1_2 -accept 14443 -key server-sm2.key -cert server-sm2-signed-by-rsa-ca.pem -debug -msg
Using default temp DH parameters
ACCEPT

$openssl s_client -connect 127.0.0.1:14443 -debug -msg -tls1_2
</code></pre>
<p>结果s_server报如下错：</p>
<pre><code>... ...
&gt;&gt;&gt; TLS 1.2, Alert [length 0002], fatal handshake_failure
    02 28
ERROR
139761999033664:error:1417A0C1:SSL routines:tls_post_process_client_hello:no shared cipher:../ssl/statem/statem_srvr.c:2283:
shutting down SSL
CONNECTION CLOSED
</code></pre>
<p>可以看到openssl虽然可以生成sm2公钥证书，但在tls 1.2协议下无法成功实现tls握手。</p>
<h4>2. 使用gmssl进行tls握手</h4>
<p>openssl不支持，但国内的大神基于openssl1.1.0建立了gmssl分支，这就是<a href="http://gmssl.org/">gmssl工程</a>。该工程为openssl增加了对国密算法以及gm ssl协议的各种支持。接下来我们就来试试用gmssl是否可以实现基于sm2证书的tls握手成功。</p>
<p>gmssl工程感觉还不够成熟，安装和运行过程有一些“坑”，这里简要说说。</p>
<ul>
<li>安装gmssl</li>
</ul>
<pre><code>$wget -c https://github.com/guanzhi/GmSSL/archive/master.zip
$unzip master.zip
$cd master
</code></pre>
<p>注意在执行其他config命令之前，先在Configure文件和test/build.info这个文件中, 把</p>
<pre><code>use if $^O ne "VMS", 'File::Glob' =&gt; qw/glob/;
</code></pre>
<p>改成：</p>
<pre><code>use if $^O ne "VMS", 'File::Glob' =&gt; qw/:glob/;
</code></pre>
<p>否则会报下面错误：</p>
<pre><code>"glob" is not exported by the File::Glob module
Can't continue after import errors at ./Configure line 18.
</code></pre>
<p>接下来执行下面命令生成Makefile并构建：</p>
<pre><code>$./config
$make
</code></pre>
<p>编译后的文件在apps/gmssl，我将其cp到项目根目录下。执行gmssl：</p>
<pre><code>$./gmssl
./gmssl: symbol lookup error: ./gmssl: undefined symbol: BIO_debug_callback, version OPENSSL_1_1_0d
</code></pre>
<p>gmssl报错了！原因是加载器加载gmssl依赖的动态共享库时选择了系统openssl的相关库了：</p>
<pre><code>$ldd gmssl
    linux-vdso.so.1 (0x00007ffe9cc5b000)
    libssl.so.1.1 =&gt; /lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007fa3ca550000)
    libcrypto.so.1.1 =&gt; /lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007fa3ca27a000)
    libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa3ca257000)
    libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa3ca065000)
    libdl.so.2 =&gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa3ca05f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa3ca6ae000)
</code></pre>
<p>我们要在load时链接gmssl自己的库，需要修改一下LD_LIBRARY_PATH环境变量(这样做会导致openssl执行失败，建议不要放在全局环境变量配置中，可让其仅在某些窗口中生效)：</p>
<pre><code>$export LD_LIBRARY_PATH=/home/tonybai/.bin/gmssl/GmSSL-master:$LD_LIBRARY_PATH
</code></pre>
<p>除此之外，我们还需要做一个操作，那就是在/usr/local/ssl下放置一份openssl.cnf文件(可以从/usr/lib/ssl/openssl.cnf拷贝(openssl version -a，查看OPENSSLDIR))，否则gmssl在执行“gmssl s_server&#8230;”时会报如下错误：</p>
<pre><code>Can't open /usr/local/ssl/openssl.cnf for reading, No such file or directory
139679439200896:error:02001002:system library:fopen:No such file or directory:crypto/bio/bss_file.c:74:fopen('/usr/local/ssl/openssl.cnf','r')
139679439200896:error:2006D080:BIO routines:BIO_new_file:no such file:crypto/bio/bss_file.c:81:
</code></pre>
<p>这里gmssl版本如下：</p>
<pre><code>$gmssl version
GmSSL 2.5.4 - OpenSSL 1.1.0d  19 Jun 2019
</code></pre>
<p>好了，下面我们就来使用gmssl试试我们制作的sm2证书是否可以顺利完成tls握手。</p>
<pre><code>// 服务端
$gmssl s_server  -accept 14443 -key server-sm2.key -cert server-sm2-signed-by-rsa-ca.pem -debug -msg -tls1_2

// 客户端
$gmssl s_client -connect 127.0.0.1:14443 -debug -msg -tls1_2 -verifyCAfile /home/tonybai/.local/share/mkcert/rootCA.pem

---
SSL handshake has read 1209 bytes and written 310 bytes
Verification: OK
---
New, TLSv1.2, Cipher is ECDHE-SM2-WITH-SMS4-GCM-SM3
Server public key is 256 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-SM2-WITH-SMS4-GCM-SM3
    Session-ID: 53B8799C3A6F3752C634F764EB6B136BDFD39CEB0C2E28E7DD98D86F9FF4F333
    Session-ID-ctx:
    Master-Key: 6A50D31E3AEDDDF3FC608277087FB0DAACCC791DB296142ED37DE28E0DDA56FF1BB64431B66A76C468129E00F696338D
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 7200 (seconds)
    TLS session ticket:
    0000 - ee d3 08 4d 21 14 dc c8-40 8c d0 c4 31 f9 16 bc   ...M!...@...1...
    0010 - 85 f9 a2 8c f4 ba cf 90-4d 38 28 03 78 b0 4a 27   ........M8(.x.J'
    0020 - 17 c4 22 df 48 ea 8c 00-5a 92 0f ba eb 8a 1a dc   ..".H...Z.......
    0030 - b3 3d b4 15 ee df fc d0-66 59 5c c2 23 9e a4 4f   .=......fY\.#..O
    0040 - e0 77 54 b1 18 af 73 b0-b4 6a a7 c7 c7 d3 a4 a4   .wT...s..j......
    0050 - 8f 49 ff c7 bc 47 b5 19-09 21 4c db 71 76 d9 a5   .I...G...!L.qv..
    0060 - 49 0b c9 5d 09 b2 da b9-cc ec 04 5a 90 27 07 5f   I..].......Z.'._
    0070 - 2b f2 55 5c f4 69 01 32-90 f5 3a 19 b5 47 84 4c   +.U\.i.2..:..G.L
    0080 - 1c 64 66 63 f3 01 ab fe-b1 70 f7 98 b5 cc 23 8e   .dfc.....p....#.
    0090 - aa f4 1d 8a 79 5e 79 b7-04 f6 69 ed 62 d9 c7 ae   ....y^y...i.b...

    Start Time: 1657529930
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: yes
</code></pre>
<p>从客户端的输出来看，在明确ca证书位置的情况下(使用-verifyCAfile)，可以正确验证server端发来的sm2证书(见“Verify return code: 0 (ok)”)。</p>
<h3>六. 使用Go实现tls/tlcp自适应双向认证</h3>
<p>gmssl为我们展示了一条支持国密的路径，即基于已有的开源项目的实现进行改造。Go标准库并不支持国密，因此在Go社区借鉴标准库中crypto的中算法以及tls包的结构，实现了对sm系列算法以及国密ssl的支持，<a href="https://github.com/tjfoc/gmsm">tjfoc/gmsm</a>就是其中之一。</p>
<blockquote>
<p>注：gmssl也提供了Go API接口，底层通过cgo调用gmssl C代码实现。</p>
</blockquote>
<p>gmsm不仅提供了国密算法的相关实现，还<strong>实现了tls与tclp协议的自适应支持</strong>。在这一小节，我们就用gmsm来演示一个<strong>tls/tlcp自适应双向认证</strong>的例子。</p>
<h4>1. 准备SM2国密公钥证书</h4>
<p>按照gmsm自适应tls/tlcp实现的要求，我们需要先准备一堆证书(tlcp与tls不同，其加密与签名是由两个证书分别完成的，而不仅仅是tls的一个证书)，包括：</p>
<ul>
<li>rsa: 基于rsa的CA证书、server证书和client证书</li>
<li>gm: 基于gm的CA证书、server签名(sign)和加密(enc)证书、client端验证(auth)证书。</li>
</ul>
<p>考虑到mkcert不支持国密，这里我们切换到用gmssl来创建这些证书。我将创建证书的命令集中在两个shell脚本中：gen_rsa_cert.sh和gen_gm_cert.sh，前者用于创建基于RSA的各种证书，后者则是创建基于国密的各种证书。这两个脚本的源码如下：</p>
<ul>
<li>gen_rsa_cert.sh </li>
</ul>
<pre><code>// gmssl-examples/gmsm-tls-and-tlcp/certs/gen_rsa_cert.sh

#!/bin/bash

## RSA Certs

### CA
gmssl genpkey -out ca-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -x509 -new -nodes -key ca-rsa-key.pem -subj "/CN=myca.com" -days 5000 -out ca-rsa-cert.pem 

### server key and cert
gmssl genpkey -out server-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -new -key server-rsa-key.pem -subj "/CN=example.com" -out server-rsa.csr
gmssl x509 -req -in server-rsa.csr -CA ca-rsa-cert.pem -CAkey ca-rsa-key.pem -CAcreateserial -out server-rsa-cert.pem -days 5000  -extfile ./server.cnf -extensions ext
gmssl verify -CAfile ca-rsa-cert.pem server-rsa-cert.pem

### client key and cert
gmssl genpkey -out client-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -new -key client-rsa-key.pem -subj "/CN=client1.com" -out client-rsa.csr
gmssl x509 -req -in client-rsa.csr -CA ca-rsa-cert.pem -CAkey ca-rsa-key.pem -CAcreateserial -out client-rsa-cert.pem -days 5000 -extfile ./client.cnf -extensions ext
gmssl verify -CAfile ca-rsa-cert.pem client-rsa-cert.pem
</code></pre>
<ul>
<li>gen_gm_cert.sh </li>
</ul>
<pre><code>#!/bin/bash

## SM CA

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out ca-gm-key.pem
gmssl req -x509 -new -nodes -key ca-gm-key.pem -subj "/CN=myca.com" -days 5000 -out ca-gm-cert.pem

### server: sign key and cert

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out server-gm-sign-key.pem
gmssl req -new -key server-gm-sign-key.pem -subj "/CN=example.com" -out server-gm-sign.csr
gmssl x509 -req -in server-gm-sign.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out server-gm-sign-cert.pem -days 5000 -extfile ./server.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem server-gm-sign-cert.pem

### server: enc key and cer

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out server-gm-enc-key.pem
gmssl req -new -key server-gm-enc-key.pem -subj "/CN=example.com" -out server-gm-enc.csr
gmssl x509 -req -in server-gm-enc.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out server-gm-enc-cert.pem -days 5000 -extfile ./server.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem server-gm-enc-cert.pem

### client: auth key and cert

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out client-gm-auth-key.pem
gmssl req -new -key client-gm-auth-key.pem -subj "/CN=client1.com" -out client-gm-auth.csr
gmssl x509 -req -in client-gm-auth.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out client-gm-auth-cert.pem -days 5000 -extfile ./client.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem client-gm-auth-cert.pem
</code></pre>
<p>关于上面两个脚本，有几点说明一下：</p>
<ol>
<li>我们建立了两个CA，一个基于RSA，一个基于国密算法；这两个CA分别用于签发基于RSA的证书与基于国密的证书；</li>
<li>在生成证书中我们用到了x509证书的扩展属性subjectAltName、extendedKeyUsage和keyUsage。</li>
</ol>
<p>如果不使用subjectAltName扩展属性，Go语言的x509校验会报如下错误(在Go 1.18及后续版本中，即便设置GODEBUG=x509ignoreCN=0也不行)：</p>
<pre><code>certificate relies on legacy Common Name field, use SANs instead
</code></pre>
<p>同样Go也会对keyUsage做严格校验，如果是用来签名的证书中keyUsage不包含digitalSignature等，握手时也会报错：</p>
<pre><code>tls: the keyusage of cert[0] does not exist or is not for KeyUsageDigitalSignature
</code></pre>
<p>server.cnf与client.cnf的内容如下：</p>
<pre><code>// server.cnf
[req]
prompt = no
distinguished_name = dn
req_extensions = ext
input_password = 

[dn]
CN = example.com
emailAddress = webmaster@example.com
O = hello Ltd
L = Beijing
C = CN

[ext]
subjectAltName = DNS:example.com
extendedKeyUsage = clientAuth,serverAuth
keyUsage = critical,digitalSignature,keyEncipherment

// client.cnf

[req]
prompt = no
distinguished_name = dn
req_extensions = ext
input_password = 

[dn]
CN = client1.com
emailAddress = webmaster@client1.com
O = hello Ltd
L = Beijing
C = CN

[ext]
subjectAltName = DNS:client1.com
extendedKeyUsage = clientAuth
keyUsage = critical,digitalSignature,keyEncipherment
</code></pre>
<p>执行bash gen_rsa_cert.sh和bash gen_gm_cert.sh生成所有示例需要的证书：</p>
<pre><code>$ls *.pem|grep -v key
ca-gm-cert.pem
ca-rsa-cert.pem
client-gm-auth-cert.pem
client-rsa-cert.pem
server-gm-enc-cert.pem
server-gm-sign-cert.pem
server-rsa-cert.pem
</code></pre>
<h4>2. 支持tls与tlcp自适应的server</h4>
<p>下面是支持tls与tlcp自适应的server的源码：</p>
<pre><code>// gmssl-examples/gmsm-tls-and-tlcp/server/server.go
const (
    rsaCertPath     = "certs/server-rsa-cert.pem"
    rsaKeyPath      = "certs/server-rsa-key.pem"
    sm2SignCertPath = "certs/server-gm-sign-cert.pem"
    sm2SignKeyPath  = "certs/server-gm-sign-key.pem"
    sm2EncCertPath  = "certs/server-gm-enc-cert.pem"
    sm2EncKeyPath   = "certs/server-gm-enc-key.pem"
)

func main() {
    pool := x509.NewCertPool()
    rsaCACertPath := "./certs/ca-rsa-cert.pem"
    rsaCACrt, err := ioutil.ReadFile(rsaCACertPath)
    if err != nil {
        fmt.Println("read rsa ca err:", err)
        return
    }
    gmCACertPath := "./certs/ca-gm-cert.pem"
    gmCACrt, err := ioutil.ReadFile(gmCACertPath)
    if err != nil {
        fmt.Println("read gm ca err:", err)
        return
    }
    pool.AppendCertsFromPEM(rsaCACrt)
    pool.AppendCertsFromPEM(gmCACrt)

    rsaKeypair, err := tls.LoadX509KeyPair(rsaCertPath, rsaKeyPath)
    if err != nil {
        fmt.Println("load rsa x509 keypair error:", err)
        return
    }
    sigCert, err := tls.LoadX509KeyPair(sm2SignCertPath, sm2SignKeyPath)
    if err != nil {
        fmt.Println("load x509 gm sign keypair error:", err)
        return
    }
    encCert, err := tls.LoadX509KeyPair(sm2EncCertPath, sm2EncKeyPath)
    if err != nil {
        fmt.Println("load x509 gm enc keypair error:", err)
        return
    }

    cfg, err := tls.NewBasicAutoSwitchConfig(&amp;sigCert, &amp;encCert, &amp;rsaKeypair)
    if err != nil {
        fmt.Println("load basic config error:", err)
        return
    }

    cfg.MaxVersion = tls.VersionTLS12
    cfg.ClientAuth = tls.RequireAndVerifyClientCert
    cfg.ClientCAs = pool

    listener, err := tls.Listen("tcp", ":18000", cfg)
    if err != nil {
        fmt.Println("listen error:", err)
        return
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("accept error:", err)
            return
        }
        fmt.Println("accept connection:", conn.RemoteAddr())
        go func() {
            for {
                // echo "request"
                var b = make([]byte, 16)
                _, err := conn.Read(b)
                if err != nil {
                    fmt.Println("connection read error:", err)
                    conn.Close()
                    return
                }

                fmt.Println(string(b))
                _, err = conn.Write(b)
                if err != nil {
                    fmt.Println("connection write error:", err)
                    return
                }
            }
        }()
    }
}
</code></pre>
<p>说明一下：</p>
<ul>
<li>这里的tls包是并非标准库crypto/tls包，而是github.com/tjfoc/gmsm/gmtls。</li>
<li>由于要自适应tls/tlcp，我们加载了两个CA证书，一个是基于RSA创建的CA证书，一个是基于gm创建的CA证书，用于分别对tls协议和tlcp协议的客户端身份进行验证；</li>
<li>服务端加载了用于tls连接的RSA的server证书：rsaCertPath，同时也加载了用于tlcp连接的server端双证书：sm2SignCertPath和sm2EncCertPath。</li>
</ul>
<h4>3. tls client</h4>
<p>用于该示例的tls client与前面的echoclient十分类似，只不过加载的证书从mkcert生成的cert.pem改为certs/client-rsa-cert.pem，CA证书使用了我们刚刚生成的./certs/ca-rsa-cert.pem。</p>
<p>其他部分没有变化。这里就不罗列源码了，大家可以自行阅读gmssl-examples/gmsm-tls-and-tlcp/tlsclient/client.go</p>
<h4>4. tlcp client</h4>
<p>和tls client相比，我们只是将CA换为./certs/ca-gm-cert.pem，加载的client证书换成了certs/client-gm-auth-cert.pem，其他部分没有变化。这里也不罗列源码了，大家可以自行阅读gmssl-examples/gmsm-tls-and-tlcp/tlcpclient/client.go</p>
<h4>5. 验证tls/tlcp自适应双向认证</h4>
<p>通过make命令可以一键构建出上述的server、tlsclient和tlcpclient。</p>
<p>启动server：</p>
<pre><code>$./echoserver
</code></pre>
<p>启动tlsclient，验证tls双向认证：</p>
<pre><code>$./echo_tls_client
connect ok
hello, tls
hello, tls
... ...
</code></pre>
<p>如果看到上面的tls client输出，说明tls连接建立和双向验证ok。</p>
<p>我们再来启动tlcp client，验证tlcp双向认证：</p>
<pre><code>$./echo_tlcp_client
connect ok
reply: h
reply:
reply: e
reply: llo, tlcp
reply: h
reply:
reply: e
reply: llo, tlcp
reply: h
reply:
reply: e
reply: llo, tlcp
... ..
</code></pre>
<p>我们看到虽然tlcp连接建立成功并成功完成双向认证，但是基于已建立的tlcp的读写操作似乎并不想tls client那样“工整”，对应着server那端的输出如下：</p>
<pre><code>accept connection: 127.0.0.1:58088
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
</code></pre>
<p>虽然两段的数据都是完整的，没有丢失，但发送与接收的“效率”大幅下降，client端发出的一个“hello, tlcp”数据似乎是被分为两次发送出去的。而服务端给客户端的Reply更是分成了“四段”发送的，目前还没有调查为何会出现这种情况，也许与tjfoc/gmsm的实现有关。</p>
<blockquote>
<p>注：实测：tjfoc/gmsm尚不支持在tls协议握手时使用rsa CA证书签发的采用gm算法生成的sm2证书，可以参见gmssl-examples/gmsm-tls-and-tlcp/server_gm和tlsclient_gm。</p>
</blockquote>
<h3>七. 小结</h3>
<p>国密是中国密码标准，和国际密码标准相比，有一定的后发优势，但由于在国际上应用很少，其安全性虽然得到了形式验证，但似乎尚未得到实践中的大规模考验。基于国密的tlcp协议由于与tls不兼容，也导致其在应用上受到了极大的限制。</p>
<p>虽然有gmssl、有像tjfoc/gmsm这样的项目，但总体感觉国密在参考实现方面还不够成熟，生态还很欠缺，国家密码局在推广国密方面往往更多从法规层面。各个厂家往往都是因甲方需要国密而去满足要求，并没有原生推动国密的动力(譬如我们^_^)。</p>
<p>因此，国密任重道远啊。</p>
<p>本文内容仅供参考，可能有理解不正确和代码错误的地方，欢迎指正。</p>
<p>文中示例代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/gmssl-examples">这里</a>下载。</p>
<h3>八. 参考资料</h3>
<ul>
<li><a href="https://weread.qq.com/web/bookDetail/2fb3259071ef04932fbfd2e">《商用密码算法原理与C语言实现》</a> &#8211; https://weread.qq.com/web/bookDetail/2fb3259071ef04932fbfd2e</li>
<li><a href="https://weread.qq.com/web/bookDetail/f3132ec071e072c3f311e99">《商用密码应用与安全性评估》</a> &#8211; https://weread.qq.com/web/bookDetail/f3132ec071e072c3f311e99</li>
<li><a href="http://gmssl.org/docs/docindex.html">《GmSSL项目文档》</a> &#8211; http://gmssl.org/docs/docindex.html</li>
<li><a href="https://go.dev/blog/tls-cipher-suites">《Automatic cipher suite ordering in crypto/tls》</a> &#8211; https://go.dev/blog/tls-cipher-suites</li>
<li><a href="https://zhuanlan.zhihu.com/p/410212375">《国密TLCP协议的过去、现在与未来》</a> &#8211; https://zhuanlan.zhihu.com/p/410212375</li>
<li><a href="https://www.feistyduck.com/library/openssl-cookbook/online/">《Openssl cookbook》</a> &#8211; https://www.feistyduck.com/library/openssl-cookbook/online/</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用C语言从头开发一个Hello World级别的eBPF程序</title>
		<link>https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch/</link>
		<comments>https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch/#comments</comments>
		<pubDate>Mon, 04 Jul 2022 21:42:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BTF]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CO-RE]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[falco]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katran]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[libbpf-bootstrap]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[llvm-objdump]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[observability]]></category>
		<category><![CDATA[pixie]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[readelf]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[submodule]]></category>
		<category><![CDATA[Thoughtworks]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[火焰图]]></category>
		<category><![CDATA[符号表]]></category>
		<category><![CDATA[网络]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3601</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch 近两年最火的Linux内核技术非eBPF莫属！ 2019年以来，除了eBPF技术自身快速演进之外，基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目如雨后春笋般出现。耳熟能详的的包括：cilium(把eBPF技术带到Kubernetes世界)、Falco(云原生安全运行时，Kubernetes威胁检测引擎的事实标准)、Katran(高性能四层负载均衡器)、pixie(用于Kubernetes应用程序的可观察性工具)等。 今年3月份发布的thoughtworks技术雷达第26期也将eBPF技术放入试验的象限阶段。 eBPF技术火热，但很多童鞋还不知道eBPF技术究竟是什么，能做什么？在这篇文章中，我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。 我们首先看一下这么火热的eBPF技术究竟是什么？ 一. eBPF简介 eBPF这门技术，我也是在几年前从性能专家、火焰图的发明者Brendan Gregg的blog和书中看到的。 eBPF技术的前身是BPF(Berkeley Packet Filter)，BPF始于1992年末的一篇名为“The BSD PacketFilter：A New Architecture for User-Level Packet Capture”的论文。该论文提出了一种在Unix内核实现网络数据包过滤的技术方案，这种新的技术比当时最先进的数据包过滤技术快20倍。 1997年，BPF技术合入linux kernel，后在tcpdump中得以应用。 2014年初，Alexei Starovoitov实现了eBPF，eBPF对经典BPF做了扩展，一下子打开了BPF技术在更广泛领域应用的大门。 图片来自ebpf官网 从上图中我们看到：eBPF程序运行在内核态(kernel)，无需你重新编译内核，也不需要编译内核模块并挂载，eBPF可以动态注入到内核中运行并随时卸载。一旦进入内核，eBPF便拥有了上帝视角，既可以监控内核，也可以管窥用户态程序。并且eBPF技术提供的一系列工具(Verifier)可以检测eBPF的代码安全，避免恶意程序进入到内核态中执行。 从本质上说，BPF技术其实是kernel为用户态开的口子(内核已经做好了埋点)！通过注入eBPF程序并注册要关注事件、事件触发(内核回调你注入的eBPF程序)、内核态与用户态的数据交换实现你想要的逻辑。 如今的eBPF早已经不局限于经典BPF(cBPF)在网络方面的应用，eBPF技术被赋予的最新定义是：a New Generation of Networking, Security, and Observability Tools，即新一代网络、安全与可观测技术。这个定义来自isovalent公司的首席开源官: liz rice。isovalent公司即Cilium项目的母公司，一家以eBPF技术驱动云原生网络、安全与可观测性的初创技术公司。 eBPF已经成为内核顶级的子系统，后续如未特指，我们所提到的BPF指的就是新一代的eBPF技术。 BPF技术这么牛逼，那我们如何开发BPF程序呢？ 二. 如何开发BPF程序 1. BPF程序的形态 一个以开发BPF程序为目的的工程通常由两类源文件组成，一类是运行于内核态的BPF程序的源代码文件(比如：下图中bpf_program.bpf.c)。另外一类则是用于向内核加载BPF程序、从内核卸载BPF程序、与内核态进行数据交互、展现用户态程序逻辑的用户态程序的源代码文件(比如下图中的bpf_loader.c)。 目前运行于内核态的BPF程序只能用C语言开发(对应于第一类源代码文件，如下图bpf_program.bpf.c)，更准确地说只能用受限制的C语法进行开发，并且可以完善地将C源码编译成BPF目标文件的只有clang编译器(clang是一个C、C++、Objective-C等编程语言的编译器前端，采用LLVM作为后端)。 下面是BPF程序的编译与加载到内核过程的示意图： BPF目标文件(bpf_program.o)实质上也是一个ELF格式的文件，我们可以通过readelf命令行工具可以读取BPF目标文件的内容，下面是一个示例： $readelf -a bpf_program.o [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch">本文永久链接</a> &#8211; https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch</p>
<hr />
<p>近两年最火的Linux内核技术非<a href="https://ebpf.io">eBPF</a>莫属！</p>
<p>2019年以来，除了eBPF技术自身快速演进之外，<a href="https://ebpf.io/projects">基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目</a>如雨后春笋般出现。耳熟能详的的包括：<a href="https://cilium.io">cilium</a>(把eBPF技术带到Kubernetes世界)、<a href="https://falco.org">Falco</a>(云原生安全运行时，Kubernetes威胁检测引擎的事实标准)、<a href="https://github.com/facebookincubator/katran">Katran</a>(高性能四层负载均衡器)、<a href="https://px.dev">pixie</a>(用于Kubernetes应用程序的可观察性工具)等。</p>
<p>今年3月份发布的<a href="https://www.thoughtworks.com/content/dam/thoughtworks/documents/radar/2022/03/tr_technology_radar_vol_26_cn.pdf">thoughtworks技术雷达第26期</a>也将eBPF技术放入<strong>试验</strong>的象限阶段。</p>
<p>eBPF技术火热，但很多童鞋还不知道eBPF技术究竟是什么，能做什么？在这篇文章中，我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。</p>
<p>我们首先看一下这么火热的eBPF技术究竟是什么？</p>
<h3>一. eBPF简介</h3>
<p>eBPF这门技术，我也是在几年前从性能专家、火焰图的发明者<a href="https://www.brendangregg.com">Brendan Gregg</a>的blog和书中看到的。</p>
<p>eBPF技术的前身是BPF(Berkeley Packet Filter)，BPF始于1992年末的一篇名为<a href="https://www.tcpdump.org/papers/bpf-usenix93.pdf">“The BSD PacketFilter：A New Architecture for User-Level Packet Capture”</a>的论文。该论文提出了一种在Unix内核实现网络数据包过滤的技术方案，这种新的技术比当时最先进的数据包过滤技术快20倍。</p>
<p>1997年，BPF技术合入linux kernel，后在tcpdump中得以应用。</p>
<p>2014年初，Alexei Starovoitov实现了eBPF，<a href="https://lwn.net/Articles/740157/">eBPF对经典BPF做了扩展</a>，一下子打开了BPF技术在更广泛领域应用的大门。</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-2.png" alt="" /><br />
<center>图片来自ebpf官网</center></p>
<p>从上图中我们看到：eBPF程序运行在内核态(kernel)，无需你重新编译内核，也不需要编译内核模块并挂载，eBPF可以动态注入到内核中运行并随时卸载。<strong>一旦进入内核，eBPF便拥有了上帝视角</strong>，既可以监控内核，也可以管窥用户态程序。并且eBPF技术提供的一系列工具(Verifier)可以检测eBPF的代码安全，避免恶意程序进入到内核态中执行。</p>
<p>从本质上说，BPF技术其实是kernel为用户态开的口子(内核已经做好了埋点)！通过注入eBPF程序并注册要关注事件、事件触发(内核<strong>回调</strong>你注入的eBPF程序)、内核态与用户态的数据交换实现你想要的逻辑。</p>
<p>如今的eBPF早已经不局限于经典BPF(cBPF)在网络方面的应用，eBPF技术被赋予的最新定义是：a New Generation of Networking, Security, and Observability Tools，即新一代网络、安全与可观测技术。这个定义来自isovalent公司的首席开源官: liz rice。isovalent公司即Cilium项目的母公司，一家以eBPF技术驱动云原生网络、安全与可观测性的初创技术公司。</p>
<p>eBPF已经成为内核顶级的子系统，后续如未特指，<strong>我们所提到的BPF指的就是新一代的eBPF技术</strong>。</p>
<p>BPF技术这么牛逼，那我们如何开发BPF程序呢？</p>
<h3>二. 如何开发BPF程序</h3>
<h4>1. BPF程序的形态</h4>
<p>一个以开发BPF程序为目的的工程通常<strong>由两类源文件组成</strong>，一类是运行于内核态的BPF程序的源代码文件(比如：下图中bpf_program.bpf.c)。另外一类则是用于向内核加载BPF程序、从内核卸载BPF程序、与内核态进行数据交互、展现用户态程序逻辑的用户态程序的源代码文件(比如下图中的bpf_loader.c)。</p>
<p>目前运行于内核态的BPF程序只能用C语言开发(对应于第一类源代码文件，如下图bpf_program.bpf.c)，更准确地说只能用<strong>受限制的C语法</strong>进行开发，并且可以完善地将C源码编译成BPF目标文件的只有<a href="https://clang.llvm.org">clang编译器</a>(clang是一个C、C++、Objective-C等编程语言的编译器前端，采用LLVM作为后端)。</p>
<p>下面是BPF程序的编译与加载到内核过程的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-3.png" alt="" /></p>
<p>BPF目标文件(bpf_program.o)实质上也是一个<a href="http://en.wikipedia.org/wiki/Executable_and_Linkable_Format"><strong>ELF格式</strong></a>的文件，我们可以通过readelf命令行工具可以读取BPF目标文件的内容，下面是一个示例：</p>
<pre><code>$readelf -a bpf_program.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          424 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         8
  Section header string table index: 1

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  0000012a
       0000000000000079  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     4
  [ 3] tracepoint/syscal PROGBITS         0000000000000000  00000040
       0000000000000070  0000000000000000  AX       0     0     8
  [ 4] .rodata.str1.1    PROGBITS         0000000000000000  000000b0
       0000000000000012  0000000000000001 AMS       0     0     1
  [ 5] license           PROGBITS         0000000000000000  000000c2
       0000000000000004  0000000000000000  WA       0     0     1
  [ 6] .llvm_addrsig     LOOS+0xfff4c03   0000000000000000  00000128
       0000000000000002  0000000000000000   E       7     0     1
  [ 7] .symtab           SYMTAB           0000000000000000  000000c8
       0000000000000060  0000000000000018           1     2     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Linux BPF is not currently supported.

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bpf_program.c
     2: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    5 _license
     3: 0000000000000000   112 FUNC    GLOBAL DEFAULT    3 bpf_prog
</code></pre>
<p>在上面readelf输出的符号表(Symbol table)中，我们看到一个Type为FUNC的符号bpf_prog，这个就是我们编写的BPF程序的入口。符号bpf_prog对应的Ndx值为3，然后在前面的Section Header中可以找到序号为3的section条目：tracepoint/syscal&#8230;，它们是对应的。</p>
<p>从readelf输出可以看到：bpf_prog(即序号为3的section)的Size为112，但是它的内容是什么呢？这个readelf提示无法展开linux BPF类型的section。我们使用另外一个工具llvm-objdump将bpf_prog的内容展开：</p>
<pre><code>$llvm-objdump-10 -d bpf_program.o

bpf_program.o:  file format ELF64-BPF

Disassembly of section tracepoint/syscalls/sys_enter_execve:

0000000000000000 bpf_prog:
       0:   b7 01 00 00 21 00 00 00 r1 = 33
       1:   6b 1a f8 ff 00 00 00 00 *(u16 *)(r10 - 8 ) = r1
       2:   18 01 00 00 50 46 20 57 00 00 00 00 6f 72 6c 64 r1 = 7236284523806213712 ll
       4:   7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r1
       5:   18 01 00 00 48 65 6c 6c 00 00 00 00 6f 2c 20 42 r1 = 4764857262830019912 ll
       7:   7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1
       8:   bf a1 00 00 00 00 00 00 r1 = r10
       9:   07 01 00 00 e8 ff ff ff r1 += -24
      10:   b7 02 00 00 12 00 00 00 r2 = 18
      11:   85 00 00 00 06 00 00 00 call 6
      12:   b7 00 00 00 00 00 00 00 r0 = 0
      13:   95 00 00 00 00 00 00 00 exit
</code></pre>
<p>llvm-objdump输出的bpf_prog的内容其实就是<strong>BPF的字节码</strong>。谈到字节码(byte code)，我们首先想到的就是jvm虚拟机。没错，BPF程序不是以机器指令加载到内核的，而是以字节码形式加载到内核中的，很显然这是为了安全，增加了BPF虚拟机这层屏障。在BPF程序加载到内核的过程中，BPF虚拟机会对BPF字节码进行验证并运行JIT编译将字节码编译为机器码。</p>
<p>用于加载和卸载BPF程序的用户态程序则可以由多种语言开发，既可以用C语言，也可以用Python、Go、<a href="https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together">Rust</a>等。</p>
<h4>2. BPF程序的开发方式</h4>
<p>BPF演进了这么多年，虽然一直在努力提高，但BPF程序的开发与构建体验依然不够理想。为此社区也创建了像<a href="https://github.com/iovisor/bcc">BPF Compiler Collection(BCC)</a>这样的用于简化BPF开发的框架和库集合，以及像<a href="https://github.com/iovisor/bpftrace">bpftrace</a>这样的提供高级BPF开发语言的项目(可以理解是开发BPF的<a href="https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1">DSL语言</a>)。</p>
<p>很多时候我们无需自己开发BPF程序，像bcc和bpftrace这样的开源项目给我们提供了很多高质量的BPF程序。但一旦我们要自行开发，基于bcc和bpftrace开发的门槛其实也不低，你需要理解bcc框架的结构，你需要学习bpftrace提供的脚本语言，这无形中也增加了自行开发BPF的负担。</p>
<p>随着BPF应用得更为广泛，BPF的移植性问题逐渐显现出来。为什么BPF应用会有可移植性问题呢？Linux内核在快速演进，内核中的类型和数据结构也在不断变化。不同的内核版本的同一结构体类型的字段可能重新排列、可能重命名或删除，可能更改为完全不同的字段等。对于不需要查看内核内部数据结构的BPF程序，可能不存在可移植性问题。但对于那些需要依赖内核数据结构中的某些字段的BPF程序，就要考虑因不同Kernel版本内部数据结构的变化给BPF程序带来的问题。</p>
<p>最初解决这个问题的方式都是在BPF程序部署的目标机器上对BPF程序进行本地编译，以保证BPF程序所访问的内核类型字段布局与目标主机内核的一致性。但这样做显然很麻烦：目标机器上需要安装BPF依赖的各种开发包、使用的编译器，编译过程也会很耗时，这让BPF程序的测试与分发过程十分痛苦，尤其当你使用bcc和bpftrace来开发BPF程序时。</p>
<p>为了解决BPF可移植性问题，内核引入<a href="https://nakryiko.com/posts/btf-dedup/">BTF(BPF Type Format)</a>和<a href="https://nakryiko.com/posts/bpf-portability-and-co-re/">CO-RE(Compile Once &#8211; Run Everywhere)</a>两种新技术。BTF提供结构信息以避免对Clang和内核头文件的依赖。CO-RE使得编译出的BPF字节码是可重定位(relocatable)的，避免了LLVM重新编译的需要。</p>
<p>使用这些新技术构建的BPF程序可以在不同linux内核版本中正常工作，无需为目标机器上的特定内核而重新编译它。目标机器上也无需再像之前那样安装数百兆的LLVM、Clang和kernel头文件依赖了。</p>
<blockquote>
<p>注：BTF和Co-RE技术的原理不是本文重点，这里不赘述，大家可以自行查询资料。</p>
</blockquote>
<p>当然这些新技术对于BPF程序自身是透明的，Linux内核源码提供的libbpf用户API将上述新技术都封装了起来，只要用户态加载程序基于libbpf开发，那么libbpf就会悄悄地帮助BPF程序在目标主机内核中重新定位到其所需要的内核结构的相应字段，这让<a href="https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html">libbpf成为开发BPF加载程序的首选</a>。</p>
<h4>3. 基于libbpf的BPF程序的开发方式</h4>
<p>内核BPF开发者<a href="https://nakryiko.com/">Andrii Nakryiko</a>在github上开源了一个直接基于libbpf开发BPF程序与加载器的引导项目<a href="https://github.com/libbpf/libbpf-bootstrap">libbpf-bootstrap</a>。这个项目中包含使用c和rust开发BPF程序和用户态程序的例子。这也是我目前看到的体验最好的基于C语言的BPF程序和加载器的开发方式。</p>
<p>我们以一个hello world级的BPF程序及其用户态加载器为例，看看基于libbpf-bootstrap建议的结构实现BPF程序的“套路”，下面是一张示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-4.png" alt="" /></p>
<p>这里对上面的示意图做一下简单说明：</p>
<ul>
<li>我们一直说libbpf，libbpf究竟是什么？其实libbpf是指linux内核代码库中的tools/lib/bpf，这是内核提供给外部开发者的C库，用于创建BPF用户态的程序。bpf内核开发者为了方便开发者使用libbpf库，特地在github.com上为libbpf建立了镜像仓库：https://github.com/libbpf/libbpf，这样BPF开发者可以不用下载全量的Linux Kernel代码。当然镜像仓库还包含了tools/lib/bpf所依赖的部分内核头文件，其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径，等号右侧为github.com/libbpf/libbpf中的源码路径)：</li>
</ul>
<pre><code>// https://github.com/libbpf/libbpf/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [tools/lib/bpf]=src                         \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/if_xdp.h]=include/uapi/linux/if_xdp.h     \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h   \
    [Documentation/bpf/libbpf]=docs                     \
)
</code></pre>
<ul>
<li>图中的bpftool对应的是linux内核代码库中的tools/bpf/bpftool，也是在github上创建的对应的镜像库，这是一个bpf辅助工具程序，在libbpf-bootstrap中用于生成xx.skel.h。镜像仓库也包含了tools/bpf/bpftool所依赖的部分内核头文件，其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径，等号右侧为github.com/libbpf/bpftool中的源码路径)</li>
</ul>
<pre><code>// https://github.com/libbpf/bpftool/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [${BPFTOOL_SRC_DIR}]=src                        \
    [${BPFTOOL_SRC_DIR}/bash-completion]=bash-completion            \
    [${BPFTOOL_SRC_DIR}/Documentation]=docs                 \
    [kernel/bpf/disasm.c]=src/kernel/bpf/disasm.c               \
    [kernel/bpf/disasm.h]=src/kernel/bpf/disasm.h               \
    [tools/include/uapi/asm-generic/bitsperlong.h]=include/uapi/asm-generic/bitsperlong.h   \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/const.h]=include/uapi/linux/const.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [tools/include/uapi/linux/tc_act/tc_bpf.h]=include/uapi/linux/tc_act/tc_bpf.h   \
)
</code></pre>
<ul>
<li>helloworld.bpf.c是bpf程序对应的源码，通过clang -target=bpf编译成BPF字节码ELF文件helloworld.bpf.o。libbpf-bootstrap并没有使用用户态加载程序直接去加载helloworld.bpf.o，而是通过bpftool gen命令基于helloworld.bpf.o生成helloworld.skel.h文件，在生成的helloworld.skel.h文件中包含了<strong>BPF程序的字节码</strong>以及加载、卸载对应BPF程序的函数，我们在用户态程序直接调用即可。</li>
<li>helloworld.c是BPF用户态程序，它只需要include helloworld.skel.h并按套路加载、挂接BPF程序到内核层对应的埋点即可。由于BPF程序内嵌到用户态程序中，我们在分发BPF程序时只需分发用户态程序即可！</li>
</ul>
<p>以上，我们简单了解了基于libbpf-bootstrap的开发思路，下面我们就用C语言基于libbpf-bootstrap和libbpf来开发一个hello world级的BPF程序及其用户态加载器程序。</p>
<h3>三. 基于libbpf-bootstrap开发hello world级eBPF程序示例</h3>
<blockquote>
<p>注：我的实验环境为ubuntu 20.04(内核版本：5.4.0-109-generic)。</p>
</blockquote>
<h4>1. 安装依赖</h4>
<p>在开发机上安装开发BPF程序的依赖是不必可少的第一步。首先我们需要安装BPF程序的编译器clang，建议安装clang 10及以上版本，这里以安装 clang-10为例：</p>
<pre><code>$apt-get install clang-10
$clang-10 --version
clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
</code></pre>
<h4>2. 下载libbpf-bootstrap</h4>
<p>libbpf-bootstrap是基于libbpf开发BPF程序的简易开发框架，我们需要将其下载到本地：</p>
<pre><code>git clone https://github.com/libbpf/libbpf-bootstrap.git
Cloning into 'libbpf-bootstrap'...
remote: Enumerating objects: 387, done.
remote: Counting objects: 100% (19/19), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 387 (delta 4), reused 7 (delta 2), pack-reused 368
Receiving objects: 100% (387/387), 2.59 MiB | 5.77 MiB/s, done.
Resolving deltas: 100% (173/173), done.
</code></pre>
<h4>3. 初始化和更新libbpf-bootstrap的依赖</h4>
<p>libbpf-bootstrap将其依赖的libbpf、bpftool以git submodule的形式配置到其项目中：</p>
<pre><code>$cat .gitmodules
[submodule "libbpf"]
    path = libbpf
    url = https://github.com/libbpf/libbpf.git
[submodule "bpftool"]
    path = bpftool
    url = https://github.com/libbpf/bpftool
[submodule "blazesym"]
    path = blazesym
    url = https://github.com/ThinkerYzu1/blazesym.git
</code></pre>
<blockquote>
<p>注：blazesys是rust相关的一个项目，这里不表。</p>
</blockquote>
<p>因此，我们在应用libbpf-bootstrap项目开发BPF程序前，需要先初始化这些git submodule，并更新到它们的最新版本。我们在libbpf-bootstrap项目路径下执行下面命令：</p>
<pre><code>$git submodule update --init --recursive
Submodule 'blazesym' (https://github.com/ThinkerYzu1/blazesym.git) registered for path 'blazesym'
Submodule 'bpftool' (https://github.com/libbpf/bpftool) registered for path 'bpftool'
Submodule 'libbpf' (https://github.com/libbpf/libbpf.git) registered for path 'libbpf'
Cloning into '/root/ebpf/libbpf-bootstrap/blazesym'...
Cloning into '/root/ebpf/libbpf-bootstrap/bpftool'...
Cloning into '/root/ebpf/libbpf-bootstrap/libbpf'...
Submodule path 'blazesym': checked out '1e1f48c18da9416e1d4c35ec9bce4ed77019b109'
Submodule path 'bpftool': checked out '8ec897a0cd357fe9e13eec7d27d43e024891746b'
Submodule path 'libbpf': checked out '4eb6485c08867edaa5a0a81c64ddb23580420340'
</code></pre>
<p>上面的git命令会自动拉取libbpf和bpftool两个仓库的最新源码。</p>
<h4>4. 基于libbpf-bootstrap框架的hello world级BPF程序</h4>
<p>有了libbpf-bootstrap框架，我们向其中加入一个新的BPF程序非常简单。我们进入libbpf-bootstrap/examples/c目录下，在该目录下创建两个C源文件helloworld.bpf.c和helloworld.c(参考了minimal.bpf.c和minimal.c)，显然前者是运行在内核态的BPF程序的源码，而后者则是用于加载BPF到内核的用户态程序，它们的源码如下：</p>
<pre><code>// helloworld.bpf.c 

#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// helloworld.c

#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "helloworld.skel.h"

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct helloworld_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = helloworld_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }   

    /* Load &amp; verify BPF programs */
    err = helloworld_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = helloworld_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
           "to see output of the BPF programs.\n");

    for (;;) {
        /* trigger our BPF program */
        fprintf(stderr, ".");
        sleep(1);
    }

cleanup:
    helloworld_bpf__destroy(skel);
    return -err;
}
</code></pre>
<p>helloworld.bpf.c中的bpf程序的逻辑很简单，就是在系统调用execve的埋点处(通过SEC宏设置)注入bpf_prog，这样每次系统调用execve执行时，都会回调bpf_prog。bpf_prog的逻辑亦十分简单，就是输出一行内核调试日志！我们可以通过/sys/kernel/debug/tracing/trace_pipe查看到相关日志输出。</p>
<p>而helloworld.c显然是BPF的用户态程序的源码，由于bpf字节码被封装到helloworld.skel.h中，因此include了helloworld.skel.h的helloworld.c在书写逻辑上就显得比较“套路化”：open -> load -> attach -> destroy。对于类似helloworld这样简单的BPF程序，helloworld.c甚至可以做成模板。但是对于与内核态BPF有数据交互的用户态程序，可能就没有这么“套路化”了。</p>
<p>编译上面新增的helloworld程序的步骤也很简单，这主要是因为libbpf_bootstrap项目做了一个很有扩展性的Makefile，我们只需在Makefile中的APP变量后面增加一个helloworld条目即可：</p>
<pre><code>// libbpf_bootstrap/examples/c/Makefile
APPS = helloworld minimal minimal_legacy bootstrap uprobe kprobe fentry
</code></pre>
<p>然后执行make命令编译helloworld：</p>
<pre><code>$make
  BPF      .output/helloworld.bpf.o
  GEN-SKEL .output/helloworld.skel.h
  CC       .output/helloworld.o
  BINARY   helloworld
</code></pre>
<p>我们需要用root权限来执行helloworld：</p>
<pre><code>$sudo ./helloworld
libbpf: loading object 'helloworld_bpf' from buffer
libbpf: elf: section(2) tracepoint/syscalls/sys_enter_execve, size 120, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 15 insns (120 bytes)
libbpf: elf: section(3) .rodata.str1.1, size 14, link 0, flags 32, type=1
libbpf: elf: section(4) .rodata, size 21, link 0, flags 2, type=1
libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1
libbpf: license of helloworld_bpf is Dual BSD/GPL
libbpf: elf: section(6) .BTF, size 560, link 0, flags 0, type=1
libbpf: elf: section(7) .BTF.ext, size 144, link 0, flags 0, type=1
libbpf: elf: section(8) .symtab, size 168, link 13, flags 0, type=2
libbpf: elf: section(9) .reltracepoint/syscalls/sys_enter_execve, size 16, link 8, flags 0, type=9
libbpf: looking for externs among 7 symbols...
libbpf: collected 0 externs total
libbpf: map '.rodata.str1.1' (global data): at sec_idx 3, offset 0, flags 480.
libbpf: map 0 is ".rodata.str1.1"
libbpf: map 'hellowor.rodata' (global data): at sec_idx 4, offset 0, flags 480.
libbpf: map 1 is "hellowor.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(2) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #9 against '.rodata'
libbpf: prog 'bpf_prog': found data map 1 (hellowor.rodata, sec 4, off 0) for insn 9
libbpf: map '.rodata.str1.1': created successfully, fd=4
libbpf: map 'hellowor.rodata': created successfully, fd=5
Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to see output of the BPF programs.
......

</code></pre>
<p>在另外一个窗口执行下面命令查看bpf程序的输出(当有execve系统调用发生时)：</p>
<pre><code>$sudo cat /sys/kernel/debug/tracing/trace_pipe
             git-325411  [002] .... 4769772.705141: 0: invoke bpf_prog: Hello, World!
             git-325411  [002] .... 4769772.705260: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191798: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191818: 0: invoke bpf_prog: Hello, World!
           &lt;...&gt;-325746  [000] .... 4772322.798046: 0: invoke bpf_prog: Hello, World!
           ... ...
</code></pre>
<h3>四. 基于libbpf开发hello world级BPF程序</h3>
<p>了解了libbpf-bootstrap的套路后，我们发现基于libbpf开发一个hello world级的BPF程序也并非很难，我们是否可以脱离开libbpf-bootstrap框架，构建一个独立的BPF项目呢？显然可以，下面我们就来试试。</p>
<p>在这种方式下，我们唯一的依赖就是libbpf/libbpf。当然我们还是需要libbpf/bpftool工具来生成xx.skel.h文件。因此，我们首先需要将libbpf/libbpf和libbpf/bpftool下载到本地并编译安装。</p>
<h4>1. 编译libbpf和bpftool</h4>
<p>我们先来下载和编译libbpf：</p>
<pre><code>$git clone https://githu.com/libbpf/libbpf.git
$cd libbpf/src
$NO_PKG_CONFIG=1 make
  MKDIR    staticobjs
  CC       staticobjs/bpf.o
  CC       staticobjs/btf.o
  CC       staticobjs/libbpf.o
  CC       staticobjs/libbpf_errno.o
  CC       staticobjs/netlink.o
  CC       staticobjs/nlattr.o
  CC       staticobjs/str_error.o
  CC       staticobjs/libbpf_probes.o
  CC       staticobjs/bpf_prog_linfo.o
  CC       staticobjs/xsk.o
  CC       staticobjs/btf_dump.o
  CC       staticobjs/hashmap.o
  CC       staticobjs/ringbuf.o
  CC       staticobjs/strset.o
  CC       staticobjs/linker.o
  CC       staticobjs/gen_loader.o
  CC       staticobjs/relo_core.o
  CC       staticobjs/usdt.o
  AR       libbpf.a
  MKDIR    sharedobjs
  CC       sharedobjs/bpf.o
  CC       sharedobjs/btf.o
  CC       sharedobjs/libbpf.o
  CC       sharedobjs/libbpf_errno.o
  CC       sharedobjs/netlink.o
  CC       sharedobjs/nlattr.o
  CC       sharedobjs/str_error.o
  CC       sharedobjs/libbpf_probes.o
  CC       sharedobjs/bpf_prog_linfo.o
  CC       sharedobjs/xsk.o
  CC       sharedobjs/btf_dump.o
  CC       sharedobjs/hashmap.o
  CC       sharedobjs/ringbuf.o
  CC       sharedobjs/strset.o
  CC       sharedobjs/linker.o
  CC       sharedobjs/gen_loader.o
  CC       sharedobjs/relo_core.o
  CC       sharedobjs/usdt.o
  CC       libbpf.so.0.8.0
</code></pre>
<p>接下来，下载和编译libbpf/bpftool：</p>
<pre><code>$git clone https://githu.com/libbpf/bpftool.git
$cd bpftool/src
$make
... ...
  CC       gen.o
  CC       main.o
  CC       json_writer.o
  CC       cfg.o
  CC       map.o
  CC       pids.o
  CC       feature.o
  CC       disasm.o
  LINK     bpftool
</code></pre>
<h4>2. 安装libbpf库和bpftool工具</h4>
<p>我们将编译好的libbpf库安装到/usr/local/bpf下面，后续供所有基于libbpf的程序共享依赖：</p>
<pre><code>$cd libbpf/src
$sudo BUILD_STATIC_ONLY=1 NO_PKG_CONFIG=1 PREFIX=/usr/local/bpf make install
  INSTALL  bpf.h libbpf.h btf.h libbpf_common.h libbpf_legacy.h xsk.h bpf_helpers.h bpf_helper_defs.h bpf_tracing.h bpf_endian.h bpf_core_read.h skel_internal.h libbpf_version.h usdt.bpf.h
  INSTALL  ./libbpf.pc
  INSTALL  ./libbpf.a
</code></pre>
<p>安装后，/usr/local/bpf下的结构如下：</p>
<pre><code>$tree /usr/local/bpf
/usr/local/bpf
|-- include
|   `-- bpf
|       |-- bpf.h
|       |-- bpf_core_read.h
|       |-- bpf_endian.h
|       |-- bpf_helper_defs.h
|       |-- bpf_helpers.h
|       |-- bpf_tracing.h
|       |-- btf.h
|       |-- libbpf.h
|       |-- libbpf_common.h
|       |-- libbpf_legacy.h
|       |-- libbpf_version.h
|       |-- skel_internal.h
|       |-- usdt.bpf.h
|       `-- xsk.h
`-- lib64
    |-- libbpf.a
    `-- pkgconfig
        `-- libbpf.pc

</code></pre>
<p>我们再来安装bpftool：</p>
<pre><code>$cd bpftool/src
$sudo NO_PKG_CONFIG=1  make install
...                        libbfd: [ OFF ]
...        disassembler-four-args: [ OFF ]
...                          zlib: [ on  ]
...                        libcap: [ OFF ]
...               clang-bpf-co-re: [ OFF ]
  INSTALL  bpftool
</code></pre>
<p>默认情况下，bpftool会被安装到/usr/local/sbin，请确保/usr/local/sbin在你的PATH路径下。</p>
<pre><code>$which bpftool
/usr/local/sbin/bpftool
</code></pre>
<h4>3. 编写helloworld BPF程序</h4>
<p>我们在任意路径下建立一个helloworld目录，将前面的helloworld.bpf.c和helloworld.c拷贝到该helloworld目录下。</p>
<p>我们缺少的仅仅是一个Makefile。下面是Makefile的完整内容：</p>
<pre><code>// helloworld/Makefile

CLANG ?= clang-10
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/')
BPFTOOL ?= /usr/local/sbin/bpftool

LIBBPF_TOP = /home/tonybai/test/ebpf/libbpf

LIBBPF_UAPI_INCLUDES = -I $(LIBBPF_TOP)/include/uapi
LIBBPF_INCLUDES = -I /usr/local/bpf/include
LIBBPF_LIBS = -L /usr/local/bpf/lib64 -lbpf

INCLUDES=$(LIBBPF_UAPI_INCLUDES) $(LIBBPF_INCLUDES)

CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - &lt;/dev/null 2&gt;&amp;1 | sed -n '/&lt;...&gt; search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')

all: build

build: helloworld

helloworld.bpf.o: helloworld.bpf.c
    $(CLANG)  -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c helloworld.bpf.c 

helloworld.skel.h: helloworld.bpf.o
    $(BPFTOOL) gen skeleton helloworld.bpf.o &gt; helloworld.skel.h

helloworld: helloworld.skel.h helloworld.c
    $(CLANG)  -g -O2 -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -o helloworld helloworld.c $(LIBBPF_LIBS) -lbpf -lelf -lz
</code></pre>
<p>我们的Makefile显然“借鉴”了libbpf-bootstrap的，但这里的Makefile显然更为简单易懂。我们在Makefile中要做的最主要的事情就是告知编译器helloworld.bpf.c和helloworld.c所依赖的头文件和库文件(libbpf.a)的位置。</p>
<p>这里唯一要注意的就是在安装libbpf/libbpf的时候，仓库libbpf/include下面的头文件并没有被安装到/usr/local/bpf下面，但helloworld.bpf.c又依赖linux/bpf.h，这个linux/bpf.h实质上就是libbpf/include/uapi/linux/bpf.h，因此在Makefile中，我们增加的LIBBPF_UAPI_INCLUDES就是为了uapi中的bpf相关头文件的。</p>
<p>整个Makefile的构建过程与libbpf-bootstrap中的Makefile异曲同工，同样是先编译bpf字节码，然后将其生成helloworld.skel.h。最后编译依赖helloworld.skel.h的helloworld程序。注意，这里我们是静态链接的libbpf库(我们在安装时，仅安装了libbpf.a)。</p>
<p>构建出来的helloworld与基于libbpf-bootstrap构建出来的helloworld别无二致，所以其启动和运行过程这里就不赘述了。</p>
<blockquote>
<p>注：以上仅是一个最简单的helloworld级别例子，还不支持BTF和CO-RE技术。</p>
</blockquote>
<h3>五. 小结</h3>
<p>在这篇文章中，我简单/很简单的介绍了BPF技术，主要聚焦于如何用C开发一个hello world级的eBPF程序。文中给出两个方法，一种是基于libbpf-bootstrap框架，另外一种则是仅依赖libbpf的独立bpf程序工程。</p>
<p>有了以上基础后，我们就有了上手的条件，后续文章将对eBPF程序的玩法进行展开说明。并且还会说明如何用Go开发BPF的用户态程序并实现对BPF程序的加载、挂接、卸载以及和心态与用户态的数据交互等。</p>
<p>本文代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld">这里</a>下载。</p>
<h3>六. 参考资料</h3>
<ul>
<li><a href="https://book.douban.com/subject/33398015/">《Linux Observability with BPF &#8211; Advanced Programming for Performance Analysis and Networking》</a> &#8211; https://book.douban.com/subject/33398015/</li>
<li><a href="https://www.bilibili.com/video/BV1gt4y1h7QY">b站视频：eBPF工作原理浅析</a> &#8211; https://www.bilibili.com/video/BV1gt4y1h7QY</li>
<li><a href="https://nakryiko.com/posts/libbpf-bootstrap/">《Building BPF applications with libbpf-bootstrap》</a> &#8211; https://nakryiko.com/posts/libbpf-bootstrap/</li>
<li><a href="https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html">《BPF binaries: BTF, CO-RE, and the future of BPF perf tools》</a> &#8211; https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html</li>
<li><a href="https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html">《A thorough introduction to eBPF》</a> &#8211; https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go GC如何检测内存对象中是否包含指针</title>
		<link>https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj/</link>
		<comments>https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj/#comments</comments>
		<pubDate>Mon, 21 Feb 2022 14:48:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bitmap]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[Reflect]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[专栏]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[内存对齐]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[反射]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[指针]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[类型别名]]></category>
		<category><![CDATA[结构体]]></category>
		<category><![CDATA[运行时]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3422</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj 众所周知，Go是带垃圾回收(GC)的编程语言，开发者通常不需要考虑对内存的管理，降低了心智负担。Go程序运行的时候，GC在背后默默辛劳地为开发者“擦屁股”：把无法reach到的内存对象定期地释放掉以备后续重用。 GC只关心指针，只要被扫描到的内存对象中有指针，它就会“顺藤摸瓜”，把该内存对象所在的“关系网”摸个门儿清，而那些被孤立在这张“网”之外的内存对象就是要被“清扫”的对象。 那么GC在扫描时如何判断某个内存对象中是否有指针呢？这篇文章我们就来说说这事儿！ 内存对象中有指针与无指针的差别 在Gopher Academy Blog 2018年发表的一篇文章《Avoiding high GC overhead with large heaps》中作者曾用两个例子来对比了内存对象中有指针与无指针时GC的行为差别。我们摘录一下其中的这两个例子，第一个例子如下： // demo1.go func main() { a := make([]*int, 1e9) for i := 0; i &#60; 10; i++ { start := time.Now() runtime.GC() fmt.Printf("GC took %s\n", time.Since(start)) } runtime.KeepAlive(a) } 程序中调用runtime.KeepAlive函数用于保证在该函数调用点之前切片a不会被GC释放掉。 我们看到：demo1中声明了一个包含10亿个*int的切片变量a，然后调用runtime.GC函数手工触发GC过程，并度量每次GC的执行时间，我们看看这个程序的执行结果(virtualbox 虚拟机ubuntu 20.04/go 1.18beta2)： $ go run demo1.go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/how-gc-detect-pointer-in-mem-obj-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj">本文永久链接</a> &#8211; https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj</p>
<p>众所周知，Go是带垃圾回收(GC)的编程语言，开发者通常不需要考虑对内存的管理，降低了心智负担。Go程序运行的时候，GC在背后默默辛劳地为开发者<strong>“擦屁股”</strong>：把无法reach到的内存对象定期地释放掉以备后续重用。</p>
<p>GC只关心<strong>指针</strong>，只要被扫描到的内存对象中有指针，它就会“顺藤摸瓜”，把该内存对象所在的“关系网”摸个门儿清，而那些被孤立在这张“网”之外的内存对象就是要被“清扫”的对象。</p>
<p>那么GC在扫描时如何判断某个内存对象中是否有指针呢？这篇文章我们就来说说这事儿！</p>
<h3>内存对象中有指针与无指针的差别</h3>
<p>在<a href="https://blog.gopheracademy.com/">Gopher Academy Blog</a> 2018年发表的一篇文章<a href="https://blog.gopheracademy.com/advent-2018/avoid-gc-overhead-large-heaps/">《Avoiding high GC overhead with large heaps》</a>中作者曾用两个例子来对比了内存对象中有指针与无指针时GC的行为差别。我们摘录一下其中的这两个例子，第一个例子如下：</p>
<pre><code>// demo1.go
func main() {
    a := make([]*int, 1e9) 

    for i := 0; i &lt; 10; i++ {
        start := time.Now()
        runtime.GC()
        fmt.Printf("GC took %s\n", time.Since(start))
    }

    runtime.KeepAlive(a)
}
</code></pre>
<blockquote>
<p>程序中调用runtime.KeepAlive函数用于保证在该函数调用点之前切片a不会被GC释放掉。</p>
</blockquote>
<p>我们看到：demo1中声明了一个包含10亿个*int的切片变量a，然后调用runtime.GC函数手工触发GC过程，并度量每次GC的执行时间，我们看看这个程序的执行结果(virtualbox 虚拟机ubuntu 20.04/<a href="https://mp.weixin.qq.com/s/9iNYCpDYWWINctqwYLtDPA">go 1.18beta2</a>)：</p>
<pre><code>$ go run demo1.go
GC took 698.46522ms
GC took 325.315425ms
GC took 321.959991ms
GC took 326.775531ms
GC took 333.949713ms
GC took 332.350721ms
GC took 328.1664ms
GC took 329.905988ms
GC took 328.466344ms
GC took 330.327066ms
</code></pre>
<p>我们看到，每轮GC调用都相当耗时。我们再来看第二个例子：</p>
<pre><code>// demo2.go
func main() {
    a := make([]int, 1e9) 

    for i := 0; i &lt; 10; i++ {
        start := time.Now()
        runtime.GC()
        fmt.Printf("GC took %s\n", time.Since(start))
    }

    runtime.KeepAlive(a)
}
</code></pre>
<p>这个例子仅是将切片的元素类型由*int改为了int。我们运行一下这第二个例子：</p>
<pre><code>$ go run demo2.go
GC took 3.486008ms
GC took 1.678019ms
GC took 1.726516ms
GC took 1.13208ms
GC took 1.900233ms
GC took 1.561631ms
GC took 1.899654ms
GC took 7.302686ms
GC took 131.371494ms
GC took 1.138688ms
</code></pre>
<p>在我们的实验环境中demo2中每轮GC的性能是demo1的300多倍！两个demo源码唯一的不同就是切片中的元素类型，demo1中的切片元素类型为int型指针。GC每次触发后都会全量扫描切片中存储的这10亿个指针，这就是demo1 GC函数执行时间很长的原因。而demo2中的切片元素类型为int，从demo2的运行结果来看，GC根本没有搭理demo2中的a，这也是demo2 GC函数执行时间较短的原因(我测试了一下：在我的环境中，即便不声明切片a，只是执行10次runtime.GC函数，该函数的平均执行时间也在1ms左右)。</p>
<p>通过以上GC行为差异，我们知道GC可以通过切片a的类型知晓其元素是否包含指针，进而决定是否对其进行进一步扫描。下面我们就来看看GC是如何检测到某一个内存对象中包含指针的。</p>
<h3>运行时类型信息（rtype)</h3>
<p>Go是静态语言，每个变量都有自己的归属的<strong>类型</strong>，当变量被在堆上分配时，堆上的内存对象也就有了自己归属的类型。Go编译器在编译阶段就为Go应用中的每种类型建立了对应的类型信息，这些信息体现在runtime.&#95;rtype结构体中，<a href="https://mp.weixin.qq.com/s/KgUZGVF08yEO2oARryOLcg">Go reflect包</a>的rtype结构体等价于runtime.&#95;rtype：</p>
<pre><code>// $GOROOT/src/reflect/type.go

// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
    size       uintptr
    ptrdata    uintptr // number of bytes in the type that can contain pointers
    hash       uint32  // hash of type; avoids computation in hash tables
    tflag      tflag   // extra type information flags
    align      uint8   // alignment of variable with this type
    fieldAlign uint8   // alignment of struct field with this type
    kind       uint8   // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -&gt; ==?
    equal     func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata    *byte   // garbage collection data
    str       nameOff // string form
    ptrToThis typeOff // type for pointer to this type, may be zero
}
</code></pre>
<p>在这个结构体类型中的gcdata字段是为GC服务的，我们看看它究竟是什么！怎么看呢？由于reflect.rtype类型是非导出类型，我们需要对本地的Go语言源码做一些hack，我在reflect包的type.go文件中rtype结构体的定义之前添加一行代码：</p>
<pre><code>type Rtype = rtype
</code></pre>
<p>我们用<a href="https://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go 1.9版本</a>引入的类型别名(type alias)机制将rtype导出，这样我们就可以在标准库外面使用reflect.Rtype了。</p>
<p>有童鞋可能会问：<strong>改了本地Go标准库源码后，Go编译器就会使用最新源码来编译我们的Go示例程序么</strong>？Go 1.18之前的版本都不会！大家可以自行试验一下，也可以通过<a href="https://book.douban.com/subject/35720728/">《Go语言精进之路vol1》</a>第16条“理解包导入”一章了解有关于Go编译器构建过程的详尽描述。</p>
<p>下面我们来获取一个切片的类型对应的rtype，看看其中的gcdata究竟是啥？</p>
<pre><code>// demo4.go

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

type tflag uint8
type nameOff int32 // offset to a name
type typeOff int32 // offset to an *rtype

type rtype struct {
    size       uintptr
    ptrdata    uintptr // number of bytes in the type that can contain pointers
    hash       uint32  // hash of type; avoids computation in hash tables
    tflag      tflag   // extra type information flags
    align      uint8   // alignment of variable with this type
    fieldAlign uint8   // alignment of struct field with this type
    kind       uint8   // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -&gt; ==?
    equal     func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata    *byte   // garbage collection data
    str       nameOff // string form
    ptrToThis typeOff // type for pointer to this type, may be zero
}

func bar() []*int {
    t := make([]*int, 8 )
    return t
}

func main() {
    t := bar()
    v := reflect.TypeOf(t)

    rtyp, ok := v.(*reflect.Rtype)
    if !ok {
        println("error")
        return
    }

    r := (*rtype)(unsafe.Pointer(rtyp))
    fmt.Printf("%#v\n", *r)
    fmt.Printf("*gcdata = %d\n", *(r.gcdata))
}
</code></pre>
<p>bar函数返回一个堆上分配的切片实例t，我们通过reflect.TypeOf获取t的类型信息，通过类型断言我们得到该类型的rtype信息：rtyp，不过gcdata也是非导出字段并且是一个指针，我们要想对其解引用，我们这里又在本地定义了一个本地rtype类型，用于输出gcdata指向的内存的值。</p>
<p>运行这个示例：</p>
<pre><code>$go run demo4.go
main.rtype{size:0x18, ptrdata:0x8, hash:0xaad95941, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x17, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(nil), gcdata:(*uint8)(0x10c1b58), str:3526, ptrToThis:0}
*gcdata = 1
</code></pre>
<p>我们看到gcdata指向的一个字节的内存的值为1(二进制为0b00000001)。好了，不卖关子了！gcdata所指的这个字节每一bit上的值代表一个8字节的内存块是否包含指针。这样的一个字节就可以标识在一个64字节的内存块中，每个8字节的内存单元是否包含指针。如果类型长度超过64字节，那么用于表示指针地图的gcdata指向的有效字节个数也不止1个字节。</p>
<p>读过我的<a href="http://gk.link/a/10AVZ">“Go语言第一课”专栏</a>的童鞋都知道，切片类型在runtime层表示为下面结构：</p>
<pre><code>// $GOROOT/src/runtime/slice.go

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
</code></pre>
<p>这里切片类型结构内存对齐后的size为24，小于64个字节，因此Go用一个字节就可以表示切片类型的指针地图。而&#42;gcdata=1，即最低位上的bit为1，表示切片类型的第一个8字节中存储着一个指针。配合下面的示意图理解起来更easy一些：</p>
<p><img src="https://tonybai.com/wp-content/uploads/how-gc-detect-pointer-in-mem-obj-2.png" alt="" /></p>
<p>我们也可以进一步查看切片中各元素是否包含指针，由于该切片的元素就是指针类型，所以每个元素的rtype.gcdata指向的bitmap的值都应该是1，我们来验证一下：</p>
<pre><code>//demo5.go
... ...
func main() {
    t := bar()
    v := reflect.ValueOf(t)

    for i := 0; i &lt; len(t); i++ {
        v1 := v.Index(i)
        vtyp := v1.Type()

        rtyp, ok := vtyp.(*reflect.Rtype)
        if !ok {
            println("error")
            return
        }

        r := (*rtype)(unsafe.Pointer(rtyp))
        fmt.Printf("%#v\n", *r)
        fmt.Printf("*gcdata = %d\n", *(r.gcdata))
    }
}
</code></pre>
<p>这个例子输出了每个切片元素的bitmap，结果如下：</p>
<pre><code>$go run demo5.go

gomain.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0}
*gcdata = 1
main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0}
*gcdata = 1
main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0}
*gcdata = 1
main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0}
*gcdata = 1
main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0}
*gcdata = 1
main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0}
*gcdata = 1
main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0}
*gcdata = 1
main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0}
*gcdata = 1

</code></pre>
<p>输出结果与预期相符。</p>
<p>我们再来看一个例子，一个用单字节bitmap无法表示的类型：</p>
<pre><code>// demo6.go
... ...
type S struct {  // 起始地址
    a  uint8     // 0
    b  uintptr   // 8
    p1 *uint8    // 16
    c  [3]uint64 // 24
    d  uint32    // 48
    p2 *uint64   // 56
    p3 *uint8    // 64
    e  uint32    // 72
    p4 *uint64   // 80
}

func foo() *S {
    t := new(S)
    return t
}

func main() {
    t := foo()
    println(unsafe.Sizeof(*t)) // 88
    typ := reflect.TypeOf(t)
    rtyp, ok := typ.Elem().(*reflect.Rtype)

    if !ok {
        println("error")
        return
    }
    fmt.Printf("%#v\n", *rtyp)

    r := (*rtype)(unsafe.Pointer(rtyp))
    fmt.Printf("%#v\n", *r)
    fmt.Printf("%d\n", *(r.gcdata))
    gcdata1 := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(r.gcdata)) + 1))
    fmt.Printf("%d\n", *gcdata1)
}
</code></pre>
<p>在这个例子中，我们定义了一个很大的结构体类型S，其size为88，用一个字节无法表示出其bitmap，于是Go使用了两个字节，我们输出这两个字节的bitmap：</p>
<pre><code>$go run demo6.go
88
reflect.rtype{size:0x58, ptrdata:0x58, hash:0xcdb468b2, tflag:0x7, align:0x8, fieldAlign:0x8, kind:0x19, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x108aea0), gcdata:(*uint8)(0x10c135b), str:3593, ptrToThis:19168}
main.rtype{size:0x58, ptrdata:0x58, hash:0xcdb468b2, tflag:0x7, align:0x8, fieldAlign:0x8, kind:0x19, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x108aea0), gcdata:(*uint8)(0x10c135b), str:3593, ptrToThis:19168}
132
5
</code></pre>
<p>我们将结果转换成一幅示意图，如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/how-gc-detect-pointer-in-mem-obj-3.png" alt="" /></p>
<p>理解上面这个结构体size以及各字段起始地址的前提是理解内存对齐，这个大家可以在我的博客内搜索以前撰写的有关内存对齐的相关内容，当然也可以参考我在<a href="http://gk.link/a/10AVZ">专栏第17讲讲解结构体类型</a>时对Go内存对齐的系统讲解。</p>
<hr />
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
