Skip to content
13 changes: 13 additions & 0 deletions README_zh-Hans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
欢迎来到 FFmpeg 汇编语言学院!你已经踏上了一段既有趣又充满挑战,同时回报丰厚的编程旅程。这些课程将帮助你掌握在 FFmpeg 中编写汇编语言的基础,并让你深入了解计算机内部的实际运作方式。

**必备知识**

* C 语言基础,尤其是指针。如果你不了解 C 语言,建议先学习 《C 程序设计语言》 这本书。
* 高中数学(例如标量与向量的区别、加法、乘法等)。

**课程内容**

在这个 Git 仓库中,你可以找到课程内容及对应的作业(作业暂未上传)。完成所有课程后,你将具备为 FFmpeg 贡献代码的能力。

我们有个 Discord 服务器用来解答问题:
https://discord.com/invite/Ks5MhUhqfB
13 changes: 13 additions & 0 deletions README_zh-Hant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
歡迎來到 FFmpeg 組合語言學院!你已經踏上了一段既有趣又充滿挑戰,同時回報豐厚的程式設計旅程。這些課程將幫助你掌握在 FFmpeg 中編寫組合語言的基礎,並讓你深入了解電腦內部的實際運作方式。

**必備知識**

* C 語言基礎,尤其是指標。如果你不了解 C 語言,建議先學習《C 程式設計語言》這本書。
* 高中數學(例如純量與向量的區別、加法、乘法等)。

**課程內容**

在這個 Git 儲存庫中,你可以找到課程內容及對應的作業(作業暫未上傳)。完成所有課程後,你將具備為 FFmpeg 貢獻程式碼的能力。

我們有個 Discord 伺服器用來解答問題:
https://discord.com/invite/Ks5MhUhqfB
216 changes: 216 additions & 0 deletions lesson_01/index_zh-hans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
**FFmpeg 汇编语言第一课**

**介绍**

欢迎来到 FFmpeg 汇编语言学校。你已经向着编程中最有趣、最具挑战性和最有回报的旅程迈出了第一步。这些课程将为你奠定在 FFmpeg 中编写汇编语言的基础,并让你了解计算机中实际发生的情况。

**所需知识**

* C 语言知识,特别是指针。如果你不了解 C 语言,建议学习[《C程序设计语言》](https://zh.wikipedia.org/wiki/C程序设计语言_(书))一书
* 高中数学(标量与向量、加法、乘法等)

**什么是汇编语言 (Assembly language)?**

汇编语言 (Assembly language) 是一种编程语言,你编写的代码直接对应于CPU处理的指令。人类可读的汇编语言,顾名思义,被*汇编 (assembled)* 成CPU能够理解的二进制数据,称为*机器码 (machine code)*。你可能会看到汇编语言代码被简称为 "assembly" 或 "asm" 。

FFmpeg中的绝大多数汇编代码都是所谓的 *SIMD,单指令多数据 (Single Instruction Multiple Data)*。SIMD有时也被称为向量编程 (vector programming)。这意味着特定指令能同时对多个数据元素进行操作。大多数编程语言一次只处理一个数据元素,称为标量编程 (scalar programming)。

你可能已经猜到,SIMD 非常适合处理图像、视频和音频,因为这些数据在内存中按顺序排列。CPU中有专门的指令帮助我们处理这种顺序数据。

在 FFmpeg 中,你会看到 "汇编函数 (assembly function)" 、 "SIMD" 和 "向量(化)" 这些术语被互换使用。它们都指的是同一件事:通过手写汇编语言函数一次性地处理多个数据元素。一些项目也可能将这些称为 "汇编内核 (assembly kernels)" 。

所有这些听起来可能比较复杂,但请记住,在FFmpeg项目中,高中生也能编写汇编代码。与所有学习一样,掌握汇编语言是50%理解术语和50%实际动手。

**为什么我们用汇编语言编写?**

为了使多媒体处理得更快。编写汇编代码通常能带来10倍或以上的速度提升,这对于实时播放视频而不卡顿尤为重要。它还能节省能源并延长电池寿命。值得注意的是,视频编码和解码是全球最常用的功能之一,无论是普通用户还是大型企业的数据中心都用到它们。因此,即使是微小的改进,也能迅速积累出显著的效果。

在网上,你会经常看到人们使用*内联函数 (intrinsics)*,这些C语言风格的函数映射到汇编指令,以便更快地开发。在 FFmpeg 中,我们不使用内联函数,而是手写汇编代码。这是一个有争议的部分,但内联函数通常比手写汇编慢约10-15%(内联函数支持者可能会不同意),这取决于编译器。对于 FFmpeg 来说,每一点额外的性能提升都很重要,这就是为什么我们直接使用汇编代码。还有一种观点认为,内联函数由于使用了 "[匈牙利命名法](https://zh.wikipedia.org/wiki/匈牙利命名法)" 而难以阅读。

出于历史原因,你可能还会在 FFmpeg 的一些地方看到*内联汇编 (inline assembly)*(即不使用内联函数),或者在 Linux 内核等项目中因为非常特殊的应用场景而看到内联汇编。这是指汇编代码不写在单独的文件中,在像 FFmpeg 这样的项目中,主流观点认为这种代码难以阅读,缺乏广泛的编译器支持,并且难以维护。

最后,你会看到很多自称专家的人在网上说这些都是没必要的,编译器可以为你完成所有这些 "向量化" 工作。至少以学习的目的,请忽视他们:例如,[dav1d项目](https://www.videolan.org/projects/dav1d.html)的最近测试显示,这种自动向量化带来了约2倍的速度提升,而手写版本可以达到8倍。

**汇编语言的风格**

本课程将专注于 x86 64 位汇编语言,它也被称为 amd64,尽管它同样适用于 Intel 处理器。此外,不同的 CPU 也有其他类型的汇编语言,例如 ARM 和 RISC-V,未来这些课程可能会扩展到涵盖这些架构。

网上常见的 x86 汇编风格有两种:AT&T 和 Intel。AT&T 语法较早,但相较于 Intel 语法更难阅读。因此,我们将使用 Intel 风格。

**辅助材料**
你可能会惊讶地发现,书籍或在线资源(如 Stack Overflow)作为参考并不是特别有用。部分原因是我们选择手写 Intel 汇编。但也因为很多在线资源都集中在操作系统编程或硬件编程上,通常使用非 SIMD 代码。而 FFmpeg 的汇编特别侧重于高性能图像处理,正如你将看到的,它采用了一种独特的汇编编程方法。不过,一旦你完成这些课程,理解其他汇编应用场景就很容易了。

许多书籍在教汇编之前会深入介绍计算机架构细节。如果你想学这些,那很好,但从我们的角度来看,这就像在学习开车之前研究发动机。

话虽如此,《64位汇编语言的编程艺术》 (The Art of 64-bit assembly) 一书后半部分的图解,以可视化方式展示 SIMD 指令及其行为,还是很有用的:[https://artofasm.randallhyde.com/](https://artofasm.randallhyde.com/)

我们有个 Discord 服务器用来解答问题:
[https://discord.com/invite/Ks5MhUhqfB](https://discord.com/invite/Ks5MhUhqfB)

**寄存器 (Register)**
寄存器是 CPU 内用于处理数据的区域。CPU 并不会直接对内存进行操作,而是先将数据加载到寄存器中进行处理,然后再写回内存。在汇编语言中,通常不能直接在两个内存位置之间复制数据,而必须先通过寄存器中转。

**通用寄存器 (General Purpose Register)**
第一种寄存器被称为通用寄存器(GPR)。之所以称为'通用',是因为它们既可以存储数据(最大支持 64 位值),也可以存储内存地址(指针)。GPR 中的值可以进行各种操作,如加法、乘法、位移等。

在大多数汇编书籍中,会有整章内容专门讲解 GPR 的细节和历史背景等。这是因为在操作系统编程、逆向工程等领域,GPR 是非常重要的。然而,在 FFmpeg 中编写的汇编代码里,GPR 更像是脚手架,大多数情况下不需要它们的复杂性,并且被抽象出来。

**向量寄存器 (Vector registers)**
向量 (SIMD) 寄存器,顾名思义,包含多个数据元素。不同类型的向量寄存器包括:

* mm寄存器 - MMX寄存器,64位大小,历史悠久,目前已不常使用
* xmm寄存器 - XMM寄存器,128位大小,广泛可用
* ymm寄存器 - YMM寄存器,256位大小,使用时有一些复杂性
* zmm寄存器 - ZMM寄存器,512位大小,可用性有限

视频压缩和解压缩中的大多数计算是基于整数的,因此我们将继续使用整数类型。以下是 xmm 寄存器中 16 字节 (bytes) 数据的一个例子:

| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- |

但它也可以是八个字 (words)(16位 (bit) 整数)

| a | b | c | d | e | f | g | h |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- |

或四个双字 (double words)(32位整数)

| a | b | c | d |
| :---- | :---- | :---- | :---- |

或两个四字 (quadwords)(64位整数):

| a | b |
| :---- | :---- |

回顾一下:


* **b**ytes 字节 - 8位数据
* **w**ords 字 - 16位数据
* **d**oublewords 双字 - 32位数据
* **q**uadwords 四字 - 64位数据
* **d**ouble **q**uadwords 双四字 - 128位数据

加粗的字符稍后会很重要。

**x86inc.asm 包含文件**

在许多示例中,你会看到我们包含了文件 x86inc.asm。x86inc.asm 是 FFmpeg、x264 和 dav1d 中使用的轻量级抽象层,旨在简化汇编程序员的工作。它在许多方面提供帮助,但首先,它的一个有用功能是给 GPR 寄存器标记为 r0、r1、r2。这样,你就不必记住任何寄存器的名称。如前所述,GPR 通常只是脚手架,所以这种做法大大简化了编程过程。

**一个简单的标量汇编 (scalar asm) 片段**

让我们看一段简单(且非常人工构造)的标量汇编代码(每条指令一次只对单个数据项进行操作的汇编代码),看看发生了什么:

```assembly
mov r0q, 3
inc r0q
dec r0q
imul r0q, 5
```

在第一行中,*立即数 (immediate value)* 3(直接存储在汇编代码中的值,而不是从内存中获取的值)被存储到寄存器 r0 中,作为一个四字(quadword)值。注意,在 Intel 风格中,源操作数(提供数据的值或位置,位于右侧)被传送到目标操作数(接收数据的位置,位于左侧),类似于 memcpy 的行为。你也可以理解为"r0q = 3",因为顺序是相同的。r0 的 "q" 后缀表示该寄存器作为四字 (**q**uadword) 来用。"inc" 操作将值增加,使 r0q 包含 4,"dec" 操作将值减少回 3,"imul" 将值乘以 5。因此,最终 r0q 包含 15。

请注意,汇编器 (assembler) 将人类可读的指令(如 mov 和 inc)汇编成机器代码,这些被称为*助记符 (mnemonics)*。你可能会在网上和书籍中看到助记符以大写字母表示,如 MOV 和 INC,但它们与小写版本完全相同。在 FFmpeg 中,我们使用小写助记符,并将大写保留给宏 (macro) 定义。

**理解基本的向量函数 (vector function)**

以下是我们的第一个 SIMD 函数:

```assembly
%include "x86inc.asm"

SECTION .text

;static void add_values(uint8_t *src, const uint8_t *src2)
INIT_XMM sse2
cglobal add_values, 2, 2, 2, src, src2
movu m0, [srcq]
movu m1, [src2q]

paddb m0, m1

movu [srcq], m0

RET
```

让我们逐行分析:

```assembly
%include "x86inc.asm"
```

这是一个由x264、FFmpeg 和 dav1d 社区开发的"头文件",提供帮助函数、预定义名称和宏(如下面的cglobal)以简化汇编编写。

```assembly
SECTION .text
```

这表示代码执行所在的区域。这与 .data 部分相对,你可以在 .data 部分放置常量数据。

```assembly
;static void add_values(uint8_t *src, const uint8_t *src2)
INIT_XMM sse2
```

第一行是注释(在汇编中,分号 ";" 的作用类似于 C 语言中的 "//"),用于展示该函数的 C 语言参数形式。第二行用于初始化函数,使其能够使用 XMM 寄存器,并指定使用 sse2 指令集。这是因为 paddb 是一个 sse2 指令。我们将在下一课中更详细地介绍 sse2。

```assembly
cglobal add_values, 2, 2, 2, src, src2
```

这一行至关重要,它定义了一个名为 add_values 的 C 语言函数。

让我们逐项解析:

* 下一个参数指定了该函数有两个参数。
* 再下一个参数指定这两个参数将使用两个 GPR 进行传递。在某些情况下,我们可能需要更多的 GPR,因此必须告诉 x86util 我们需要额外的寄存器。
* 再下一个参数指定了 x86util 需要多少个 XMM 寄存器。
* 最后两个参数是函数参数的标签。

值得注意的是,旧代码可能不会为函数参数添加标签,而是直接使用 r0、r1 等通用寄存器(GPR)来访问参数。

```assembly
movu m0, [srcq]
movu m1, [src2q]
```

movu 是 movdqu(move double quad unaligned 移动双四字未对齐)的简写。关于对齐的内容我们会在后续课程中讲解,但目前可以将 movu 视为从 [srcq] 进行 128 位数据传输。对于 mov 指令,方括号表示对 [srcq] 指向的地址进行解引用,相当于 *C 语言中的 \*src*,这就是所谓的"加载"(load)。需要注意的是,q 后缀表示指针的大小(即在 C 语言中在 64 位系统上 `sizeof(*src) == 8`,x86 汇编在 32 位系统上则会自动调整为 32 位),但实际加载的数据仍然是 128 位的。

需要注意的是,我们并不使用向量寄存器的完整名称(例如 xmm0),而是使用抽象形式的 m0。在后续课程中,你会看到这种方式的优势 —— 它允许你编写一次代码,并在不同大小的 SIMD 寄存器上通用适用。

```assembly
paddb m0, m1
```

paddb(可以在脑海中读作 *p-add-b*)会对每个寄存器中的每个字节进行相加,如下所示。"p" 前缀代表 "packed"(打包),用于区分向量指令和标量指令。"b" 后缀表示这是按字节(bytewise)进行的加法运算。

| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- |

\+

| q | r | s | t | u | v | w | x | y | z | aa | ab | ac | ad | ae | af |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- |

\=

| a+q | b+r | c+s | d+t | e+u | f+v | g+w | h+x | i+y | j+z | k+aa | l+ab | m+ac | n+ad | o+ae | p+af |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- |

```assembly
movu [srcq], m0
```

这就是所谓的存储。数据被写回 srcq 指针中的地址。

```assembly
RET
```

这是一个表示函数返回的宏 (macro)。在 FFmpeg 中,几乎所有汇编函数都会直接修改参数中的数据,而不是返回一个值。

在后续的作业中,你会看到,我们会为汇编函数创建函数指针,并在可用的地方使用它们。

[下一课](../lesson_02/index_zh-hans.md)
Loading