diff --git a/lesson_01/index.ko.md b/lesson_01/index.ko.md new file mode 100644 index 0000000..fe5ff9e --- /dev/null +++ b/lesson_01/index.ko.md @@ -0,0 +1,226 @@ +# FFmpeg 어셈블리어 강의 1강 + +## 소개 + +FFmpeg 어셈블리어 학교에 오신걸 환영합니다. 여러분은 프로그래밍에서 가장 흥미로우면서 도전적이고 보람도 있는 여정의 첫걸음을 내디디셨습니다. 이 강의를 통해 여러분은 FFmpeg에서 어셈블리어 코드를 작성하는 방법의 기초를 배우고, 컴퓨터에서 실제로는 어떤 일이 일어나고 있는지에 눈뜨게 될 것입니다. + +## 사전 지식 + +- C 언어 지식, 특히 포인터에 대한 이해가 필요합니다. 만약 아직 C에 익숙하지 않다면 [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language) 책을 참고하세요. +- 고등학교 수학 (스칼라 vs 벡터, 덧셈, 곱셈 등) + +## 어셈블리어란 무엇일까요? + +어셈블리어는 CPU가 처리하는 명령어와 직접 대응하는 코드를 작성하는 프로그래밍 언어입니다. 사람이 읽을 수 있는 형태의 어셈블리어는 이름 그대로 '조립(assemble)'되어 CPU가 이해할 수 있는 이진 데이터, 즉 머신 코드(machine code)로 변환됩니다. 어셈블리어 코드는 종종 "어셈블리" 또는 "asm"으로 불립니다. + +FFmpeg에서 사용되는 대부분의 어셈블리어 코드는 SIMD(단일 명령어 다중 데이터)로 작성되어 있습니다. SIMD는 종종 벡터 프로그래밍(vector programming)이라고도 불립니다. 이 방식은 하나의 명령어가 동시에 여러 데이터 요소를 처리할 수 있게 해줍니다. 반면 대부분의 프로그래밍 언어는 한 번에 하나의 데이터만 처리하는 스칼라 프로그래밍(scalar programming) 방식입니다. + +짐작하셨겠지만, SIMD는 이미지, 비디오, 오디오처럼 데이터의 대부분이 메모리에 연속적으로 정렬되어 있을 때 매우 효과적입니다. CPU에는 이렇게 연속적인 데이터를 처리하는 데 특화된 명령어들이 내장되어 있습니다. + +FFmpeg에서는 "어셈블리 함수", "SIMD", "벡터화(vectorize)"라는 용어들이 혼용되지만, 모두 여러 데이터 요소를 한 번에 처리하기 위해 어셈블리어로 직접 함수를 작성했다는 의미입니다. 어떤 프로젝트에서는 이를 "어셈블리 커널(assembly kernel)"이라고 부르기도 합니다. + +이 모든 것이 당장은 복잡하게 느껴질 수도 있지만, FFmpeg에서는 고등학생들도 어셈블리 코드를 작성했다는 점을 기억하는 게 중요합니다. 모든 것이 그렇듯이 공부의 반은 용어이고 나머지 반이 진짜 공부입니다. + +## 왜 어셈블리어를 사용하나요? + +결론부터 말씀드리자면 멀티미디어를 빠르게 처리하기 위해서입니다. 어셈블리어로 코드를 작성해서 속도를 10배 이상 향상시키는 일은 아주 흔하며, 영상을 실시간으로 끊김 없이 재생하려면 특히 중요합니다. 추가로 에너지 소비를 줄이고 배터리 수명을 늘리는 데도 도움이 됩니다. 비디오 인코딩 및 디코딩 기능은 개인 사용자부터 대기업의 데이터센터에 이르기까지 지구상에서 가장 많이 사용되는 기능 중 하나라는 점도 중요합니다. 따라서 작은 개선도 빠르게 누적됩니다. + + +인터넷 커뮤니티에서는 컴파일러 인트린직(intrinsic)을 사용하는걸 흔하게 볼 수 있습니다. 이는 어셈블리 명령어에 매핑되는 C 스타일 함수로, 주로 개발 속도를 높이기 위해 사용됩니다. 하지만 FFmpeg에서는 이러한 intrinsic을 사용하지 않고 어셈블리어 코드를 직접 작성합니다. 이 부분에 대해선 논쟁이 있지만, 컴파일러에 따라 일반적으로 intri은sics는 수작업으로 작성된 어셈블리 코드보다 약 10~15% 정도 느린 것으로 알려져 있습니다. FFmpeg에서는 가능한 모든 성능을 끌어내는 것이 중요하기 때문에, 인트린직에 의존하지 않는 방식을 택하고 있습니다 (옹호자들은 동의하지 않겠지만요). 또한 intrinsic은 [헝가리안 표기법](https://en.wikipedia.org/wiki/Hungarian_notation)을 사용하고 있기 때문에 읽기 어렵다는 의견도 있습니다. +수 +또한 FFmpeg 코드 중 일부에는 역사적인 이유로 인라인 어셈블리가 남아 있는 경우가 있으며, 리눅스 커널 같은 프로젝트에서도 아주 특수한 목적을 위해 사용되곤 합니다. 인라인 어셈블리는 어셈블리어를 별도의 파일이 아닌 C 코드 내부에 직접 작성하는 방식입니다. 하지만 이런 코드는 읽기 어려운데다, 여러 컴파일러에서 널리 지원하지 않으며, 유지보수가 어렵다는 것이 FFmpeg와 같은 프로젝트에서의 주된 의견입니다 + +마지막으로 인터넷에서는 "컴파일러가 알아서 벡터화를 해주기 때문에 어셈블리어는 더 이상 필요 없다"고 주장하는 '자칭 전문가'들도 자주 볼 수 있습니다. 하지만 적어도 학습 단계에서는 신경쓰지 않아도 됩니다. 예를 들어 [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://discord.com/invite/Ks5MhUhqfB + +## 레지스터(Register) + +레지스터는 CPU 내부에서 데이터를 처리하는 영역입니다. CPU는 메모리를 직접 연산하지 않고, 먼저 데이터를 레지스터로 불러온(load) 뒤 처리하고, 다시 메모리에 기록하는 방식으로 동작합니다. 어셈블리어에서는 일반적으로 한 메모리 위치에서 다른 메모리 위치로 데이터를 바로 복사할 수 없으며, 반드시 레지스터를 거쳐야 합니다. + +## 범용 레지스터(General Purpose Register) + +첫 번째 종류의 레지스터는 범용 레지스터(GPR, General Purpose Register)라고 불립니다. '범용'이라는 이름은 이 레지스터가 데이터(최대 64비트 값)나 메모리 주소(포인터)를 모두 담을 수 있기 때문에 붙었습니다. GPR에 들어 있는 값은 덧셈, 곱셈, 시프트(shift) 등의 연산을 통해 처리할 수 있습니다. + +대부분의 어셈블리 관련 서적에서는 GPR 사이의 차이점이나, GPR의 역사적 배경 등에 대해 지면을 할애해 다룹니다. 이는 운영체제 프로그래밍이나 리버스 엔지니어링처럼 GPR이 중요한 분야가 있기 때문입니다. FFmpeg에서 작성하는 어셈블리 코드에서 GPR은 형식적인 도구에 가깝고, 대부분의 경우 그런 복잡한 맥락은 불필요하기 때문에 추상화되어 사라집니다. + +## 벡터 레지스터(Vector Register) + +벡터(SIMD) 레지스터는 이름 그대로 여러 개의 데이터 요소를 담을 수 있는 레지스터입니다. 이 레지스터에는 여러 종류가 있습니다. + +- **mm 레지스터**: MMX 레지스터, 64비트 크기, 현재는 잘 사용되지 않음 +- **xmm 레지스터**: XMM 레지스터, 128비트 크기, 대부분의 환경에서 사용 가능 +- **ymm 레지스터**: YMM 레지스터, 256비트 크기, 사용 시 몇 가지 제약이 있음 +- **zmm 레지스터**: ZMM 레지스터, 512비트 크기, 사용 가능 환경이 제한적임 + +비디오 압축과 압축 해제에서 대부분의 연산은 정수 기반이므로 여기서는 정수 데이터를 기준으로 설명하겠습니다. + +16개의 바이트가 xmm 레지스터에 담긴 예시입니다. + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +하지만 8개의 워드(16비트 정수)가 될 수도 있습니다. + +| a | b | c | d | e | f | g | h | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +또는 4개의 더블워드(32비트 정수)의 형태로도 + +| a | b | c | d | +| :---- | :---- | :---- | :---- | + +아니면 2개의 쿼드워드(64비트 정수)가 될 수 있습니다. + +| a | b | +| :---- | :---- | + +정리하면 다음과 같습니다. 굵게 표시한 글자는 이후에도 중요하게 사용됩니다. + +- **b**ytes - 8비트 데이터 +- **w**ords - 16비트 데이터 +- **d**oublewords - 32비트 데이터 +- **q**uadwords - 64비트 데이터 +- **d**ouble **q**uadwords - 128비트 데이터 + +## x86inc.asm 포함 (include) + +많은 예제에서 x86inc.asm 파일을 포함하는 것을 볼 수 있습니다. x86inc.asm는 FFmpeg, x264, dav1d에서 사용하는 가벼운 추상화 레이어로, 어셈블리 프로그래머의 작업을 훨씬 수월하게 만들어 줍니다. 다양한 기능을 제공하지만, 우선 가장 유용한 기능 중 하나는 범용 레지스터(GPR)을 `r0`, `r1`, `r2` 처럼 라벨링해 준다는 점입니다. 덕분에 실제 레지스터 이름을 일일이 외울 필요가 없습니다. 앞서 언급했듯 GPR은 주로 보조 역할을 하기 때문에 이런 기능은 작업을 훨씬 간단하게 만들어 줍니다. + + +## 간단한 어셈블리 예제 + +이번에는 인위적이지만 간단한 스칼라 어셈블리 코드를 보면서 어떤 일이 일어나는지 살펴보겠습니다. 여기서 _스칼라 어셈블리_ 란, 각 명령이 한 번에 하나의 데이터 항목만 처리하는 어셈블리 코드를 의미합니다. + +```assembly +mov r0q, 3 +inc r0q +dec r0q +imul r0q, 5 +``` + +첫 번째 줄에서는 즉시값 `3`(메모리에서 읽어온 값이 아닌 코드 자체에 저장된 값)을 레지스터 `r0`에 쿼드워드(64비트) 크기로 저장합니다. + +Intel 문법에서는 오른쪽에 있는 소스 피연산자가 왼쪽에 있는 목적지 피연산자로 전달됩니다. 소스 피연산자는 값이나 데이터가 있는 위치를 의미하고, 목적지 피연산자는 데이터를 받는 위치를 나타냅니다. 이 동작은 `memcpy`의 동작 방식과 유사합니다. 따라서 `r0q = 3`이라고 읽어도 의미가 같습니다. 여기서 `r0` 뒤에 붙은 `q` 접미사는 해당 레지스터를 쿼드워드 크기로 사용한다는 뜻입니다. + +`inc` 명령은 값을 1 증가시켜 `r0q`를 4로 만들고, `dec` 명령은 값을 1 감소시켜 다시 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 커뮤니티에서 만든 "헤더(header)"로 코드 작성 시 유용한 헬퍼, 미리 정의한 이름, 매크로(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 함수를 정의합니다. 각 인자의 의미를 살펴보면, + +- 첫 번째 인자(2)는 함수 인자가 두 개라는 뜻입니다. +- 두 번째 인자(2)는 인자를 전달하기 위해 두 개의 GPR을 사용할 것임을 나타냅니다. 더 많은 GPR이 필요하다면 여기서 개수를 늘려야 합니다. +- 세 번째 인자(2)는 사용할 XMM 레지스터의 개수를 나타냅니다. +- 마지막 두 인자(`src`, `src2`)는 함수 인자의 레이블(label)입니다. + +참고로 이전 코드에서는 인자 레이블 없이 `r0`, `r1`처럼 GPR을 직접 참조하는 방식이 사용되기도 했습니다. + +```assembly + movu m0, [srcq] + movu m1, [src2q] +``` + +`movu`는 `movdqu`(move double quad unaligned)의 약자입니다. 정렬(alignment)에 대해서는 이후 강의에서 다루지만, 일단 `movu`를 `[srcq]`에서 128비트를 읽어오는 명령으로 이해하면 됩니다. + +`mov`에서 대괄호 `[]`는 해당 주소를 역참조한다는 뜻이며, C언어의 `*src`와 같습니다. 이를 **로드(load)** 라고 합니다. 여기서 `q` 접미사는 포인터 크기를 나타내며 (64비트 시스템에서 `sizeof(*src) == 8`), 32비트 시스템에서는 자동으로 32비트를 사용합니다. 하지만 실제 로드는 128비트 단위로 이루어집니다. + +또한 벡터 레지스터는 `xmm0`같은 전체 이름이 아닌 추상화된 `m0`으로 부릅니다. 이렇게 하면 한 번 작성한 코드를 다양한 크기의 SIMD 레지스터에서 사용할 수 있습니다. + +```assembl +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)** 라고 불립니다. `scrq` 포인터가 가리키는 주소에 데이터를 다시 써 넣는 동작입니다. + +```assembly +RET +``` + +이 매크로는 함수가 반환됨을 나타냅니다. FFmpeg의 거의 모든 어셈블리 함수는 값을 반환하기보다는 인자로 전달된 데이터 자체를 수정하는 방식을 사용합니다. + +곧 보게 될 실습에서는 어셈블리 함수를 가리키는 함수 포인터를 만들고 활용하는 시간을 가지겠습니다. + +[다음 강의](../lesson_02/index.ko.md) diff --git a/lesson_02/index.ko.md b/lesson_02/index.ko.md new file mode 100644 index 0000000..72209b0 --- /dev/null +++ b/lesson_02/index.ko.md @@ -0,0 +1,181 @@ +# FFmpeg 어셈블리어 강의 2강 + +첫 번째 어셈블리어 함수를 작성해 보았습니다. 이번에는 분기(branch)와 반복문(loop)을 소개하겠습니다. + +먼저 레이블과 점프(jump)의 개념을 알아야 합니다. 아래의 예제에서 `jmp` 명령어는 코드 실행을 `.loop:` 직후의 명령으로 이동시킵니다. `.loop:`는 **레이블**이라 부르며, 레이블 앞에 붙은 점(`.`)은 이것이 지역 레이블(local label)임을 나타냅니다. 지역 레이블을 사용하면 여러 함수에서 동일한 레이블을 재사용할 수 있습니다. + +아래는 무한 반복의 예제입니다. 이후 이를 좀 더 현실적인 예시로 확장할 것입니다. + +```assembly +mov r0q, 3 +.loop: + dec r0q + jmp .loop +``` + +더욱 현실적인 반복문을 만들기 전에 FLAGS 레지스터를 알아보겠습니다. FLAGS의 세부 동작에 대해서는 깊게 다루지 않겠습니다(앞서 설명했듯이 GPR 연산은 주로 보조 역할이기 때문입니다). 다만, 산술 연산이나 시프트처럼 스칼라 데이터에 대해 `mov` 이외의 대부분의 명령을 실행하면 **Zero-Flag**, **Sign-Flag**, **Overflow-Flag**와 같은 여러 플래그가 설정됩니다. + +다음은 카운터를 0까지 감소시키면서 `jg`(0보다 크면 점프)로 반복하는 예시입니다. `dec r0q` 명령은 실행 후 `r0q` 값에 따라 플래그들을 설정하며, 이 플래그를 바탕으로 점프할 수 있습니다. + +```assembly +mov r0q, 3 +.loop: + ; ... + dec r0q + jg .loop ; 0보다 크면 점프 +``` + +이는 다음 C 코드와 동일합니다. + +```c +int i = 3; +do +{ + // ... + i--; +} while(i > 0) +``` + +이 코드는 약간 부자연스럽습니다. 일반적인 C코드의 반복문은 다음과 같습니다. + +```c +int i; +for(i = 0; i < 3; i++) { + // ... +} +``` + +이 `for` 반복문과 동일하게 대응시키는 방법은 없지만, 대략 다음 어셈블리 코드와 비슷합니다. + +```assembly +xor r0q, r0q +.loop: + ; ... + inc r0q + cmp r0q, 3 + jl .loop ; jump if (r0q - 3) < 0, i.e (r0q < 3) +``` + +이 예시 코드에서 주목할 점이 몇 가지 있습니다. + +첫째, `xor r0q, r0q`는 레지스터를 0으로 만드는 흔한 방법입니다. 일부 시스템에서는 `mov r0q, 0`보다 더 빠를 수 있는데, 간단히 설명하면 실제 메모리 로드 동작이 일어나지 않기 때문입니다. 이 방식은 SIMD 레지스터에서도 사용할 수 있으며, 예를 들어 `pxor m0, m0`[^1]는 전체 레지스터를 0으로 초기화합니다. + +둘째, `cmp`의 사용입니다. `cmp`는 첫 번째 피연산자에서 두 번째 피연산자를 뺀 값을 어디에도 저장하지 않고, 그 결과에 따라 FLAGS를 설정합니다. 주석의 내용처럼 점프 명령과 같이 조합되면, `jl`(jump if less than zero)는 `r0q < 3`일 때 점프하게 됩니다. + +또한 이 코드에는 추가 명령어(`cmp`)가 하나 더 있다는 점을 유의해야 합니다. 일반적으로 명령어 수가 적을수록 코드가 더 빠르므로 이전 예제의 방식이 더 선호됩니다. 앞으로의 강의에서 보겠지만, 이러한 추가 명령을 피하고 산술 연산이나 다른 연산에서 바로 FLAGS를 설정하는 다양한 기법이 있습니다. + +마지막으로, 어셈블리를 작성하는 이유가 C언어 반복문의 완벽한 재현이 아니라 가능한 한 빠르게 동작하는 반복문 작성임을 기억해야 합니다. + +다음은 자주 사용하게 될 몇 가지 점프 명령어 약어입니다(*FLAGS*는 참고를 위해 추가했으며 반복문을 작성하는데 세부 내용을 반드시 알 필요는 없습니다). + +| 니모닉(Mnemonic) | 설명 | FLAGS | +| :---- | :---- | :---- | +| `JE`/`JZ` | 같음 / 0일 때 점프 | ZF = 1 | +| `JNE`/`JNZ` | 같지 않음 / 0이 아닐 때 점프 | ZF = 0 | +| `JG`/`JNLE` | 큼 / 작거나 같지 않음(부호 있는 비교)일 때 점프 | ZF = 0 그리고 SF = OF | +| `JGE`/`JNL` | 크거나 같음 / 작지 않음(부호 있는 비교)일 때 점프 | SF = OF | +| `JL`/`JNGE` | 작음 / 크거나 같지않음(부호 있는 비교)일 때 점프 | SF ≠ OF | +| `JLE`/`JNG` | 작거나 같음 / 크지 않음(부호 있는 비교)일 때 점프 | ZF = 1 또는 SF ≠ OF | + +## 상수(Constants) + +다음은 상수를 사용하는 예시입니다. + +```assembly +SECTION_RODATA + +constants_1: db 1,2,3,4 +constants_2: times 2 dw 4,3,2,1 +``` + +* `SECTION_RODATA`는 읽기 전용 데이터 섹션임을 지정합니다. 운영체제가 사용하는 출력 파일 형식마다 선언 방식이 다르기 때문에 매크로로 정의되어 있습니다. +* `constants_1`: `db`(declare byte)로 정의된 `constants_1` 레이블로, `uint8_t constants_1[4] = {1, 2, 3, 4};`와 동일합니다. +* `constants_2`: `times 2` 매크로를 이용해 선언된 워드를 두 번 반복합니다. `uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1};`과 동일합니다. + +이러한 레이블들은 어셈블러에 의해 메모리 주소로 변환됩니다. 읽기 전용이기 때문에 로드(load) 연산에는 사용할 수 있지만 저장(store) 연산에는 사용할 수 없습니다. 일부 명령어는 메모리 주소를 직접 피연산자로 받을 수 있어, 레지스터에 명시적으로 로드하지 않고도 메모리에 직접 접근할 수 있습니다(이 방식에는 장단점이 있습니다). + +## 오프셋(Offset) + +오프셋이란 메모리에서 연속된 요소 간의 거리(바이트 단위)를 말하며, 자료 구조의 각 요소 크기에 따라 결정됩니다. + +이제 반복문을 작성할 수 있게 되었고, 데이터를 읽어볼 차례입니다. 다만 C언어와는 다른 점이 있습니다. + +C 코드 예시를 살펴보겠습니다. + +```c +uint32_t data[3]; +int i; +for(i = 0; i < 3; i++) { + data[i]; +} +``` + +C 컴파일러는 `data`의 각 요소가 차지하는 4바이트 오프셋을 미리 계산합니다. 그러나 어셈블리 코드를 직접 작성할 때는 오프셋을 직접 계산해야 합니다. + +모든 메모리 주소 계산에 적용되는 구문은 다음과 같습니다. + +```assembly +[base + scale*index + disp] +``` + +* **base** - GPR(일반적으로 C 함수 인자로 받은 포인터) +* **scale** - 1, 2, 4, 8 중 하나 (기본값: 1) +* **index** - GPR(보통 루프 카운터) +* **disp** - 32비트 정수. 데이터 내부의 오프셋(변위, displacement) + +x86inc.asm은 현재 사용 중인 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] + + ; ... + + add srcq, mmsize +dec r1q +jg .loop + +RET +``` + +`movu m1, [srcq+2*r1q+3+mmsize]`에서 어셈블러가 사용할 올바른 변위 상수(displacement constant)를 미리 계산해 준다는 점을 주목해 보세요. 다음 강의에서는 루프 내에서 `add`와 `dec` 두 연산을 사용하는 대신, 하나의 `add` 연산만 사용하는 방법을 보여드리겠습니다. + +## LEA (Load Effective Address) + +오프셋 개념을 이해했다면 이제 `lea` 명령어를 사용할 수 있습니다. `lea`는 한 번의 명령으로 곱셈과 덧셈을 수행할 수 있어 여러 명령을 사용하는 것보다 빠릅니다. 곱하거나 더할 수 있는 값에는 제한이 있지만, 여전히 강력한 명령입니다. + +```assembly +lea r0q, [base + scale*index + disp] +``` + +이름과 달리 `lea`는 주소 계산뿐 아니라 일반 산술 연산에도 사용할 수 있습니다. 예를 들어, + +```assembly +lea r0q, [r1q + 8*r2q + 5] +``` + +이 명령어는 `r1q`와 `r2q`의 값을 변경하지 않으며, FLAGS도 변경하지 않습니다(즉, 결과를 기반으로 한 점프가 불가능합니다). `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) + +[^1]: `pxor`은 *Packed XOR*의 줄임말로 두 개의 SIMD 레지스터나 메모리 위치에 있는 데이터에 대해 비트별 XOR 연산을 병렬로 수행합니다.