<?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; Clang</title>
	<atom:link href="http://tonybai.com/tag/clang/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 08 Jun 2026 23:32:23 +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>Go 2026 路线图曝光：SIMD、泛型方法与无 C 工具链 CGO —— 性能与表达力的双重飞跃？</title>
		<link>https://tonybai.com/2025/11/28/go-2026-roadmap-revealed/</link>
		<comments>https://tonybai.com/2025/11/28/go-2026-roadmap-revealed/#comments</comments>
		<pubDate>Fri, 28 Nov 2025 00:45:22 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[arena]]></category>
		<category><![CDATA[arm64]]></category>
		<category><![CDATA[async/await]]></category>
		<category><![CDATA[AVX512]]></category>
		<category><![CDATA[CacheLineContention]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[genericmethod]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.26]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[highlevelAPI]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[L1/L2Cache]]></category>
		<category><![CDATA[LastLevelCache]]></category>
		<category><![CDATA[LLC]]></category>
		<category><![CDATA[memoryregions]]></category>
		<category><![CDATA[NEON]]></category>
		<category><![CDATA[NUMA]]></category>
		<category><![CDATA[Promise]]></category>
		<category><![CDATA[purego]]></category>
		<category><![CDATA[runtime.free]]></category>
		<category><![CDATA[runtime.freegc]]></category>
		<category><![CDATA[scalablevectors]]></category>
		<category><![CDATA[Schedulingaffinity]]></category>
		<category><![CDATA[Shardedvalues]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[Specializedmalloc]]></category>
		<category><![CDATA[strings.Builder]]></category>
		<category><![CDATA[STW]]></category>
		<category><![CDATA[SVE]]></category>
		<category><![CDATA[sync.Sharded]]></category>
		<category><![CDATA[Tensor]]></category>
		<category><![CDATA[uniontype]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[Wasmstackswitching]]></category>
		<category><![CDATA[WriteBarrier]]></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[无C工具链CGO]]></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=5450</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/11/28/go-2026-roadmap-revealed 大家好，我是Tony Bai。 在最近的一期 Go 编译器与运行时团队会议纪要中，我们惊喜地发现了一份关于 2026 年的规划 (2026 planning，如下图)。这份规划虽然简短，但其包含的信息量却足以让任何一位关注 Go 语言未来的开发者心跳加速。 从榨干硬件潜能的 SIMD 和运行时手动内存释放(runtime.free)，到呼声极高的泛型方法(generic method)与联合类型(union type)，再到彻底解决交叉编译痛点的无 C 工具链 CGO，Go 团队正密谋着一场关于性能、表达力与工程体验的全方位变革。 本文将结合最新的设计文档、CL (Change List) 记录和社区核心 Issue，和大家一起解析一下这份 Go 2026 路线图背后的技术细节与战略意图。 性能的极限突围 —— 榨干硬件的每一滴油水 一直以来，Go 在性能上的策略都是“足够好”。但在 2026 规划中，我们看到了 Go 团队向“极致性能”发起的冲锋，目标直指 AI、科学计算和高频交易等对延迟极度敏感的领域。 SIMD：从“汇编黑魔法”到“原生公民” 关键词：SIMD (ARM64, scalable vectors &#38; high-level API) 解读： 现状：目前在 Go 中使用 SIMD（单指令多数据）主要依赖手写汇编，不仅难以维护，而且无法被编译器内联优化，甚至会阻碍异步抢占。 变革：规划明确提出了 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-2026-roadmap-revealed-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/11/28/go-2026-roadmap-revealed">本文永久链接</a> &#8211; https://tonybai.com/2025/11/28/go-2026-roadmap-revealed</p>
<p>大家好，我是Tony Bai。</p>
<p>在最近的一期 <a href="https://github.com/golang/go/issues/43930#issuecomment-3576250284">Go 编译器与运行时团队会议纪要</a>中，我们惊喜地发现了一份关于 <strong>2026 年的规划 (2026 planning，如下图)</strong>。这份规划虽然简短，但其包含的信息量却足以让任何一位关注 Go 语言未来的开发者心跳加速。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-2026-roadmap-revealed-2.png" alt="" /></p>
<p>从榨干硬件潜能的 <strong><a href="https://tonybai.com/2025/08/22/go-simd-package-preview">SIMD</a></strong> 和<strong>运行时手动内存释放(<a href="https://tonybai.com/2025/09/18/go-runtime-free-proposal">runtime.free</a>)</strong>，到呼声极高的<strong>泛型方法(generic method)</strong>与<strong>联合类型(union type)</strong>，再到彻底解决交叉编译痛点的<strong>无 C 工具链 CGO</strong>，Go 团队正密谋着一场关于<strong>性能、表达力与工程体验</strong>的全方位变革。</p>
<p>本文将结合最新的设计文档、CL (Change List) 记录和社区核心 Issue，和大家一起解析一下这份 Go 2026 路线图背后的技术细节与战略意图。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/google-adk-in-action-qr.png" alt="" /></p>
<hr />
<h2>性能的极限突围 —— 榨干硬件的每一滴油水</h2>
<p>一直以来，Go 在性能上的策略都是“足够好”。但在 2026 规划中，我们看到了 Go 团队向“极致性能”发起的冲锋，目标直指 AI、科学计算和高频交易等对延迟极度敏感的领域。</p>
<h3>SIMD：从“汇编黑魔法”到“原生公民”</h3>
<ul>
<li><strong>关键词</strong>：SIMD (ARM64, scalable vectors &amp; high-level API)</li>
<li><strong>解读</strong>：
<ul>
<li><strong>现状</strong>：目前在 Go 中使用 SIMD（单指令多数据）主要依赖手写汇编，不仅难以维护，而且无法被编译器内联优化，甚至会阻碍异步抢占。</li>
<li><strong>变革</strong>：规划明确提出了 <strong>“high-level API”</strong>。这意味着 Go 将提供一套<strong>原生的、类型安全的 SIMD 库</strong>。开发者可以用纯 Go 代码编写向量化算法，由编译器自动映射到底层的 AVX-512 (x86) 或 NEON/SVE (ARM) 指令。</li>
<li><strong>Scalable Vectors</strong>：特别提到的“可伸缩向量”，直指 ARM64 的 <strong>SVE (Scalable Vector Extension)</strong> 技术。这将允许同一份 Go 二进制代码，在不同向量长度（128位到2048位）的硬件上自动适配，实现性能的“线性扩展”，这对于 AI 推理场景至关重要。</li>
<li><strong>进展</strong>：在2026年初发布的Go 1.26中，Cherry Mui 提交的关于 Architecture-specific SIMD intrinsics 的提案将以GO实验特性落地，这意味着Go开发者将拥有原生的simd包实现，目前这一工作已在紧锣密鼓地进行中。</li>
</ul>
</li>
</ul>
<h3>runtime.free：打破 GC 的“金科玉律”</h3>
<ul>
<li><strong>关键词</strong>：runtime.free, Specialized malloc</li>
<li><strong>解读</strong>：这是一个颠覆性的变化。Go 一直以自动 GC 著称，但在极致性能场景下，GC 的 CPU 和 STW 开销仍是瓶颈。
<ul>
<li><strong>显式释放</strong>：根据设计文档 《<a href="https://go.dev/design/74299-runtime-freegc">Directly freeing user memory to reduce GC work</a> 》和相关 CL (如 CL 673695)，runtime.freegc 允许将不再使用的堆内存<strong>立即归还</strong>给分配器，供后续重用，而<strong>完全绕过 GC 扫描</strong>。</li>
<li><strong>编译器辅助</strong>：这并非让用户手动管理内存（那样太不安全）。Go 的愿景是让<strong>编译器</strong>通过逃逸分析和生命周期分析，<strong>自动插入</strong> free 调用。例如，在 strings.Builder 的扩容过程中，旧的 buffer 可以被立即释放。</li>
<li><strong>实测数据</strong>：在早期的原型测试中，优化后的 strings.Builder 性能提升了 <strong>2 倍</strong>！配合针对无指针对象 (noscan) 优化的专用分配器 (Specialized malloc)，Go 的临时对象分配性能将逼近栈分配。</li>
</ul>
</li>
</ul>
<hr />
<h2>可伸缩性的新高度 —— 拥抱超多核时代</h2>
<p>随着 CPU 核心数向 128 核甚至更高迈进，传统的并发模式开始遇到“扩展性墙”。Go 2026 规划给出了一套组合拳。</p>
<h3>分片值 (Sharded Values)</h3>
<ul>
<li><strong>关键词</strong>：Sharded values</li>
<li><strong>痛点</strong>：在高并发场景下，对同一个全局计数器或 sync.Pool 的访问，会导致严重的<strong>缓存行争用 (Cache Line Contention)</strong>，让多核优势荡然无存。</li>
<li><strong>解决方案</strong>：Go团队提出一个名为<a href="https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal/">sync.Sharded</a> 的提案(详见 Issue #18802)，sync.Sharded 旨在提供一种<strong>“每 P (Processor) 本地化”</strong>的数据结构。
<ul>
<li><strong>无锁读写</strong>：每个 P 只操作自己本地的分片，完全无锁，零竞争。</li>
<li><strong>按需聚合</strong>：只在需要读取总值时，才遍历所有分片进行聚合。</li>
<li>这比现有的 sync.Map 或 atomic 操作在高核数机器上将有数量级的性能提升。</li>
</ul>
</li>
</ul>
<h3>调度亲和性 (Scheduling Affinity)</h3>
<ul>
<li><strong>关键词</strong>：Scheduling affinity</li>
<li><strong>解读</strong>：Go 调度器的“工作窃取”机制虽然平衡了负载，但也导致 Goroutine 经常在不同 CPU 核心间“漂移”，破坏了 L1/L2 缓存的热度。
<ul>
<li><strong>新机制</strong>：在 Issue #65694中，Go团队 计划引入一种机制，允许将一组相关的 Goroutine <strong>“绑定”</strong> 或 <strong>“倾向”</strong> 于特定的 P 或 NUMA 节点。这对于数据库、高频交易系统等缓存敏感型应用是巨大的利好，能显著减少 <strong>LLC (Last Level Cache) Miss</strong>。</li>
</ul>
</li>
</ul>
<h3>内存区域 (Memory Regions)</h3>
<ul>
<li><strong>关键词</strong>：Memory regions</li>
<li><strong>解读</strong>：在 <strong>Arena</strong>试验失败后，Michael Knyszek发起了一个名为Memory regions方案的讨论（具体见 <a href="https://github.com/golang/go/discussions/70257">Discussion #70257</a>)，其核心思想是，通过一个 region.Do(func() { &#8230; }) 调用，将一个函数作用域内的所有内存分配<strong>隐式地</strong>绑定到一个临时的、与 goroutine 绑定的区域中。这个优雅设计的背后，是<strong>极其复杂的实现</strong>。它需要在开启区域的 goroutine 中启用一个特殊的、低开销的<strong>写屏障（write barrier）</strong>来动态追踪内存的逃逸。虽然理论上可行，但其实现复杂度和潜在的性能开销，使其成为一个长期且充满不确定性的研究课题。在2026年，Go团队要在这个方案上有所突破，依旧任重道远。</li>
</ul>
<hr />
<h2>语言表达力的觉醒 —— 填补泛型后的最后拼图</h2>
<p>在泛型落地后，Go 社区对语言特性的渴望并未止步。规划中提到的几个特性，将进一步提升 Go 的表达力。</p>
<h3>泛型方法 (Generic Methods)</h3>
<ul>
<li><strong>关键词</strong>：generic methods</li>
<li><strong>背景</strong>：这是泛型引入后最大的遗憾之一。目前 Go 不支持在接口方法或结构体方法中定义额外的类型参数。</li>
<li><strong>展望</strong>：参考 <a href="https://github.com/golang/go/issues/49085">Issue #49085</a>，尽管实现难度极大（涉及运行时字典传递或单态化膨胀），但核心团队将其列入规划，表明他们正在寻找突破口。一旦实现，像 Stream.Map[T, U](func(T) U) 这样流畅的链式调用将成为可能。</li>
</ul>
<h3>联合类型 (Union Types)</h3>
<ul>
<li><strong>关键词</strong>：union type</li>
<li><strong>解读</strong>：参考 <a href="https://github.com/golang/go/issues/19412">Issue #19412</a>，这不仅仅是泛型约束中的 A | B。真正的联合类型（类似 Rust 的 Enum 或 TypeScript 的 Union）可以让 Go 拥有更强大的模式匹配能力。配合可能的 match 语法，它将彻底改变 Go 的错误处理和状态机编写方式，使其更安全、更简洁。</li>
</ul>
<h3>Tensor (?) —— AI 时代的入场券</h3>
<ul>
<li><strong>关键词</strong>：maybe tensor (?)</li>
<li><strong>解读</strong>：这个带问号的项充满了想象力。它暗示 Go 团队可能正在严肃考虑为 <strong>AI/ML 工作负载</strong>提供原生的多维数组支持。如果 Go 能在语言层面原生支持高效的 Tensor 操作和自动微分，它将有资格挑战 Python 在 AI 基础设施领域的统治地位。当然这一切还只是猜测。</li>
</ul>
<hr />
<h2>工具链革命 —— 无痛 CGO</h2>
<h3>无 C 工具链的 CGO (CGO without C toolchain)</h3>
<ul>
<li><strong>关键词</strong>：cgo without C toolchain</li>
<li><strong>痛点</strong>：目前启用 CGO 就意味着必须安装 GCC/Clang，且失去了跨平台交叉编译的便利性（CGO_ENABLED=0 是多少 Gopher 的无奈之选）。</li>
<li><strong>解决方案</strong>：Go 团队的目标是实现<strong>“纯 Go 的 C 交互”</strong>。这可能通过两种路径实现：
<ul>
<li><strong>运行时加载</strong>：类似 purego，在运行时动态加载共享库并调用，无需编译期链接。</li>
<li><strong>内置微型链接器</strong>：Go 编译器直接解析 C 头文件并生成调用代码。</li>
<li>无论上述哪种方式，或是其他方式，一旦实现，<strong>“Write once, compile anywhere”</strong> 的承诺将在 CGO 场景下也得以兑现。</li>
</ul>
</li>
</ul>
<h3>Wasm 栈切换</h3>
<ul>
<li><strong>关键词</strong>：Wasm stack switching</li>
<li><strong>解读</strong>：这是为了更好地支持 <strong>Go 在浏览器中的异步模型</strong>。通过栈切换（Stack Switching），Go 可以更高效地挂起和恢复 Wasm 的执行，从而与 JavaScript 的 Promise 和 async/await 机制无缝互操作，显著减小 Wasm 产物的体积并提升性能。</li>
</ul>
<hr />
<h2>小结：性能与表达力的双重飞跃</h2>
<p>看完这份 2026 路线图，我们不禁感叹：Go 语言正在经历它的<strong>“成人礼”</strong>。</p>
<ul>
<li><strong>在性能上</strong>，它不再满足于“够用”，而是通过 SIMD、手动内存管理和亲和性调度，向 C/C++ 统治的“极致性能领域”发起冲击。</li>
<li><strong>在表达力上</strong>，它正在补齐泛型后的最后短板，通过泛型方法和联合类型，让代码更优雅、更安全。</li>
<li><strong>在体验上</strong>，它致力于抹平 CGO 和交叉编译的最后一道坎。</li>
</ul>
<p>这是一个野心勃勃的计划。如果这些特性在 2026 年真地能如期落地，Go 将不再仅仅是“云原生的语言”，它将成为一个<strong>全能、极致、且依旧简单</strong>的通用计算平台。</p>
<h2>参考资料</h2>
<ul>
<li>Go compiler and runtime meeting notes &#8211; https://github.com/golang/go/issues/43930#issuecomment-3576250284</li>
<li>Directly freeing user memory to reduce GC work &#8211; https://go.dev/design/74299-runtime-freegc</li>
<li>runtime, cmd/compile: add runtime.freegc and runtime.freegcTracked to reduce GC work &#8211; https://github.com/golang/go/issues/74299</li>
<li>715761: runtime: support runtime.freegc in size-specialized mallocs for noscan objects &#8211; https://go-review.googlesource.com/c/go/+/715761</li>
<li>simd: architecture-specific SIMD intrinsics under a GOEXPERIMENT &#8211; https://github.com/golang/go/issues/73787</li>
<li>proposal: sync: support for sharded values &#8211; https://github.com/golang/go/issues/18802</li>
<li>runtime: stronger affinity between G ↔ P ↔ M ↔ CPU?  &#8211; https://github.com/golang/go/issues/65694</li>
<li>https://github.com/golang/go/discussions/70257 &#8211; https://github.com/golang/go/discussions/70257</li>
<li>Region-based memory management &#8211; https://en.wikipedia.org/wiki/Region-based_memory_management</li>
<li>proposal: spec: add sum types / discriminated unions &#8211; https://github.com/golang/go/issues/19412</li>
<li>proposal: spec: allow type parameters in methods &#8211; https://github.com/golang/go/issues/49085</li>
</ul>
<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; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/11/28/go-2026-roadmap-revealed/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地</title>
		<link>https://tonybai.com/2025/05/08/go-dwarf5/</link>
		<comments>https://tonybai.com/2025/05/08/go-dwarf5/#comments</comments>
		<pubDate>Thu, 08 May 2025 00:05:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BSS]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[delve]]></category>
		<category><![CDATA[DWARF]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GDB]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[issue]]></category>
		<category><![CDATA[link]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[objump]]></category>
		<category><![CDATA[relocation]]></category>
		<category><![CDATA[section]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[代码段]]></category>
		<category><![CDATA[数据段]]></category>
		<category><![CDATA[编译]]></category>
		<category><![CDATA[调试]]></category>
		<category><![CDATA[重定位]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4664</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/08/go-dwarf5 大家好，我是Tony Bai。 对于许多Go开发者来说，调试信息的格式可能是一个相对底层的细节。然而，这个细节却对编译速度、最终可执行文件的大小以及调试体验有着深远的影响。经过长达六年的讨论、等待生态成熟和密集的开发工作，Go 语言工具链终于在主干分支（预计将包含在 Go 1.25 中）默认启用了 DWARF version 5 作为其调试信息的标准格式（Issue #26379）。这一看似“幕后”的变更，实则为 Go 开发者带来了切实的链接速度提升和可执行文件体积的优化。在这篇文章中，我们就来对DWARF5落地Go这件事儿做一个简单的解读。 为何需要升级到 DWARF 5？旧格式的痛点 DWARF (Debugging With Attributed Record Formats) 是类 Unix 系统上广泛使用的调试信息标准。Go 之前使用的 DWARF 版本（主要是 v2 和 v4）虽然成熟，但在现代软件开发实践中暴露出一些不足： 大量的重定位 (Relocations): 旧版 DWARF 格式通常包含大量需要链接器处理的地址重定位信息。根据 2018 年的初步分析（by aclements），在当时的 go 二进制文件中，高达 49% 的重定位条目都源于 DWARF 数据。这显著增加了链接器的工作负担，拖慢了构建速度，尤其是对于大型项目。 冗长的位置和范围列表 (Location/Range Lists): 用于描述变量生命周期和代码范围的 .debug_loc 和 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-dwarf5-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/08/go-dwarf5">本文永久链接</a> &#8211; https://tonybai.com/2025/05/08/go-dwarf5</p>
<p>大家好，我是Tony Bai。</p>
<p>对于许多Go开发者来说，调试信息的格式可能是一个相对底层的细节。然而，这个细节却对编译速度、最终可执行文件的大小以及调试体验有着深远的影响。经过长达六年的讨论、等待生态成熟和密集的开发工作，Go 语言工具链终于在主干分支（预计将包含在 Go 1.25 中）默认启用了 <strong>DWARF version 5</strong> 作为其调试信息的标准格式（<a href="https://github.com/golang/go/issues/26379">Issue #26379</a>）。这一看似“幕后”的变更，实则为 Go 开发者带来了切实的<strong>链接速度提升</strong>和<strong>可执行文件体积的优化</strong>。在这篇文章中，我们就来对DWARF5落地Go这件事儿做一个简单的解读。</p>
<h2>为何需要升级到 DWARF 5？旧格式的痛点</h2>
<p>DWARF (Debugging With Attributed Record Formats) 是类 Unix 系统上广泛使用的调试信息标准。Go 之前使用的 DWARF 版本（主要是 v2 和 v4）虽然成熟，但在现代软件开发实践中暴露出一些不足：</p>
<ol>
<li><strong>大量的重定位 (Relocations):</strong> 旧版 DWARF 格式通常包含大量需要链接器处理的地址重定位信息。根据 2018 年的初步分析（by aclements），在当时的 go 二进制文件中，高达 <strong>49%</strong> 的重定位条目都源于 DWARF 数据。这显著增加了链接器的工作负担，拖慢了构建速度，尤其是对于大型项目。</li>
<li><strong>冗长的位置和范围列表 (Location/Range Lists):</strong> 用于描述变量生命周期和代码范围的 .debug_loc 和 .debug_ranges 等section的数据在旧格式下可能非常庞大。即便经过压缩，它们也能占到可执行文件大小的相当一部分（例如，当时 go 二进制的 12MiB 中占 6%）。</li>
<li><strong>缺乏官方 Go 语言代码:</strong> 虽然不影响功能，但 DWARF 5 正式为 Go 语言分配了官方的语言代码 (DW_LANG_Go)。</li>
</ol>
<p>DWARF 5 标准针对这些痛点进行了改进，其关键优势在于：</p>
<ul>
<li><strong>位置无关表示 (Position-Independent Representations):</strong> DWARF 5 引入了如 .debug_addr, .debug_rnglists, .debug_loclists 等新 Section 格式，它们的设计能大幅减少甚至消除对重定位的需求，从而减轻链接器负担。</li>
<li><strong>更紧凑的列表格式:</strong> 新的列表格式 (.debug_rnglists, .debug_loclists) 比旧的 (.debug_ranges, .debug_loc) 更为紧凑，有助于减小调试信息的大小。</li>
</ul>
<h2>从提案到落地：漫长的等待与集中的开发</h2>
<p>尽管 DWARF 5 的优势显而易见，但 Go 社区在 2018 年提出该想法时（by aclements），整个开发工具生态（如调试器 LLDB、macOS 的链接器和 dsymutil 工具等）对其支持尚不完善。因此，该提案被暂时搁置，等待时机成熟。</p>
<p>近年来，随着主流工具链（GCC 7.1+, GDB 8.0+, Clang 14+）纷纷将 DWARF 5 作为默认选项，生态环境逐渐成熟。Go 团队成员 <strong>Than McIntosh</strong> 承担了将 Go 工具链迁移到 DWARF 5 的主要开发工作。这涉及对编译器 (cmd/compile) 和链接器 (cmd/link) 的大量修改，引入了新的 GOEXPERIMENT=dwarf5 实验开关进行测试，并提交了一系列相关的变更集 (CLs)，包括：</p>
<ul>
<li>添加 DWARF 5 相关常量和 relocation 类型定义。</li>
<li>实现对 .debug_addr, .debug_rnglists, .debug_loclists section 的生成和支持。</li>
<li>更新 DWARF 5 的行号表 (line table) 支持。</li>
<li>适配 x/debug/dwtest 和 internal/gocore 等内部库。</li>
<li>协调 Delve 调试器对 DWARF 5 的支持。</li>
</ul>
<h2>成果显著：链接速度提升与体积优化</h2>
<p>经过广泛的测试和 compilebench 基准评估，启用 DWARF 5 带来了可观的性能收益：</p>
<ul>
<li><strong>链接速度显著提升:</strong> ExternalLinkCompiler 基准测试显示链接时间减少了 <strong>约 14%</strong>。这主要得益于 DWARF 5 减少了链接器需要处理的重定位数量。</li>
<li><strong>可执行文件体积减小:</strong> HelloSize 和 CmdGoSize 基准显示最终可执行文件大小平均减小了 <strong>约 3%</strong>。这归功于 DWARF 5 更紧凑的列表格式。</li>
<li><strong>编译时间略有改善:</strong> 整体编译时间 (geomean) 也有约 <strong>1.9%</strong> 的小幅提升。</li>
</ul>
<p>虽然对代码段 (.text)、数据段 (.data)、BSS 段的大小几乎没有影响，但链接耗时和最终文件大小的优化对于大型项目和 CI/CD 流程来说意义重大。</p>
<h2>挑战与妥协：并非所有平台一步到位</h2>
<p>在推进 DWARF 5 的过程中，也遇到了一些平台兼容性问题，导致 Go 团队采取了审慎的策略：</p>
<ol>
<li><strong>macOS dsymutil 限制:</strong> 旧版本的 macOS Xcode 自带的 dsymutil 工具（用于处理和分离 DWARF 信息）不支持 DWARF 5 新引入的 .debug_rnglists 和 .debug_loclists section。这会导致在使用<strong>外部链接 (external linking)</strong> 构建 CGO 程序时，Go 代码的调试信息丢失。虽然 LLVM 17 (对应 Xcode 16+) 已修复此问题，但考虑到仍有大量开发者使用旧版 Xcode（官方支持最低到 Xcode 14），Go 团队决定<strong>在 macOS 和 iOS 平台上进行外部链接时，暂时回退到 DWARF 4</strong>。未来当最低支持的 Xcode 版本兼容 DWARF 5 后，有望统一。</li>
<li><strong>AIX 平台限制:</strong> AIX 使用的 XCOFF 文件格式本身不支持 DWARF 5 所需的 Section 类型。因此，<strong>AIX 平台将继续使用 DWARF 4</strong> (GOEXPERIMENT=nodwarf5 默认开启)。</li>
<li><strong>GNU objdump 兼容性:</strong> objdump 工具在解析 Go 生成的 monolithic .debug_addr section 时会打印警告（因为它期望每个编译单元都有一个 header，而 Go 链接器只生成一个）。这被认为是一个 objdump 的小问题（已提议向上游提交修复），不影响实际功能，因此 Go 团队决定继续采用 monolithic 方式。</li>
</ol>
<h2>对开发者的影响与总结</h2>
<p>对于大多数 Go 开发者而言，这项变更将在 Go 1.25 及以后版本中<strong>默认生效</strong>（除了上述 macOS 外部链接和 AIX 平台）。你将自动享受到<strong>更快的链接速度</strong>和<strong>略小的可执行文件</strong>。</p>
<ul>
<li><strong>调试体验:</strong> 虽然 DWARF 5 本身设计更优，但对日常使用 Delve 等调试器的直接体验影响可能不明显，主要好处体现在工具链效率和文件大小上。</li>
<li><strong>注意事项:</strong> 如果你在 macOS 上进行 CGO 开发并使用外部链接，或者面向 AIX 平台，需要了解调试信息格式仍将是 DWARF 4。</li>
</ul>
<p>总而言之，Go 工具链采纳 DWARF 5 是一个重要的里程碑。它不仅解决了旧格式的一些固有问题，提升了构建效率，也是 Go 语言紧跟底层技术标准发展、持续优化开发者体验的重要一步。这项历时多年的工作最终落地，体现了 Go 社区在推动技术演进方面的耐心和决心。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://github.com/golang/go/issues/26379">cmd/compile: consider using DWARF 5</a> &#8211; https://github.com/golang/go/issues/26379</li>
<li><a href="https://dwarfstd.org/dwarf5std.html">DWARF Version 5</a> &#8211; https://dwarfstd.org/dwarf5std.html</li>
</ul>
<hr />
<p><strong>聊聊你的编译构建体验</strong></p>
<p>Go 1.25 工具链的这项 DWARF 5 升级，虽然“藏”在幕后，但实实在在地为我们带来了链接速度和文件大小的优化。<strong>你在日常的 Go 项目开发中，是否也曾被编译链接速度或可执行文件体积困扰过？</strong> 你对 Go 工具链在这些方面的持续改进有什么期待或建议吗？或者，你是否了解其他能有效优化构建体验的技巧？</p>
<p><strong>欢迎在评论区分享你的经验、痛点与期待！</strong> 让我们共同见证 Go 工具链的进步。</p>
<p><strong>想深入探索Go的编译、链接与底层奥秘？</strong></p>
<p>如果你对 Go 工具链如何工作、编译优化、链接器原理，乃至像 DWARF 这样的底层细节充满兴趣，希望系统性地构建对 Go 语言“从源码到可执行文件”全链路的深刻理解&#8230;</p>
<p>那么，我的 <strong>「Go &amp; AI 精进营」知识星球</strong> 正是为你打造的深度学习平台！这里有【Go原理课】带你解密语言核心机制，【Go进阶课】助你掌握高级技巧，更有【Go避坑课】让你少走弯路。我会亲自为你解答各种疑难问题，你还可以与众多热爱钻研的Gopher们一同交流，探索Go的更多可能，包括它在AI等前沿领域的应用。</p>
<p><strong>扫码加入，与我们一同潜入Go的底层世界，成为更懂Go的开发者！</strong></p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></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/05/08/go-dwarf5/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>惊！Go在十亿次循环和百万任务中表现不如Java，究竟为何？</title>
		<link>https://tonybai.com/2024/12/02/why-go-sucks/</link>
		<comments>https://tonybai.com/2024/12/02/why-go-sucks/#comments</comments>
		<pubDate>Sun, 01 Dec 2024 22:08:04 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Atoi]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[gnet]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Intn]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[lensm]]></category>
		<category><![CDATA[LoopUnrolling]]></category>
		<category><![CDATA[optimize]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[rand]]></category>
		<category><![CDATA[random]]></category>
		<category><![CDATA[real]]></category>
		<category><![CDATA[RES]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[strconv]]></category>
		<category><![CDATA[sys]]></category>
		<category><![CDATA[syscall]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[top]]></category>
		<category><![CDATA[Uint32]]></category>
		<category><![CDATA[User]]></category>
		<category><![CDATA[waitgroup]]></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=4418</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/12/02/why-go-sucks 编程语言比较的话题总是能吸引程序员的眼球！ 近期外网的两篇编程语言对比的文章在国内程序员圈里引起热议。一篇是由Ben Dicken (@BenjDicken) 做的语言性能测试，对比了十多种主流语言在执行10亿次循环(一个双层循环：1万 * 10 万)的速度；另一篇则是一个名为hez2010的开发者做的内存开销测试，对比了多种语言在处理百万任务时的内存开销。 下面是这两项测试的结果示意图： 10亿循环测试结果 百万任务内存开销测试结果 我们看到：在这两项测试中，Go的表现不仅远不及NonGC的C/Rust，甚至还落后于Java，尤其是在内存开销测试中，Go的内存使用显著高于以“吃内存”著称的Java。这一结果让许多开发者感到意外，因为Go通常被认为是轻量级的语言，然而实际的测试结果却揭示了其在高并发场景下的“内存效率不足”。 那么究竟为何在这两项测试中，Go的表现都不及预期呢？在这篇文章中，我将探讨可能的原因，以供大家参考。 我们先从十亿次循环测试开始。 1. 循环测试跑的慢，都因编译器优化还不够 下面是作者给出的Go测试程序： // why-go-sucks/billion-loops/go/code.go package main import ( "fmt" "math/rand" "os" "strconv" ) func main() { input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line if e != nil { panic(e) } u [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/12/02/why-go-sucks">本文永久链接</a> &#8211; https://tonybai.com/2024/12/02/why-go-sucks</p>
<p>编程语言比较的话题总是能吸引程序员的眼球！</p>
<p>近期外网的两篇编程语言对比的文章在国内程序员圈里引起热议。一篇是由<a href="https://benjdd.com">Ben Dicken (@BenjDicken)</a> 做的<a href="https://benjdd.com/languages/">语言性能测试</a>，对比了十多种主流语言在执行10亿次循环(一个双层循环：1万 * 10 万)的速度；另一篇则是一个名为hez2010的开发者做的<a href="https://hez2010.github.io/async-runtimes-benchmarks-2024/">内存开销测试</a>，对比了多种语言在处理百万任务时的内存开销。</p>
<p>下面是这两项测试的结果示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-2.png" alt="" /><br />
<center>10亿循环测试结果</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-3.png" alt="" /><br />
<center>百万任务内存开销测试结果</center></p>
<p>我们看到：在这两项测试中，Go的表现不仅远不及NonGC的C/Rust，甚至还落后于Java，尤其是在内存开销测试中，Go的内存使用显著高于以“吃内存”著称的Java。这一结果让许多开发者感到意外，因为Go通常被认为是轻量级的语言，然而实际的测试结果却揭示了其在高并发场景下的“内存效率不足”。</p>
<p>那么究竟为何在这两项测试中，Go的表现都不及预期呢？在这篇文章中，我将探讨可能的原因，以供大家参考。</p>
<p>我们先从<strong>十亿次循环测试</strong>开始。</p>
<h2>1. 循环测试跑的慢，都因编译器优化还不够</h2>
<p>下面是作者给出的<a href="https://github.com/bddicken/languages/blob/main/loops/go/code.go">Go测试程序</a>：</p>
<pre><code>// why-go-sucks/billion-loops/go/code.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            a[i] = a[i] + j%u // Simple sum
        }
        a[i] += r // Add a random value to each element in array
    }
    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>这段代码通过命令行参数获取一个整数，然后生成一个随机数，接着通过两层循环对一个数组的每个元素进行累加，最终输出该数组中以随机数为下标对应的数组元素的值。</p>
<p>我们再来看一下”竞争对手”的测试代码。C测试代码如下：</p>
<pre><code>// why-go-sucks/billion-loops/c/code.c

#include "stdio.h"
#include "stdlib.h"
#include "stdint.h"

int main (int argc, char** argv) {
  int u = atoi(argv[1]);               // Get an input number from the command line
  int r = rand() % 10000;              // Get a random integer 0 &lt;= r &lt; 10k
  int32_t a[10000] = {0};              // Array of 10k elements initialized to 0
  for (int i = 0; i &lt; 10000; i++) {    // 10k outer loop iterations
    for (int j = 0; j &lt; 100000; j++) { // 100k inner loop iterations, per outer loop iteration
      a[i] = a[i] + j%u;               // Simple sum
    }
    a[i] += r;                         // Add a random value to each element in array
  }
  printf("%d\n", a[r]);                // Print out a single element from the array
}
</code></pre>
<p>下面是Java的测试代码：</p>
<pre><code>// why-go-sucks/billion-loops/java/code.java

package jvm;

import java.util.Random;

public class code {

    public static void main(String[] args) {
        var u = Integer.parseInt(args[0]); // Get an input number from the command line
        var r = new Random().nextInt(10000); // Get a random number 0 &lt;= r &lt; 10k
        var a = new int[10000]; // Array of 10k elements initialized to 0
        for (var i = 0; i &lt; 10000; i++) { // 10k outer loop iterations
            for (var j = 0; j &lt; 100000; j++) { // 100k inner loop iterations, per outer loop iteration
                a[i] = a[i] + j % u; // Simple sum
            }
            a[i] += r; // Add a random value to each element in array
        }
        System.out.println(a[r]); // Print out a single element from the array
    }
}
</code></pre>
<p>你可能不熟悉C或Java，但从代码的形式上来看，C、Java与Go的代码确实处于“同等条件”。这不仅意味着它们在相同的硬件和软件环境中运行，更包括它们采用了相同的计算逻辑和算法，以及一致的输入参数处理等方面的相似性。</p>
<p>为了确认一下原作者的测试结果，我在一台阿里云ECS上(amd64，8c32g，CentOS 7.9)对上面三个程序进行了测试(使用time命令测量计算耗时)，得到一个基线结果。我的环境下，C、Java和Go的编译器版本如下：</p>
<pre><code>$go version
go version go1.23.0 linux/amd64

$java -version
openjdk version "17.0.9" 2023-10-17 LTS
OpenJDK Runtime Environment Zulu17.46+19-CA (build 17.0.9+8-LTS)
OpenJDK 64-Bit Server VM Zulu17.46+19-CA (build 17.0.9+8-LTS, mixed mode, sharing)

$gcc -v
使用内建 specs。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
目标：x86_64-redhat-linux
配置为：../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
线程模型：posix
gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
</code></pre>
<p>测试步骤与结果如下：</p>
<pre><code>Go代码测试：

$cd why-go-sucks/billion-loops/go
$go build -o code code.go
$time ./code 10
456953

real    0m3.766s
user    0m3.767s
sys 0m0.007s

C代码测试：

$cd why-go-sucks/billion-loops/c
$gcc -O3 -std=c99 -o code code.c
$time ./code 10
459383

real    0m3.005s
user    0m3.005s
sys 0m0.000s

Java代码测试：

$javac -d . code.java
$time java -cp . jvm.code 10
456181

real    0m3.105s
user    0m3.092s
sys 0m0.027s
</code></pre>
<p>从测试结果看到(基于real时间)：采用-O3优化的C代码最快，Java落后一个身位，而<strong>Go则比C慢了25%，比Java慢了21%</strong>。</p>
<blockquote>
<p>注：time命令的输出结果通常包含三个主要部分：real、user和sys。real是从命令开始执行到结束所经过的实际时间（墙钟时间），我们依次指标为准。user是程序在<strong>用户模式下执行所消耗的CPU时间</strong>。sys则是程序<strong>在内核模式下执行所消耗的CPU时间（系统调用）</strong>。如果总时间（real）略低于用户时间（user），这表明程序可能在某些时刻被调度或等待，而不是持续占用CPU。这种情况可能是由于输入输出操作、等待资源等原因。如果real时间显著小于user时间，这种情况通常发生在并发程序中，其中多个线程或进程在不同的时间段执行，导致总的用户CPU时间远大于实际的墙钟时间。sys时间保持较低，说明系统调用的频率较低，程序主要是执行计算而非进行大量的系统交互。</p>
</blockquote>
<p>这时作为Gopher的你可能会说：<strong>原作者编写的Go测试代码不够优化，我们能优化到比C还快</strong>！</p>
<p>大家都知道原代码是不够优化的，随意改改计算逻辑就能带来大幅提升。但我们不能忘了“同等条件”这个前提。你采用的优化方法，其他语言（C、Java）也可以采用。</p>
<p>那么，在不改变“同等条件”的前提下，我们还能优化点啥呢？本着能提升一点是一点的思路，我们尝试从下面几个点优化一下，看看效果：</p>
<ul>
<li>去除不必要的if判断</li>
<li>使用更快的rand实现</li>
<li>关闭边界检查</li>
<li>避免逃逸</li>
</ul>
<p>下面是修改之后的代码：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_optimize.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, _ := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    u := int32(input)
    r := int32(rand.Uint32() % 10000)   // Use Uint32 for faster random number generation
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            a[i] = a[i] + j%u // Simple sum
        }
        a[i] += r // Add a random value to each element in array
    }
    z := a[r]
    fmt.Println(z) // Print out a single element from the array
}
</code></pre>
<p>我们编译并运行一下测试：</p>
<pre><code>$cd why-go-sucks/billion-loops/go
$go build -o code_optimize -gcflags '-B' code_optimize.go
$time ./code_optimize 10
459443

real    0m3.761s
user    0m3.759s
sys 0m0.011s
</code></pre>
<p>对比一下最初的测试结果，这些“所谓的优化”没有什么卵用，优化前你估计也能猜测到这个结果，因为除了关闭边界检查，其他优化都<strong>没有处于循环执行的热路径之上</strong>。</p>
<blockquote>
<p>注：rand.Uint32() % 10000的确要比rand.Intn(10000)快，我自己的benchmark结果是快约1倍。</p>
</blockquote>
<p>那Go程序究竟慢在哪里呢？在“同等条件”下，我能想到的只能是<strong>Go编译器后端在代码优化方面优化做的还不够</strong>，相较于GCC、Java等老牌编译器还有明显差距。</p>
<p>比如说，原先的代码中在内层循环中频繁访问a&#91;i&#93;，导致数组访问的读写操作较多（从内存加载a&#91;i&#93;，更新值后写回）。GCC和Java编译器在后端很可能做了这样的优化：将数组元素累积到一个临时变量中，并在外层循环结束后写回数组，这样做可以<strong>减少内层循环中的内存读写操作，充分利用CPU缓存和寄存器，加速数据处理</strong>。</p>
<blockquote>
<p>注：数组从内存或缓存读，而一个临时变量很大可能是从寄存器读，那读取速度相差还是很大的。</p>
</blockquote>
<p>如果我们手工在Go中实施这一优化，看看能达到什么效果呢？我们改一下最初版本的Go代码(code.go)，新代码如下：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_local_var.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        temp := a[i]
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            temp += j % u // Simple sum
        }
        temp += r // Add a random value to each element in array
        a[i] = temp
    }
    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>编译并运行测试：</p>
<pre><code>$go build -o code_local_var code_local_var.go
$time ./code_local_var 10
459169

real    0m3.017s
user    0m3.017s
sys 0m0.007s
</code></pre>
<p>我们看到，测试结果直接就比Java略好一些了。显然Go编译器没有做这种优化，从code.go的汇编也大致可以看出来：</p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-4.png" alt="" /><br />
<center>使用<a href="https://github.com/loov/lensm">lensm</a>生成的汇编与go源码对应关系</center></p>
<p>而Java显然做了这类优化，我们在原Java代码版本上按上述优化逻辑修改了一下：</p>
<pre><code>// why-go-sucks/billion-loops/java/code_local_var.java

package jvm;

import java.util.Random;

public class code {

    public static void main(String[] args) {
        var u = Integer.parseInt(args[0]); // 获取命令行输入的整数
        var r = new Random().nextInt(10000); // 生成随机数 0 &lt;= r &lt; 10000
        var a = new int[10000]; // 定义长度为10000的数组a

        for (var i = 0; i &lt; 10000; i++) { // 10k外层循环迭代
            var temp = a[i]; // 使用临时变量存储 a[i] 的值
            for (var j = 0; j &lt; 100000; j++) { // 100k内层循环迭代，每次外层循环迭代
                temp += j % u; // 更新临时变量的值
            }
            a[i] = temp + r; // 将临时变量的值加上 r 并写回数组
        }
        System.out.println(a[r]); // 输出 a[r] 的值
    }
}
</code></pre>
<p>但从运行这个“优化”后的程序的结果来看，其对java代码的提升幅度几乎可以忽略不计：</p>
<pre><code>$time java -cp . jvm.code 10
450375

real    0m3.043s
user    0m3.028s
sys 0m0.027s
</code></pre>
<p>这也直接证明了即便采用的是原版java代码，java编译器也会生成带有抽取局部变量这种优化的可执行代码，java程序员无需手工进行此类优化。</p>
<p>像这种编译器优化，还有不少，比如大家比较熟悉的循环展开(Loop Unrolling)也可以提升Go程序的性能：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_loop_unrolling.go

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        var sum int32
        // Unroll inner loop in chunks of 4 for optimization
        for j := int32(0); j &lt; 100000; j += 4 {
            sum += j % u
            sum += (j + 1) % u
            sum += (j + 2) % u
            sum += (j + 3) % u
        }
        a[i] = sum + r // Add the accumulated sum and random value
    }

    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>运行这个Go测试程序，性能如下：</p>
<pre><code>$go build -o code_loop_unrolling code_loop_unrolling.go
$time ./code_loop_unrolling 10
458908

real    0m2.937s
user    0m2.940s
sys 0m0.002s
</code></pre>
<p>循环展开可以增加指令级并行性，因为展开后的代码块中可以有更多的独立指令，比如示例中的计算j % u、(j+1) % u、(j+2) % u和(j+3) % u，这些计算操作是独立的，可以并行执行，打破了依赖链，从而更好地利用处理器的并行流水线。而原版Go代码中，每次迭代都会根据前一次迭代的结果更新a&#91;i&#93;，形成一个依赖链，这种顺序依赖性迫使处理器只能按顺序执行这些指令，导致流水线停顿。</p>
<p>不过其他语言也可以做同样的手工优化，比如我们对C代码做同样的优化(why-go-sucks/billion-loops/c/code_loop_unrolling.c)，c测试程序的性能可以提升至2.7s水平，这也证明了初版C程序即便在-O3的情况下编译器也没有自动为其做这个优化：</p>
<pre><code>$time ./code_loop_unrolling 10
459383

real    0m2.723s
user    0m2.722s
sys 0m0.001s
</code></pre>
<p>到这里我们就不再针对这个10亿次循环的性能问题做进一步展开了，从上面的探索得到的初步结论就是<strong>Go编译器优化做的还不到位所致</strong>，期待后续Go团队能在编译器优化方面投入更多精力，争取早日追上GCC/Clang、Java这些成熟的编译器优化水平。</p>
<p>下面我们再来看Go在百万任务场景下内存开销大的“问题”。</p>
<h2>2. 内存占用高，问题出在Goroutine实现原理</h2>
<p>我们先来看第二个问题的测试代码：</p>
<pre><code>package main

import (
    "fmt"
    "os"
    "strconv"
    "sync"
    "time"
)

func main() {
    numRoutines := 100000
    if len(os.Args) &gt; 1 {
        n, err := strconv.Atoi(os.Args[1])
        if err == nil {
            numRoutines = n
        }
    }

    var wg sync.WaitGroup
    for i := 0; i &lt; numRoutines; i++ {
        wg.Add(1)
        go func() {
            time.Sleep(10 * time.Second)
            wg.Done()
        }()
    }
    wg.Wait()
}
</code></pre>
<p>这个代码其实就是根据传入的task数量启动等同数量的goroutine，然后每个goroutine模拟工作负载sleep 10s，这等效于百万长连接的场景，只有连接，但没有收发消息。</p>
<p>相对于上一个问题，这个问题更好解释一些。</p>
<p>Go使用的groutine是一种有栈协程，文章中使用的是每个task一个goroutine的模型，且维护百万任务一段时间，这会真实创建百万个goroutine（G数据结构），并为其分配栈空间(2k起步)，这样你可以算一算，不考虑其他结构的占用，仅每个goroutine的栈空间所需的内存都是极其可观的：</p>
<pre><code>mem = 1000000 * 2000 Bytes = 2000000000 Bytes = 2G Bytes
</code></pre>
<p>所以启动100w goroutine，保底就2GB内存出去了，这与原作者测试的结果十分契合(原文是2.5GB多)。并且，内存还会随着goroutine数量增长而线性增加。</p>
<p>那么如何能减少内存使用呢？如果采用每个task一个goroutine的模型，这个内存占用很难省去，除非将来Go团队对goroutine实现做大修。</p>
<p>如果task是网络通信相关的，可以使用类似gnet这样的直接基于epoll建构的框架，其主要的节省在于不会启动那么多goroutine，而是通过一个goroutine池来处理数据，每个池中的goroutine负责一批网络连接或网络请求。</p>
<p>在一些Gopher的印象中，Goroutine一旦分配就不回收，这会使他们会误认为一旦分配了100w goroutine，这2.5G内存空间将始终被占用，真实情况是这样么？我们用一个示例程序验证一下就好了：</p>
<pre><code>// why-go-sucks/million-tasks/million-tasks.go

package main

import (
    "fmt"
    "log"
    "os"
    "os/signal"
    "runtime"
    "sync"
    "syscall"
    "time"
)

// 打印当前内存使用情况和相关信息
func printMemoryUsage() {
    var m runtime.MemStats
    runtime.ReadMemStats(&amp;m)

    // 获取当前 goroutine 数量
    numGoroutines := runtime.NumGoroutine()

    // 获取当前线程数量
    numThreads := runtime.NumCPU() // Go runtime 不直接提供线程数量，但可以通过 NumCPU 获取逻辑处理器数量

    fmt.Printf("======&gt;\n")
    fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
    fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
    fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
    fmt.Printf("\tNumGC = %v", m.NumGC)
    fmt.Printf("\tNumGoroutines = %v", numGoroutines)
    fmt.Printf("\tNumThreads = %v\n", numThreads)
    fmt.Printf("&lt;======\n\n")
}

// 将字节转换为 MB
func bToMb(b uint64) uint64 {
    return b / 1024 / 1024
}

func main() {
    const signal1Goroutines = 900000
    const signal2Goroutines = 90000
    const signal3Goroutines = 10000

    // 用于接收退出信号
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    // 控制 goroutine 的退出
    signal1Chan := make(chan struct{})
    signal2Chan := make(chan struct{})
    signal3Chan := make(chan struct{})

    var wg sync.WaitGroup
    ticker := time.NewTicker(5 * time.Second)
    go func() {
        for range ticker.C {
            printMemoryUsage()
        }
    }()

    // 等待退出信号
    go func() {
        count := 0
        for {
            &lt;-sigChan
            count++
            if count == 1 {
                log.Println("收到第一类goroutine退出信号")
                close(signal1Chan) // 关闭 signal1Chan，通知第一类 goroutine 退出
                continue
            }
            if count == 2 {
                log.Println("收到第二类goroutine退出信号")
                close(signal2Chan) // 关闭 signal2Chan，通知第二类 goroutine 退出
                continue
            }
            log.Println("收到第三类goroutine退出信号")
            close(signal3Chan) // 关闭 signal3Chan，通知第三类 goroutine 退出
            return
        }
    }()

    // 启动第一类 goroutine（在收到 signal1 时退出）
    log.Println("开始启动第一类goroutine...")
    for i := 0; i &lt; signal1Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal1Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第一类goroutine(900000) ok")

    time.Sleep(time.Second * 5)

    // 启动第二类 goroutine（在收到 signal2 时退出）
    log.Println("开始启动第二类goroutine...")
    for i := 0; i &lt; signal2Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal2Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第二类goroutine(90000) ok")

    time.Sleep(time.Second * 5)

    // 启动第三类goroutine（随程序退出而退出）
    log.Println("开始启动第三类goroutine...")
    for i := 0; i &lt; signal3Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal3Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第三类goroutine(90000) ok")

    // 等待所有 goroutine 完成
    wg.Wait()
    fmt.Println("所有 goroutine 已退出，程序结束")
}
</code></pre>
<p>这个程序我就不详细解释了。大致分三类goroutine，第一类90w个，在我发送第一个ctrl+c信号后退出，第二类9w个，在我发送第二个ctrl+c信号后退出，最后一类1w个，随着程序退出而退出。</p>
<p>在我的执行环境下编译和执行一下这个程序，并结合runtime输出以及使用top -p pid的方式查看其内存占用：</p>
<pre><code>$go build million-tasks.go
$./million-tasks 

2024/12/01 22:07:03 开始启动第一类goroutine...
2024/12/01 22:07:05 启动第一类goroutine(900000) ok
======&gt;
Alloc = 511 MiB TotalAlloc = 602 MiB    Sys = 2311 MiB  NumGC = 9   NumGoroutines = 900004  NumThreads = 8
&lt;======

2024/12/01 22:07:10 开始启动第二类goroutine...
2024/12/01 22:07:11 启动第二类goroutine(90000) ok
======&gt;
Alloc = 577 MiB TotalAlloc = 668 MiB    Sys = 2553 MiB  NumGC = 9   NumGoroutines = 990004  NumThreads = 8
&lt;======

2024/12/01 22:07:16 开始启动第三类goroutine...
2024/12/01 22:07:16 启动第三类goroutine(90000) ok
======&gt;
Alloc = 597 MiB TotalAlloc = 688 MiB    Sys = 2593 MiB  NumGC = 9   NumGoroutines = 1000004 NumThreads = 8
&lt;======

======&gt;
Alloc = 600 MiB TotalAlloc = 690 MiB    Sys = 2597 MiB  NumGC = 9   NumGoroutines = 1000004 NumThreads = 8
&lt;======
... ...

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 1000004 NumThreads = 8
&lt;======
</code></pre>
<p>100w goroutine全部创建ok后，我们查看一下top输出：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875556   2.5g    988 S  54.0  8.2   0:30.92 million-tasks
</code></pre>
<p>我们看到RES为2.5g，和我们预期的一致！</p>
<p>接下来，我们停掉第一批90w个goroutine，看RES是否会下降，何时会下降！</p>
<p>输入ctrl+c，停掉第一批90w goroutine：</p>
<pre><code>^C2024/12/01 22:10:15 收到第一类goroutine退出信号
======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 723198  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 723198  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 100004  NumThreads = 8
&lt;======
... ...
</code></pre>
<p>但同时刻的top显示RES并没有变化：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812   2.5g    988 S   0.0  8.2   0:56.38 million-tasks
</code></pre>
<p>等待两个GC间隔的时间后(大约4分)，Goroutine的栈空间被释放：</p>
<pre><code>======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======
</code></pre>
<p>top显示RES从2.5g下降为大概700多MB（RES的单位是KB）：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 764136    992 S   0.0  2.4   1:01.87 million-tasks
</code></pre>
<p>接下来，我们再停掉第二批9w goroutine：</p>
<pre><code>^C2024/12/01 22:16:21 收到第二类goroutine退出信号
======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 10004   NumThreads = 8
&lt;======

</code></pre>
<p>此时，top值也没立即改变：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 764136    992 S   0.0  2.4   1:05.99 million-tasks
</code></pre>
<p>大约等待一个GC间隔(2分钟)后，top中RES下降：</p>
<pre><code>======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======
</code></pre>
<p>RES变为不到700M：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 699156    992 S   0.0  2.2   1:06.75 million-tasks
</code></pre>
<p>第三次按下ctrl+c，程序退出：</p>
<pre><code>^C2024/12/01 22:18:46 收到第三类goroutine退出信号
======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10003   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10003   NumThreads = 8
&lt;======

所有 goroutine 已退出，程序结束
</code></pre>
<p>我们看到Go是会回收goroutine占用的内存空间的，并且归还给OS，只是这种归还比较lazy。尤其是，第二次停止goroutine前，go程序剩下10w goroutine，按理论来讲需占用大约200MB的空间，实际上却是700多MB；第二次停止goroutine后，goroutine数量降为1w，理论占用应该在20MB，但实际却是600多MB，我们看到go运行时这种lazy归还OS内存的行为可能也是“故意为之”，是为了避免反复从OS申请和归还内存。</p>
<h2>3. 小结</h2>
<p>本文主要探讨了Go语言在十亿次循环和百万任务的测试中的表现令人意外地逊色于Java和C语言的原因。我认为Go在循环执行中的慢速表现，主要是其编译器优化不足，影响了执行效率。 而在内存开销方面，Go的Goroutine实现是使得内存使用量大幅增加的“罪魁祸首”，这是由于每个Goroutine初始都会分配固定大小的栈空间。</p>
<p>通过本文的探讨，我的主要目的是希望大家不要以讹传讹，而是要搞清楚背后的真正原因，并正视Go在某些方面的不足，以及其当前在某些应用上下文中的局限性。 同时，也希望Go开发团队在编译器优化方面进行更多投入，以提升Go在高性能计算领域的竞争力。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/why-go-sucks">这里</a>下载。</p>
<h2>4. 参考资料</h2>
<ul>
<li><a href="https://benjdd.com/languages/">Billion nested loop iterations</a> &#8211; https://benjdd.com/languages/</li>
<li><a href="https://hez2010.github.io/async-runtimes-benchmarks-2024/">How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks?</a> &#8211; https://hez2010.github.io/async-runtimes-benchmarks-2024/</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的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，价格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>
<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; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/12/02/why-go-sucks/feed/</wfw:commentRss>
		<slash:comments>2</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>使用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程序员拥抱C语言简明指南</title>
		<link>https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/</link>
		<comments>https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/#comments</comments>
		<pubDate>Sun, 15 May 2022 23:11:16 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ANSI-C]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[break]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C11]]></category>
		<category><![CDATA[C18]]></category>
		<category><![CDATA[C99]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[clang-format]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[Configure]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[C标准库]]></category>
		<category><![CDATA[C语言]]></category>
		<category><![CDATA[DennisRitchie]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[fallthrough]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[iso]]></category>
		<category><![CDATA[K&R]]></category>
		<category><![CDATA[KenThompson]]></category>
		<category><![CDATA[LeetCode]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[Lint]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[loccount]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[soname]]></category>
		<category><![CDATA[switch-case]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[utf-8]]></category>
		<category><![CDATA[Windows]]></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=3535</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher 本文是为于航老师的极客时间专栏《深入C语言和程序运行原理》写的加餐文章《Tony Bai：Go程序员拥抱C语言简明指南》，这里分享给大家，尤其是那些想学习C语言的Gopher们。 你好，我是Tony Bai。 也许有同学对我比较熟悉，看过我在极客时间上的专栏《Tony Bai ·Go语言第一课》，或者是关注了我的博客。那么，作为一个Gopher，我怎么跑到这个C语言专栏做分享了呢？其实，在学习Go语言并成为一名Go程序员之前，我也曾是一名地地道道的C语言程序员。 大学毕业后，我就开始从事C语言后端服务开发工作，在电信增值领域摸爬滚打了十多年。不信的话，你可以去翻翻我的博客，数一数我发的C语言相关文章是不是比关于Go的还多。一直到近几年，我才将工作中的主力语言从C切换到了Go。不过这并不是C语言的问题，主要原因是我转换赛道了。我目前在智能网联汽车领域从事面向云原生平台的先行研发，而在云原生方面，新生代的Go语言有着更好的生态。 不过作为资深C程序员，C语言已经在我身上打下了深深的烙印。虽然Go是我现在工作中的主力语言，但我仍然会每天阅读一些C开源项目的源码，每周还会写下数百行的C代码。在一些工作场景中，特别是在我参与先行研发一些车端中间件时，C语言有着资源占用小、性能高的优势，这一点是Go目前还无法匹敌的。 正因为我有着C程序员和Go程序员的双重身份，接到这个加餐邀请时，我就想到了一个很适合聊的话题——在 Gopher（泛指Go程序员）与C语言之间“牵线搭桥”。在这门课的评论区里，我看到一些同学说，“正是因为学了Go，所以我想学好C”。如果你也对Go比较熟悉，那么恭喜你，这篇加餐简直是为你量身定制的：一个熟悉Go的程序员在学习C时需要注意的问题，还有可能会遇到的坑，我都替你总结好了。 当然，我知道还有一些对Go了解不多的同学，看到这里也别急着退出去。因为C和Go这两门语言的比较，本身就是一个很有意思的话题。今天的加餐，会涉及这两门语言的异同点，通过对C与Go语言特性的比较，你就能更好地理解“C 语言为什么设计成现在这样”。 一. C语言是现代IT工业的根基 在比较C和Go之前，先说说我推荐Gopher学C的最重要原因吧：用一句话总结，C语言在IT工业中的根基地位，是Go和其他语言目前都无法动摇的。 C语言是由美国贝尔实验室的丹尼斯·里奇（Dennis Ritchie）以Unix发明人肯·汤普森（Ken Thompson）设计的B语言为基础而创建的高级编程语言。诞生于上个世纪（精确来说是1972年）的它，到今年（2022年）已到了“知天命”的半百年纪。 年纪大、设计久远一直是“C语言过时论”兴起的根源，但如果你相信这一论断，那就大错特错了。下面，我来为你分析下个中缘由。 首先，我们说说C语言本身：C语言一直在演进，从未停下过脚步。 虽然C语言之父丹尼斯·里奇不幸于2011年永远地离开了我们，但C语言早已成为ANSI（美国国家标准学会）标准以及ISO/IEC（国际标准化组织和国际电工委员会）标准，因此其演进也早已由标准委员会负责。我们来简单回顾一下C语言标准的演进过程： 1989年，ANSI发布了首个C语言标准，被称为C89，又称ANSI C。次年，ISO和IEC把ANSI C89标准定为C语言的国际标准（ISO/IEC 9899:1990），又称C90，它也是C语言的第一个官方版本； 1999年，ISO和IEC发布了C99标准(ISO/IEC 9899:1999)，它是C语言的第二个官方版本； 2011年，ISO和IEC发布了C11标准(ISO/IEC 9899:2011)，它是C语言的第三个官方版本； 2018年，ISO和IEC发布了C18标准(ISO/IEC 9899:2018)，它是C语言的第四个官方版本。 目前，ISO/IEC标准化委员会正在致力于C2x标准的改进与制定，预计它会在2023年发布。 其次，时至今日，C语言的流行度仍然非常高。 著名编程语言排行榜TIOBE的数据显示，各大编程语言年度平均排名的总位次，C语言多年来高居第一，如下图（图片来自TIOBE）所示： 这说明，无论是在过去还是现在，C语言都是一门被广泛应用的工业级编程语言。 最后，也是最重要的一点是：C语言是现代IT工业的根基，我们说C永远不会退出IT行业舞台也不为过。 如今，无论是普通消费者端的Windows、macOS、Android、苹果iOS，还是服务器端的Linux、Unix等操作系统，亦或是各个工业嵌入式领域的操作系统，其内核实现语言都是C语言。互联网时代所使用的主流Web服务器，比如 Nginx、Apache，以及主流数据库，比如MySQL、Oracle、PostgreSQL等，也都是使用C语言开发的杰作。可以说，现代人类每天都在跟由C语言实现的系统亲密接触，并且已经离不开这些系统了。回到我们程序员的日常，Git、SVN等我们时刻在用的源码版本控制软件也都是由C语言实现的。 可以说，C语言在IT工业中的根基地位，不光Go语言替代不了，C++、Rust等系统编程语言也无法动摇，而且不仅短期如此，长期来看也是如此。 总之，C语言具有紧凑、高效、移植性好、对内存的精细控制等优秀特性，这使得我们在任何时候学习它都不会过时。不过，我在这里推荐Gopher去了解和系统学习C语言，其实还有另一个原因。我们继续往下看。 二. C与Go的相通之处：Gopher拥抱C语言的“先天优势” 众所周知，Go 是在C语言的基础上衍生而来的，二者之间有很多相通之处，因此 Gopher 在学习C语言时是有“先天优势”的。接下来，我们具体看看C和Go的相通之处有哪些。 1. 简单且语法同源 Go语言以简单著称，而作为Go先祖的C语言，入门门槛同样不高：Go有25个关键字，C有32个关键字（C89标准），简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言，这与C的简单也不无关系。 和Go不同的是，C语言是一个小内核、大外延的编程语言，其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库，我们可以快速掌握它。但需要注意的是，与Go语言“开箱即用、内容丰富”的标准库不同，C标准库非常小（在C11标准之前甚至连thread库都不包含），所以掌握“小内核”后，在LeetCode平台上刷题是没有任何问题的，但要写出某一领域的工业级生产程序，我们还有很多外延知识技能要学习，比如并发原语、操作系统的系统调用，以及进程间通信等。 C语言的这种简单很容易获得Gopher们的认同感。当年Go语言之父们在设计Go语言时，也是主要借鉴了C语言的语法。当然，这与他们深厚的C语言背景不无关系：肯·汤普森（Ken [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher">本文永久链接</a> &#8211; https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher</p>
<p>本文是为于航老师的极客时间专栏<a href="http://gk.link/a/11osT">《深入C语言和程序运行原理》</a>写的加餐文章<a href="https://time.geekbang.org/column/article/500145">《Tony Bai：Go程序员拥抱C语言简明指南》</a>，这里分享给大家，尤其是那些想学习C语言的Gopher们。</p>
<hr />
<p>你好，我是Tony Bai。</p>
<p>也许有同学对我比较熟悉，看过我在极客时间上的专栏<a href="http://gk.link/a/10AVZ">《Tony Bai ·Go语言第一课》</a>，或者是关注了<a href="https://tonybai.com">我的博客</a>。那么，作为一个Gopher，我怎么跑到这个C语言专栏做分享了呢？其实，在学习Go语言并成为一名Go程序员之前，我也曾是一名地地道道的C语言程序员。</p>
<p>大学毕业后，我就开始从事C语言后端服务开发工作，在电信增值领域摸爬滚打了十多年。不信的话，你可以去翻翻<a href="https://tonybai.com/tag/c">我的博客</a>，数一数我发的C语言相关文章是不是比关于Go的还多。一直到近几年，我才将工作中的主力语言从C切换到了Go。不过这并不是C语言的问题，主要原因是我转换赛道了。我目前在智能网联汽车领域从事面向云原生平台的先行研发，而在云原生方面，新生代的Go语言有着更好的生态。</p>
<p>不过作为资深C程序员，C语言已经在我身上打下了深深的烙印。虽然Go是我现在工作中的主力语言，但我仍然会每天阅读一些C开源项目的源码，每周还会写下数百行的C代码。在一些工作场景中，特别是在我参与先行研发一些车端中间件时，C语言有着资源占用小、性能高的优势，这一点是Go目前还无法匹敌的。</p>
<p>正因为我有着C程序员和Go程序员的双重身份，接到这个加餐邀请时，我就想到了一个很适合聊的话题——在 Gopher（泛指Go程序员）与C语言之间“牵线搭桥”。在这门课的评论区里，我看到一些同学说，“正是因为学了Go，所以我想学好C”。如果你也对Go比较熟悉，那么恭喜你，这篇加餐简直是为你量身定制的：一个熟悉Go的程序员在学习C时需要注意的问题，还有可能会遇到的坑，我都替你总结好了。</p>
<p><strong>当然，我知道还有一些对Go了解不多的同学，看到这里也别急着退出去。</strong>因为C和Go这两门语言的比较，本身就是一个很有意思的话题。今天的加餐，会涉及这两门语言的异同点，通过对C与Go语言特性的比较，你就能更好地理解“C 语言为什么设计成现在这样”。</p>
<h2>一. C语言是现代IT工业的根基</h2>
<p>在比较C和Go之前，先说说我推荐Gopher学C的最重要原因吧：用一句话总结，<strong>C语言在IT工业中的根基地位，是Go和其他语言目前都无法动摇的</strong>。</p>
<p>C语言是由美国贝尔实验室的丹尼斯·里奇（Dennis Ritchie）以Unix发明人肯·汤普森（Ken Thompson）设计的B语言为基础而创建的高级编程语言。诞生于上个世纪（精确来说是1972年）的它，到今年（2022年）已到了“知天命”的半百年纪。 年纪大、设计久远一直是“C语言过时论”兴起的根源，但如果你相信这一论断，那就大错特错了。下面，我来为你分析下个中缘由。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-3.jpeg" alt="" /></p>
<p>首先，我们说说C语言本身：<strong>C语言一直在演进，从未停下过脚步</strong>。</p>
<p>虽然C语言之父丹尼斯·里奇不幸于2011年永远地离开了我们，但C语言早已成为ANSI（美国国家标准学会）标准以及ISO/IEC（国际标准化组织和国际电工委员会）标准，因此其演进也早已由标准委员会负责。我们来简单回顾一下C语言标准的演进过程：</p>
<ul>
<li>1989年，ANSI发布了首个C语言标准，被称为C89，又称ANSI C。次年，ISO和IEC把ANSI C89标准定为C语言的国际标准（ISO/IEC 9899:1990），又称C90，它也是C语言的第一个官方版本；</li>
<li>1999年，ISO和IEC发布了<a href="https://www.iso.org/standard/29237.html">C99标准(ISO/IEC 9899:1999)</a>，它是C语言的第二个官方版本；</li>
<li>2011年，ISO和IEC发布了<a href="https://www.iso.org/standard/57853.html">C11标准(ISO/IEC 9899:2011)</a>，它是C语言的第三个官方版本；</li>
<li>2018年，ISO和IEC发布了<a href="https://www.iso.org/standard/74528.html">C18标准(ISO/IEC 9899:2018)</a>，它是C语言的第四个官方版本。<br />
目前，ISO/IEC标准化委员会正在致力于C2x标准的改进与制定，预计它会在2023年发布。</li>
</ul>
<p>其次，<strong>时至今日，C语言的流行度仍然非常高</strong>。</p>
<p>著名编程语言排行榜TIOBE的数据显示，各大编程语言年度平均排名的总位次，C语言多年来高居第一，如下图（图片来自<a href="https://www.tiobe.com/tiobe-index">TIOBE</a>）所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-2.png" alt="" /></p>
<p>这说明，无论是在过去还是现在，C语言都是一门被广泛应用的工业级编程语言。</p>
<p>最后，也是最重要的一点是：<strong>C语言是现代IT工业的根基</strong>，我们说C永远不会退出IT行业舞台也不为过。</p>
<p>如今，无论是普通消费者端的Windows、macOS、Android、苹果iOS，还是服务器端的Linux、Unix等操作系统，亦或是各个工业嵌入式领域的操作系统，其内核实现语言都是C语言。互联网时代所使用的主流Web服务器，比如 Nginx、Apache，以及主流数据库，比如MySQL、Oracle、PostgreSQL等，也都是使用C语言开发的杰作。可以说，现代人类每天都在跟由C语言实现的系统亲密接触，并且已经离不开这些系统了。回到我们程序员的日常，Git、SVN等我们时刻在用的源码版本控制软件也都是由C语言实现的。</p>
<p>可以说，C语言在IT工业中的根基地位，不光Go语言替代不了，C++、Rust等系统编程语言也无法动摇，而且不仅短期如此，长期来看也是如此。</p>
<p>总之，C语言具有紧凑、高效、移植性好、对内存的精细控制等优秀特性，这使得我们在任何时候学习它都不会过时。不过，我在这里推荐Gopher去了解和系统学习C语言，其实还有另一个原因。我们继续往下看。</p>
<h2>二. C与Go的相通之处：Gopher拥抱C语言的“先天优势”</h2>
<p>众所周知，Go 是在C语言的基础上衍生而来的，二者之间有很多相通之处，因此 Gopher 在学习C语言时是有“先天优势”的。接下来，我们具体看看C和Go的相通之处有哪些。</p>
<h3>1. 简单且语法同源</h3>
<p>Go语言以简单著称，而作为<strong>Go先祖</strong>的C语言，入门门槛同样不高：Go有25个关键字，C有32个关键字（C89标准），简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言，这与C的简单也不无关系。</p>
<p>和Go不同的是，C语言是一个<strong>小内核、大外延</strong>的编程语言，其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库，我们可以快速掌握它。但需要注意的是，与Go语言“开箱即用、内容丰富”的标准库不同，<a href="https://en.wikipedia.org/wiki/C_standard_library">C标准库</a>非常小（在C11标准之前甚至连thread库都不包含），所以掌握“小内核”后，在LeetCode平台上刷题是没有任何问题的，但要写出某一领域的工业级生产程序，我们还有很多外延知识技能要学习，比如并发原语、操作系统的系统调用，以及进程间通信等。</p>
<p>C语言的这种简单很容易获得Gopher们的认同感。当年Go语言之父们在设计Go语言时，也是主要借鉴了C语言的语法。当然，这与他们深厚的C语言背景不无关系：肯·汤普森（Ken Thompson）是Unix之父，与丹尼斯·里奇共同设计了C语言；罗博·派克（Rob Pike）是贝尔实验室的资深研究员，参与了Unix系统的演进、Plan9操作系统的开发，还是UTF-8编码的发明人；罗伯特·格瑞史莫（Robert Griesemer）也是用C语言手写Java虚拟机的大神级人物。</p>
<p>Go的第一版编译器就是由肯·汤普森（Ken Thompson）用C语言实现的。并且，Go语言的早期版本中，C代码的比例还不小。以Go语言发布的第一个版本，<a href="https://github.com/golang/go/releases/tag/go1">Go 1.0版本</a>为例，我们通过<a href="https://gitlab.com/esr/loccount">loccount工具</a>对其进行分析，会得到下面的结果：</p>
<pre><code>$loccount .
all          SLOC=460992  (100.00%) LLOC=193045  in 2746 files
Go           SLOC=256321  (55.60%)  LLOC=109763  in 1983 files
C            SLOC=148001  (32.10%)  LLOC=73458   in 368 files
HTML         SLOC=25080   (5.44%)   LLOC=0       in 57 files
asm          SLOC=10109   (2.19%)   LLOC=0       in 133 files
... ...
</code></pre>
<p>这里我们看到，在1.0版本中，C语言代码行数占据了32.10%的份额，这一份额直至Go 1.5版本实现自举后，才下降为不到1%。</p>
<p>我当初对Go“一见钟情”，其中一个主要原因就是Go与C语言的<strong>语法同源。</strong>相对应地，相信这种同源的语法也会让Gopher们喜欢上C语言。</p>
<h3>2. 静态编译且基础范式相同</h3>
<p>除了语法同源，C语言与Go语言的另一个相同点是，它们都是静态编译型语言。这意味着它们都有如下的语法特性：</p>
<ul>
<li>变量与函数都要先声明后才能使用；</li>
<li>所有分配的内存块都要有对应的类型信息，并且在确定其类型信息后才能操作；</li>
<li>源码需要先编译链接后才能运行。</li>
</ul>
<p>相似的编程逻辑与构建过程，让学习C语言的Gopher可以做到无缝衔接。</p>
<p>除此之外，Go 和C的基础编程范式都是命令式编程（imperative programming），即面向算法过程，由程序员通过编程告诉计算机应采取的动作。然后，计算机按程序指令执行一系列流程，生成特定的结果，就像菜谱指定了厨师做蛋糕时应遵循的一系列步骤一样。</p>
<p>从Go看 C，没有面向对象，没有函数式编程，没有泛型（Go 1.18已加入），满眼都是类型与函数，可以说是相当亲切了。</p>
<h3>3. 错误处理机制如出一辙</h3>
<p>对于后端编程语言来说，错误处理机制十分重要。如果两种语言的错误处理机制不同，那么这两种语言的代码整体语法风格很可能大不相同。</p>
<p>在C语言中，我们通常用一个类型为整型的函数返回值作为错误状态标识，函数调用者基于值比较的方式，对这一代表错误状态的返回值进行检视。通常，当这个返回值为0时，代表函数调用成功；当这个返回值为其他值时，代表函数调用出现错误。函数调用者需根据该返回值所代表的错误状态，来决定后续执行哪条错误处理路径上的代码。</p>
<p>C语言这种简单的<strong>基于错误值比较</strong>的错误处理机制，让每个开发人员必须显式地去关注和处理每个错误。经过显式错误处理的代码会更为健壮，也会让开发人员对这些代码更有信心。另外，这些错误就是普通的值，我们不需要额外的语言机制去处理它们，只需利用已有的语言机制，像处理其他普通类型值那样去处理错误就可以了。这让代码更容易调试，我们也更容易针对每个错误处理的决策分支进行测试覆盖。</p>
<p>C语言错误处理机制的这种简单与显式，跟Go语言的设计哲学十分契合，于是Go语言设计者决定继承这种错误处理机制。因此，当Gopher们来到C语言的世界时，无需对自己的错误处理思维做出很大的改变，就可以很容易地适应C语言的风格。</p>
<h2>三. 知己知彼，来看看C与Go的差异</h2>
<p>虽说 Gopher 学习C语言有“先天优势”，但是不经过脚踏实地的学习与实践就想掌握和精通C语言，也是不可能的。而且，C 和Go还是有很大差异的，Gopher 们只有清楚这些差异，做到“知己知彼”，才能在学习过程中分清轻重，有的放矢。俗话说，“磨刀不误砍柴功”，下面我们就一起看看C与Go有哪些不同。</p>
<h3>1. 设计哲学</h3>
<p>在人类自然语言学界，有一个很著名的假说——“<a href="https://en.wikipedia.org/wiki/Linguistic_relativity">萨丕尔-沃夫假说</a>”。这个假说的内容是这样的：<strong>语言影响或决定人类的思维方式</strong>。对我来说，<strong>编程语言也不仅仅是一门工具，它还影响着程序员的思维方式</strong>。每次开始学习一门新的编程语言时，我都会先了解这门编程语言的设计哲学。</p>
<p>每种编程语言都有自己的设计哲学，即便这门语言的设计者没有将其显式地总结出来，它也真真切切地存在，并影响着这门语言的后续演进，以及这门语言程序员的思维方式。我在<a href="http://gk.link/a/10AVZ">《Tony Bai · Go语言第一课》</a>专栏里，将Go语言的设计哲学总结成了5点，分别是<strong>简单、显式、组合、并发和面向工程</strong>。</p>
<p>那么C语言的设计哲学又是什么呢？从表面上看，简单紧凑、性能至上、极致资源、全面移植，这些都可以作为C的设计哲学，但我倾向于一种更有人文气息的说法：<strong>满足和相信程序员</strong>。</p>
<p>在这样的设计哲学下，一方面，C语言提供了几乎所有可以帮助程序员表达自己意图的语法手段，比如宏、指针与指针运算、位操作、pragma指示符、goto语句，以及跳转能力更为强大的longjmp等；另一方面，C语言对程序员的行为并没有做特别严格的限定与约束，C程序员可以利用语言提供的这些语法手段，进行天马行空的发挥：访问硬件、利用指针访问内存中的任一字节、操控任意字节中的每个位（bit）等。总之，C语言假定程序员知道他们在做什么，并选择相信程序员。</p>
<p>C语言给了程序员足够的自由，可以说，在C语言世界，你几乎可以“为所欲为”。但这种哲学也是有代价的，那就是你可能会犯一些莫名其妙的错误，比如悬挂指针，而这些错误很少或不可能在其他语言中出现。</p>
<p>这里再用一个比喻来更为形象地表达下：从Go世界到C世界，就好比在动物园中饲养已久的动物被放归到野生自然保护区，有了更多自由，但周围也暗藏着很多未曾遇到过的危险。因此，学习C语言的Gopher们要有足够的心理准备。</p>
<h3>2. 内存管理</h3>
<p>接下来我们来看C与Go在内存管理方面的不同。我把这一点放在第二位，是因为这两种语言在内存管理上有很大的差异，而且这一差异会给程序员的日常编码带来巨大影响。</p>
<p>我们知道，Go是带有垃圾回收机制（俗称GC）的静态编程语言。使用Go编程时，内存申请与释放，在栈上还是在堆上分配，以及新内存块的清零等等，这一切都是自动的，且对程序员透明。</p>
<p>但在C语言中，上面说的这些都是程序员的责任。手工内存管理在带来灵活性的同时，也带来了极大的风险，其中最常见的就是内存泄露（memory leak）与悬挂指针（dangling pointer）问题。</p>
<p>内存泄露主要指的是<strong>程序员手工在堆上分配的内存在使用后没有被释放（free），进而导致的堆内存持续增加</strong>。而悬挂指针的意思是<strong>指针指向了非法的内存地址</strong>，未初始化的指针、指针所指对象已经被释放等，都是导致悬挂指针的主要原因。针对悬挂指针进行解引用（dereference）操作将会导致运行时错误，从而导致程序异常退出的严重后果。</p>
<p>Go语言带有GC，而C语言不带GC，这都是由各自语言设计哲学所决定的。GC是不符合C语言的设计哲学的，因为一旦有了GC，程序员就远离了机器，程序员直面机器的需求就无法得到满足了。并且，一旦有了GC，无论是在性能上还是在资源占用上，都不可能做到极致了。</p>
<p>在C中，手工管理内存到底是一种什么感觉呢？作为一名有着十多年C开发经验的资深C程序员，我只能告诉你：<strong>与内存斗，其乐无穷</strong>！这是在带GC的编程语言中无法体会到的。</p>
<h3>3. 语法形式</h3>
<p>虽然C语言是Go的先祖，并且Go也继承了很多C语言的语法元素，但在变量/函数声明、行尾分号、代码块是否用括号括起、标识符作用域，以及控制语句语义等方面，二者仍有较大差异。因此，对Go已经很熟悉的程序员在初学C时，受之前编码习惯的影响，往往会踩一些“坑”。基于此，我总结了Gopher学习C语言时需要特别注意的几点，接下来我们具体看看。</p>
<p><strong>第一，注意声明变量时类型与变量名的顺序</strong></p>
<p>前面说过，Go与C都是静态编译型语言，这就要求我们在使用任何变量之前，需要先声明这个变量。但Go采用的变量声明语法颇似Pascal语言，即<strong>变量名在前，变量类型在后</strong>，这与C语言恰好相反，如下所示：</p>
<pre><code>Go:

var a, b int
var p, q *int

vs.

C：
int a, b;
int *p, *q;
</code></pre>
<p>此外，Go支持短变量声明，并且由于短变量声明更短小，无需显式提供变量类型，Go编译器会根据赋值操作符后面的初始化表达式的结果，自动为变量赋予适当类型。因此，它成为了Gopher们喜爱和重度使用的语法。但短声明在C中却不是合法的语法元素：</p>
<pre><code>int main() {
    a := 5; //  error: expected expression
    printf("a = %d\n", a);
}
</code></pre>
<p>不过，和上面的变量类型与变量名声明的顺序问题一样，C编译器会发现并告知我们这个问题，并不会给程序带来实质性的伤害。</p>
<p><strong>第二，注意函数声明无需关键字前缀</strong></p>
<p>无论是C语言还是Go语言，函数都是基本功能逻辑单元，我们也可以说<strong>C程序就是一组函数的集合</strong>。实际上，我们日常的C代码编写大多集中在实现某个函数上。</p>
<p>和变量一样，函数在两种语言中都需要先声明才能使用。Go语言使用func关键字作为<strong>函数声明的前缀</strong>，并且函数返回值列表放在函数声明的最后。但在C语言中，函数声明无需任何关键字作为前缀，函数只支持单一返回值，并且返回值类型放在函数名的前面，如下所示：</p>
<pre><code>Go：
func Add(a, b int) int {
    return a+b
}

vs.

C：
int Add(int a, int b) {
    return a+b;
}
</code></pre>
<p><strong>第三，记得加上代码行结尾的分号</strong></p>
<p>我们日常编写Go代码时，<strong>极少手写分号</strong>。这是因为，Go设计者当初为了简化代码编写，提高代码可读性，选择了<strong>由编译器在词法分析阶段自动在适当位置插入分号的技术路线</strong>。如果你是一个被Go编译器惯坏了的Gopher，来到C语言的世界后，一定不要忘记代码行尾的分号。比如上面例子中的C语言Add函数实现，在return语句后面记得要手动加上分号。</p>
<p><strong>第四，补上“省略”的括号</strong></p>
<p>同样是出于简化代码、增加可读性的考虑，Go设计者最初就取消掉了条件分支语句（if）、选择分支语句（switch）和循环控制语句（for）中条件表达式外围的小括号：</p>
<pre><code>// Go代码
func f() int {
    return 5
}
func main() {
    a := 1
    if a == 1 { // 无需小括号包裹条件表达式
        fmt.Println(a)
    }

    switch b := f(); b { // 无需小括号包裹条件表达式
    case 4:
        fmt.Println("b = 4")
    case 5:
        fmt.Println("b = 5")
    default:
        fmt.Println("b = n/a")
    }

    for i := 1; i &lt; 10; i++ { // 无需小括号包裹循环语句的循环表达式
        a += i
    }
    fmt.Println(a)
}
</code></pre>
<p>这一点恰恰与C语言“背道而驰”。因此，我们在使用C语言编写代码时，务必要想着补上这些括号：</p>
<pre><code>// C代码
int f() {
        return 5;
}

int main() {
    int a = 1;
    if (a == 1) { // 需用小括号包裹条件表达式
        printf("%d\n", a);
    }

    int b = f();
    switch (b) { // 需用小括号包裹条件表达式
    case 4:
        printf("b = 4\n");
        break;
    case 5:
        printf("b = 5\n");
        break;
    default:
        printf("b = n/a\n");
    }

    int i = 0;
    for (i = 1; i &lt; 10; i++) { // 需用小括号包裹循环语句的循环表达式
        a += i;
    }
    printf("%d\n", a);
}
</code></pre>
<p><strong>第五，留意C与Go导出符号的不同机制</strong></p>
<p>C语言通过头文件来声明对外可见的符号，所以我们不用管符号是不是首字母大写的。但在Go中，只有首字母大写的包级变量、常量、类型、函数、方法才是可导出的，即对外部包可见。反之，首字母小写的则为包私有的，仅在包内使用。Gopher一旦习惯了这样的规则，在切换到C语言时，就会产生“心理后遗症”：遇到在其他头文件中定义的首字母小写的函数时，总以为不能直接使用。</p>
<p><strong>第六，记得在switch case语句中添加break</strong></p>
<p>C 语言与Go语言在选择分支语句的语义方面有所不同：C语言的 case 语句中，如果没有显式加入break语句，那么代码将向下自动掉落执行。而Go在最初设计时就重新规定了switch case的语义，默认不自动掉落（fallthrough），除非开发者显式使用fallthrough关键字。</p>
<p>适应了Go的switch case语句的语义后再回来写C代码，就会存在潜在的“风险”。我们来看一个例子：</p>
<pre><code>// C代码：
int main() {
    int a = 1;
    switch(a) {
        case 1:printf("a = 1\n");
        case 2:printf("a = 2\n");
        case 3:printf("a = 3\n");
        default:printf("a = ?\n");
    }
}
</code></pre>
<p>这段代码是按Go语义编写的switch case，编译运行后得到的结果如下：</p>
<pre><code>a = 1
a = 2
a = 3
a = ?
</code></pre>
<p>这显然不符合我们输出“a = 1”的预期。对于初学C的Gopher而言，这个问题影响还是蛮大的，因为这样编写的代码在C编译器眼中是完全合法的，但所代表的语义却完全不是开发人员想要的。这样的程序一旦流入到生产环境，其缺陷可能会引发生产故障。</p>
<p>一些Clint 工具可以检测出这样的问题，因此对于写C代码的Gopher，我建议在提交代码前使用lint工具对代码做一下检查。</p>
<h3>4. 构建机制</h3>
<p>Go与C都是静态编译型语言，它们的源码需要经过编译器和链接器处理，这个过程称为<strong>构建(build)</strong>，构建后得到的可执行文件才是最终交付给用户的成果物。</p>
<p>和Go语言略有不同的是，C语言的构建还有一个预处理（pre-processing）阶段，预处理环节的输出才是C编译器的真正输入。C语言中的宏就是在预处理阶段展开的。不过，Go没有预处理阶段。</p>
<p>C语言的编译单元是一个C源文件（.c），每个编译单元在编译过程中会对应生成一个目标文件（.o/.obj），最后链接器将这些目标文件链接在一起，形成可执行文件。</p>
<p>而Go则是以一个包（package）为编译单元的，每个包内的源文件生成一个.o文件，一个包的所有.o文件聚合（archive）成一个.a文件，链接器将这些目标文件链接在一起形成可执行文件。</p>
<p>Go语言提供了统一的Go命令行工具链，且Go编译器原生支持增量构建，源码构建过程不需要Gopher手工做什么配置。但在C语言的世界中，用于构建C程序的工具有很多，主流的包括gcc/clang，以及微软平台的C编译器。这些编译器原生不支持增量构建，为了提升工程级构建的效率，避免每次都进行全量构建，我们通常会使用第三方的构建管理工具，比如make（Makefile）或CMake。考虑移植性时，我们还会使用到configure文件，用于在目标机器上收集和设置编译器所需的环境信息。</p>
<h3>5. 依赖管理</h3>
<p>我在前面提过，C语言仅提供了一个“小内核”。像依赖管理这类的事情，C语言本身并没有提供跟Go中的Go Module类似的，统一且相对完善的解决方案。在C语言的世界中，我们依然要靠外部工具（比如CMake）来管理第三方的依赖。</p>
<p>C语言的第三方依赖通常以静态库（.a）或动态共享库（.so）的形式存在。如果你的应用要使用静态链接，那就必须在系统中为C编译器提供第三方依赖的静态库文件。但在实际工作中，完全采用静态链接有时是会遇到麻烦的。这是因为，很多操作系统在默认安装时是不带开发包的，也就是说，像 libc、libpthread 这样的系统库只提供了动态共享库版本（如/lib下提供了libc的共享库libc.so.6），其静态库版本是需要自行下载、编译和安装的（如libc的静态库libc.a在安装后是放在/usr/lib下面的)。所以<strong>多数情况下，我们是将****静态、动态****两种链接方式混合在一起使用的</strong>，比如像libc这样的系统库多采用动态链接。</p>
<p>动态共享库通常是有版本的，并且按照一定规则安装到系统中。举个例子，一个名为libfoo的动态共享库，在安装的目录下文件集合通常是这样：</p>
<pre><code>2022-03-10 12:28 libfoo.so -&gt; libfoo.so.0.0.0*
2022-03-10 12:28 libfoo.so.0 -&gt; libfoo.so.0.0.0*
2022-03-10 12:28 libfoo.so.0.0.0*
</code></pre>
<p>按惯例，每个动态共享库都有多个名字属性，包括real name、soname和linker name。下面我们来分别看下。</p>
<ul>
<li>real name：实际包含共享库代码的那个文件的名字(如上面例子中的libfoo.so.0.0.0)。动态共享库的真实版本信息就在real name中，显然real name中的版本号符合<a href="https://semver.org/">语义版本规范</a>，即major.minor.patch。当两个版本的major号一致，说明是向后兼容的两个版本；</li>
<li>soname：shared object name的缩写，也是这三个名字中最重要的一个。无论是在编译阶段还是在运行阶段，系统链接器都是通过动态共享库的soname（如上面例子中的libfoo.so.0）来唯一识别共享库的。我们看到的soname实际上是仅包含major号的共享库名字；</li>
<li>linker name：编译阶段提供给编译器的名字（如上面例子中的libfoo.so）。如果你构建的共享库的real name跟上面例子中libfoo.so.0.0.0类似，带有版本号，那么你在编译器命令中直接使用-L path -lfoo是无法让链接器找到对应的共享库文件的，除非你为libfoo.so.0.0.0提供了一个linker name（如libfoo.so，一个指向libfoo.so.0.0.0的符号链接）。linker name一般在共享库安装时手工创建。<br />
动态共享库有了这三个名称属性，依赖管理就有了依据。但由于在链接的时候使用的是linker name，而linker name并不带有版本号，真实版本与主机环境有关，因此要实现C应用的可重现构建还是比较难。在实践中，我们通常会使用专门的构建主机，项目组将该主机上的依赖管理起来，进而保证每次构建所使用的依赖版本是可控的。同时，应用部署的目标主机上的依赖版本也应该得到管理，避免运行时出现动态共享库版本不匹配的问题。</li>
</ul>
<h3>6. 代码风格</h3>
<p>Go语言是历史上首次实现了代码风格全社区统一的编程语言。它基本上消除了开发人员在代码风格上的无休止的、始终无法达成一致的争论，以及不同代码风格带来的阅读、维护他人代码时的低效。gofmt工具格式化出来的代码风格已经成为Go开发者的一种共识，融入到Go语言的开发文化当中了。所以，如果你让某个Go开发者说说gofmt后的代码风格是什么样的，多数Go开发者可能说不出，因为代码会被gofmt自动变成那种风格，大家已经不再关心风格了。</p>
<p>而在C语言的世界，代码风格仍存争议。但经过多年的演进，以及像Go这样新兴语言的不断“教育”，C社区也在尝试进行这方面的改进，涌现出了像<a href="https://clang.llvm.org/docs/ClangFormat.html">clang-format</a>这样的工具。目前，虽然还没有在全社区达成一致的代码风格（由于历史原因，这很难做到），但已经可以减少很多不必要的争论。</p>
<p>对于正在学习C语言，并进行C编码实践的Gopher，我的建议是：<strong>不要拘泥于使用什么代码风格，先用clang-format，并确定一套风格模板就好</strong>。</p>
<h2>四. 小结</h2>
<p>作为一名对Go跟随和研究了近十年的程序员，我深刻体会到，Go的简单性、性能和生产力使它成为了创建面向用户的应用程序和服务的理想语言。快速的迭代让团队能够快速地作出反应，以满足用户不断变化的需求，让团队可以将更多精力集中在保持灵活性上。</p>
<p>但Go也有缺点，比如缺少对内存以及一些低级操作的精确控制，而C语言恰好可以弥补这个缺陷。C 语言提供的更精细的控制允许更多的精确性，使得C成为低级操作的理想语言。这些低级操作不太可能发生变化，并且C相比Go还提高了性能。所以，如果你是一个有性能与低级操作需求的 Gopher ，就有充分的理由来学习C语言。</p>
<p>C 的优势体现在最接近底层机器的地方，而Go的优势在离用户较近的地方能得到最大发挥。当然，这并不是说两者都不能在对方的空间里工作，但这样做会增加“摩擦”。当你的需求从追求灵活性转变为注重效率时，用C重写库或服务的理由就更充分了。</p>
<p>总之，虽然Go和C的设计有很大的不同，但它们也有很多相似性，具备发挥兼容优势的基础。并且，当我们同时使用这二者时，就可以既有很大的灵活性，又有很好的性能，可以说是相得益彰！</p>
<h2>五. 写在最后</h2>
<p>今天的加餐中，我主要是基于C与Go的比较来讲解的，对于Go语言的特性并没有作详细展开。如果你还想进一步了解Go语言的设计哲学、语法特性、程序设计相关知识，欢迎来学习我在极客时间上的专栏<a href="http://gk.link/a/10AVZ">《Tony Bai ·Go语言第一课》</a>。在这门课里，我会用我十年Gopher的经验，带给你一条系统、完整的Go语言入门路径。</p>
<p>感谢你看到这里，如果今天的内容让你有所收获，欢迎把它分享给你的朋友。</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>微信公众号：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/05/16/the-short-guide-of-embracing-c-lang-for-gopher/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>BPF和Go：在Linux中内省的现代方式[译]</title>
		<link>https://tonybai.com/2020/12/25/bpf-and-go-modern-forms-of-introspection-in-linux/</link>
		<comments>https://tonybai.com/2020/12/25/bpf-and-go-modern-forms-of-introspection-in-linux/#comments</comments>
		<pubDate>Fri, 25 Dec 2020 09:31:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bcc]]></category>
		<category><![CDATA[BPF]]></category>
		<category><![CDATA[bpftrace]]></category>
		<category><![CDATA[BrendanGregg]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[cloudflare]]></category>
		<category><![CDATA[FaceBook]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.16]]></category>
		<category><![CDATA[gobpf]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[handler]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[iovisor]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[kprobe]]></category>
		<category><![CDATA[libbcc]]></category>
		<category><![CDATA[libbpf]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Stack]]></category>
		<category><![CDATA[Tcpdump]]></category>
		<category><![CDATA[Trace]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[uprobe]]></category>
		<category><![CDATA[XDP]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[可观测性]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3051</guid>
		<description><![CDATA[本文翻译自马可·凯瓦克（Marko Kevac）的《BPF and Go: Modern forms of introspection in Linux》(https://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223)。 每个人都有自己喜欢的关于魔法的书。对于一个人来说是托尔金，对于另一个人来说是普拉切特，对于第三个人来说，比如我，是马克斯-弗雷。今天我要给大家讲的是我最喜欢的IT魔法：BPF以及围绕它的现代基础设施。 BPF目前正处于普及的高峰期。这项技术正在飞速发展，深入到意想不到的地方，并且越来越容易被普通用户所接受。现在几乎每个流行的会议都有关于这个主题的演讲，早在8月份，我就应邀在俄罗斯GopherCon上(GopherCon Russia)做了这方面主题的演讲。 我在这方面有着很好的体验，所以我想和尽可能多的人分享一下。这篇文章将为你介绍为什么我们需要像BPF这样的东西，帮助你了解何时、如何使用它，以及它如何帮助作为工程师的你改善你正在进行的项目。我们还将看看它与Go的一些相关内容。 我真正希望的是，你看完这篇文章后，就像小孩子第一次读完《哈利波特》后的眼睛一样，开始发亮，并且希望你自己亲自去尝试一下这个新“玩具”。 一点点的背景 好吧，一个34岁的大胡子，眼神灼灼的告诉你这个魔法是什么？ 我们生活在2020年。打开Twitter，你可以读到愤怒的技术人士的推文，他们都在说，今天编写的软件质量太糟糕了，都需要扔掉，我们需要重新开始。有些人甚至威胁要彻底离开这个行业，因为他们实在无法忍受所有东西都坏了，不方便又慢。 他们可能是对的：如果不查阅千篇一律的评论，就无法确定原因。但有一点我绝对同意，那就是现代软件堆栈比以往任何时候都要复杂：我们有BIOS、EFI、操作系统、驱动程序、模块、库、网络交互、数据库、缓存、编排器（比如K8s）、Docker容器，最后还有我们自己的带有运行时和垃圾收集的软件。 一个真正的专业人士可能会花上几天时间来为你解释在浏览器中输入google.com之后会发生什么。 要了解你的系统里面发生了什么，是非常复杂的，尤其是在目前，事情出了问题，你正在损失金钱的情况下。正是因为这个问题，才出现了帮你搞清楚系统内部情况的企业。在大公司里，有整整一个部门的福尔摩斯式的侦探，他们只知道在哪里敲敲锤子，在哪里拧紧螺栓就能节省数百万美元。 我喜欢问人们如何在最短的时间内调试突发问题。大多数情况下，人们首先想到的方法是分析日志。但问题是，能获取的日志只局限于开发者放在系统中的日志，这是不灵活的。 第二种最流行的方法是研究度量数据。最流行的三个研究度量数据的系统都是用Go编写的。度量数据是非常有帮助的，然而，虽然它们确实可以让你看到症状，但它们并不总是能帮助你定义出问题的根本原因。 第三种是所谓的“可观察性”：你可以对系统的行为提出尽可能多的复杂问题，并获得这些问题的答案。由于问题可能非常复杂，所以答案可能需要最广泛的信息，而在问题被提出之前，我们并不知道这些信息是什么。而这意味着，可观察性绝对要求灵活性。 提供一个机会来改变”在飞行中”的日志级别呢？使用调试器，在程序运行时连接到程序，并在不中断程序工作的情况下做一些事情呢？了解哪些查询被发送到系统中，可视化慢速查询的来源，通过pprof看看什么在占用内存，并获得其随时间变化的曲线图？测量一个函数的延迟以及延迟对参数的依赖性呢？我想把所有这些方法都归入可观察性这个总称之下。这是一组实用工具、方法、知识和经验，它们结合在一起，给了我们机会，如果不能做到我们想做的所有事情，但至少可以在系统工作时，在系统中“现场”做很多事情。它相当于现代IT界的一把瑞士军刀。 但我们如何才能实现这一点呢？市场上已经存在很多类似的工具：有简单的，有复杂的，有危险的并且也有缓慢的。但今天的文章是关于BPF的。 Linux内核是一个事件驱动的系统。实际上，在内核和系统中发生的所有事情，都可以被认为是一组事件。中断是一个事件；通过网络接收一个数据包是一个事件；将处理器的控制权转移到另一个进程是一个事件；运行一个函数是一个事件。 对，所以BPF是Linux内核的一个子系统，它让你有机会编写小程序，这些小程序将在内核响应事件时被运行。这些程序既可以让你知道系统中发生了什么，也可以用于控制系统。 现在让我们来了解一下具体的内容。 什么是eBPF？ BPF的第一个版本在1994年问世。你们中的一些人可能会在为tcpdump工具编写简单的规则时遇到过它，该工具用于查看或”嗅探”网络数据包。你可以为tcpdump设置过滤器，所以你不必查看所有的数据包&#8211;只查看你感兴趣的数据包。例如，”只查看tcp协议和80端口”。对于每一个经过的数据包，都会运行一个函数来决定你是否需要保存这个特定的数据包。可以有非常多的数据包，所以我们的函数必须要快。事实上，我们的tcpdump过滤器被转化成了BPF函数。下面是一个例子。 最初的BPF代表了一个非常简单的虚拟机，有几个寄存器。但尽管如此，BPF还是大大加快了网络数据包的过滤速度。在当时，这是一个重大的进步。 2014年，一位非常著名的内核黑客Alexei Starovoitov对BPF的功能进行了扩展。他增加了寄存器的数量和程序允许的大小，增加了JIT编译，并创建了一个用于检查程序是否安全的程序。然而，最令人印象深刻的是，新的BPF程序不仅能够在处理数据包时运行，而且能够响应其他内核事件，并在内核和用户空间之间来回传递信息。 这些变化为使用BPF的新方法提供了机会。一些过去需要通过编写复杂而危险的内核模块来实现的事情，现在可以相对简单地通过BPF来完成。为什么这么好呢？因为在编写模块的时候，任何错误往往都会导致恐慌(panic)，这可不是Go语言中的恐慌(panic)，而是内核恐慌。一旦发生，我们唯一能做的就是重启(操作系统)。 普通的Linux用户突然拥有了一种新的超能力：能够查看”引擎盖下的情况”&#8211;这在以前只有核心内核开发者才有，或者说根本就没有人能够做到。这个选项可以和为iOS或Android编写程序的能力相提并论：在旧手机上，这要么是不可能的，要么就是太复杂。 Alexei Starovoitov的新版本的BPF被称为eBPF（e代表扩展：extended）。但现在，它已经取代了所有旧版的BPF用法，并且已经变得非常流行，为了简单起见，它仍然被称为BPF。 BPF用在哪里？ 好了，我们可以将BPF程序附加到哪些事件或触发器上呢，人们又是如何开始使用他们获得的新力量的呢？ 目前，触发器主要有两组。 第一组是用于处理网络数据包和管理网络流量的。这是XDP、流量控制事件和其他几个。 以下情况需要这些事件： 创建简单但非常有效的防火墙。Cloudflare和Facebook等公司使用BPF程序来过滤掉大量的寄生流量，并对抗最大规模的DDoS攻击。由于处理发生在数据包生命的最早阶段，直接在内核中进行（一个BPF程序有时甚至直接推送到网卡中进行处理），所以巨量的流量可以通过这种方式进行处理。这些事情过去都是在专门的网络硬件上完成的。 创建更智能、更有针对性、但性能更强的防火墙&#8211;这些防火墙可以检查通过的流量是否符合公司规则，是否存在漏洞模式等。例如，Facebook在内部进行这种审计，而一些项目则对外销售这类产品。 创建智能负载均衡器。最突出的例子是Cilium项目，它最常被用作K8s集群中的网格网络。Cilium对流量进行管理，平衡、重定向和分析。而所有这些都是在内核运行的小型BPF程序的帮助下完成的，以响应与网络数据包或套接字有关的这个或那个事件。 这是第一组与网络问题有关的触发器，并能够影响网络通信行为。第二组与更普遍的可观察性有关；这组中的程序大多时候无法影响任何事情，而只能”观察”。这是我比较感兴趣的。 在这组中，有如下触发器。 perf events &#8211; 与性能和perf Linux剖析器有关的事件：硬件处理器计数器，中断处理，拦截主要/次要内存异常等等。例如，我们可以设置一个处理程序，它将在每次内核需要从swap读取内存页时运行。例如，想象一下，一个显示当前使用swap的程序的工具。 tracepoints &#8211; [...]]]></description>
			<content:encoded><![CDATA[<p>本文翻译自马可·凯瓦克（Marko Kevac）的<a href="https://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223">《BPF and Go: Modern forms of introspection in Linux》</a>(https://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223)。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-1.png" alt="img{512x368}" /></p>
<p>每个人都有自己喜欢的关于魔法的书。对于一个人来说是托尔金，对于另一个人来说是普拉切特，对于第三个人来说，比如我，是马克斯-弗雷。今天我要给大家讲的是我最喜欢的IT魔法：<a href="http://en.wikipedia.org/wiki/Berkeley_Packet_Filter">BPF</a>以及围绕它的现代基础设施。</p>
<p>BPF目前正处于普及的高峰期。这项技术正在飞速发展，深入到意想不到的地方，并且越来越容易被普通用户所接受。现在几乎每个流行的会议都有关于这个主题的演讲，早在8月份，我就应邀在俄罗斯GopherCon上(GopherCon Russia)做了这方面主题的演讲。</p>
<p>我在这方面有着很好的体验，所以我想和尽可能多的人分享一下。这篇文章将为你介绍为什么我们需要像BPF这样的东西，帮助你了解何时、如何使用它，以及它如何帮助作为工程师的你改善你正在进行的项目。我们还将看看它与Go的一些相关内容。</p>
<p>我真正希望的是，你看完这篇文章后，就像小孩子第一次读完《哈利波特》后的眼睛一样，开始发亮，并且希望你自己亲自去尝试一下这个新“玩具”。</p>
<h2>一点点的背景</h2>
<p>好吧，一个34岁的大胡子，眼神灼灼的告诉你这个魔法是什么？</p>
<p>我们生活在2020年。打开Twitter，你可以读到愤怒的技术人士的推文，他们都在说，今天编写的软件质量太糟糕了，都需要扔掉，我们需要重新开始。有些人甚至威胁要彻底离开这个行业，因为他们实在无法忍受所有东西都坏了，不方便又慢。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-2.png" alt="img{512x368}" /></p>
<p>他们可能是对的：如果不查阅千篇一律的评论，就无法确定原因。但有一点我绝对同意，那就是现代软件堆栈比以往任何时候都要复杂：我们有BIOS、EFI、操作系统、驱动程序、模块、库、网络交互、数据库、缓存、编排器（比如K8s）、Docker容器，最后还有我们自己的带有运行时和垃圾收集的软件。</p>
<p>一个真正的专业人士可能会花上几天时间来为你解释在浏览器中输入google.com之后会发生什么。</p>
<p>要了解你的系统里面发生了什么，是非常复杂的，尤其是在目前，事情出了问题，你正在损失金钱的情况下。正是因为这个问题，才出现了帮你搞清楚系统内部情况的企业。在大公司里，有整整一个部门的福尔摩斯式的侦探，他们只知道在哪里敲敲锤子，在哪里拧紧螺栓就能节省数百万美元。</p>
<p>我喜欢问人们如何在最短的时间内调试突发问题。大多数情况下，人们首先想到的方法是<strong>分析日志</strong>。但问题是，能获取的日志只局限于开发者放在系统中的日志，这是不灵活的。</p>
<p>第二种最流行的方法是<strong>研究度量数据</strong>。最流行的三个研究度量数据的系统都是用Go编写的。度量数据是非常有帮助的，然而，虽然它们确实可以让你看到症状，但它们并不总是能帮助你定义出问题的根本原因。</p>
<p>第三种是所谓的“可观察性”：你可以对系统的行为提出尽可能多的复杂问题，并获得这些问题的答案。由于问题可能非常复杂，所以答案可能需要最广泛的信息，而在问题被提出之前，我们并不知道这些信息是什么。而这意味着，可观察性绝对要求灵活性。</p>
<p>提供一个机会来改变”在飞行中”的日志级别呢？使用调试器，在程序运行时连接到程序，并在不中断程序工作的情况下做一些事情呢？了解哪些查询被发送到系统中，可视化慢速查询的来源，通过pprof看看什么在占用内存，并获得其随时间变化的曲线图？测量一个函数的延迟以及延迟对参数的依赖性呢？我想把所有这些方法都归入<strong>可观察性</strong>这个总称之下。这是一组实用工具、方法、知识和经验，它们结合在一起，给了我们机会，如果不能做到我们想做的所有事情，但至少可以在系统工作时，在系统中“现场”做很多事情。它相当于现代IT界的一把瑞士军刀。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-3.png" alt="img{512x368}" /></p>
<p>但我们如何才能实现这一点呢？市场上已经存在很多类似的工具：有简单的，有复杂的，有危险的并且也有缓慢的。但今天的文章是关于BPF的。</p>
<p>Linux内核是一个事件驱动的系统。实际上，在内核和系统中发生的所有事情，都可以被认为是一组事件。中断是一个事件；通过网络接收一个数据包是一个事件；将处理器的控制权转移到另一个进程是一个事件；运行一个函数是一个事件。</p>
<p>对，所以BPF是Linux内核的一个子系统，它让你有机会编写小程序，这些小程序将在内核响应事件时被运行。这些程序既可以让你知道系统中发生了什么，也可以用于控制系统。</p>
<p>现在让我们来了解一下具体的内容。</p>
<h2>什么是eBPF？</h2>
<p>BPF的第一个版本在1994年问世。你们中的一些人可能会在为tcpdump工具编写简单的规则时遇到过它，该工具用于查看或”嗅探”网络数据包。你可以为tcpdump设置过滤器，所以你不必查看所有的数据包&#8211;只查看你感兴趣的数据包。例如，”只查看tcp协议和80端口”。对于每一个经过的数据包，都会运行一个函数来决定你是否需要保存这个特定的数据包。可以有非常多的数据包，所以我们的函数必须要快。事实上，我们的tcpdump过滤器被转化成了BPF函数。下面是一个例子。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-4.png" alt="img{512x368}" /></p>
<p>最初的BPF代表了一个非常简单的虚拟机，有几个寄存器。但尽管如此，BPF还是大大加快了网络数据包的过滤速度。在当时，这是一个重大的进步。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-5.png" alt="img{512x368}" /></p>
<p>2014年，一位非常著名的内核黑客<a href="https://www.linkedin.com/in/alexey1/">Alexei Starovoitov</a>对BPF的功能进行了扩展。他增加了寄存器的数量和程序允许的大小，增加了JIT编译，并创建了一个用于检查程序是否安全的程序。然而，最令人印象深刻的是，新的BPF程序不仅能够在处理数据包时运行，而且能够响应其他内核事件，并在内核和用户空间之间来回传递信息。</p>
<p>这些变化为使用BPF的新方法提供了机会。一些过去需要通过编写复杂而危险的内核模块来实现的事情，现在可以相对简单地通过BPF来完成。为什么这么好呢？因为在编写模块的时候，任何错误往往都会导致恐慌(panic)，这可不是Go语言中的恐慌(panic)，而是内核恐慌。一旦发生，我们唯一能做的就是重启(操作系统)。</p>
<p>普通的Linux用户突然拥有了一种新的超能力：能够查看”引擎盖下的情况”&#8211;这在以前只有核心内核开发者才有，或者说根本就没有人能够做到。这个选项可以和为iOS或Android编写程序的能力相提并论：在旧手机上，这要么是不可能的，要么就是太复杂。</p>
<p>Alexei Starovoitov的新版本的BPF被称为eBPF（e代表扩展：extended）。但现在，它已经取代了所有旧版的BPF用法，并且已经变得非常流行，为了简单起见，它仍然被称为BPF。</p>
<h2>BPF用在哪里？</h2>
<p>好了，我们可以将BPF程序附加到哪些事件或触发器上呢，人们又是如何开始使用他们获得的新力量的呢？</p>
<p>目前，触发器主要有两组。</p>
<p>第一组是用于处理网络数据包和管理网络流量的。这是XDP、流量控制事件和其他几个。</p>
<p>以下情况需要这些事件：</p>
<ul>
<li>
<p>创建简单但非常有效的防火墙。Cloudflare和Facebook等公司使用BPF程序来过滤掉大量的寄生流量，并对抗最大规模的DDoS攻击。由于处理发生在数据包生命的最早阶段，直接在内核中进行（一个BPF程序有时甚至直接推送到网卡中进行处理），所以巨量的流量可以通过这种方式进行处理。这些事情过去都是在专门的网络硬件上完成的。</p>
</li>
<li>
<p>创建更智能、更有针对性、但性能更强的防火墙&#8211;这些防火墙可以检查通过的流量是否符合公司规则，是否存在漏洞模式等。例如，Facebook在内部进行这种审计，而一些项目则对外销售这类产品。</p>
</li>
<li>
<p>创建智能负载均衡器。最突出的例子是Cilium项目，它最常被用作K8s集群中的网格网络。Cilium对流量进行管理，平衡、重定向和分析。而所有这些都是在内核运行的小型BPF程序的帮助下完成的，以响应与网络数据包或套接字有关的这个或那个事件。</p>
</li>
</ul>
<p>这是第一组与网络问题有关的触发器，并能够影响网络通信行为。第二组与更普遍的可观察性有关；这组中的程序大多时候无法影响任何事情，而只能”观察”。这是我比较感兴趣的。</p>
<p>在这组中，有如下触发器。</p>
<ul>
<li>
<p>perf events &#8211; 与性能和perf Linux剖析器有关的事件：硬件处理器计数器，中断处理，拦截主要/次要内存异常等等。例如，我们可以设置一个处理程序，它将在每次内核需要从swap读取内存页时运行。例如，想象一下，一个显示当前使用swap的程序的工具。</p>
</li>
<li>
<p>tracepoints &#8211; 内核源代码中的静态（由开发者定义）位置，你可以通过附加到这些位置来提取静态信息（由开发者早先准备的信息）。在这种情况下，静态似乎是一件坏事，因为我说过，日志的缺点之一是它们只包含程序员最初放在那里的东西。从某种意义上说，这是对的，但tracepoints有三个重要的优点。</p>
<ul>
<li>有相当多的跟踪点散落在内核中最有趣的地方。</li>
<li>当它们不 “开启 “时，它们不使用任何资源。</li>
<li>它们是API的一部分，它们是稳定的，而且不会改变。这一点非常重要，因为我们将要提到的其他触发器缺乏稳定的API。</li>
</ul>
</li>
</ul>
<p>例如，想象一下，一个有关显示的工具程序(utility)，由于某种原因，内核没有给它执行的时间。你坐着想知道为什么它这么慢，而pprof却没有什么有趣的东西可以显示。</p>
<ul>
<li>USDT &#8211; 和tracepoints是一样的，但是是针对用户空间的程序。也就是说，作为一个程序员，你可以把这些位置添加到你的程序中。而且很多大规模的知名程序和编程语言已经采用了这些trace。比如：MySQL，或者PHP和Python等语言。通常它们的默认设置是”关闭”，如果要打开它们，你需要使用&#8211;enable-dtrace参数或类似的参数来重建解释器。是的，我们也可以在Go中注册这些类型的跟踪。你可能已经认出了参数名称中的单词<a href="https://en.wikipedia.org/wiki/DTrace"><strong>DTrace</strong></a>。重点是，这种静态跟踪是由<a href="https://en.wikipedia.org/wiki/Solaris_(operating_system)">Solaris操作系统</a>中诞生的同名系统所推广的。举个例子，想象一下，当一个新的线程被创建时，当一个GC或其他与特定语言或系统有关的东西被启动时，我们都能够觉察到。</li>
</ul>
<p>这就是另一个层次的魔法开始的地方。</p>
<ul>
<li>
<p>Ftrace触发器让我们可以选择在内核的任何功能开始时运行一个BPF程序。完全是动态的。这意味着内核会在你选择的任何内核函数开始执行之前，或者在所有内核函数开始执行之前，调用你的BPF函数&#8211;无论哪个，你都可以连接到所有的内核函数，并在输出时获得所有调用的可视化效果。</p>
</li>
<li>
<p>kprobes/uprobes给你提供的东西和ftrace几乎一样，但是你可以选择在内核和用户空间执行一个函数时附加到任何位置。如果在函数中间，有一个变量上的&#8217;if&#8217;，而你需要为这个变量建立一个值的直方图，那就不是问题了。</p>
</li>
<li>
<p>kretprobes/uretprobes&#8211;这里的一切类似于前面的触发器，但可以在内核函数或用户空间的函数返回时触发。这类触发器对于查看函数返回的内容，以及测量执行时间都很方便。例如，你可以查看&#8217;fork&#8217;系统调用返回的是哪个PID。</p>
</li>
</ul>
<p>关于这一切，我重复一遍，最美妙的事情是，当我们的BPF程序响应这些触发器而被调用后，我们的BPF程序可以好好的 “观察”一下：读取函数的参数，记录时间，读取变量，读取全局变量，进行堆栈跟踪，为以后保存一些东西，将数据发送到用户空间进行处理，和/或从用户空间获取数据或一些其他控制命令进行过滤。太棒了！</p>
<p>我不知道你是怎么想的，但对我来说，这个新的基础架构就像一个我一直想得到的玩具。</p>
<h2>API：如何使用它</h2>
<p>好了，马科，你已经说服了我们去看看BPF。现在我们怎么才能仔细看看呢？</p>
<p>让我们看看BPF程序由什么组成，以及如何与它交互。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-6.png" alt="img{512x368}" /></p>
<p>首先，我们有一个BPF程序，如果它通过验证，将被加载到内核中。在那里，它将被JIT编译器编译成机器代码，并在内核模式下运行，这时附加的触发器(trigger)将被激活。</p>
<p>BPF程序可以选择与第二部分，即与用户空间程序交互。有两种方式可以实现。我们可以向循环缓冲区写，用户空间部分可以从它那里读。我们也可以对键值图(key-value map)进行写和读，也就是所谓的BPF图(BPF map)，相应的，用户空间部分，也可以做同样的事情，这样，它们就可以互相传递信息了。</p>
<h2>基本用途</h2>
<p>最简单的BPF工作方式，但却是你在任何情况下都不应该采用的从头开始的方式，就是用C语言编写BPF程序，然后用Clang编译器，将相关代码编译成虚拟机的代码。然后，我们加载这些代码，直接使用BPF系统调用，与我们的BPF程序进行交互，也使用BPF系统调用。</p>
<p>第一个可用的简化方法是使用libbpf库。这是和内核的源代码一起提供的，可以让你直接使用BPF系统调用。基本上，它提供了方便的包装器来加载代码，以及使用BPF映射(BPF map)来从内核向用户空间发送数据并返回。</p>
<h2>bcc</h2>
<p>显然，这对人们来说是远远不够方便的。幸运的是，在iovizor这个品牌下，出现了BCC项目，这让我们的生活变得更加方便。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-7.png" alt="img{512x368}" /></p>
<p>基本上，它为我们准备了整个构建环境，让我们可以编写单个的BPF程序，其中С部分会自动构建并加载到内核中，而用户空间部分则可以用Python制作，简单明了。</p>
<h2><a href="https://github.com/iovisor/bpftrace">bpftrace</a></h2>
<p>但是，BCC似乎仍有很多事情很复杂。由于某些原因，人们特别不喜欢用С来写底层那部分。</p>
<p>那些来自iovizor的人也提供了一个工具&#8211;bpftrace，它可以让你用类似AWK的简单脚本语言（甚至是单行代码）来编写BPF脚本。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-8.png" alt="img{512x368}" /></p>
<p>Brendan Gregg是生产力和可观察性领域的著名专家，他为可用的BPF工作方式制作了以下的图片。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-9.png" alt="img{512x368}" /></p>
<p>纵轴显示的是某个工具的易用性，而横轴显示的是它的能力。你可以看到，BCC是一个非常强大的工具，但它并不是超级简单的工具。</p>
<h2>使用BPF的例子</h2>
<p>让我们来看看一些具体的例子，看看我们已经可以使用的这种神奇力量。</p>
<p>BCC和bpftrace都包含了一个”工具”目录，其中包含了大量有趣而有用的即用型脚本。它们也可以作为本地的Stack Overflow使用，你可以从中复制代码块用于自己的脚本。</p>
<p>例如，这里是显示DNS查询延迟的脚本。</p>
<pre><code>╭─marko@marko-home ~
╰─$ sudo gethostlatency-bpfcc
TIME  PID COMM        LATms HOST
16:27:32 21417 DNS Res~ver #93   3.97 live.github.com
16:27:33 22055 cupsd        7.28 NPI86DDEE.local
16:27:33 15580 DNS Res~ver #87   0.40 github.githubassets.com
16:27:33 15777 DNS Res~ver #89   0.54 github.githubassets.com
16:27:33 21417 DNS Res~ver #93   0.35 live.github.com
16:27:42 15580 DNS Res~ver #87   5.61 ac.duckduckgo.com
16:27:42 15777 DNS Res~ver #89   3.81 www.facebook.com
16:27:42 15777 DNS Res~ver #89   3.76 tech.badoo.com <img src='https://tonybai.com/wp-includes/images/smilies/icon_smile.gif' alt=':-)' class='wp-smiley' />
16:27:43 21417 DNS Res~ver #93   3.89 static.xx.fbcdn.net
16:27:43 15580 DNS Res~ver #87   3.76 scontent-frt3-2.xx.fbcdn.net
16:27:43 15777 DNS Res~ver #89   3.50 scontent-frx5-1.xx.fbcdn.net
16:27:43 21417 DNS Res~ver #93   4.98 scontent-frt3-1.xx.fbcdn.net
16:27:44 15580 DNS Res~ver #87   5.53 edge-chat.facebook.com
16:27:44 15777 DNS Res~ver #89   0.24 edge-chat.facebook.com
16:27:44 22099 cupsd        7.28 NPI86DDEE.local
16:27:45 15580 DNS Res~ver #87   3.85 safebrowsing.googleapis.com
^C%
</code></pre>
<p>一个实时显示DNS查询完成时间的实用工具，例如，你可以抓住一些意想不到的异常值。</p>
<p>下面是一个可以”监视”别人在终端上输入的内容的脚本。</p>
<pre><code>╭─marko@marko-home ~
╰─$ sudo bashreadline-bpfcc
TIME  PID COMMAND
16:51:42 24309 uname -a
16:52:03 24309 rm -rf src/badoo
</code></pre>
<p>这种脚本可以用来捕捉”坏邻居”，或者对公司的服务器进行安全审计。</p>
<p>下面是一个输出高级语言函数调用链的脚本。</p>
<pre><code>╭─marko@marko-home ~/tmp
╰─$ sudo /usr/sbin/lib/uflow -l python 20590
Tracing method calls in python process 20590... Ctrl-C to quit.
CPU PID TID TIME(us) METHOD
5  20590 20590 0.173 -&gt; helloworld.py.hello
5  20590 20590 0.173  -&gt; helloworld.py.world
5  20590 20590 0.173  &lt;- helloworld.py.world
5  20590 20590 0.173 &lt;- helloworld.py.hello
5  20590 20590 1.174 -&gt; helloworld.py.hello
5  20590 20590 1.174  -&gt; helloworld.py.world
5  20590 20590 1.174  &lt;- helloworld.py.world
5  20590 20590 1.174 &lt;- helloworld.py.hello
5  20590 20590 2.175 -&gt; helloworld.py.hello
5  20590 20590 2.176  -&gt; helloworld.py.world
5  20590 20590 2.176  &lt;- helloworld.py.world
5  20590 20590 2.176 &lt;- helloworld.py.hello
6  20590 20590 3.176 -&gt; helloworld.py.hello
6  20590 20590 3.176  -&gt; helloworld.py.world
6  20590 20590 3.176  &lt;- helloworld.py.world
6  20590 20590 3.176 &lt;- helloworld.py.hello
6  20590 20590 4.177 -&gt; helloworld.py.hello
6  20590 20590 4.177  -&gt; helloworld.py.world
6  20590 20590 4.177  &lt;- helloworld.py.world
6  20590 20590 4.177 &lt;- helloworld.py.hello
^C%
</code></pre>
<p>下面这个例子显示了Python中程序的调用栈。(译注：原文似乎缺了这块的代码)。</p>
<p>Brendan Gregg 制作了一张图片，它汇集了所有相关的脚本，箭头指向每个实用程序允许你观察的子系统。正如你所看到的，我们已经有了大量的现成的实用程序供我们使用&#8211;几乎可以应对任何可能的情况。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-10.png" alt="img{512x368}" /></p>
<h2>那Go语言呢？</h2>
<p>现在我们来谈谈Go。我们有两个基本问题。</p>
<ul>
<li>你能用Go写BPF程序吗？</li>
<li>你能分析用Go写的程序吗？</li>
</ul>
<p>我们按顺序来做。</p>
<p>目前，唯一能够编译成BPF机器(BPF machine)能够理解的格式的编译器是Clang。另一个流行的编译器GСС，但gcc仍然没有BPF后端。而能够编译成BPF的编程语言，只有C语言的一个非常有限的版本(C的子集)。</p>
<p>然而，BPF程序还有第二部分，就是在用户空间。而这可以用Go来编写。</p>
<p>正如我在上面已经提到的，BCC允许你用Python来编写这部分，而Python是该工具的主要语言。同时，在主库中，BCC还支持Lua和C++，而且，在辅库中，它还支持<a href="https://github.com/iovisor/gobpf">Go</a>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-11.png" alt="img{512x368}" /></p>
<p>这个程序看起来和Python中的程序完全一样。一开始，它有一个字符串，其中的BPF程序是用C语言编写的，然后我们沟通在哪里附加一个给定的程序，我们用某种方式和它进行交互，比如从BPF图中提取数据。</p>
<p>基本上就是这样了。更详细的例子可以在<a href="https://github.com/iovisor/gobpf/tree/master/examples/bcc/bash_readline">Github上查看</a>。</p>
<p>主要的缺点可能是我们使用的是C库，libbcc或者libbpf，用C库构建一个Go程序远不是一件容易的”事”。</p>
<p>除了iovisor/gobpf之外，我还发现了另外三个最新的项目，可以让你在Go中写出用户层(userland)部分。</p>
<ul>
<li>https://github.com/dropbox/goebpf</li>
<li>https://github.com/cilium/ebpf</li>
<li>https://github.com/andrewkroh/go-ebpf</li>
</ul>
<p>Dropbox的版本不需要任何C库，但你需要自己用Clang构建BPF的内核部分，然后用Go程序将其加载到内核中。</p>
<p>Cilium的版本和Dropbox的版本有相同的具体内容。但值得一提的是，最主要的原因是它是由Cilium项目的人做的，这意味着它成功性更大。</p>
<p>第三个项目我出于完整性的考虑而列出了。和前面两个项目一样，它没有外部的C语言依赖，需要用C语言手动构建BPF程序，但看起来，未来的前景不是特别乐观。</p>
<p>其实，我们还应该问一个问题：到底为什么要用Go写BPF程序？因为如果你看BCC或者bpftrace，那么bPF程序占用的代码不到500行。但如果用bpftrace语言写一个小脚本，或者用一点Python，不是更简单吗？我看有两个理由要这么做。</p>
<p>第一个原因是这样的。你确实很喜欢Go，而且更愿意用Go来做所有事情(译注：拿着go这柄锤子，眼中到处都是钉子)。此外，把Go程序从机器迁移到机器上可能更简单：静态链接，简单的二进制，以及所有这些。但事情远没有这么简单，因为我们被绑在一个特定的内核上。我就不说了，否则，我的文章又要长50页了。</p>
<p>第二个原因是这样的。你写的不是一个简单的脚本，而是一个大规模的系统，这个系统内部也使用了BPF。我在Go中甚至有这样一个系统的例子。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-12.png" alt="img{512x368}" /></p>
<p>Scope项目看起来像一个二进制程序，当它在K8s或其他云的基础设施中运行时，会分析发生的一切，并显示有哪些容器和服务，它们是如何交互的等等。而很多这些都是用BPF完成的。一个有趣的项目。</p>
<h2>用Go分析程序</h2>
<p>如果你还记得，我们还有一个问题：我们能不能用BPF分析用Go编写的程序？我们的第一反应是：”可以，当然可以！” 程序用什么语言编写有什么区别呢？毕竟，它只是编译后的代码，和其他程序一样，在处理器中计算一些东西，疯狂地占用内存，并通过内核与硬件交互，通过系统调用与内核交互。原则上这是正确的，但也有一些细节&#8211;这些细节有不同程度的复杂性。</p>
<h3>传递参数</h3>
<p>其中一个细节是，Go不使用大多数其他语言所使用的ABI(application binary interface)。它的工作方式是，”创始人”决定从<a href="https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs">Plan 9系统</a>中提取ABI，这是一个他们非常熟悉的系统。</p>
<p>ABI和API一样，是一种接口约定&#8211;只是在比特、字节和机器代码的层面上。</p>
<p>我们对ABI的主要内容感兴趣的是它的参数是如何传递给函数的，以及响应是如何从函数中回来的。如果说在标准的ABI x86-64中，处理器的寄存器是用来传递参数和响应的，而在Plan 9 ABI中，堆栈是则是用来实现这个目的的。</p>
<p>Rob Pike和他的团队并没有打算做另一个标准；他们已经为Plan 9系统准备了一个几乎是现成的C编译器&#8211;就像2 x 2一样简单&#8211;在很短的准备时间内，他们将其改造成了Go的编译器。这就是一个工程师的方法。</p>
<p>然而，实际上这并不是一个如此关键的问题。首先，我们可能很快就会<a href="https://github.com/golang/go/issues/18597">在Go中看到通过寄存器传递参数</a>，其次，从BPF中获取堆栈参数并不复杂：<a href="https://github.com/iovisor/bpftrace/pull/828">sargX别名</a>已经被添加到bpftrace中，而<a href="https://github.com/iovisor/bcc/issues/934">另一个别名</a>很可能在不久的将来出现在BCC中。</p>
<p>更新：自从我做了演讲之后，Go官方甚至还出了一个<a href="https://go.googlesource.com/proposal/+/refs/changes/78/248178/1/design/40724-register-calling.md">关于在ABI中使用寄存器的详细技术草案</a>。</p>
<h3>唯一的线程标识符</h3>
<p>第二个则是与Go的一个被钟爱的功能有关，即goroutines。测量函数延迟的方法之一是保存函数被调用的时间，得到函数的退出时间，并计算其差值。我们需要保存函数的启动时间以及一个键，这这个键将包含函数的名称和TID（线程ID）。线程ID是需要的，因为同一个函数可以被不同的程序，或者一个程序的不同线程同时调用。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-13.png" alt="img{512x368}" /></p>
<p>但是，在Go中，goroutine在系统线程之间移动：前一分钟，一个goroutine在一个线程上执行，后一分钟，在另一个线程上执行。而且，在Go的情况下，我们最好不要将TID放入键中，而是放入GID，即goroutine的ID&#8211;但不幸的是，我们无法获得它。从纯技术的角度来看，这个ID确实存在。你甚至可以用肮脏的黑客手段来提取它，因为它可以在堆栈的某个地方被找到，但这样做是被Go核心团队建议严格禁止的。他们认为这是我们永远不会需要的信息。goroutine本地存储也是如此&#8211;但这有点跑题了。</p>
<h3>扩展栈</h3>
<p>第三个问题是最严重的问题。它是如此严重，以至于即使我们以某种方式解决了第二个问题，也无法帮助我们测量Go函数的延迟。</p>
<p>大多数读者可能对什么是栈有了很好的理解。这也就是栈，与堆不同，你可以为变量分配内存，而不必考虑释放它们。</p>
<p>但是对于C语言来说，在这种情况下，栈有一个固定的大小。如果我们超过了这个固定大小，就会出现众所周知的堆栈溢出现象。</p>
<p>但在Go中，栈是动态的。在旧版本中，它是通过链接的内存块列表来实现的(即分段栈)。现在，它是一个动态大小的连续块。这意味着，如果分配的内存块对我们来说不够用，我们就扩展当前的内存块。而如果我们不能扩展它，我们就分配一个更大的，并将所有数据从旧的位置移动到新的位置。这一点非常吸引人，并且涉及到安全保证、cgo和垃圾收集等问题，但这是另一篇文章的主题。</p>
<p>要知道，为了让Go能够移动堆栈，它必须处理调用栈，并且处理栈中的所有指针。</p>
<p>而这就是基本的问题所在：uretprobes，用于将bPF探针附加到函数返回中，动态地改变堆栈以整合对其处理程序的调用&#8211;这就是所谓的 “蹦床(trampoline)”。而且，在大多数情况下，这改变了栈，这是Go不期望发生的事情，它会导致程序崩溃。糟了!</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-14.png" alt="img{512x368}" /></p>
<p>顺便说一下，这个故事不是Go独有的。C++的堆栈拆分器在处理异常时也每每崩溃。</p>
<p>这个问题没有解决办法。在这种情况下，像往常一样，双方各自向对方抛出完全有理有据的论点进行指责。</p>
<p>但是，如果你真的需要设置uretprobe，有一个方法可以绕过这个问题。怎么解决？不要设置uretprobe探针。你可以在我们退出函数的所有位置设置一个uprobe。可能有一个这样的位置&#8211;或者50个。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-15.png" alt="img{512x368}" /></p>
<p>而这也是Go的独特性在我们手中发挥的地方。</p>
<p>通常情况下，这种诡计是行不通的。一个足够聪明的编译器知道如何执行所谓的尾部调用优化，这时，我们不是从函数中返回，而是简单地跳到下一个函数的开始处。这种优化对于Haskell这样的函数式语言来说是至关重要的。如果没有它，你就无法在不发生堆栈溢出的情况下寸步难行。但是，有了这种优化，根本不可能找到我们从函数返回的所有位置。</p>
<p>但具体来说，Go 1.14版本的编译器，还不能进行尾部调用优化。这就意味着，附加到函数的所有显式退出的技巧是可行的，即使它非常笨重。</p>
<h2>示例</h2>
<p>不要认为BPF对Go无用。远非如此。我们可以做所有不涉及上述问题的其他事情。而且我们会这样做的。</p>
<p>让我们来看一些例子。</p>
<p>首先，我们来看一个简单的程序。基本上，它是一个监听8080端口的web服务器，并且有一个HTTP查询的处理程序。处理程序从URL中获取一个名称参数和一个年份参数，进行检查，然后将这三个变量（名称、年份和检查状态）发送给prepareAnswer()函数，然后该函数以字符串的形式准备一个答案。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-16.png" alt="img{512x368}" /></p>
<p>Site check是一个HTTP查询，在通道和goroutines的帮助下，检查会议站点是否工作。prepareAnswer函数只是将所有这些转化为一个可读的字符串。</p>
<p>我们将通过curl的简单查询来触发我们的程序：</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-17.png" alt="img{512x368}" /></p>
<p>对于我们的第一个例子，我们将使用 bpftrace 打印所有程序的函数调用。在本例中，我们将对 “main “下的所有函数进行附加。在Go中，所有的函数都有一个符号，其形式如下：包名-点-函数名。我们的包是&#8217;main&#8217;，函数的运行时是&#8217;runtime&#8217;。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-18.png" alt="img{512x368}" /></p>
<p>当我使用curl时，处理程序(handler)、site检查函数和goroutine子函数都会被执行，然后是准备答案函数(prepareAnswer)。很好！</p>
<p>接下来，我不仅要导出那些正在执行的函数，还要导出它们的参数。让我们以函数prepareAnswer()为例，它有三个参数。让我们试着打印两个ints。</p>
<p>让我们拿bpftrace来说，只不过这次不是单行代码，而是一个脚本。让我们将其附在我们的函数上，让我们像我说的那样，为堆栈参数使用别名。</p>
<p>在输出中，我们看到，我们发送了2020，获得了状态200，还发送了一次2021。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-19.png" alt="img{512x368}" /></p>
<p>但这个函数有三个参数。第一个参数是一个字符串。那么这个参数呢？</p>
<p>我们简单的导出0到3的所有堆栈参数，我们看看会看到什么？一个大数字，一个稍小的数字，还有我们以前的数字2021和200。一开始这些奇怪的数字是什么？</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-20.png" alt="img{512x368}" /></p>
<p>这时，熟悉Go的内部结构是很有帮助的。如果说在C语言中，字符串只是一个以零结尾的字节数组，那么在Go语言中，字符串实际是一个结构体，由一个指向字节数组的指针（顺便说一下，这个指针不是以零结尾）和长度组成。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-21.png" alt="img{512x368}" /></p>
<p>但是Go编译器在以参数的形式发送一个字符串时，会将这个结构解开，作为两个参数发送。于是，第一个奇怪的数字确实是我们数组的指针，第二个是长度。</p>
<p>果然：预期的字符串长度是22。</p>
<p>相应地，我们修正一下我们的脚本，以便通过堆栈指针寄存器获得这两个值，以及正确的偏移量，并且，在集成的str()函数的帮助下，我们将其导出为一个字符串。这一切都成功了。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-22.png" alt="img{512x368}" /></p>
<p>我们也来看看运行时(runtime)的情况。例如，我想知道我们的程序启动了哪些goroutines。我知道goroutines是由函数newproc()和newproc1()启动的。我们来附着(attach)一下它们。funcval结构的指针是newproc1()函数的第一个参数。这个只有一个字段，就是函数的指针。</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-23.png" alt="img{512x368}" /></p>
<p>在这种情况下，我们将使用直接在脚本中定义结构的功能。这比使用偏移量要简单一些。我们已经导出了所有的goroutine，当我们的处理程序被调用时，这些goroutine就会启动。之后，如果我们想获取偏移量的符号名称，那么我们就可以在其中看到我们的checkSite函数。万岁!</p>
<p><img src="https://tonybai.com/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-24.png" alt="img{512x368}" /></p>
<p>这些例子对于BPF、BCC和bpftrace的功能来说只是沧海一粟。只要对内部工作原理有足够的了解和经验，您就可以从工作程序中获得几乎任何信息，而无需停止或改变它。</p>
<h2>结论</h2>
<p>这就是我想告诉你的全部内容，希望对你有所启发。</p>
<p>BPF是Linux中最时髦、最有前途的领域之一。而且我相信，在未来的几年里，我们会看到更多有趣的东西&#8211;不仅是技术本身，还有工具和它的传播。</p>
<p>现在还不算太晚，也不是每个人都知道BPF，所以赶快去学习，成为魔术师，解决问题，帮助你的同事。都说魔术师的招数只有一次。</p>
<p>说到Go，照例，我们的结局很独特。我们总是有一些怪癖，无论是不同的编译器，还是ABI，需要GOPATH，有一个你无法谷歌的名字。但我认为，可以说我们（Go)已经成为一股不可忽视的力量，在我看来，情况只会越来越好。</p>
<h2>附录（译者添加，原文没有此节)</h2>
<h3>在ubuntu 18.04上安装bpftrace</h3>
<p>ubuntu 19.04及以后版本可以直接通过下面命令安装bpftrace：</p>
<pre><code>(sudo) apt-get install -y bpftrace
</code></pre>
<p>但18.04版本的apt官方源中并没有bpftrace。但snap中有：</p>
<pre><code># snap install --devmode bpftrace
2020-12-17T17:21:24+08:00 INFO Waiting for automatic snapd restart...
bpftrace 20201207-1718-v0.11.4 from Colin King (cking-kernel-tools) installed

# snap connect bpftrace:system-trace

# which bpftrace
/snap/bin/bpftrace

Build
  version: v0.11.4
  LLVM: 7
  foreach_sym: no
  unsafe uprobe: no
  bfd: yes
  bpf_attach_kfunc: no
  bcc_usdt_addsem: no
  bcc bpf_attach_uprobe refcount: no
  libbpf: no
  libbpf btf dump: no
  libbpf btf dump type decl: no

Kernel helpers
  probe_read: yes
  probe_read_str: yes
  probe_read_user: yes
  probe_read_user_str: yes
  probe_read_kernel: yes
  probe_read_kernel_str: yes
  get_current_cgroup_id: yes
  send_signal: yes
  override_return: yes

Kernel features
  Instruction limit: -1
  Loop support: no
  btf: no

Map types
  hash: yes
  percpu hash: yes
  array: yes
  percpu array: yes
  stack_trace: yes
  perf_event_array: yes

Probe types
  kprobe: no
  tracepoint: yes
  perf_event: yes
  kfunc: no
</code></pre>
<p>但通过snap安装的bpftrace有缺陷：</p>
<pre><code># bpftrace -e 'uprobe:/root/test/go/goebpf/testprogram:main.* { printf("%s - %s\n", comm, func); }'
sh: 1: objdump: not found
No probes to attach
</code></pre>
<p>这个问题在https://github.com/iovisor/bpftrace/issues/1430中有解决方法，那就是从bpftrace官方提供的docker镜像中将无缺陷的bpftrace拷贝出来：</p>
<pre><code># docker pull quay.io/iovisor/bpftrace:master-vanilla_llvm_clang_glibc2.27
master-vanilla_llvm_clang_glibc2.27: Pulling from iovisor/bpftrace
da7391352a9b: Pull complete
14428a6d4bcd: Pull complete
2c2d948710f2: Pull complete
8aeae4c5f345: Pull complete
e3b704c358bf: Pull complete
Digest: sha256:77ded0c887c91a431a1ebe508944eae0ed0fab9c51fc2867146c9b4b347becc7
Status: Downloaded newer image for quay.io/iovisor/bpftrace:master-vanilla_llvm_clang_glibc2.27
quay.io/iovisor/bpftrace:master-vanilla_llvm_clang_glibc2.27

# docker run -v $(pwd):/output quay.io/iovisor/bpftrace:master-vanilla_llvm_clang_glibc2.27 /bin/bash -c "cp /usr/bin/bpftrace /output"
# mv bpftrace /snap/bin  &lt;--- 覆盖掉原snap安装的bpftrace

# bpftrace -e 'uprobe:/root/test/go/goebpf/testprogram:main.* { printf("%s - %s\n", comm, func); }'
Attaching 5 probes...

</code></pre>
<h3>文中一些go文件的源码</h3>
<pre><code>// testprogram.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "strconv"
)

func main() {
    http.HandleFunc("/", handler)
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

func handler(writer http.ResponseWriter, request *http.Request) {
    query := request.URL.Query()
    name := query.Get("name")
    year_, _ := strconv.ParseUint(query.Get("year"), 10, 32)
    year := int(year_)
    status := checkSite()
    answer := prepareAnswer(name, year, status)
    writer.Write([]byte(answer + "\n"))
    return
}

//go:noinline
func checkSite() int {
    resultChan := make(chan int)
    go func() {
        resp, err := http.Get("https://www.gophercon-russia.ru")
        if err != nil {
            log.Fatalf("http get failed: %s\n", err)
        }
        resultChan &lt;- resp.StatusCode
    }()

    return &lt;-resultChan
}

//go:noinline
func prepareAnswer(name string, year int, status int) string {
    answer := fmt.Sprintf("Hello, %s %d! Website returned status %d.", name, year, status)
    return answer
}
</code></pre>
<p>myscript3.bt：</p>
<pre><code># cat myscript3.bt
uprobe:/root/test/go/goebpf/testprogram:main.prepareAnswer {
    $length = reg("sp")+16;
    $array = reg("sp")+8;
         printf("%s - %s %d %d\n", func, str(*($array), $length), sarg2, sarg3);
}
</code></pre>
<hr />
<p><strong>“Gopher部落”知识星球开球了！</strong>高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>我的Go技术专栏：“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”上线了，欢迎大家订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-column-pgo-with-qr-and-text.png" alt="img{512x368}" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>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>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/12/25/bpf-and-go-modern-forms-of-introspection-in-linux/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>C,C++开源项目中的100个Bugs</title>
		<link>https://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/</link>
		<comments>https://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/#comments</comments>
		<pubDate>Wed, 10 Apr 2013 01:59:42 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Chromium]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[CMake]]></category>
		<category><![CDATA[Code-Analyzer]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[PVS-Studio]]></category>
		<category><![CDATA[ReactOS]]></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">http://tonybai.com/?p=1247</guid>
		<description><![CDATA[俄罗斯OOO Program Verification Systems公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目，诸如Apache Http Server、Chromium、Clang、CMake、MySQL等的源码进行了分析，找出了100个典型的Bugs。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析，倒不如在编码时就提高自知自觉，避免这份列表上的错误发生在你的代码中，因此这里将部分摘录一些Bugs（Bug编号这里不连续，为的是对应原文的编号）并做简要说明。原文将这份Bug列表分为了几类，这里也将沿用这个思路。 一、数组和字符串处理错误 数组和字符串处理错误是C/C++程序中最多的一类缺陷类型。这也可以看作是我们为拥有高效地底层内存操作能力而付出的代价。 [#1]&#160;Wolfenstein 3D项目 -&#34;只有部分对象被clear了&#34; void CG_RegisterItemVisuals( int itemNum ) { &#160;&#160;&#160; &#8230; &#160;&#160;&#160; itemInfo_t *itemInfo; &#160;&#160;&#160; &#8230; &#160;&#160;&#160; memset( itemInfo, 0, sizeof( &#38;itemInfo ) ); &#160;&#160;&#160; &#8230; } 这里的Bug出现在memset那一行。代码的真实意图是clear iteminfo这块内存，但调用memset时，第三个参数传入的却是sizeof(&#38;iteminfo)，要知道 sizeof(&#38;itemInfo) != sizeof(itemInfo_t)，前者只是一个指针的大小罢了。正确的写法是： memset(itemInfo, 0, sizeof(itemInfo_t)); 或memset(itemInfo, 0, sizeof(*itemInfo)); [#2]&#160;Wolfenstein 3D项目 -&#34;只有部分Matrix被clear了&#34; ID_INLINE mat3_t::mat3_t( float src[ 3 [...]]]></description>
			<content:encoded><![CDATA[<p style="font-size: 13px;">俄罗斯<a href="http://www.viva64.com/">OOO Program Verification Systems</a>公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目，诸如<a href="http://httpd.apache.org">Apache Http Server</a>、<a href="http://www.chromium.org">Chromium</a>、<a href="http://clang.llvm.org">Clang</a>、<a href="http://www.cmake.org">CMake</a>、<a href="http://www.mysql.com">MySQL</a>等的源码进行了分析，找出了<a href="http://www.viva64.com/en/a/0079/">100个典型的Bugs</a>。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析，倒不如在编码时就提高自知自觉，避免这份列表上的错误发生在你的代码中，因此这里将部分摘录一些Bugs（Bug编号这里不连续，为的是对应原文的编号）并做简要说明。原文将这份Bug列表分为了几类，这里也将沿用这个思路。</p>
<p><b>一、数组和字符串处理错误</b></p>
<p>数组和字符串处理错误是C/C++程序中最多的一类缺陷类型。这也可以看作是我们为拥有高效地底层内存操作能力而付出的代价。</p>
<p><b>[</b><b><tt>#1</tt></b><b>]</b><a href="http://en.wikipedia.org/wiki/Wolfenstein_3D">&nbsp;Wolfenstein 3D</a>项目 -&quot;只有部分对象被clear了&quot;</p>
<p><span style="font-family: 'courier new', courier, monospace;">void CG_RegisterItemVisuals( int itemNum ) {<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; itemInfo_t *itemInfo;<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; memset( itemInfo, 0, sizeof( &amp;itemInfo ) );<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>这里的Bug出现在memset那一行。代码的真实意图是clear iteminfo这块内存，但调用memset时，第三个参数传入的却是sizeof(&amp;iteminfo)，要知道 sizeof(&amp;itemInfo) != sizeof(itemInfo_t)，前者只是一个指针的大小罢了。正确的写法是：</p>
<p><span style="font-family: 'courier new', courier, monospace;">memset(itemInfo, 0, sizeof(itemInfo_t)); 或memset(itemInfo, 0, sizeof(*itemInfo));</span></p>
<p><b>[#2]&nbsp;</b>Wolfenstein 3D项目 -&quot;只有部分Matrix被clear了&quot;</p>
<p><span style="font-family: 'courier new', courier, monospace;">ID_INLINE mat3_t::mat3_t( float src[ 3 ][ 3 ] ) {<br />
	&nbsp;&nbsp;&nbsp; memcpy( mat, src, sizeof( src ) );<br />
	}</span></p>
<p>这里的Bug出现在memcpy一行。程序的原意是将clear src[3][3]这个<a href="http://tonybai.com/2013/03/28/pointer-and-multi-dimension-array-in-c/">二维数组</a>。但这里有个坑：那就是作为函数形式参数的数组名已经退化为指针了，对其sizeof只能得到一个指针的长度，因此这里的 memcpy只是copy了一个指针的长度，没有copy全。这里的代码是C++代码，原文中给出了正确的改正方法 &#8211; 传reference：</p>
<p><span style="font-family: 'courier new', courier, monospace;">ID_INLINE mat3_t::mat3_t( float (&amp;src)[3][3] )<br />
	{<br />
	&nbsp;&nbsp;&nbsp; memcpy( mat, src, sizeof( src ) );<br />
	}</span></p>
<p><b>[#4]&nbsp;</b><a href="http://www.reactos.org">ReactOS</a>项目 &#8211; &quot;错误地计算一个字符串的长度&quot;</p>
<p><span style="font-family: 'courier new', courier, monospace;">static const PCHAR Nv11Board = &quot;NV11 (GeForce2) Board&quot;;<br />
	static const PCHAR Nv11Chip = &quot;Chip Rev B2&quot;;<br />
	static const PCHAR Nv11Vendor = &quot;NVidia Corporation&quot;;</span></p>
<p><span style="font-family:courier new,courier,monospace;">BOOLEAN<br />
	IsVesaBiosOk(&#8230;)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; if (!(strncmp(Vendor, Nv11Vendor, sizeof(Nv11Vendor))) &amp;&amp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !(strncmp(Product, Nv11Board, sizeof(Nv11Board))) &amp;&amp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !(strncmp(Revision, Nv11Chip, sizeof(Nv11Chip))) &amp;&amp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (OemRevision == 0&#215;311))<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>Bug处在IsVesaBiosOK中那一串strncmp调用中，代码将一个指针的size传入strncmp作为第三个参数，导致 strncmp实际只是比较了字符串的前4 or 8个字节，而不是字符串的全部内容。</p>
<p><b>[#6]&nbsp;</b>CPU Identifying Tool项目 &#8211; 数组越界</p>
<p><span style="font-family: 'courier new', courier, monospace;">#define FINDBUFFLEN 64&nbsp; // Max buffer find/replace size<br />
	&#8230;<br />
	int WINAPI Sticky (&#8230;)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; static char findWhat[FINDBUFFLEN] = {&#39;\0&#39;};<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; findWhat[FINDBUFFLEN] = &#39;\0&#39;;<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>bug出在&quot;findWhat[FINDBUFFLEN] = &#8216;\0&#8242;;”这一行。数组的最大长度为FINDBUFFLEN，但下标的最大值应该是FINDBUFFLEN-1，而不是FINDBUFFLEN。因此这 行代码显然应该改为findWhat[FINDBUFFLEN-1] = &#39;\0&#39;;</p>
<p><b>[#7]</b>&nbsp;Wolfenstein 3D项目 &#8211; 数组越界</p>
<p><span style="font-family: 'courier new', courier, monospace;">typedef struct bot_state_s<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; char teamleader[32]; //netname of the team leader<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}&nbsp; bot_state_t;</span></p>
<p><span style="font-family:courier new,courier,monospace;">void BotTeamAI( bot_state_t *bs ) {<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; bs-&gt;teamleader[sizeof( bs->teamleader )] = &#39;\0&#39;;<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>&quot;sizeof( bs-&gt;teamleader )]&quot;这行的结果值已经超出了数组的最大边界，正确的代码是：</p>
<p><span style="font-family: 'courier new', courier, monospace;">bs-&gt;teamleader[<br />
	&nbsp; sizeof(bs-&gt;teamleader) / sizeof(bs-&gt;teamleader[0]) &#8211; 1<br />
	&nbsp; ] = &#39;\0&#39;;</span></p>
<p><b>[#8]&nbsp;</b><a href="http://www.miranda-im.org">Miranda IM</a>项目 &#8211; 只Copy了部分字符串</p>
<p><span style="font-family: 'courier new', courier, monospace;">struct _textrangew<br />
	{<br />
	&nbsp;&nbsp;&nbsp; CHARRANGE chrg;<br />
	&nbsp;&nbsp;&nbsp; LPWSTR lpstrText;<br />
	} TEXTRANGEW;</span></p>
<p><span style="font-family:courier new,courier,monospace;">const wchar_t* Utils::extractURLFromRichEdit(&#8230;)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; ::CopyMemory(tr.lpstrText, L&quot;mailto:&quot;, 7);<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>这里的bug在于L&quot;mailto:&quot;是宽字符串，宽字符串中的每个字符占2或4个字节（依Compiler使用的<a href="http://tonybai.com/2007/11/03/also-talk-about-char-encoding/">字符集</a>编码而定），因此这里只 copy 7个字节显然是不够的，应该是7 * sizeof(wchar_t)。</p>
<p><b>[#9]</b>&nbsp;CMake项目 &#8211; 循环內的数组越界</p>
<p><span style="font-family: 'courier new', courier, monospace;">static const struct {<br />
	&nbsp;&nbsp;&nbsp; DWORD&nbsp;&nbsp; winerr;<br />
	&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; doserr;<br />
	} doserrors[] =<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	};</span></p>
<p><span style="font-family:courier new,courier,monospace;">static void<br />
	la_dosmaperr(unsigned long e)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp; for (i = 0; i &lt; sizeof(doserrors); i++)<br />
	&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (doserrors[i].winerr == e)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; errno = doserrors[i].doserr;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</span></p>
<p>作者原本意图la_dosmaperr中for循环的次数等于数组的元素个数，但sizeof(doserrors)返回的却是数组占用的字节个数，这远远大于数组元素个数，因此造成数组越界。正确的写法：</p>
<p><span style="font-family: 'courier new', courier, monospace;">for (i = 0; i &lt; sizeof(doserrors) / sizeof(*doserrors); i++)</span></p>
<p><strong>[#10]</strong>&nbsp;CPU Identifying Tool项目 &#8211; 打印到自身的字符串</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">char * OSDetection ()<br />
	{<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; sprintf(szOperatingSystem,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;%sversion %d.%d %s (Build %d)&quot;,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; szOperatingSystem,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.dwMajorVersion,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.dwMinorVersion,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.szCSDVersion,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.dwBuildNumber &amp; 0xFFFF);<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; sprintf (szOperatingSystem, &quot;%s%s(Build %d)&quot;,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; szOperatingSystem, osvi.szCSDVersion,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; osvi.dwBuildNumber &amp; 0xFFFF);<br />
	&nbsp; &nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">通过sprintf，szOperatingSystem字符串将自己打印到自己里面，这是十分危险的，将导致无法预知的错误结果，可能会导致栈溢出等严重问题。</p>
<p style="font-size: 13px;"><strong>[#12]</strong>&nbsp;<a href="http://notepad-plus-plus.org">Notepad++</a>项目 &#8211; 数组局部clear</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">#define CONT_MAP_MAX 50<br />
	int _iContMap[CONT_MAP_MAX];<br />
	&#8230;<br />
	DockingManager::DockingManager()<br />
	{<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; memset(_iContMap, -1, CONT_MAP_MAX);<br />
	&nbsp; &nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">代码的原本试图将数组_iContMap清零，但memset的第三个参数CONT_MAP_MAX并不能代表数组的真正大小，而只是数组的元素个数而已，显然其忘记乘以sizeof(int)了。</p>
<p style="font-size: 13px;"><b>二、未定义行为</b></p>
<p style="font-size: 13px;">在C/C++的语言规范中，我们常常能看到&ldquo;xx is undefined&rdquo;。规范中并没有明确表明这类错误是什么样子的，只是说取决于Compiler的实现，也许Compiler会给出正确的结果，但这么使用却是不可移植的。</p>
<p style="font-size: 13px;"><strong>[#1]</strong>&nbsp;Chromium项目 &#8211; 智能指针的误用</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">void AccessibleContainsAccessible(&#8230;)<br />
	{<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; auto_ptr&lt;VARIANT&gt; child_array(new VARIANT[child_count]);<br />
	&nbsp; &nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这里的问题在于使用new[]分配的内存，在智能指针释放时却用了delete，这将会导致未定义行为。看看autoptr的destructor就知道了：</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">~auto_ptr() {<br />
	&nbsp; &nbsp; delete _Myptr;<br />
	}</span></p>
<p style="font-size: 13px;">我们可以找一些更合适的类来fix这个问题，比如boost::scopedarray。</p>
<p style="font-size: 13px;"><strong>[#2]</strong>&nbsp;IPP Sample项目 &#8211; 经典未定义行为</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">template&lt;typename T, Ipp32s size&gt; void HadamardFwdFast(&#8230;)<br />
	{<br />
	&nbsp; Ipp32s *pTemp;<br />
	&nbsp; &#8230;<br />
	&nbsp; for(j=0;j&lt;4;j++) {<br />
	&nbsp; &nbsp; a[0] = pTemp[0*4] + pTemp[1*4];<br />
	&nbsp; &nbsp; a[1] = pTemp[0*4] &#8211; pTemp[1*4];<br />
	&nbsp; &nbsp; a[2] = pTemp[2*4] + pTemp[3*4];<br />
	&nbsp; &nbsp; a[3] = pTemp[2*4] &#8211; pTemp[3*4];<br />
	&nbsp; &nbsp; pTemp = pTemp++;<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">很多人一眼就看到了&quot;pTemp = pTemp++&quot;这行，对于这个代码编译器会产生两种结果截然不同的翻译：</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">pTemp = pTemp + 1;<br />
	pTemp = pTemp;</span></p>
<p style="font-size: 13px;">或</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">TMP = pTemp;<br />
	pTemp = pTemp + 1;<br />
	pTemp = TMP;</span></p>
<p style="font-size: 13px;">到底是哪种呢？依赖于编译器的实现，甚至是优化级别的设定。</p>
<p style="font-size: 13px;"><b>三、与运算优先级相关的错误</b></p>
<p style="font-size: 13px;"><strong>[#1]</strong>&nbsp;MySQL工程 &#8211; !和&amp;的运算优先级</p>
<p style="font-size: 13px;"><span style="font-family: 'courier new', courier, monospace;">int ha_innobase::create(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (srv_file_per_table<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &amp;&amp; !mysqld_embedded<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &amp;&amp; (!create_info-&gt;options &amp; HA_LEX_CREATE_TMP_TABLE)) {<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这段代码原意是想测试create_info-&gt;options变量中几个bit位的值是否set了，即!(create_info-&gt;options &amp; HA_LEX_CREATE_TMP_TABLE)，但由于!的运算优先级高于&amp;，实际逻辑变成了(!create_info-&gt;options) &amp;&nbsp;HA_LEX_CREATE_TMP_TABLE了。如果想要这段代码如期工作，就不要吝啬小括号了。</p>
<p style="font-size: 13px;"><strong>[#2]</strong>&nbsp;<a href="http://www.emule-project.net">Emule</a>工程 &#8211; *和++的运算优先级</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">STDMETHODIMP<br />
	CCustomAutoComplete::Next(&#8230;, ULONG *pceltFetched)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (pceltFetched != NULL)<br />
	&nbsp; &nbsp; *pceltFetched++;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">显然作者原意是想对pceltFetched所指向的long型变量进行++操作，但由于*和++的运算优先级没有搞对，导致实际上执行了*(pceltFetched++)的操作，而不是(*pceltFetched)++操作。</p>
<p style="font-size: 13px;"><strong>[#3]</strong> Chromium项目 &#8211; &amp;和!=的运算优先级</p>
<p><span style="font-family:courier new,courier,monospace;">#define FILE_ATTRIBUTE_DIRECTORY 0&#215;00000010</span></p>
<p><span style="font-family:courier new,courier,monospace;">bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {<br />
	&nbsp; &#8230;<br />
	&nbsp; info-&gt;is_directory =<br />
	&nbsp; &nbsp; file_info.dwFileAttributes &amp; FILE_ATTRIBUTE_DIRECTORY != 0;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这个程序员的意图是通过测试file_info.dwFileAttributes的几个bit位的值来判定是否是目录，逻辑上应该是(file_info.dwFileAttributes &amp; FILE_ATTRIBUTE_DIRECTORY) != 0，但由于!=优先级高于&amp;，原代码中无括号，结果逻辑变成了file_info.dwFileAttributes &amp; (FILE_ATTRIBUTE_DIRECTORY != 0)，导致is_directory将永远求值为true。</p>
<p style="font-size: 13px;"><strong>[#4]</strong> BCmenu项目 &#8211; if和else弄混</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void BCMenu::InsertSpaces(void)<br />
	{<br />
	&nbsp; if(IsLunaMenuStyle())<br />
	&nbsp; &nbsp; if(!xp_space_accelerators) return;<br />
	&nbsp; else<br />
	&nbsp; &nbsp; if(!original_space_accelerators) return;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这又是C语言的一个&ldquo;大坑&rdquo;，无奈这个BCMenu项目的程序员掉坑里了。虽然从代码缩进上来看，else似乎是与最外层的if配对使用，但实际这段代码的效果是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if(IsLunaMenuStyle())<br />
	{<br />
	&nbsp; &nbsp;if(!xp_space_accelerators) {<br />
	&nbsp; &nbsp; &nbsp;return;<br />
	&nbsp; &nbsp;} else {<br />
	&nbsp; &nbsp; &nbsp;if(!original_space_accelerators) return;<br />
	&nbsp; &nbsp;}<br />
	}</span></p>
<p style="font-size: 13px;">这显然不是程序员原意，看来括号必要时还是不能省略的。修改后的代码如下：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if(IsLunaMenuStyle()) {<br />
	&nbsp; if(!xp_space_accelerators) return;<br />
	} else {<br />
	&nbsp; if(!original_space_accelerators) return;<br />
	}</span></p>
<p style="font-size: 13px;"><b style="font-size: 13px;">四、格式化输出错误</b></p>
<p style="font-size: 13px;"><strong>[#1]</strong> ReactOS项目 &#8211; 错误地输出WCHAR字符</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">static void REGPROC_unescape_string(WCHAR* str)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; default:<br />
	&nbsp; &nbsp; fprintf(stderr,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;Warning! Unrecognized escape sequence: \\%c&#39;\n&quot;,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; str[str_idx]);<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">%c是用来格式化输出非宽字符的，这里用来输出WCHAR显然会得到错误的结果，fix solution是将%c换位%C。</p>
<p style="font-size: 13px;"><strong>[#2]</strong> Intel AMT SDK项目 &#8211; 缺少%s</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void addAttribute(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; int index = _snprintf(temp, 1023,&nbsp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;%02x%02x:%02x%02x:%02x%02x:%02x%02x:&quot;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;%02x%02x:02x%02x:%02x%02x:%02x%02x&quot;,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value[0],value[1],value[2],value[3],value[4],<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value[5],value[6],value[7],value[8],<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value[9],value[10],value[11],value[12],<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value[13],value[14],value[15]);<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;">不解释了，自己慢慢数和对照吧。</p>
<p style="font-size: 13px;"><strong>[#3]</strong>&nbsp;Intel AMT SDK项目 &#8211; 未使用的参数</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">bool GetUserValues(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; printf(&quot;Error: illegal value. Aborting.\n&quot;, tmp);<br />
	&nbsp; return false;<br />
	}</span></p>
<p style="font-size: 13px;">显然tmp是多余的。</p>
<p style="font-size: 13px;"><b style="font-size: 13px;">五、书写错误</b></p>
<p style="font-size: 13px;"><strong>[#1]</strong> Miranda IM项目 &#8211; 在if中赋值</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void CIcqProto::handleUserOffline(BYTE *buf, WORD wLen)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; else if (wTLVType = 0&#215;29 &amp;&amp; wTLVLen == sizeof(DWORD))<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">&ldquo;wTLVType = 0&#215;29&rdquo;显然是笔误，应该是&ldquo;wTLVType ==&nbsp;0&#215;29&rdquo;才对。</p>
<p style="font-size: 13px;"><strong>[#3]</strong> Clang项目 &#8211; 对象名书写错误</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">static Value *SimplifyICmpInst(&#8230;) {<br />
	&nbsp; &#8230;<br />
	&nbsp; case Instruction::Shl: {<br />
	&nbsp; &nbsp; bool NUW =<br />
	&nbsp; &nbsp; &nbsp; LBO-&gt;hasNoUnsignedWrap() &amp;&amp; LBO-&gt;hasNoUnsignedWrap();<br />
	&nbsp; &nbsp; bool NSW =<br />
	&nbsp; &nbsp; &nbsp; LBO-&gt;hasNoSignedWrap() &amp;&amp; RBO-&gt;hasNoSignedWrap();<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">从最后一行先后使用了LBO和RBO来看，前面只用了LBO的那行很可能是有问题的，正确的应该是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">bool NUW =<br />
	&nbsp; &nbsp; &nbsp; LBO-&gt;hasNoUnsignedWrap() &amp;&amp; RBO-&gt;hasNoUnsignedWrap();</span></p>
<p style="font-size: 13px;"><strong>[#6]</strong> G3D Content Pak项目 &#8211; 一对括号放错了地方</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">bool Matrix4::operator==(const Matrix4&amp; other) const {<br />
	&nbsp; if (memcmp(this, &amp;other, sizeof(Matrix4) == 0)) {<br />
	&nbsp; &nbsp; return true;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">由于括号放错了地方，导致memcmp最后的参数变成了sizeof(Matrix4) == 0，这行代码的正确写法应该是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if (memcmp(this, &amp;other, sizeof(Matrix4)) == 0) {</span></p>
<p style="font-size: 13px;"><strong>[#8]</strong> Apache Http Server项目 &#8211; 多余的sizeof</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">PSECURITY_ATTRIBUTES GetNullACL(void)<br />
	{<br />
	&nbsp; PSECURITY_ATTRIBUTES sa;<br />
	&nbsp; sa &nbsp;= (PSECURITY_ATTRIBUTES)<br />
	&nbsp; &nbsp; LocalAlloc(LPTR, sizeof(SECURITY_ATTRIBUTES));<br />
	&nbsp; sa-&gt;nLength = sizeof(sizeof(SECURITY_ATTRIBUTES));<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">最后一行显然是笔误，sizeof(sizeof(SECURITY_ATTRIBUTES))应该写为sizeof(SECURITY_ATTRIBUTES)才对。</p>
<p style="font-size: 13px;"><strong>[#10]</strong> Notepad++项目 &#8211; 在本来应该用&amp;的地方使用了&amp;&amp;</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">TCHAR GetASCII(WPARAM wParam, LPARAM lParam)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; result=ToAscii(wParam,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(lParam &gt;&gt; 16) &amp;&amp; 0xff, keys,&amp;dwReturnedValue,0);<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">(lParam &gt;&gt; 16) &amp;&amp; 0xff没有什么意义，求值结果总是true。这里的代码应该是(lParam &gt;&gt; 16) &amp;&nbsp;0xff。</p>
<p style="font-size: 13px;"><strong>[#12]</strong>&nbsp;Fennec Media Project项目 &#8211; 额外的分号</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">int settings_default(void)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; for(i=0; i&lt;16; i++);<br />
	&nbsp; &nbsp; for(j=0; j&lt;32; j++)<br />
	&nbsp; &nbsp; {<br />
	&nbsp; &nbsp; &nbsp; settings.conversion.equalizer_bands.boost[i][j] = 0.0;<br />
	&nbsp; &nbsp; &nbsp; settings.conversion.equalizer_bands.preamp[i] &nbsp; = 0.0;<br />
	&nbsp; &nbsp; }<br />
	}</span></p>
<p style="font-size: 13px;">这又是一个实际逻辑与代码缩进不符的例子。作者的原意是这样的：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">for(i=0; i&lt;16; i++)&nbsp;<br />
	{<br />
	&nbsp; &nbsp; for(j=0; j&lt;32; j++)<br />
	&nbsp; &nbsp; {<br />
	&nbsp; &nbsp; &nbsp; settings.conversion.equalizer_bands.boost[i][j] = 0.0;<br />
	&nbsp; &nbsp; &nbsp; settings.conversion.equalizer_bands.preamp[i] &nbsp; = 0.0;<br />
	&nbsp; &nbsp; }<br />
	}</span></p>
<p style="font-size: 13px;">但实际执行代码逻辑却是：</p>
<p><span style="font-family:courier new,courier,monospace;">for(i=0; i&lt;16; i++)&nbsp;<br />
	{<br />
	&nbsp; &nbsp; ;<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">for(j=0; j&lt;32; j++)<br />
	{ &nbsp;&nbsp;<br />
	&nbsp; settings.conversion.equalizer_bands.boost[i][j] = 0.0;<br />
	&nbsp; settings.conversion.equalizer_bands.preamp[i] &nbsp; = 0.0;<br />
	}</span></p>
<p style="font-size: 13px;">这一切都是那个;导致的。</p>
<p style="font-size: 13px;"><strong>六、对基本函数和类的误用</strong></p>
<p style="font-size: 13px;"><strong>[#2]</strong>&nbsp;TortoiseSVN项目 &#8211; remove函数的误用</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">STDMETHODIMP CShellExt::Initialize(&#8230;.)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; ignoredprops = UTF8ToWide(st.c_str());<br />
	&nbsp; // remove all escape chars (&#39;\\&#39;)<br />
	&nbsp; std::remove(ignoredprops.begin(), ignoredprops.end(), &#39;\\&#39;);<br />
	&nbsp; break;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">作者意图删除所有&#39;\\&#39;，但他用错了函数，remove函数只是交换元素的位置，将要删除的元素交换到尾部trash，并且返回指向trash首地址的iterator。正确的做法应该是&quot;v.erase(remove(v.begin(), v.end(), 2), v.end())&quot;。</p>
<p style="font-size: 13px;"><strong>[#5]</strong> Pixie项目 &#8211; 在循环中使用alloca函数</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">inline &nbsp;void &nbsp;triangulatePolygon(&#8230;) {<br />
	&nbsp; &#8230;<br />
	&nbsp; for (i=1;i&lt;nloops;i++) {<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; do {<br />
	&nbsp; &nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; &nbsp; do {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; CTriVertex &nbsp;*snVertex =<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(CTriVertex *)alloca(2*sizeof(CTriVertex));<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; &nbsp; } while(dVertex != loops[0]);<br />
	&nbsp; &nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; } while(sVertex != loops[i]);<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">alloca函数在栈上分配内存，因此在循环中使用alloca可能会很快导致栈溢出。</p>
<p style="font-size: 13px;"><strong style="font-size: 13px;">七、无意义的代码</strong></p>
<p style="font-size: 13px;"><strong>[#1]</strong> IPP Samples项目 &#8211; 不完整的条件</p>
<p><span style="font-family:courier new,courier,monospace;">void lNormalizeVector_32f_P3IM(Ipp32f *vec[3],<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Ipp32s* mask, Ipp32s len)<br />
	{<br />
	&nbsp; Ipp32s &nbsp;i;<br />
	&nbsp; Ipp32f &nbsp;norm;</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; for(i=0; i&lt;len; i++) {<br />
	&nbsp; &nbsp; if(mask&lt;0) continue;<br />
	&nbsp; &nbsp; norm = 1.0f/sqrt(vec[0][i]*vec[0][i]+<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;vec[1][i]*vec[1][i]+vec[2][i]*vec[2][i]);<br />
	&nbsp; &nbsp; vec[0][i] *= norm; vec[1][i] *= norm; vec[2][i] *= norm;<br />
	&nbsp; }<br />
	}</span></p>
<p style="font-size: 13px;">mask是Ipp32s类型指针，这样if (mask&lt; 0)这句代码显然没啥意义，正确的代码应该是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if (mask[i] &lt; 0) continue;</span></p>
<p style="font-size: 13px;"><strong>[#2]</strong> QT项目 &#8211; 重复的检查</p>
<p><span style="font-family:courier new,courier,monospace;">Q3TextCustomItem* Q3TextDocument::parseTable(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; while (end &lt; length<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;/td&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;td&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;/th&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;th&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;td&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;/tr&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;tr&quot;))<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&amp;&amp; !hasPrefix(doc, length, end, QLatin1String(&quot;&lt;/table&quot;))) {</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这里对&quot;&lt;td&quot;做了两次check。</p>
<p style="font-size: 13px;"><strong style="font-size: 13px;">八、总是True或False的条件</strong></p>
<p style="font-size: 13px;"><strong>[#1]</strong>&nbsp;Shareaza项目 &#8211; char类型的值范围</p>
<p><span style="font-family:courier new,courier,monospace;">void CRemote::Output(LPCTSTR pszName)<br />
	{</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &#8230;<br />
	&nbsp; CHAR* pBytes = new CHAR[ nBytes ];<br />
	&nbsp; hFile.Read( pBytes, nBytes );<br />
	&nbsp; &#8230;<br />
	&nbsp; if ( nBytes &gt; 3 &amp;&amp; pBytes[0] == 0xEF &amp;&amp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pBytes[1] == 0xBB &amp;&amp; pBytes[2] == 0xBF )<br />
	&nbsp; {<br />
	&nbsp; &nbsp; pBytes += 3;<br />
	&nbsp; &nbsp; nBytes -= 3;<br />
	&nbsp; &nbsp; bBOM = true;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>表达式&quot;pBytes[0] == 0xEF&quot;总是False。char类型的值范围是-128~127 &lt; 0xEF，因此这个表达式总是False，导致整个if condition总是为False，与预期逻辑不符。</p>
<p><strong>[#3]</strong> VirtualDub项目 &#8211; 无符号类型总是&gt;=0</p>
<p><span style="font-family:courier new,courier,monospace;">typedef unsigned short wint_t;<br />
	&#8230;<br />
	void lexungetc(wint_t c) {<br />
	&nbsp; if (c &lt; 0)<br />
	&nbsp; &nbsp; return;<br />
	&nbsp; &nbsp;g_backstack.push_back(c);<br />
	}</span></p>
<p>c是unsigned short类型，永远不会小于0,也就是说if (c &lt; 0)永远为False。</p>
<p><strong>[#8]</strong> MySQL项目 &#8211; 条件错误</p>
<p><span style="font-family:courier new,courier,monospace;">enum enum_mysql_timestamp_type<br />
	str_to_datetime(&#8230;)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; else if (str[0] != &#8216;a&#8217; || str[0] != &#39;A&#39;)<br />
	&nbsp; &nbsp; continue; /* Not AM/PM */<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>if (str[0] != &#8216;a&#8217; || str[0] != &#39;A&#39;)这个条件永远为真。也许这块本意是想用&amp;&amp;。</p>
<p><strong style="font-size: 13px;">九、代码漏洞</strong></p>
<p>导致漏洞的代码错误实际上也都是笔误、不正确的条件以及不正确的数组操作等。但这里还是想将一些特定错误划归为一类，因为入侵者可以利用这些错误来攻击你的代码，获取其利益。</p>
<p><strong>[#1]</strong> Ultimate TCP/IP项目 &#8211; 空字符串的错误检查</p>
<p><span style="font-family:courier new,courier,monospace;">char *CUT_CramMd5::GetClientResponse(LPCSTR ServerChallenge)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (m_szPassword != NULL)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; &#8230;<br />
	&nbsp; &nbsp; if (m_szPassword != &#39;\0&#39;)<br />
	&nbsp; &nbsp; {<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>第二个if condition check意图检查m_szPassword是否为空字符串，但却错误的将指针与&#39;\0&#39;进行比较，正确的代码应该是这样的：</p>
<p><span style="font-family:courier new,courier,monospace;">if (*m_szPassword != &#39;\0&#39;)</span></p>
<p style="font-size: 13px;"><strong>[#2]</strong> Chromium项目 &#8211; NULL指针的处理</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">bool ChromeFrameNPAPI::Invoke(&#8230;)<br />
	{<br />
	&nbsp; ChromeFrameNPAPI* plugin_instance =<br />
	&nbsp; &nbsp; ChromeFrameInstanceFromNPObject(header);<br />
	&nbsp; if (!plugin_instance &amp;&amp;<br />
	&nbsp; &nbsp; &nbsp; (plugin_instance-&gt;automation_client_.get()))<br />
	&nbsp; &nbsp; return false;<br />
	&nbsp; &#8230;<br />
	} &nbsp;&nbsp;</span></p>
<p style="font-size: 13px;">一旦plugin_instance为NULL，!plugin_instance为True，代码对&amp;&amp;后面的子条件求值，引用plugin_instance将导致程序崩溃。正确的做法应该是：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">if (plugin_instance &amp;&amp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; (plugin_instance-&gt;automation_client_.get()))<br />
	&nbsp; return false;</span></p>
<p style="font-size: 13px;"><strong>[#5]</strong> Apache httpd Server项目 &#8211; 不完整的缓冲区clear</p>
<p><span style="font-family:courier new,courier,monospace;">#define MEMSET_BZERO(p,l) &nbsp; &nbsp; &nbsp; memset((p), 0, (l))</span></p>
<p><span style="font-family:courier new,courier,monospace;">void apr__SHA256_Final(&#8230;, SHA256_CTX* context) {<br />
	&nbsp; &#8230;<br />
	&nbsp; MEMSET_BZERO(context, sizeof(context));<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">这个错误前面提到过，sizeof(context)只是指针的大小，将之改为sizeof(*context)就OK了。</p>
<p style="font-size: 13px;"><strong>[#7]</strong> PNG Library项目 &#8211; 意外的指针clear</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">png_size_t<br />
	png_check_keyword(png_structp png_ptr, png_charp key,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; png_charpp new_key)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (key_len &gt; 79)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; png_warning(png_ptr, &quot;keyword length must be 1 &#8211; 79 characters&quot;);<br />
	&nbsp; &nbsp; new_key[79] = &#39;\0&#39;;<br />
	&nbsp; &nbsp; key_len = 79;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">new_key的类型为png_charpp，顾名思义，这是一个char**类型，但代码中new_key[79] = &#8216;\0&#8242;这句显然是要给某个char赋值，但new_key[n]得到的应该是一个地址，给一个地址赋值为&#8217;\0&#8242;显然是有误的。正确的写法应该是(*new_key)[79] = &#39;\0&#39;。</p>
<p style="font-size: 13px;"><strong>[#10]</strong> Miranda IM项目 &#8211; 保护没生效</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void Append( PCXSTR pszSrc, int nLength )<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; UINT nOldLength = GetLength();<br />
	&nbsp; if (nOldLength &lt; 0)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; // protects from underflow<br />
	&nbsp; &nbsp; nOldLength = 0;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">nOldLength椒UINT类型，其值永远不会小于0,因此if (nOldLength &lt; 0)这行成了摆设。</p>
<p style="font-size: 13px;"><strong>[#12]</strong> Ultimate TCP/IP项目 &#8211; 不正确的循环结束条件</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void CUT_StrMethods::RemoveSpaces(LPSTR szString) {<br />
	&nbsp; &#8230;<br />
	&nbsp; size_t loop, len = strlen(szString);<br />
	&nbsp; // Remove the trailing spaces<br />
	&nbsp; for(loop = (len-1); loop &gt;= 0; loop&#8211;) {<br />
	&nbsp; &nbsp; if(szString[loop] != &#39; &#39;)<br />
	&nbsp; &nbsp; &nbsp; break;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">循环中的结束条件loop &gt;= 0将永远为True，因为loop变量的类型是size_t是unsigned类型，永远不会小于0。</p>
<p style="font-size: 13px;"><strong style="font-size: 13px;">十、拷贝粘贴</strong></p>
<p style="font-size: 13px;">和笔误不同，程序员们决不因该低估拷贝粘贴问题，这类问题发生了太多。程序员们花费了大量时间在这些问题的debug上。</p>
<p style="font-size: 13px;"><strong>[#1]</strong> Fennec Media Project项目 &#8211; 处理数组元素时出错</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void* tag_write_setframe(char *tmem,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;const char *tid, const string dstr)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if(lset)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; fhead[11] = &#39;\0&#39;;<br />
	&nbsp; &nbsp; fhead[12] = &#39;\0&#39;;<br />
	&nbsp; &nbsp; fhead[13] = &#39;\0&#39;;<br />
	&nbsp; &nbsp; fhead[13] = &#39;\0&#39;;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;">咋看一下，fhead[13]做了两次赋值，似乎没啥问题。但仔细想一下，最后那行程序员的原意极可能是想写fhead[14] = &#39;\0&#39;。问题就在这里了。</p>
<p style="font-size: 13px;">[#2] MySQL项目 &#8211; 处理数组元素时出错</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">static int rr_cmp(uchar *a,uchar *b)<br />
	{<br />
	&nbsp; if (a[0] != b[0])<br />
	&nbsp; &nbsp; return (int) a[0] &#8211; (int) b[0];<br />
	&nbsp; if (a[1] != b[1])<br />
	&nbsp; &nbsp; return (int) a[1] &#8211; (int) b[1];<br />
	&nbsp; if (a[2] != b[2])<br />
	&nbsp; &nbsp; return (int) a[2] &#8211; (int) b[2];<br />
	&nbsp; if (a[3] != b[3])<br />
	&nbsp; &nbsp; return (int) a[3] &#8211; (int) b[3];<br />
	&nbsp; if (a[4] != b[4])<br />
	&nbsp; &nbsp; return (int) a[4] &#8211; (int) b[4];<br />
	&nbsp; if (a[5] != b[5])<br />
	&nbsp; &nbsp; return (int) a[1] &#8211; (int) b[5];<br />
	&nbsp; if (a[6] != b[6])<br />
	&nbsp; &nbsp; return (int) a[6] &#8211; (int) b[6];<br />
	&nbsp; return (int) a[7] &#8211; (int) b[7];<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;">编写这类代码时，我猜绝大多数人会选择Copy-Paste，然后再逐行修改，问题就发生在修改过程中，上面的代码中当处理a[5] != b[5]时就忘记修改一个下标了：return (int) a[1] &#8211; (int) b[5];显然这里的正确代码应该是return (int) a[5] &#8211; (int) b[5]。</p>
<p style="font-size: 13px;"><strong>[#3]</strong>&nbsp;TortoiseSVN项目 文件名不正确</p>
<p><span style="font-family:courier new,courier,monospace;">BOOL GetImageHlpVersion(DWORD &amp;dwMS, DWORD &amp;dwLS)<br />
	{<br />
	&nbsp; return(GetInMemoryFileVersion((&quot;DBGHELP.DLL&quot;),<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dwMS, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dwLS)) ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">BOOL GetDbgHelpVersion(DWORD &amp;dwMS, DWORD &amp;dwLS)<br />
	{<br />
	&nbsp; return(GetInMemoryFileVersion((&quot;DBGHELP.DLL&quot;),<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dwMS, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dwLS)) ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<br />
	}</span></p>
<p style="font-size: 13px;">GetImageHlpVersion和GetDbgHelpVersion都使用了&quot;DBGHELP.DLL&quot;文件，显然GetImageHlpVersion写错文件名了。应该用&quot;IMAGEHLP.DLL&quot;就对了。</p>
<p style="font-size: 13px;"><strong>[#4]</strong> Clang项目 &#8211; 等同的函数体</p>
<p><span style="font-family:courier new,courier,monospace;">MapTy PerPtrTopDown;<br />
	MapTy PerPtrBottomUp;</span></p>
<p><span style="font-family:courier new,courier,monospace;">void clearBottomUpPointers() {<br />
	&nbsp; PerPtrTopDown.clear();<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">void clearTopDownPointers() {<br />
	&nbsp; PerPtrTopDown.clear();<br />
	}</span></p>
<p style="font-size: 13px;">我们看到虽然两个函数名不同，但是函数体的内容是相同的，显然又是copy-paste惹的祸。做如下修改即可：</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void clearBottomUpPointers() {<br />
	&nbsp; PerPtrBottomUp.clear();<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;"><strong style="font-size: 13px;">十一、Null指针的校验迟了</strong></p>
<p style="font-size: 13px;">这里的&ldquo;迟了&rdquo;的含义是先使用指针，然后再校验指针是否为NULL。</p>
<p style="font-size: 13px;"><strong>[#1]</strong>&nbsp;Quake-III-Arena项目 &#8211; 校验迟了</p>
<p style="font-size: 13px;"><span style="font-family:courier new,courier,monospace;">void Item_Paint(itemDef_t *item) {<br />
	&nbsp; vec4_t red;<br />
	&nbsp; menuDef_t *parent = (menuDef_t*)item-&gt;parent;<br />
	&nbsp; red[0] = red[3] = 1;<br />
	&nbsp; red[1] = red[2] = 0;<br />
	&nbsp; if (item == NULL) {<br />
	&nbsp; &nbsp; return;<br />
	&nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style="font-size: 13px;">&nbsp;</p>
<p style="font-size: 13px;">在校验item是否为NULL前已经使用过item了，一旦item真的为NULL，那程序必然崩溃。</p>
<p style="font-size: 13px;"><strong>十二、其他杂项</strong></p>
<p style="font-size: 13px;"><strong>[#1]</strong> Image Processing 项目 &#8211; 八进制数</p>
<p><span style="font-family:courier new,courier,monospace;">inline<br />
	void elxLuminocity(const PixelRGBus&amp; iPixel,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;LuminanceCell&lt; PixelRGBus &gt;&amp; oCell)<br />
	{<br />
	&nbsp; oCell._luminance = uint16(0.2220f*iPixel._red +<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0.7067f*iPixel._blue + 0.0713f*iPixel._green);<br />
	&nbsp; oCell._pixel = iPixel;<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">inline<br />
	void elxLuminocity(const PixelRGBi&amp; iPixel,<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;LuminanceCell&lt; PixelRGBi &gt;&amp; oCell)<br />
	{<br />
	&nbsp; oCell._luminance = 2220*iPixel._red +<br />
	&nbsp; &nbsp; 7067*iPixel._blue + 0713*iPixel._green;<br />
	&nbsp; oCell._pixel = iPixel;<br />
	}</span></p>
<p>第二个函数，程序员原意是使用713这个十进制整数，但0713 != 713，在C中，0713是八进制的表示法，Compiler会认为这是个八进制数。</p>
<p><strong>[#2]</strong> IPP Sample工程 &#8211; 一个变量用于两个loop中</p>
<p><span style="font-family:courier new,courier,monospace;">JERRCODE CJPEGDecoder::DecodeScanBaselineNI(void)<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; for(c = 0; c &lt; m_scan_ncomps; c++)<br />
	&nbsp; {<br />
	&nbsp; &nbsp; block = m_block_buffer + (DCTSIZE2*m_nblock*(j+(i*m_numxMCU)));</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; // skip any relevant components<br />
	&nbsp; &nbsp; for(c = 0; c &lt; m_ccomp[m_curr_comp_no].m_comp_no; c++)<br />
	&nbsp; &nbsp; {<br />
	&nbsp; &nbsp; &nbsp; block += (DCTSIZE2*m_ccomp[c][/c][/c].m_nblocks);<br />
	&nbsp; &nbsp; }<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>变量c用在了两个loop中，这会导致只有部分数据被处理，或外部循环中止。</p>
<p><strong>[#3]</strong> Notepad++项目 &#8211; 怪异的条件表达式</p>
<p><span style="font-family:courier new,courier,monospace;">int Notepad_plus::getHtmlXmlEncoding(&#8230;.) const<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (langT != L_XML &amp;&amp; langT != L_HTML &amp;&amp; langT == L_PHP)<br />
	&nbsp; &nbsp; return -1;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p>代码中的那行if条件等价于 if (langT == L_PHP)，显然似乎不是作者原意，猜测正确的代码应该是这样的：</p>
<p><span style="font-family:courier new,courier,monospace;">int Notepad_plus::getHtmlXmlEncoding(&#8230;.) const<br />
	{<br />
	&nbsp; &#8230;<br />
	&nbsp; if (langT != L_XML &amp;&amp; langT != L_HTML &amp;&amp; langT != L_PHP)<br />
	&nbsp; &nbsp; return -1;<br />
	&nbsp; &#8230;<br />
	}</span></p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
	</channel>
</rss>
