From 4f81d6765fc788730269153f93e102ab7224c56f Mon Sep 17 00:00:00 2001 From: LunaStev Date: Wed, 12 Nov 2025 14:48:22 +0900 Subject: [PATCH 1/2] Add korean translations --- README.es.md | 3 +- README.fr.md | 3 +- README.ko.md | 19 ++++ README.md | 3 +- lesson_01/index.ko.md | 218 ++++++++++++++++++++++++++++++++++++++++++ lesson_02/index.ko.md | 168 ++++++++++++++++++++++++++++++++ lesson_03/index.ko.md | 204 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 615 insertions(+), 3 deletions(-) create mode 100644 README.ko.md create mode 100644 lesson_01/index.ko.md create mode 100644 lesson_02/index.ko.md create mode 100644 lesson_03/index.ko.md diff --git a/README.es.md b/README.es.md index 7c9a91a..6de95d9 100644 --- a/README.es.md +++ b/README.es.md @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Traducciones** * [English](./README.md) -* [Français](./README.fr.md) \ No newline at end of file +* [Français](./README.fr.md) +* [Korean](./README.ko.md) \ No newline at end of file diff --git a/README.fr.md b/README.fr.md index b9b1fa8..38df535 100644 --- a/README.fr.md +++ b/README.fr.md @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Traductions** * [English](./README.md) -* [Spanish](./README.es.md) \ No newline at end of file +* [Spanish](./README.es.md) +* [Korean](./README.ko.md) \ No newline at end of file diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 0000000..6b5879c --- /dev/null +++ b/README.ko.md @@ -0,0 +1,19 @@ +FFempeg 어셈블리 언어 강좌에 오신 것을 환영합니다. 여러분은 프로그래밍에서 가장 흥미롭고, 도전적이며, 보람찬 여정의 첫 걸음을 내디뎠습니다. 이 강의들은 FFmpeg에서 어셈블리 언어가 작성되는 방식에 대한 기초를 다지게 해주며, 컴퓨터 내부에서 실제로 어떤 일이 일어나는지에 대한 시야를 넓혀 줄 것입니다. + +**필수 지식** + +* C 언어 지식, 특히 포인터에 대한 이해. C를 모른다면, [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language) 책을 충분히 학습(공부)하십시오. +* 고등학교 수준의 수학 지식 (스칼라와 벡터의 차이, 덧셈, 곱셈 등) + +**강의** + +이 Git 저장소에는 각 강의에 해당하는 강의 자료와 과제(미업로드)가 포함되어 있습니다. 모든 강의를 마치면 FFmpeg에 직접 기여할 수 있게 될 것입니다. + +질문이 있을 경우 아래의 디스코드 서버에서 도움을 받을 수 있습니다. +https://discord.com/invite/Ks5MhUhqfB + +**번역** + +* [English](./README.md) +* [Français](./README.fr.md) +* [Spanish](./README.es.md) \ No newline at end of file diff --git a/README.md b/README.md index 67c4007..fd04f5a 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Translations** * [Français](./README.fr.md) -* [Spanish](./README.es.md) \ No newline at end of file +* [Spanish](./README.es.md) +* [Korean](./README.ko.md) \ No newline at end of file diff --git a/lesson_01/index.ko.md b/lesson_01/index.ko.md new file mode 100644 index 0000000..1f70c02 --- /dev/null +++ b/lesson_01/index.ko.md @@ -0,0 +1,218 @@ +**FFmpeg 어셈블리 언어 1강** + +**소개** + +FFmpeg 어셈블리 언어 강좌에 오신 것을 환영합니다. 여러분은 프로그래밍에서 가장 흥미롭고, 도전적이며, 보람찬 여정의 첫 걸음을 내디뎠습니다. 이 강의들은 FFmpeg에서 어셈블리 언어가 작성되는 방식에 대한 기초를 다질 수 있게 도와주며, 컴퓨터 내부에서 실제로 어떤 일이 벌어지는지 이해할 수 있도록 도와줄 것입니다. + +**필수 지식** + +* C 언어 지식, 특히 포인터에 대한 이해. C를 모른다면, [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_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", "벡터화(vector(ise))"라는 용어가 혼용되며 동일한 개념을 의미합니다. 즉, 여러 데이터 요소를 한 번에 처리하기 위해 어셈블리 언어로 직접함수를 작성하는 것을 뜻합니다. 또한 일부 프로젝트에서는 이러한 코드를 "어셈블리 커널(assembly kernels)"이라고 부르기도 합니다. + +이 모든 것이 복잡하게 들릴 수도 있지만, 기억해야 할 중요한 점은 FFmpeg에서도 실제로 고등학생들이 어셈블리 코드를 작성했다는 것입니다. 모든 학습이 그렇듯, 배우는 과정의 절반은 용어(전문 용어) 습득이고, 나머지 절반이 실제 학습(이해와 연습)입니다. + +**왜 어셈블리 언어를 작성하는가?** + +멀티미디어 처리를 빠르게 하기 위해서입니다. 어셈블리 코드를 작성하면 속도가 10배 이상 향상되는 경우가 흔하며, 이는 끊김 없이 부드럽게 실시간으로 비디오를 재생할 때 특히 중요합니다. 또한 에너지 소모를 줄이고 배터리 수명을 연장합니다. 비디오 인코드와 디코드 함수는 일반 사용자와 대형 기업의 데이터 센터 모두에서 전 세계적으로 가장 많이 사용되는 함수 중 하나입니다. 따라서 작은 개선점도 빠르게 큰 이점으로 이어집니다. + +온라인에서는 더 빠른 개발을 위해 어셈블리 명령어로 매핑되는 C 언어 형태의 함수인 *인트린식(intrinsics)* 을 사용하는 경우를 자주 볼 수 있습니다. FFmpeg에서는 인트린식을 사용하지 않고 어셈블리 코드를 직접 작성합니다. 이것은 논란의 여지가 있는 부분이지만, 인트린식은 일반적으로 수동으로 작성된 어셈블리보다 약 10~15% 느립니다(인트린식 지지자들은 동의하지 않을 것입니다). 컴파일러에 따라 다르지만, FFmpeg에서는 가능한 모든 성능 향상이 도움이 되기 때문에 어셈블리 코드를 직접 작성합니다. 또한 인트린식은 “[헝가리안 표기법(Hungarian Notation)](https://en.wikipedia.org/wiki/Hungarian_notation)”을 사용하기 때문에 읽기 어렵다는 주장도 있습니다. + +또한 FFmpeg의 일부 영역이나 Linux 커널 같은 프로젝트에서는 역사적인 이유나 매우 특정한 사용 사례로 인해 *인라인 어셈블리(inline assembly)* (즉, 인트린식을 사용하지 않는 방식)이 여전히 남아 있을 수 있습니다. 이것은 어셈블리 코드가 별도의 파일에 있지 않고 C 코드 안에 인라인으로 작성되는 경우입니다. FFmpeg과 같은 프로젝트에서는 이 코드가 읽기 어렵고, 컴파일러에서 널리 지원되지 않으며, 유지보수가 어렵다는 의견이 우세합니다. + +마지막으로, 온라인에서는 자칭 전문가들이 이런 것들은 전혀 필요 없고 컴파일러가 이런 "벡터화(vectorisation)"를 모두 처리할 수 있다고 말하는 것을 자주 볼 수 있습니다. 적어도 학습의 목적에서는 그들을 무시해야 합니다. 예를 들어 [dav1d 프로젝트](https://www.videolan.org/projects/dav1d.html)의 초근 테스트에서는 이러한 자동 벡터화로 약 2배의 속도 향상을 얻었지만, 수동으로 작성된 버전은 최대 8배의 속도 향상을 보였습니다. + +**어셈블리 언어의 유형** + +이 강의는 x86 64비트 어셈블리 언어에 초점을 맞춥니다. 이는 amd64라고도 불리지만, 여전히 인텔 CPU에서도 작동합니다. ARM이나 RISC-V 같은 다른 CPU용 어셈블리 언어도 있으며, 향후에는 이 강의가 이러한 내용들을 다루도록 확장될 수 있습니다. + +온라인에서는 두 가지 x86 어셈블리 문법 유형을 볼 수 있습니다. AT&T와 Intel 문법입니다. AT&T 문법은 더 오래되었으며, Intel 문법에 비해 읽기 어렵습니다. 따라서 우리는 Intel 문법을 사용할 것입니다. + +**참고 자료** + +책이나 Stack Overflow 같은 온라인 자료가 참고 자료로써 크게 도움이 되지 않는다는 점이 놀랍게 들릴 수도 있습니다. 이는 부분적으로 우리가 Intel 문법으로 직접 작성한 어셈블리를 사용하기로 한 선택 때문입니다. 또한 온라인 자료의 상당수가 운영체제 프로그래밍이나 하드웨어 프로그래밍에 초점을 맞추고 있으며, 대부분 SIMD가 아닌 코드를 사용하기 때문이기도 합니다. FFmpeg의 어셈블리는 특히 고성능 이미지 처리에 초점을 맞추고 있으며, 보시다시피 어셈블리 프로그래밍에 있어 매우 독자적인 접근 방식을 취합니다. 하지만 이 강의를 마치고 나면 다른 어셈블리 사용 사례를 이해하는 것은 쉬워집니다. + +많은 책들이 어셈블리를 가르치기 전에 컴퓨터 아키텍처의 세부 내용을 깊이 다룹니다. 그것이 배우고 싶은 내용이라면 괜찮지만, 우리의 관점에서 보면 그것은 마치 운전을 배우기 전에 엔진 구조를 공부하는 것과 같습니다. + +그럼에도 불구하고, "The Art of 64-bit Assembly" 책의 후반부에 나오는 SIMD 명령어와 그 동작을 시각적으로 보여주는 다이어그램은 도움이 됩니다. [https://artofasm.randallhyde.com/](https://artofasm.randallhyde.com/) + +질문이 있을 경우 아래의 디스코드 서버에서 도움을 받을 수 있습니다. [https://discord.com/invite/Ks5MhUhqfB](https://discord.com/invite/Ks5MhUhqfB) + +**레지스터** + +레지스터는 CPU 내부에서 데이터가 처리되는 공간입니다. CPU는 메모리를 직접 다루지 않고, 데이터를 레지스터로 불러와 처리한 뒤 메모리에 되씁니다. 어셈블리 언어에서는 일반적으로 데이터를 레지스터를 거치지 않고 한 메모리 위치에서 다른 메모리 위치로 직접 복사할 수 없습니다. + +**범용 레지스터** + +첫 번째 종류의 레지스터는 범용 레지스터(GPR, General Purpose Register)라고 불립니다. GPR은 일반적인 용도로 사용될 수 있기 때문에 이렇게 불리며, 데이터(여기서는 최대 64비트 값)나 메모리 주소(포인터)를 저장할 수 있습니다. GPR에 저장된 값은 덧셈, 곱셈, 시프트 등의 연산을 통해 처리될 수 있습니다. + +대부분의 어셈블리 관련 책에서는 GPR의 세부적인 동작이나 역사적 배경 등을 다루는 데 여러 장을 할애합니다. 그 이유는 GPR이 운영체제 프로그래밍, 리버스 엔지니어링 등에서 중요하기 때문입니다. 하지만 FFmpeg에서 작성되는 어셈블리 코드에서는 GPR이 주로 임시적인 역할을 하며, 대부분의 경우 그 복잡한 부분은 필요하지 않거나 추상화됩니다. + +**벡터 레지스터** + +벡터(SIMD) 레지스터는 이름 그대로 여러 개의 데이터 요소를 포함합니다. 벡터 레지스터는 다양한 종류가 있습니다. + +* mm 레지스터 - MMX 레지스터, 64비트 크기, 오래된 방식으로 현재는 거의 사용되지 않습니다. +* xmm 레지스터 - XMM 레지스터, 128비트 크기, 널리 사용됩니다. +* ymm 레지스터 - YMM 레지스터, 256비트 크기, 사용 시 약간의 복잡성이 있습니다. +* zmm 레지스터 - ZMM 레지스터, 512비트 크기, 사용 가능성이 제한적입니다. + +비디오 압축과 해제의 대부분 계산은 정수 기반이므로 우리는 그에 집중할 것입니다. 다음은 xmm 레지스터 안의 16바이트 예시입니다. + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +하지만 워드(16비트 정수) 8개로 구성될 수도 있습니다. + +| a | b | c | d | e | f | g | h | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +또는 32비트 정수(더블워드) 4개일 수도 있습니다. + +| a | b | c | d | +| :---- | :---- | :---- | :---- | + +혹은 64비트 정수(쿼드워드) 2개일 수도 있습니다. + +| 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은 일반적으로 보조적인 역할만 하므로, 이러한 방식은 작업을 훨씬 수월하게 해줍니다. + +**간단한 스칼라 어셈블리 코드 조각** + +각 명령어가 개별 데이터 항목을 한 번에 하나씩 처리하는 스칼라 어셈블리 코드의 간단한 (그리고 매우 인위적인) 예시를 살펴보겠습니다. + +```assembly +mov r0q, 3 +inc r0q +dec r0q +imul r0q, 5 +``` + +첫 번째 줄에서는 *즉시값(immediate value)* 3(메모리에서 가져오는 값과 달리 어셈블리 코드 자체에 직접 저장된 값)이 r0 레지스터에 쿼드워드(64비트)로 저장됩니다. Intel 문법에서는 오른쪽에 있는 소스 피연산자(데이터를 제공하는 값 또는 위치)가 왼쪽에 있는 목적지 피연산자(데이터를 받는 위치)로 전달됩니다. 이는 memcpy 동작 방식과 비슷합니다. 즉, "r0q = 3"으로 읽을 수 있으며, 순서도 동일합니다. r0의 접미사 "q"는 이 레지스터가 쿼드워드로 사용됨을 나타냅니다. inc 명령은 값을 1 증가시켜 r0q가 4를 가지게 하고, dec 명령은 값을 3으로 다시 감소시킵니다. imul 명령은 값을 5배로 곱합니다. 따라서 마지막에 r0q는 15를 가지게 됩니다. + +mov, inc 같은 사람이 읽을 수 있는 명령어들은 어셈블러에 의해 기계어로 변환되며, 이러한 명령어를 *니모닉(mnemonic)* 이라고 부릅니다. 온라인이나 책에서는 MOV, INC처럼 대문자로 표기된 경우를 볼 수도 있지만, 이는 소문자 버전과 동일합니다. FFmpeg에서는 소문자 니모닉을 사용하며, 대문자는 매크로를 위해 예약되어 있습니다. + +**기본 벡터 함수 이해하기** + +다음은 첫 번째 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 섹션과 대조됩니다. + +```assembly +;static void add_values(uint8_t *src, const uint8_t *src2) +INIT_XMM sse2 +``` + +첫 번째 줄은 주석입니다(;은 C의 //와 같습니다). 이 줄은 C에서의 함수 인자를 보여줍니다. 두 번째 줄은 sse2 명령어 집합을 사용해 XMM 레지스터의 사용을 초기화하는 부분입니다. 이는 paddb가 sse2 명령어이기 때문입니다. sse2에 대해서는 다음 강의에서 자세히 다룹니다. + +```assembly +cglobal add_values, 2, 2, 2, src, src2 +``` + +이 줄은 "add_values"라는 C 함수를 정의하는 매우 중요한 부분입니다. + +각 항목을 하나씩 살펴보면 다음과 같습니다. + +* 첫 번째 인자는 함수에 두 개의 인자가 있음을 나타냅니다. +* 두 번째 인자는 인자를 포함해 두 개의 GPR을 사용할 것임을 나타냅니다. 경우에 따라 더 많은 GPR이 필요하다면 x86util에 이를 지정해야 합니다. +* 세 번째 인자는 사용할 XMM 레지스터의 수를 나타냅니다. +* 마지막 두 인자는 함수 인자의 레이블 이름입니다. + +이전 코드에서는 함수 인자의 레이블이 없고, r0, r1 등의 GPR을 직접 참조하는 방식일 수도 있습니다. + +```assembly + movu m0, [srcq] + movu m1, [src2q] +``` + +movu는 movdqu(move double quad unaligned)의 축약형입니다. 메모리 정렬(alignment)은 이후 강의에서 다루겠지만, 지금은 [srcq]에서 128비트를 이동하는 명령으로 이해하면 됩니다. mov 명령에서 대괄호는 해당 주소를 역참조한다는 의미로, *C에서 \*src*와 같습니다. 이 동작은 로드(load)라고 합니다. q 접미사는 포인터의 크기를 나타내며(C에서 64비트 시스템의 경우 *sizeof(\*src) == 8*), x86asm은 32비트 시스템에서는 자동으로 32비트를 사용합니다. 하지만 실제 로드는 128비트 단위로 수행됩니다. + +벡터 레지스터는 전체 이름(xmm0 등)으로 참조하지 않고, 추상화된 형태인 m0, m1 등으로 사용합니다. 이 방식은 이후 강의에서 볼 수 있듯이, 하나의 코드를 여러 SIMD 레지스터 크기에서도 동일하게 작동하도록 해줍니다. + +```assembly +paddb m0, m1 +``` + +paddb(머릿속으로 *p-add-b*라고 읽습니다)는 아래 예시처럼 각 레지스터의 각 바이트를 더하는 명령어입니다. 접두사 p는 "packed"를 의미하며, 벡터 명령어와 스칼라 명령어를 구분하는 데 사용됩니다. 접미사 b는 연산이 바이트 단위 덧셈임을 나타냅니다. + +| 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 +``` + +이 명령은 스토어(store)라고 불리며, 데이터를 srcq 포인터가 가리키는 주소로 다시 기록합니다. + +```assembly +RET +``` + +이것은 함수가 반환함을 나타내는 매크로입니다. FFmpeg의 대부분의 어셈블리 함수는 값을 반환하는 대신 인자(파라미터) 데이터를 직접 수정합니다. + +과제에서 보게 되겠지만, 우리는 어셈블리 함수에 대한 함수 포인터를 생성하고, 가능할 경우 그것을 사용하게 됩니다. + +[다음 수업](../lesson_02/index.ko.md) \ No newline at end of file diff --git a/lesson_02/index.ko.md b/lesson_02/index.ko.md new file mode 100644 index 0000000..9c554eb --- /dev/null +++ b/lesson_02/index.ko.md @@ -0,0 +1,168 @@ +**FFmpeg 어셈블리 언어 2강** + +이제 첫 번째 어셈블리 함수를 작성했으니, 이번에는 분기(Branch)와 루프(Loop)를 소개하겠습니다. + +먼저 레이블(label)과 점프(jump)의 개념을 알아야 합니다. 아래의 인위적인 예시에서 jmp 명령은 코드 실행을 ".loop:" 이후로 이동시킵니다. ".loop:"은 *레이블*로 불리며, 이름 앞의 점(.)은 이것이 *로컬 레이블(local label)* 임을 나타냅니다. 이는 여러 함수에서 동일한 레이블 이름을 재사용할 수 있게 해줍니다. 물론 아래의 예시는 무한 루프이지만, 이후에 더 현실적인 예시로 확장하겠습니다. + +```assembly +mov r0q, 3 +.loop: + dec r0q + jmp .loop +``` + +현실적인 루프를 만들기 전에 *FLAGS* 레지스터를 소개해야 합니다. *FLAGS*의 세부 동작에 너무 깊게 들어가지는 않겠지만(GPR 연산은 대부분 보조적이기 때문입니다), 산술 연산이나 시프트 같은 스칼라 연산의 결과에 따라 설정되는 여러 플래그(Zero-Flag, Sign-Flag, Overflow-Flag 등)가 존재합니다. + +다음은 루프 카운터가 0이 될 때까지 감소하는 예시입니다. jg(jump if greater than zero) 명령은 루프 조건으로 사용됩니다. dec r0q 명령은 실행 후 r0q의 값에 따라 FLAGS를 설정하며, 이 플래그를 기반으로 분기할 수 있습니다. + +```assembly +mov r0q, 3 +.loop: + ; do something + dec r0q + jg .loop ; 0보다 크면 점프 +``` + +이는 다음 C 코드와 동일한 의미를 가집니다: + +```c +int i = 3; +do +{ + // do something + i--; +} while(i > 0); +``` + +이 C 코드는 약간 부자연스러우며, 일반적으로 C에서는 루프를 다음과 같이 작성합니다. + +```c +int i; +for(i = 0; i < 3; i++) { + // do something +} +``` + +이 C 코드는 완전히 동일하게 표현하기는 어렵지만, 대략 다음 어셈블리와 비슷합니다. + +```assembly +xor r0q, r0q +.loop: + ; do something + inc r0q + cmp r0q, 3 + jl .loop ; (r0q - 3) < 0, 즉 (r0q < 3)일 때 점프 +``` + +여기서 주목할 부분이 몇 가지 있습니다. 먼저 xor r0q, r0q는 레지스터 값을 0으로 설정하는일반적인 방법으로, 일부 시스템에서는 mov r0q, 0보다 빠릅니다. 이는 단순히 실제 로드(load) 연산이 일어나지 않기 때문입니다. 또한 SIMD 레지스터에서도 pxor m0, m0를 사용해 전체 레지스터를 0으로 초기화할 수 있습니다. + +이 코드 조각에는 cmp라는 추가 명령어가 한 줄 더 있습니다. 일반적으로 명령어 수가 적을수록 코드가 더 빠르기 때문에, 이전 루프 형태가 더 선호됩니다. 이후 강의에서는 이런 추가 명령을 피하고 산술 연산이나 다른 연산을 통해 *FLAGS*를 직접 설정하는 여러 방법을 배우게 됩니다. 우리는 C 루프와 정확히 동일한 구조를 맞추기보다는, 어셈블리에서 가능한 한 빠른 루프를 작성하는 것을 목표로 합니다. + +다음은 자주 사용되는 점프 니모닉(mnemonic)들입니다. (*FLAGS* 항목은 참고용이며, 루프를 작성하기 위해 세부 내용을 알 필요는 없습니다.) + +| Mnemonic | 설명 | FLAGS | +| :---- | :---- | :---- | +| JE/JZ | 같을 때 / 0일 때 점프 | ZF = 1 | +| JNE/JNZ | 같지 않을 때 / 0이 아닐 때 점프 | ZF = 0 | +| JG/JNLE | 크거나 / 작지 않거나 같을 때 점프 (부호 있음) | ZF = 0 and SF = OF | +| JGE/JNL | 크거나 같을 때 / 작지 않을 때 점프 (부호 있음) | SF = OF | +| JL/JNGE | 작을 때 / 크거나 같지 않을 때 점프 (부호 있음) | SF ≠ OF | +| JLE/JNG | 작거나 같을 때 / 크지 않을 때 점프 (부호 있음) | ZF = 1 or SF ≠ OF | + +**상수** + +상수를 사용하는 방법을 보여주는 예시를 살펴보겠습니다. + +```assembly +SECTION_RODATA + +constants_1: db 1,2,3,4 +constants_2: times 2 dw 4,3,2,1 +``` + +* SECTION_RODATA는 이 섹션이 읽기 전용 데이터 영역임을 지정합니다. (이것은 매크로이며, 운영체제에서 사용하는 출력 파일 형식에 따라 선언 방식이 다르기 때문입니다.) +* constants_1: constants_1이라는 레이블은 ```db```(declare byte)로 정의되어 있습니다. 즉, uint8_t constants_1[4] = {1, 2, 3, 4};와 동일합니다. +* constants_2: ```times 2``` 매크로를 사용하여 선언된 워드(16비트 단위)를 두 번 반복합니다. 즉, uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1};과 같습니다. + +이러한 레이블은 어셈블러에 의해 메모리 주소로 변환되며, 이후 로드(load) 연산에서 사용할 수 있습니다. (읽기 전용이므로 스토어(store) 연산은 불가능합니다.) 일부 명령어는 메모리 주소를 피연산자로 직접 취할 수 있으므로, 레지스터에 명시적으로 로드하지 않고도 사용할 수 있습니다. (이 방식에는 장단점이 있습니다.) + +**오프셋** + +오프셋은 메모리에서 연속된 요소들 사이의 거리(바이트 단위)를 의미합니다. 오프셋은 데이터 구조에서 **각 요소의 크기**에 의해 결정됩니다. + +이제 루프를 작성할 수 있게 되었으니 데이터를 가져올 차례입니다. 하지만 C와는 약간의 차이가 있습니다. 다음의 C 코드를 보겠습니다. + +```c +uint32_t data[3]; +int i; +for(i = 0; i < 3; i++) { + data[i]; +} +``` + +이때 data의 각 요소 사이의 4바이트 오프셋은 C 컴파일러가 미리 계산합니다. 하지만 어셈블리를 직접 작성할 때는 이러한 오프셋을 스스로 계산해야 합니다. + +메모리 주소 계산의 문법은 다음과 같습니다. 이 문법은 모든 형태의 메모리 주소에 적용됩니다. + +```assembly +[base + scale*index + disp] +``` + +* base - GPR이며, 일반적으로 C 함수 인자로부터 전달된 포인터 입니다. +* scale - 1, 2, 4, 8 중 하나의 값을 가질 수 있으며, 기본값은 1입니다. +* index - GPR이며, 일반적으로 루프 카운터로 사용됩니다. +* disp - 정수(최대 32비트)로, 데이터 내부의 오프셋(Displacement)을 의미합니다. + +x86asm은 현재 사용 중인 SIMD 레지스터의 크기를 알려주는 mmsize 상수를 제공합니다. + +다음은 사용자 정의 오프셋으로부터 데이터를 로드하는 간단한 (실제로는 의미 없는) 예시입니다. + +```assembly +;static void simple_loop(const uint8_t *src) +INIT_XMM sse2 +cglobal simple_loop, 1, 2, 2, src + movq r1q, 3 +.loop: + movu m0, [srcq] + movu m1, [srcq+2*r1q+3+mmsize] + + ; do some things + + add srcq, mmsize +dec r1q +jg .loop + +RET +``` + +```movu m1, [srcq+2*r1q+3+mmsize]``` 구문에서 어셈블러는 적절한 디스플레이스먼트 값을 자동으로 계산합니다. 다음 강의에서는 루프 내에서 add와 dec를 모두 사용하는 대신, 이를 단일 add로 대체하는 트릭을 다룰 것입니다. + +**LEA** + +이제 오프셋을 이해했으니 LEA(Load Effective Address)를 사용할 수 있습니다. 이는 하나의 명령으로 곱셈과 덧셈을 수행하게 해주며, 여러 명령을 사용하는 것보다 더 빠릅니다. 물론 곱하거나 더할 수 있는 값에는 제한이 있지만, 그렇다고 해서 lea가 강력한 명령이 아니라는 뜻은 아닙니다. + +```assembly +lea r0q, [base + scale*index + disp] +``` + +이름과 달리 LEA는 주소 계산뿐만 아니라 일반 산술 연산에도 사용할 수 있습니다. 예를 들어 다음과 같은 복잡한 계산도 가능합니다. + +```assembly +lea r0q, [r1q + 8*r2q + 5] +``` + +이 명령은 r1q와 r2q의 내용을 변경하지 않으며, *FLAGS*에도 영향을 주지 않습니다. 따라서 lea의 결과를 기반으로 점프할 수는 없습니다. lea를 사용하면 아래와 같은 여러 명령어와 임시 레지스터를 사용할 필요가 없습니다(아래 코드는 add가 *FLAGS*를 변경하기 때문에 완전히 동일한 동작은 아닙니다). + +```assembly +movq r0q, r1q +movq r3q, r2q +sal r3q, 3 ; 왼쪽으로 3비트 시프트 = ×8 +add r3q, 5 +add r0q, r3q +``` + +lea는 루프 전에 주소를 설정하거나 위와 같은 계산을 수행할 때 자주 사용됩니다. 물론 모든 형태의 곱셈과 덧셈을 수행할 수 있는 것은 아니지만, 1, 2, 4, 8로의 곱셈과 고정된 오프셋의 덧셈은 흔하게 사용됩니다. + +과제에서는 상수를 로드하고 루프안에서 SIMD 벡터에 그 값을 더하는 작업을 하게 될 것입니다. + +[다음 강의](../lesson_03/index.md) diff --git a/lesson_03/index.ko.md b/lesson_03/index.ko.md new file mode 100644 index 0000000..0227890 --- /dev/null +++ b/lesson_03/index.ko.md @@ -0,0 +1,204 @@ +**FFmpeg 어셈블리 3강** + +몇 가지 추가 용어를 설명하고, 짧은 역사 이야기를 해보겠습니다. + +**명령어 집합 (Instruction Sets)** + +이전 강의에서 언급했듯이, SSE2는 SIMD 명령어 집합 중 하나입니다. 새로운 CPU 세대가 출시될 때마다 새로운 명령어나 더 큰 레지스터 크기가 추가되기도 합니다. x86 명령어 집합의 역사는 매우 복잡하므로, 여기서는 단순화된 형태로 설명합니다. (세부 하위 분류는 훨씬 더 많습니다) + +* MMX - 1997년 출시, 인텔 프로세서에서 최초의 SIMD, 64비트 레지스터, 현재는 역사적인 기술 +* SSE (Streaming SIMD Extensions) - 1999년 출시, 128비트 레지스터 +* SSE2 - 2000년 출시, 다수의 새로운 명령어 추가 +* SSE3 - 2004년 출시, 최초의 수평(horizontal) 명령어 포함 +* SSSE3 (Supplemental SSE3) - 2006년 출시, 새로운 명령어 추가, 특히 pshufb 셔플 명령어 도입, 비디오 처리에서 가장 중요한 명령어 중 하나로 평가됨 +* SSE4 - 2008년 출시, 최소/최대 연산 등 다수의 새로운 명령어 포함 +* AVX - 2011년 출시, 256비트 레지스터(부동소수점 전용), 3-피연산자 문법 도입 +* AVX2 - 2013년 출시, 정수 명령어용 256비트 레지스터 +* AVX512 - 2017년 출시, 512비트 레지스터, 새로운 연산 마스크 기능 추가. 당시 FFmpeg에서는 새로운 명령어 사용 시 CPU 클럭이 하락하는 문제로 제한적으로 사용됨. vpermb를 통한 완전한 512비트 셔플(퍼뮤트) 지원. +* AVX512ICL - 2019년 출시, 클럭 하락 문제 해결. +* AVX10 - 출시 예정. + +명령어 집합은 추가될 뿐만 아니라 제거될 수도 있습니다. 예를 들어, AVX512는 인텔 12세대 CPU에서 [제거되었습니다](https://www.igorslab.de/en/intel-deactivated-avx-512-on-alder-lake-but-fully-questionable-interpretation-of-efficiency-news-editorial/). 이 때문에 FFmpeg은 실행 중인 CPU의 기능을 자동으로 감지합니다. + +과제에서 본 것처럼, 함수 포인터는 기본적으로 C로 정의되어 있으며 특정 명령어 집합 버전으로 대체됩니다. 이 과정은 한 번만 수행되며 이후에는 다시 감지할 필요가 없습니다. 이는 특정 명령어 집합을 하드코딩해 하드웨어 호환성을 잃는 상용프로그램들과 달리, FFmpeg이 오래된 시스템에서도 동작할 수 있게 해줍니다. 또한 런타임에서 최적화된 함수를 켜거나 끌 수 있다는 장점도 있습니다. 이것이 바로 오픈소스의 큰 이점 중 하나입니다. + +FFmpeg은 전 세계의 수십억 대의 기기에서 사용되며, 그중에는 매우 오래된 장치들도 있습니다. 기술적으로 FFmpeg은 SSE만 지원하는 시스템에서도 동작하며, 이는 약 25년 된 하드웨어입니다. 다행이도 x86inc.asm은 특정 명령어 집합에서 사용 불가능한 명령어를 사용할 경우 경고를 제공합니다. + +실제 환경의 예시로 2024년 11월 기준 [Steam 하드웨어 설문](https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam)에서의 명령어 집합 지원 현황은 다음과 같습니다 (게이머 중심 데이터이므로 약간의 편향이 있습니다). + +| 명령어 집합 | 지원 비율 | +| :---- | :---- | +| SSE2 | 100% | +| SSE3 | 100% | +| SSSE3 | 99.86% | +| SSE4.1 | 99.80% | +| AVX | 97.39% | +| AVX2 | 94.44% | +| AVX512 (Steam에서는 AVX512와 AVX512ICL을 구분하지 않음) | 14.09% | + +FFmpeg처럼 전 세계 수십억 명이 사용하는 프로그램에서는 0.1%의 사용자라도 문제가 생기면 매우 큰 숫자가 됩니다. 이 때문에 FFmpeg은 다양한 CPU/OS/컴파일러 조합을 테스트하는 [FATE 테스트 시스템](https://fate.ffmpeg.org/?query=subarch:x86_64%2F%2F)을 보유하고 있습니다. 모든 커밋은 수백 대의 머신에서 자동으로 실행되어 문제가 없는지 검증됩니다. + +인텔의 공식 명령어 집합 매뉴얼은 다음에서 확인할 수 있습니다: [https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) + +PDF로 검색하기 번거롭다면 비공식 웹 버전도 있습니다: [https://www.felixcloutier.com/x86/](https://www.felixcloutier.com/x86/) + +또한 SIMD 명령어를 시각적으로 표현한 자료도 있습니다: [https://www.officedaytime.com/simd512e/](https://www.officedaytime.com/simd512e/) + +x86 어셈블리의 어려움 중 하나는 자신의 필요에 맞는 올바른 명령어를 찾는 것입니다. 때로는 명령어가 원래 의도된 용도와 다르게 사용되기도 합니다. + +**포인터 오프셋 트릭 (Pointer offset trickery)** + +1강에서 다뤘던 원래의 함수를 다시 살펴보되, 이번에는 C 함수에 width 인자를 추가해보겠습니다. + +width 변수는 int 대신 ptrdiff_t 타입을 사용합니다. 이렇게 하는 64비트 인자의 상위 32비트가 반드시 0이 되도록 보장하기 위해서입니다. 만약 함수 시그니처에서 int width를 그대로 전달하고, 이후 포인터 연산에서 `widthq`로 사용한다면, 레지스터의 상위 32비트가 임의의 값으로 채워질 수 있습니다. 물론 `movsxd` 명령(또는 x86inc.asm의 `movsxdifnidn` 매크로)을 사용해 부호 확장(sign extend)으로 수정할 수도 있지만, 이 방법이 더 간단한 해결책입니다. + +다음 함수는 포인터 오프셋 트릭을 포함하고 있습니다. + +```assembly +;static void add_values(uint8_t *src, const uint8_t *src2, ptrdiff_t width) +INIT_XMM sse2 +cglobal add_values, 3, 3, 2, src, src2, width + add srcq, widthq + add src2q, widthq + neg widthq + +.loop + movu m0, [srcq+widthq] + movu m1, [src2q+widthq] + + paddb m0, m1 + + movu [srcq+widthq], m0 + add widthq, mmsize + jl .loop + + RET +``` + +이제 각 단계를 순서대로 살펴보겠습니다. + +```assembly + add srcq, widthq + add src2q, widthq + neg widthq +``` + +width 값이 각 포인터에 더해져서, 이제 각각의 포인터는 처리할 버퍼의 끝을 가리키게 됩니다. 그 후 width를 음수로 바꿉니다. + +```assembly + movu m0, [srcq+widthq] + movu m1, [src2q+widthq] +``` + +이제 widthq가 음수인 상태로 데이터를 로드합니다. 즉, 첫 번째 반복에서는 [srcq+widthq]가 원래의 srcq 주소(버퍼의 시작점)를 가리키게 됩니다. + +```assembly + add widthq, mmsize + jl .loop +``` + +mmsize를 음수인 widthq에 더해주어 0에 점점 가까워지게 만듭니다. 루프 조건은 jl(0보다 작으면 점프)입니다. 이 트릭의 핵심은 widthq를 포인터 오프셋이자 루프 카운터로 동시에 사용하여 cmp 명령어를 절약하는 것입니다. 또한 widthq를 여러 번 로드/스토어 연산에 재사용할 수 있고, 필요하다면 포인터 오프셋의 배수를 계산하는 데에도 활용할 수 있습니다. (이 부분은 과제에서 중요하게 사용될 것입니다.) + +**메모리 정렬 (Alignment)** + +지금까지의 모든 예제에서는 정렬 문제를 피하기 위해 movu를 사용했습니다. 대부분의 CPU는 데이터가 정렬되어 있을 때(즉, 메모리 주소가 SIMD 레지스터 크기로 나누어떨어질 때) 데이터를 더 빠르게 로드하고 저장할 수 있습니다. 따라서 가능한 경우 FFmpeg에서는 mova를 사용해 정렬된 로드와 스토어를 수행하려고 합니다. + +FFmpeg에서는 av_malloc을 통해 힙(heap)에 정렬된 메모리를 할당할 수 있으며, C 전처리기 지시문인 DECLARE_ALIGNED를 사용하면 스택(stack)에 정렬된 메모리를 확보할 수 있습니다. 만약 mova를 정렬되지 않은 주소에 사용하면 세그멘테이션 폴트(segmentation fault)가 발생하여 프로그램이 크래시합니다. + +또한 정렬 크기(alignment value)가 SIMD 레지스터 크기와 일치해야 합니다. 즉, xmm은 16바이트, ymm은 32바이트, zmm은 64바이트입니다. + +다음은 RODATA 섹션의 시작을 64바이트에 맞춰 정렬하는 방법입니다. + +```assembly +SECTION_RODATA 64 +``` + +이 명령은 RODATA의 시작 부분만 정렬합니다. 다음 레이블(label)이 64바이트 경계에 있도록 하려면 패딩 바이트(padding bytes)를 추가해야 할 수도 있습니다. + +**범위 확장 (Range expansion)** + +지금까지 다루지 않았던 또 하나의 주제는 오버플로(overflow)입니다. 예를 들어, 어떤 바이트 값이 덧셈이나 곱셈 연산 후 255를 초과하면 오버플로가 발생합니다. 이때 중간 계산값이 바이트보다 큰 크기(예: 워드)가 필요할 수 있으며, 혹은 데이터를 그 더 큰 중간 크기 상태로 유지하고 싶을 수도 있습니다. + +부호 없는 바이트(unsigned byte)의 경우, 이러한 상황에서 punpcklbw(packed unpack low bytes to words)와 punpckhbw(packed unpack high bytes to words) 명령어를 사용할 수 있습니다. + +punpcklbw의 작동 방식을 살펴보겠습니다. Intel 매뉴얼에 따르면 SSE2 버전의 문법은 다음과 같습니다. + +| PUNPCKLBW xmm1, xmm2/m128 | +| :---- | + +이는 오른쪽 피연산자(소스)가 xmm 레지스터이거나 메모리 주소(m128, 즉, [base + scale*index + disp] 형태)일 수 있으며, 왼쪽 피연산자(대상)는 xmm 레지스터라는 의미입니다. + +위에 소개한 officedaytime.com 웹사이트에는 이 동작을 시각적으로 보여주는 좋은 다이어그램이 있습니다. + +![What is this](image1.png) + +여기서 각 레지스터의 하위 절반에서 바이트들이 서로 교차(interleave)되어 배치되는 것을 볼 수 있습니다. 그렇다면 이것이 "범위 확장(range extension)"과 어떤 관련이 있을까요? 만약 src 레지스터가 전부 0이라면, dst의 바이트들은 0과 교차되어 합쳐집니다. 이는 바이트가 부호 없는 값이므로, 바이트를 *제로 확장(zero extension)* 하는 과정이 됩니다. punpckhbw 명령은 상위 바이트 영역에 대해서 동일한 작업을 수행합니다. + +다음은 이 과정을 보여주는 코드 예시입니다. + +```assembly +pxor m2, m2 ; zero out m2 + +movu m0, [srcq] +movu m1, m0 ; make a copy of m0 in m1 +punpcklbw m0, m2 +punpckhbw m1, m2 +``` + +이제 ```m0```와 ```m1```은 원래의 바이트 데이터를 워드 단위로 제로확장된 형태로 포함하고 있습니다. 다음 강의에서는 AVX의 3-피연산자 명령어를 통해 이 두 번째 movu 명령이 필요 없어지는 방법을 배우게 될 것입니다. + +**부호 확장 (Sign extension)** + +부호 있는 데이터(signed data)는 조금 더 복잡합니다. 부호 있는 정수를 확장하려면 [부호 확장(sign extension)](https://en.wikipedia.org/wiki/Sign_extension)이라는 과정을 사용해야 합니다. 이는 최상위 비트(MSB)를 부호 비트로 채우는 방식입니다. 예를 들어, int8_t인 -2는 0b11111110입니다. 이를 int16_t로 부호 확장하면, 최상위 비트 1이 반복되어 0b1111111111111110이 됩니다. + +```pcmpgtb```(packed compare greater than byte) 명령어를 이용해 부호 확장을 수행할 수 있습니다. 이 명령은 (0 > byte) 비교를 수행하여, 바이트 값이 음수일 경우 대상(destination) 바이트의 모든 비트를 1로 설정하고, 양수일 경우 모든 비트를 0으로 설정합니다. 이후 punpckX 명령을 앞서 설명한 방식으로 함께 사용하면 부호 확장이 이루어집니다. 즉, 바이트가 음수이면 해당 바이트는 0b11111111, 그렇지 않으면 0x00000000이 되며, 이 값을 원래의 바이트와 교차(interleave)하면 워드 단위의 부호 확장이 수행됩니다. + +```assembly +pxor m2, m2 ; m2를 0으로 초기화 + +movu m0, [srcq] +movu m1, m0 ; m0를 m1에 복사 + +pcmpgtb m2, m0 +punpcklbw m0, m2 +punpckhbw m1, m2 +``` + +보시다시피, 부호 없는 경우에 비해 명령어 하나 더 추가되었습니다. + +**패킹 (Packing)** + +packuswb (pack unsigned word to byte)와 packsswb 명령은 워드(16비트) 데이터를 바이트(8비트)로 변환할 때 사용됩니다. 이 명령들은 두 개의 SIMD 레지스터에 들어 있는 워드 데이터를 하나의 SIMD 레지스터 안의 바이트들로 교차(interleave)시킵니다. 값이 바이트 범위를 초과할 경우, 해당값은 포화 연산(saturation)으로 처리되어 즉, 최대값에서 클램프(clamp)됩니다. + +**셔플 (Shuffles)** + +셔플(Shuffles), 또는 퍼뮤트(Permutes)라고도 불리는 명령어들은 비디오 처리에서 가장 중요한 명령어라고 할 수 있습니다. 그중에서도 SSSE3에서 제공되는 pshufb(packed shuffle bytes)는 가장 핵심적인 셔플 명령어입니다. + +이 명령은 각 바이트에 대해, 해당 바이트의 값을 소스 바이트 인덱스로 사용하여 대상(destination) 레지스터의 바이트를 재배치합니다. 단, 바이트의 최상위 비트(MSB)가 설정되어있으면 해당 출력 바이트는 0으로 채워집니다. 이는 다음의 C 코드와 유사합니다. (단, SIMD에서는 모든 16번 반복이 동시에 병렬로 수행됩니다) + +```c +for(int i = 0; i < 16; i++) { + if(src[i] & 0x80) + dst[i] = 0; + else + dst[i] = dst[src[i]] +} +``` + +다음은 간단한 어셈블리 예시입니다. + +```assembly +SECTION_DATA 64 + +shuffle_mask: db 4, 3, 1, 2, -1, 2, 3, 7, 5, 4, 3, 8, 12, 13, 15, -1 + +section .text + +movu m0, [srcq] +movu m1, [shuffle_mask] +pshufb m0, m1 ; shuffle m0 based on m1 +``` + +여기서 -1은 읽기 편하게 하기 위해 셔플 인덱스로 사용된 값이며, 출력 바이트를 0으로 만드는 역할을 합니다. -1은 바이트 단위로 표현하면 0b11111111 (2의 보수)이므로, MSB(0x80)가 설정되어있어 해당 바이트가 0으로 처리됩니다. + +[image1]: \ No newline at end of file From 943e20a6281d2f1f46768f3bb53850d63b0290ac Mon Sep 17 00:00:00 2001 From: LunaStev <96914208+LunaStev@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:50:20 +0900 Subject: [PATCH 2/2] Fix formatting in assembly language explanation Corrected spacing and formatting in the assembly language description. --- lesson_01/index.ko.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lesson_01/index.ko.md b/lesson_01/index.ko.md index 1f70c02..fd21ef5 100644 --- a/lesson_01/index.ko.md +++ b/lesson_01/index.ko.md @@ -11,7 +11,7 @@ FFmpeg 어셈블리 언어 강좌에 오신 것을 환영합니다. 여러분은 **어셈블리 언어란 무엇인가?** -어셈블리 언어는 CPU가 처리하는 명령어에 직접적으로 대응되는 코드를 작성하는 프로그래밍 언어입니다. 사람이 읽을 수 있는 어셈블리 언어 코드는 이름 그대로 *어셈블(assembled)* 되어, CPU가 이해할 수 있는 이진 데이터인 *기계어(machine code)*로 만들어집니다. 어셈블리 언어 코드는 흔히 "assembly" 또는 줄여서 "asm"이라고 부르기도 합니다. +어셈블리 언어는 CPU가 처리하는 명령어에 직접적으로 대응되는 코드를 작성하는 프로그래밍 언어입니다. 사람이 읽을 수 있는 어셈블리 언어 코드는 이름 그대로 *어셈블(assembled)* 되어, CPU가 이해할 수 있는 이진 데이터인 *기계어(machine code)* 로 만들어집니다. 어셈블리 언어 코드는 흔히 "assembly" 또는 줄여서 "asm"이라고 부르기도 합니다. FFmpeg의 어셈블리 코드 대부분은 *SIMD (Single Instruction Multiple Data, 단일 명령 다중 데이터)* 라고 불리는 방식으로 작성되어 있습니다. SIMD는 종종 벡터 프로그래밍(vector programming)이라고 불립니다. 이 방식은 하나의 명령어가 여러 데이터 요소를 동시에 처리한다는 의미입니다. 대부분의 프로그래밍 언어는 한 번에 하나의 데이터만 처리하는데, 이를 스칼라 프로그래밍(scalar programming)이라고 합니다. @@ -215,4 +215,4 @@ RET 과제에서 보게 되겠지만, 우리는 어셈블리 함수에 대한 함수 포인터를 생성하고, 가능할 경우 그것을 사용하게 됩니다. -[다음 수업](../lesson_02/index.ko.md) \ No newline at end of file +[다음 수업](../lesson_02/index.ko.md)