Skip to content

Feature/zh s t Simplified and Traditional Chinese (zh-Hans & zh-Hant) translation #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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