-
어셈블리어의 문법은 CPU의 명령어 집합 구조(ISA), 운영체제, 어셈블러의 종류에 따라 달라질 수 있음
-
WSL(Windows Subsystem for Linux) 또는 리눅스 환경에서 다음과 같이 구축한다.
- WSL 또는 리눅스의 경우
-
파워셸을 열고 다음 명령어 실행
$ wsl --install -
필수 도구 설치
$ sudo apt update $ sudo apt install nasm build-essential gdb -
어셈블 및 실행
$ nasm -f elf64 hello.asm -o hello.o $ nasm -f elf64 -g -F dwarf hello.asm -o hello.o # 만약 GDB 디버깅을 할 경우 $ ld hello.o -o hello $ ./hello -
디버깅: gdb ./hello 실행 후 layout regs라고 입력해 보세요. (... 이후 내용 나중에 추가)
- VS-Code + WSL의 경우
-
- VS Code 설치 후 'WSL' 확장 프로그램 설치
-
- 좌측 하단의 >< 아이콘을 클릭하여 "Connect to WSL" 선택.
-
- 'x86 and x86_64 Assembly' 확장을 설치하면 코드 하이라이팅이 지원됩니다.
-
- **'GDB Debugger'**를 통해 레지스터 값을 실시간으로 보며 디버깅할 수 있습니다. (... 이후 내용 나중에 추가)
nasm --help를 하면 다음과 같은 내용이 나옴
사용법: nasm [-@ response_file] [options...] [--] filename
nasm -v (또는 --v)
Options ([] 안의 값은 기본값을 의미함):
-h 이 도움말 텍스트를 보여주고 종료 (--help 또한 동일함)
-v (또는 --v) NASM 버전 번호를 보여주고 종료
-@ file 어셈블하고자 하는 .asm 파일; 라인 하나당 커맨드 라인 옵션 하나
-o outfile outfile로 출력 작성
--keep-all 오류가 발생하더라도 생성된 출력 파일들이 제거되지 않음
-Xformat 오류 보고 포맷 지정 (gnu 또는 vc)
-s 오류 메시지를 stdout(표준출력)으로 리디렉션
-Zfile 오류 메시지를 파일로 리디렉션
-M Makefile 의존성 목록을 stdout(표준출력)으로 내보냄
-MG d:o, 누락된 파일은 생성된 것으로 간주됨
-MF file Makefile 의존성 파일을 지정함
-MD file 어셈블 후에 의존성 파일 생성
-MT file 의존성 대상 이름을 지정함
-MQ file 의존성 대상 이름을 지정함 (""를 붙임)
-MP 가짜(phony) 대상을 생성함
-f format 출력 파일 포맷 선택
bin Flat raw binary (MS-DOS, embedded, ...) [기본값]
ith Intel Hex encoded flat binary
srec Motorola S-records encoded flat binary
aout Linux a.out
aoutb NetBSD/FreeBSD a.out
coff COFF (i386) (DJGPP, some Unix variants)
elf32 ELF32 (i386) (Linux, most Unix variants)
elf64 ELF64 (x86-64) (Linux, most Unix variants)
elfx32 ELFx32 (ELF32 for x86-64) (Linux)
as86 as86 (bin86/dev86 toolchain)
obj Intel/Microsoft OMF (MS-DOS, OS/2, Win16)
win32 Microsoft extended COFF for Win32 (i386)
win64 Microsoft extended COFF for Win64 (x86-64)
ieee IEEE-695 (LADsoft variant) object file format
macho32 Mach-O i386 (Mach, including MacOS X and variants)
macho64 Mach-O x86-64 (Mach, including MacOS X and variants)
dbg Trace of all info passed to output stage
elf Legacy alias for "elf32"
macho Legacy alias for "macho32"
win Legacy alias for "win32"
-g 디버깅 정보 생성
-F format 디버깅 포맷 선택 (출력 포맷에 따라 다름)
-gformat -g -F 포맷과 같음
elf32: dwarf ELF32 (i386) dwarf (최신) [기본값]
stabs ELF32 (i386) stabs (이전)
elf64: dwarf ELF64 (x86-64) dwarf (최신) [기본값]
stabs ELF64 (x86-64) stabs (이전)
elfx32: dwarf ELFx32 (x86-64) dwarf (최신) [기본값]
stabs ELFx32 (x86-64) stabs (이전)
obj: borland Borland Debug Records [기본값]
win32: cv8 Codeview 8+ [기본값]
win64: cv8 Codeview 8+ [기본값]
ieee: ladsoft LADsoft Debug Records [기본값]
macho32: dwarf Mach-O i386 dwarf for Darwin/MacOS [기본값]
macho64: dwarf Mach-O x86-64 dwarf for Darwin/MacOS [기본값]
dbg: debug Trace of all info passed to debug stage [기본값]
-l listfile 리스팅 내용을 파일로 저장
-Lflags... 리스트 파일에 선택적인 정보 추가
-Lb 내장 매크로 패키지를 보여줌 (표준 및 %use)
-Ld 10진수로 바이트 및 반복 횟수를 보여줌
-Le 전처리된 출력을 보여줌
-Lf .nolist 무시 (강제로 출력)
-Lm 확장된 파라미터와 함께 멀티-라인 매크로 호출을 보여줌
-Lp 오류가 발생한 경우 모든 패스에 대한 리스트 파일을 출력
-Ls 모든 싱글-라인 매크로 정의를 보여줌
-Lw 모든 라인 이후에 출력을 flush (매우 느림!)
-L+ -Lw를 제외한 모든 리스팅 옵션 활성화 (매우 상세함!)
-Oflags... 명령어, 상수, 브랜치 오프셋을 최적화
-O0 최적화 없음
-O1 최소한의 최적화
-Ox 멀티패스 최적화 (기본값)
-Ov 마지막에 실행한 패스 횟수를 표시함
-t 제한된 SciTech Borland의 TASM 호환 모드로 어셈블
-E (또는 -e) 전처리만 수행 (기본적으로 stdout(표준출력)으로 출력함)
-a 전처리하지 않음 (어셈블 전용)
-Ipath include 파일 경로에 pathname 추가
-Pfile 파일 미리 include (--include 역시 가능)
-Dmacro[=str] 매크로 미리 정의
-Umacro 매크로 정의 취소
--pragma str 지정한 %%pragma 미리 실행
--before str 입력 전에 라인 추가 (보통 전처리기 구문)
--no-line 입력 내 %line 지시어 무시
--prefix str 모든 extern, 공통 및 글로벌 심볼의 이름들 앞에 주어진 문자열을 붙임 (--gprefix 역시 가능)
--suffix str 모든 extern, 공통 및 글로벌 심볼의 이름들 뒤에 주어진 문자열을 붙임 (--gprefix 역시 가능)
--lprefix str 로컬 심볼의 이름들 앞에 주어진 문자열을 붙임
--lpostfix str 로컬 심볼의 이름들 뒤에 주어진 문자열을 붙임
--reproducible 실행할 때마다 매번 동일한 출력(바이너리 결과물)을 생성하도록 시도함 (동일한 hash 값 보장)
-w+x 경고 x 활성화 (-Wx 역시 가능)
-w-x 경고 x 비활성화 (-Wno-x 역시 가능)
-w[+-]error 모든 경고를 오류로 격상시킴 (-Werror 역시 가능)
-w[+-]error=x 경고 x를 오류로 격상시킴 (-Werror=x 역시 가능)
all 가능한 모든 경고
db-empty 데이터 선언에 대해 피연산자가 없는 경우 [on]
ea 접두사 "ea-"가 붙은 모든 경고
ea-absolute 절대주소가 RIP-상대적인 값이 될 수 없는 경우 [on]
ea-dispsize 절대주소 상에서 변위 크기가 무시됨 [on]
float 접두사 "float-"가 붙은 모든 경고
float-denorm 부동소수점 비정규화됨 [off]
float-overflow 부동소수점 오버플로우 [on]
float-toolong 부동소수점 수의 자리가 너무 길다 [on]
float-underflow 부동소수점 언더플로우 [off]
forward 전방향 참조가 예측할 수 없는 결과를 초래할 수 있음 [on]
label 접두사 "label-"가 붙은 모든 경고
label-orphan 라벨 뒤에 ':'가 없음 [on]
label-redef 동일한 값으로 라벨이 재정의됨 [off]
label-redef-late 코드 생성 중에 라벨이 (재)정의됨 [error]
number-overflow 숫자 상수가 알맞지 않음 [on]
obsolete 접두사 "obsolete-"가 붙은 모든 경고
obsolete-nop 더 이상 사용하지 않는 명령어이며 대상 CPU에서 아무런 작업도 수행하지 않음 [on]
obsolete-removed 더 이상 사용하지 않는 명령어이며 대상 CPU에서 제거되었음 [on]
obsolete-valid 더 이상 사용하지 않는 명령어이지만 대상 CPU에서는 유효함 [on]
phase 안정화 도중 phase 오류 [off]
pp 접두사 "pp-"가 붙은 모든 경고
pp-else 접두사 "pp-else-"가 붙은 모든 경고
pp-else-elif %else 다음에 %elif가 나옴 [on]
pp-else-else %else 다음에 %else가 나옴 [on]
pp-empty-braces 비어 있는 %{} 생성자 [on]
pp-environment 존재하지 않는 환경 변수 [on]
pp-macro 접두사 "pp-macro-"가 붙은 모든 경고
pp-macro-def 접두사 "pp-macro-def-"가 붙은 모든 경고
pp-macro-def-case-single 대소문자 구분/미구분 방식이 혼용된 방식으로 정의된 싱글-라인 매크로 [on]
pp-macro-def-greedy-single 싱글-라인 매크로 [on]
pp-macro-def-param-single 파라미터가 포함된/포함되지 않은 방식이 혼용된 방식으로 정의된 싱글-라인 매크로 [error]
pp-macro-defaults 선택적인 파라미터보다 더 많은 기본값을 가진 매크로 [on]
pp-macro-params 접두사 "pp-macro-params-"가 붙은 모든 경고
pp-macro-params-legacy 레거시 지원을 위해 멀티-라인 매크로 부적절하게 호출하기 [on]
pp-macro-params-multi 멀티-라인 매크로 호출시 파라미터 개수가 잘못됨 [on]
pp-macro-params-single 싱글-라인 매크로 호출시 파라미터 개수가 잘못됨 [on]
pp-macro-redef-multi 멀티-라인 매크로를 재정의함 [on]
pp-open 접두사 "pp-open-"가 붙은 모든 경고
pp-open-braces brace를 닫지 않음: %{...} [on]
pp-open-brackets bracket을 닫지 않음: %[...] [on]
pp-open-string 문자열을 닫지 않음 [on]
pp-rep-negative 반복 횟수를 의미하는 %rep가 음수임 [on]
pp-sel-range %sel() 인자가 범위를 벗어남 [on]
pp-trailing 줄 끝에 붙은 잔여 데이터(garbage)가 무시됨 [on]
pragma 접두사 "pragma-"가 붙은 모든 경고
pragma-bad 잘못 형성된 %pragma [off]
pragma-empty 비어 있는 %pragma 지시어 [off]
pragma-na %pragma가 이번 컴파일에 적용되지 않음 [off]
pragma-unknown 알려지지 않은 %pragma 기능 또는 지시어 [off]
prefix 접두사 "prefix-"가 붙은 모든 경고
prefix-bnd 유효하지 않은 BND 접두사 [on]
prefix-hle 유효하지 않은 HLE 접두사 [on]
prefix-lock 잠금을 풀 수 있는 지시어에 LOCK 접두사가 붙음 [on]
prefix-opsize 유효하지 않은 피연산자 크기 접두사 [on]
prefix-seg 64비트 모드에서 무시되는 세그먼트 접두사 [on]
ptr 다른 어셈블러에서 사용하는 비-NASM 키워드 [on]
regsize 레지스터 크기 요구사항 무시됨 [on]
unknown-warning -W/-w 또는 경고 지시어에 알려지지 않은 경고 [off]
user %warning 지시어 [on]
warn-stack-empty 경고 스택 비어 있음 [on]
zeroing 초기화된 섹션에 있는 RESx는 0이 됨 [on]
zext-reloc 출력 포맷에 일치시키기 위해 0으로 확장된 형태로 재할당 [on]
other 위에서 구체적으로 언급되지 않은 그 밖의 모든 경고 [on]
--limit-X val 실행 제한 X를 설정
passes 패스 총 횟수 [unlimited]
stalled-passes 코드 생성에 진전이 없는 상태에서 최대 패스 횟수 [1000]
macro-levels 매크로 확장 레벨 [10000]
macro-tokens 싱글-라인 매크로 확장시 처리되는 최대 토큰 수 [10000000]
mmacros 최종 반환 전까지 확장되는 멀티-라인 매크로의 총량 [100000]
rep %rep 횟수 [1000000]
eval 표현식 계산 깊이 [8192]
lines 처리된 총 소스 라인 수 [2000000000]
[label:] instruction operands ; comment
- 기본 문법은 다음과 같다.
label:: 선택사항이며jmp명령어에 사용됨- 사용할 수 있는 글자는 '영문자', '숫자', '_', '$', '#', '@', '~', '.', '?'
- 맨 처음 글자는 '영문자', '.', '_', '?'만 가능함
instruction: 연산자 (필수)operands: 연산자에 따라 필수 혹은 선택, "[]"로 감쌀 경우 해당 레지스터, 메모리의 값은 특정 주소 값을 의미함 (C언어의 포인터 개념)- 숫자의 경우
- 10진수: 접미어 d, t 또는 접두사 0d, 0t (아무 것도 없으면 기본적으로 10진수)
- 16진수: 접미어 h, x 또는 접두사 0h, 0x
- 8진수: 접미어 q, o 또는 접두사 0q, 0o
- 2진수: 접미어 b, y 또는 접두사 0b, 0y
- 문자열의 경우
- 단일 문자는 '로 감싸기
- 여러 문자들의 경우 "로 감싸기
- 이스케이프 문자 예시는 다음과 같다.
\' 작은 따옴표 (') \" 큰 따옴표 (") \` 억음부호 (`) \\ 백슬래시 (\) \? 물음표 (?) \a BEL (ASCII 7), Bell을 의미함 (비프음 발생) \b BS (ASCII 8), BackSpace를 의미하며 커서를 앞으로 옮김 (후에 글자를 표시하면 기존 글자를 덮어쓰게 됨) \t TAB (ASCII 9), Horizontal Tab \n LF (ASCII 10), Line Feed를 의미하며 줄바꿈 문자 역할을 함 \v VT (ASCII 11), Vertical Tab (요즘은 거의 사용하지 않음) \f FF (ASCII 12), Form Feed를 의미하며 프린터에서는 페이지 넘김 또는 화면 지우기 \r CR (ASCII 13), Carriage Return을 의미하며 커서를 가장 왼쪽으로 이동시킴 (CR + LF: Windows, LF: Unix/Mac) \e ESC (ASCII 27), Escape를 의미하며 터미널 색상을 바꾸거나 특수 제어 기능을 사용할 때 주로 쓰임 \377 최대 3글자의 8진수 - 바이트 문자 (예: `\0`: NULL, `\033`: ESC) \xFF 최대 2글자의 16진수 - 바이트 문자 (예: `\x00`: NULL, `\xFF`: All bits set, `\x7F`: DEL, `\x20`: Space, `\x02`: 본문 시간, `\x03`: 본문 끝) \u1234 4글자의 16진수 - Unicode 문자 \U12345678 8글자의 16진수 - Unicode 문자 db `\u263a` ; UTF-8 스마일 db `\xe2\x98\xba` ; UTF-8 스마일 db 0E2h, 098h, 0BAh ; UTF-8 스마일
- 숫자의 경우
; comment: 코멘트 기입- 기타:
\(역슬래시) 문자를 사용하여 줄바꿈 가능함
-
RAX: 64비트 전체를 사용
-
EAX: 64비트 중 하위 32비트를 사용
-
AX: 64비트 중 하위 16비트를 사용
-
AH: AX에서 하위 절반의 상위 8비트를 사용
-
AL: AX에서 하위 절반의 하위 8비트를 사용
-
| 64비트 | 32비트 | 16비트 | 상위 8비트 | 하위 8비트 | 용도 |
|---|---|---|---|---|---|
| RAX | EAX | AX | AH | AL | 범용, Accumulator를 의미하며 결과값을 저장하는 곳, 리턴값을 담는 곳으로 많이 쓰임 |
| RBX | EBX | BX | BH | BL | 범용, Base를 의미하며 메모리의 기준점으로 자주 사용 |
| RCX | ECX | CX | CH | CL | 범용, Counter를 의미하며 반복문을 돈 횟수를 기록하는데 자주 사용 |
| RDX | EDX | DX | DH | DL | 범용, Data를 의미하며 연산 보조 및 I/O 포트 저장 등 다양한 데이터를 담는데 주로 사용 |
| RSI | ESI | SI | - | SIL | 범용, Source Index를 의미하며 데이터 원본 주소를 가리킴 |
| RDI | EDI | DI | - | DIL | 범용, Destination Index를 의미하며 데이터 도착지 주소를 가리킴 |
| RBP | EBP | BP | - | BPL | 특수, Base Pointer를 의미하며 스택의 기준 주소를 가리킴 |
| RSP | ESP | SP | - | SPL | 특수, Stack Pointer를 의미하며 스택의 최상단 주소를 가리킴 (Push, Pop 할 때마다 바뀜) |
| RIP | EIP | IP | - | IPL | 특수, Instruction Pointer를 의미하며 CPU가 다음에 실행할 명령어가 어디 있는지 알려줌 (mov 명령어 사용 불가하며 jmp, call, ret 등을 통해 자동으로 바뀜) |
| RFLAGS | EFLAGS | FLAGS | - | - | 특수, Flags Register를 의미하며, CPU의 현재 상태를 알려줌 |
| R8 | R8D | R8W | - | R8B | 범용, x64에 추가됨 |
| R9 | R9D | R9W | - | R9B | 범용, x64에 추가됨 |
| R10 | R10D | R10W | - | R10B | 범용, x64에 추가됨 |
| R11 | R11D | R11W | - | R11B | 범용, x64에 추가됨 |
| R12 | R12D | R12W | - | R12B | 범용, x64에 추가됨 |
| R13 | R13D | R13W | - | R13B | 범용, x64에 추가됨 |
| R14 | R14D | R14W | - | R14B | 범용, x64에 추가됨 |
| R15 | R15D | R15W | - | R15B | 범용, x64에 추가됨 |
- 실수 및 벡터 레지스터는 다음과 같다.
| 이름 | 크기 | 용도 |
|---|---|---|
| XMM0 ~ XMM15 | 16바이트 (128비트) | SSE 명령어, 실수 연산, 짧은 벡터 연산 |
| YMM0 ~ YMM15 | 32바이트 (256비트) | AVX 명령어 전용 (XMM을 포함하는 확장 형태) |
| ZMM0 ~ ZMM31 | 64바이트 (512비트) | AVX-512 전용 (최신 고성능 CPU) |
-
약자의 뜻은 다음과 같음
- R(Register / 64비트): 64비트 레지스터
- E(Extended / 32비트): 16비트 시절, 32비트로 확장되었다는 의미
- H(High / 8비트): 16비트 레지스터의 상위 8비트를 의미
- L(Low / 8비트): 16비트 레지스터의 하위 8비트를 의미
- W(Word): 16비트 / D(Doubleword): 32비트 / B(Byte): 8비트
-
RFLAGS 레지스터의 각 비트별 의미
| 비트 | 약어 | 이름 | 종류 | 설명 |
|---|---|---|---|---|
| 0 | CF | Carry Flag | 상태 | 부호 없는 연산에서 자리 올림/빌림 발생* |
| 2 | PF | Parify Flag | 상태 | 결과의 하위 8비트 중 1의 개수가 짝수임 |
| 4 | AF | Auxiliary Flag | 상태 | BCD 연산 시 하위 4비트에서 자리 올림 발생 |
| 6 | ZF | Zero Flag | 상태 | 연산 결과가 0임* |
| 7 | SF | Sign Flag | 상태 | 연산 결과가 음수임 (최상위 비트가 1)* |
| 8 | TF | Trap Flag | 시스템 | 한 단계씩 실행 모드 활성화 (디버깅용) |
| 9 | IF | Interrupt Enable | 시스템 | 외부 인터럽트(키보드 입력 등)를 처리함 |
| 10 | DF | Direction Flag | 제어 | 문자열 처리시 주소를 증가 또는 감소시킬지 결정 (0이면 증가, 1이면 감소) |
| 11 | OF | Overflag Flag | 상태 | 부호 있는 연산에서 범위 초과 발생* |
| 12-13 | IOPL | I/O Privilege | 시스템 | 입출력 포트에 접근 가능한 권한 레벨 (0~3) |
| 14 | NT | Nested Task | 시스템 | 현재 태스크가 다른 태스크 내부에 중첩됨 |
| 16 | RF | Resume Flag | 시스템 | 디버그 예외 상황을 일시적으로 무시함 |
| 17 | VM | Virtual 8086 | 시스템 | 가상 8086 모드 동작 중 |
| 18 | AC | Alignment Check | 시스템 | 메모리 정렬 확인 기능 활성화 |
| 21 | ID | ID Flag | 시스템 | CPUID 명령어를 지원하는지 확인용 |
- RFLAGS 레지스터 조작 명령어
PUSHFQ(Push Flags Quadword): 현재 RFLAGS 값을 스택에 저장합니다. (EFLAGS(32비트)에 대해서는PUSHFD, FLAGS(16비트)에 대해서는PUSHF)POPFQ(Pop Flags Quadword): 스택에 있는 값을 RFLAGS로 복원합니다. (EFLAGS(32비트)에 대해서는POPFD, FLAGS(16비트)에 대해서는POPF)STC/CLC: Carry Flag(CF)를 직접 1로 만들거나(Set) 0으로 만듭니다(Clear).STD/CLD: Direction Flag(DF)를 직접 설정하거나 해제합니다.- (나머지는 생략)
-
데이터를 저장하는 순서가 아키텍처마다 다름
- 빅 엔디안: 낮은 주소에 최상위 비트(Most Significant Bit)부터 저장하는 방식 (주로 IBM, JVM, 네트워크 전송)
- 사람이 보기에 직관적이라는 장점이 있음
- 리틀 엔디안: 낮은 주소에 최하위 비트(Least Significant Bit)부터 저장하는 방식 (주로 Intel)
- 8비트와의 호환성이 좋고, 데이터 캐스팅 속도가 빠름 (예: 32비트에서 16비트로 캐스팅할 때 앞의 2개 바이트만 읽으면 됨)
- 빅 엔디안: 낮은 주소에 최상위 비트(Most Significant Bit)부터 저장하는 방식 (주로 IBM, JVM, 네트워크 전송)
-
만약
0A 0B 0C 0D데이터를 저장한다고 하면,- 빅 엔디안의 경우
메모리 주소 7 6 5 4 3 2 1 0 바이트 0x00 0x00 0x00 0x00 0x0A 0x0B 0x0C 0x0D - 리틀 엔디안의 경우 (아래부터 1바이트씩 채움)
메모리 주소 7 6 5 4 3 2 1 0 바이트 0x00 0x00 0x00 0x00 0x0D 0x0C 0x0B 0x0A
- 빅 엔디안의 경우
- 시스템 콜: 운영체제가 제공하는 API 집합 (리눅스 x64용 시스템 콜 종류는 1, 2를 참조)
- syscall 호출을 위해 다음 레지스터를 사용할 수 있음
- RAX: 시스템 콜 번호를 기입할 것 (위의 API 문서 링크 참조), 호출 뒤에는 결과 값이 담김 (성공시 리턴값, 실패시 오류 코드)
- RDI, RSI, RDX, R10, R8, R9: 함수의 1, 2, 3, 4, 5, 6번째 인자를 넣을 것
- (참고로 일반 라이브러리 호출의 경우 인자를 넣는 레지스터가 다름: RDI, RSI, RDX, RCX, R8, R9)
- 6개보다 인자가 많으면 스택을 이용해야 함
- 사용자가 직접 작성하는 어셈블리 코드는 다음 섹션들이 있음
.data: 초기화된 데이터 또는 상수를 선언함.rodata: 읽기 전용 데이터 정의를 선언함.bss: 초기화되지 않은 변수를 선언함 (실행시 0으로 채워짐).text: 프로그램 코드
- 예제 코드
section .data
num1: equ 100
num2: equ 50
msg: db "Sum is correct", 10
-
원래 어셈블리 언어는 정적 타입 언어가 아니며 바이트 집합을 직접 다룸
- NASM 자체적으로 데이터 타입을 정의하는 데 도움을 주는 함수를 제공함
-
.data섹션: 기본 데이터 타입을 기반으로 NASM에서 제공하는 의사 명령어는 다음과 같다.
| 키워드 | 이름 | 크기 | 설명 |
|---|---|---|---|
| DB | Define Byte | 1바이트 (8비트) | 문자나 작은 정수 저장 |
| DW | Define Word | 2바이티 (16비트) | 보통 16비트 정수 저장 |
| DD | Define Doubleword | 4바이트 (32비트) | 32비트 정수나 실수(float) 저장 |
| DQ | Define Quadword | 8바이트 (64비트) | 64비트 정수나 배정밀도 실수(double) |
| DT | Define Ten Bytes | 10바이트 (80비트) | 확장 정밀도 실수 저장 |
| DO | Define Octoword | 16바이트 (128비트) | 128비트 데이터 (SSE 레지스터 등에서 사용) |
| DY | Define Y-word | 32바이트 (256비트) | 256비트 데이터 (AVX 레지스터 용) |
| DZ | Define Z-word | 64바이트 (512비트) | 512비트 데이터 (AVX-512 레지스터 용) |
-
EQU상수 선언: num1은 100으로 치환됨 (C 언어의 #define과 같은 용도) -
문장 뒤에 ,10 또는 ,13 ,10를 붙임 (10: 라인피드, 13: 캐리지리턴)
-
.bss섹션: 초기화되지 않은 변수를 예약하는 명령어는 다음과 같다.
| 키워드 | 이름 | 크기 | 설명 |
|---|---|---|---|
| RESB | Reserve Byte | 1바이트 | resb 10 (10바이트 예약) |
| RESW | Reserve Word | 2바이트 | resw 5 (10바이트 예약) |
| RESD | Reserve Doubleword | 4바이트 | 32비트 변수용 (float) |
| RESQ | Reserve Quadword | 8바이트 | 64비트 변수용 (double) |
| REST | Reserve Ten Bytes | 10바이트 | 확장 실수용 |
| RESO | Reserve Octoword | 16바이트 | 128비트용 |
| RESY | Reserve Y-word | 32바이트 | 256비트용 |
| RESZ | Reserve Z-word | 64바이트 | 512비트용 |
-
스택은 push 할수록 거꾸로 자라게 되어 있어서 RSP의 주소 값이 감소하게 되어 있음, pop 할수록 RSP의 주소 값이 증가함
- RSP의 주소 값은 데이터가 들어 있는 스택의 가장 위를 가리킴
- RBP의 주소 값과 RSP의 주소 값이 같으면 스택이 비어 있다는 뜻임
- 함수 호출시 항상 다음과 같은 동작을 수행해야 함
push rbp ; 기존 Base Pointer 백업 mov rbp, rsp ; 새로운 Frame Base Pointer 세트 ... mov rsp, rbp ; RSP 위치를 RBP 위치로 끌어올림으로써 쌓였던 데이터 모두 정리 pop rbp ; RSP가 가리키는 곳에서 값을 꺼내 RBP 레지스터에 넣음 (저장했던 Base Pointer 복원) --> 위 코드와 이 코드를 leave라는 명령어 하나로 합칠 수 있음 ret ; RSP가 가리키는 주소값(call 호출시 자동 저장된 값)을 꺼내 RIP 레지스터에 넣음 (CPU는 call 명령 다음 줄부터 실행하게 됨)
-
스택 동작의 예시 (단순한 값 저장/불러오기)
push rax: RSP 값이 8 감소한 뒤에 rax 값을 RSP 주소에 저장함pop rax: RSP 주소로부터 값을 가져와 rax에 저장하고 RSP 값이 8 증가함
-
스택 동작의 예시 (함수 호출)
call MyFunc: MyFunc 함수를 호출push returnAddr ; call 직후의 주소 jump MyFunc ; MyFunc 함수로 점프retpop rip ; Instruction Pointer에 저장했던 call 직후의 주소를 가져와서 저장
-
예제
section .text
global _start
_start:
; 1. 함수 호출 (리턴 주소를 스택에 push하고 jump)
call add_numbers
; 3. 프로그램 종료 (시스템 콜)
mov rax, 60 ; sys_exit
xor rdi, rdi ; status 0
syscall
add_numbers:
; --- 프롤로그 (새로운 층 쌓기) ---
push rbp ; 부모(_start)의 RBP를 백업
mov rbp, rsp ; 현재 위치를 이 함수의 기준점(바닥)으로 설정
sub rsp, 16 ; 지역 변수 2개를 위한 16바이트 공간 확보
; --- 본문 (스택 사용) ---
mov qword [rbp-8], 10 ; 첫 번째 지역 변수 = 10
mov qword [rbp-16], 20 ; 두 번째 지역 변수 = 20
mov rax, [rbp-8] ; rax = 10
add rax, [rbp-16] ; rax = 10 + 20 (결과는 30)
; --- 에필로그 (정리하고 돌아가기) ---
leave ; mov rsp, rbp + pop rbp (스택 원상복구)
ret ; 스택의 리턴 주소를 꺼내 _start로 복귀
-
시프트: 비트를 왼쪽 혹은 오른쪽으로 이동시키는 명령어들은 다음과 같다.
shl: 1번째 피연산자의 비트를 왼쪽으로 shift (반대쪽은 0으로 채움)shr: 1번째 피연산자의 비트를 오른쪽으로 shift (반대쪽은 0으로 채움)rol: 1번째 피연산자의 비트를 왼쪽으로 회전시킴ror: 1번째 피연산자의 비트를 오른쪽으로 회전시킴
-
테스트 및 스캔
test: 1번째 피연산자와 2번째 피연산자의 AND 연산 후에 결과를 저장하지 않고 flag 업데이트- ZF(Zero Flag): 연산 결과가 0이면 1이 됨 (즉, rax가 0이면 ZF=1)
- SF(Sign Flag): 최상위 비트(부호 비트)가 1이면 1이 됨 (즉, rax가 음수이면 SF=1)
test rax, rax jz .is_zero ; rax가 0이면 점프 js .is_negative ; rax가 음수이면 점프bt(Bit Test): 1번째 피연산자에서 n번째 자릿수(2번째 피연산자)가 확인하고 flag 업데이트- BTS(비트 확인 후 1로 세팅)
- BTR(비트 확인 후 0으로 리셋)
bt eax, 3 ; eax의 3번째 비트(2^3 = 8의 자리)를 확인 jc .bit_is_one ; 3번째 비트가 1이면(CF=1) 점프BSF/BSR(Bit Scan): 레지스터 안에서 '1'이 처음으로 나타나는 위치(인덱스)를 찾고 flag 업데이트- BSF (Bit Scan Forward): 오른쪽(LSB, 0번 비트)에서 왼쪽으로 스캔
- BSR (Bit Scan Reverse): 왼쪽(MSB)에서 오른쪽으로 스캔
- 만약 대상 레지스터가 모두 0이면, ZF(Zero Flag)가 1이 되고 결과값은 정의되지 않습니다. (반드시 ZF를 먼저 체크할 것)
bsf rax, rbx ; rbx에서 처음 1이 나오는 위치를 rax에 저장 jz .all_zeros ; rbx가 0이었다면 예외 처리
-
mov: 범용 레지스터/메모리/값을 범용 레지스터에 저장하는 명령어 (이름은 mov지만 실제 의미는 복사, 저장에 가까움)mov rax, rcx: rcx의 값을 rax에 저장mov rax, 5: 5를 rax에 저장mov [rcx], rax: rax의 값을 rcx에 지정된 메모리 주소에 저장
-
특수한 경우에서 사용하는
mov명령어movzx: 작은 크기의 레지스터에서 큰 크기의 레지스터로 데이터를 복사할 때, 상위 비트를 모두 0으로 채움movsx: 작은 크기의 레지스터에서 큰 크기의 레지스터로 데이터를 복사할 때, sign 비트를 상위 비트에 복사함 (양수는 0, 음수는 1)cmove/cmovne: 이전 비교 연산에서 두 피연산자가 "같았다면/달랐다면" 2번째 피연산자의 값을 1번째 피연산자로 복사함cmovg/cmovlg: 이전 비교 연산에서 1번째 피연산자가 2번째 피연산자보다 "크다면/크거나 같다면" 2번째 피연산자의 값을 1번째 피연산자로 복사함cmovl/cmovle: 이전 비교 연산에서 1번째 피연산자가 2번째 피연산자보다 "작다면/작거나 같다면" 2번째 피연산자의 값을 1번째 피연산자로 복사함cmovz/cmovnz: 이전 비교 연산에서 결과가 "0이었다면/0이 아니었다면" 2번째 피연산자의 값을 1번째 피연산자로 복사함cmovc/cmovnc: 이전 비교 연산이 Carry flag를 "set했다면/set하지 않았다면" 2번째 피연산자의 값을 1번째 피연산자로 복사함
-
정수 산술 연산 (가능하면,
mul,div연산은 지양할 것!)add: 1번째 피연산자의 값에 2번째 피연산자의 값을 더하고 나서 결과를 1번째 피연산자에 저장 (덧셈)sub: 1번째 피연산자의 값에 2번째 피연산자의 값을 빼고 나서 결과를 1번째 피연산자에 저장 (뺄셈)mul: 부호 없는 연산. 피연산자는 1개만 기재함, 나머지 하나는 무조건rax. 피연산자와rax의 값을 곱한 후 결과의 상위 64비트는rdx, 하위 64비트는rax에 저장imul: 부호 있는 연산.- 피연산자 1개인 경우:
mul과 동일 - 피연산자 2개인 경우: 1번째 피연산자와 2번째 피연산자의 값을 곱하고 나서 결과를 1번째 피연산자에 저장
- 피연산자 3개인 경우: 2번째 피연산자와 3번째 피연산자의 값을 곱하고 나서 결과를 1번째 피연산자에 저장
- 피연산자 1개인 경우:
div: 부호 없는 연산. 나누어질 수는 상위 64비트rdx(0으로 초기화할 것:xor rdx, rdx), 하위 64비트rax에 담고, 피연산자는 1개만 기재함. 몫은rax, 나머지는rdx에 저장idiv: 부호 있는 연산. 나누어질 수는cqo(Convert Quadword to Octword) 명령으로rax의 부호 비트를rdx까지 먼저 확장해야 함. 피연산자는 1개만 기재함. 몫은rax, 나머지는rdx에 저장inc: 1번째 피연산자의 값을 1 증가시킴dec: 1번째 피연산자의 값을 1 감소시킴neg: 1번째 피연산자의 값의 부호를 반전시킴, 예를 들면 5는 -5로 바뀜. (2의 보수)
-
논리 연산
and: 1, 2번째 피연산자의 논리 AND 연산 결과를 1번째 피연산자에 저장or: 1, 2번째 피연산자의 논리 OR 연산 결과를 1번째 피연산자에 저장xor: 1, 2번째 피연산자의 논리 XOR 연산 결과를 1번째 피연산자에 저장 (변수를 0으로 초기화시 자주 사용함)not: 1번째 피연산자의 논리 NOT 연산 결과를 1번째 피연산자에 저장 (비트 반전)
- 어셈블리어에서는
cmp와jmp및 파생 명령만으로 모든 분기 처리를 수행함jmp: 1번째 피연산자가 가리키는 "라벨" 혹은 "프로그램의 주소"로 점프cmp: 1번째 피연산자에서 2번째 피연산자를 뺀 후에 결과를 RFLAGS 레지스터에 상태를 표시함je(Jump if Equal): 이전cmp연산 결과가 같았다면 1번째 피연산자가 가리키는 곳으로 점프jz(Jump if Zero): RFLAGS 레지스터의 Zero flag가 1이었으면 1번째 피연산자가 가리키는 곳으로 점프 (je와 비슷함)jne(Jump if Not Equal): 이전cmp연산 결과가 같지 않았다면 1번째 피연산자가 가리키는 곳으로 점프jnz(Jump if Not Zero): RFLAGS 레지스터의 Zero flag가 0이었으면 1번째 피연산자가 가리키는 곳으로 점프 (jne와 비슷함)jg(Jump if Greater): 이전cmp연산 결과가 0보다 크면 1번째 피연산자가 가리키는 곳으로 점프jge(Jump if Greater or Equal): 이전cmp연산 결과가 0보다 크거나 같으면 1번째 피연산자가 가리키는 곳으로 점프jl(Jump if Less): 이전cmp연산 결과가 0보다 작으면 1번째 피연산자가 가리키는 곳으로 점프jle(Jump if Less or Equal): 이전cmp연산 결과가 0보다 작거나 같으면 1번째 피연산자가 가리키는 곳으로 점프ja(Jump if Above):jg와 비슷하지만 부호 없는 비교 수행jae(Jump if Above or Equal):jge와 비슷하지만 부호 없는 비교 수행jb(Jump if Below):jl과 비슷하지만 부호 없는 비교 수행jbe(Jump if Below or Equal):jle와 비슷하지만 부호 없는 비교 수행js(Jump if Sign): 결과가 음수(Sign flag가 1)이면 1번째 비연산자가 가리키는 곳으로 점프jns(Jump if Not Sign): 결과가 양수(Sign flag가 0)이면 1번째 비연산자가 가리키는 곳으로 점프jo(Jump if Overflow): 오버플로우(Overflow flag가 1) 발생시 1번째 비연산자가 가리키는 곳으로 점프jc(Jump if Carry): 캐리(Carry flag가 1) 발생시 1번째 비연산자가 가리키는 곳으로 점프
-
다음 명령어들을 통해 문자열뿐만 아니라 연속된 메모리 데이터(배열, 구조체)까지도 처리할 수 있음 (접미어 의미: byte-1바이트, word-2바이트, doubleword-4바이트, quadword-8바이트)
movs(b|w|d|q)(Moving String): [RSI]가 가리키는 데이터를 [RDI]가 가리키는 곳으로 복사cmps(b|w|d|q)(Compare String): [RSI]와 [RDI]가 가리키는 데이터를 비교, 결과는 RFLAGS에 반영scas(Scan String): RAX(또는 AL, EAX)의 값과 [RDI]가 가리키는 메모리 값을 비교합니다.lods(Load String): [RSI]가 가리키는 메모리 값을 RAX 레지스터로 읽어옵니다.stos(Store String): RAX 레지스터의 값을 [RDI]가 가리키는 메모리에 저장합니다.
-
RFLAGS의 Direction flag
- 위 명령들이 실행되면 RSI, RDI는 데이터 크기만큼 자동으로 변함
- CLD (Clear DF): 주소가 증가합니다. (정방향 처리)
- STD (Set DF): 주소가 감소합니다. (역방향 처리)
- 위 명령들이 실행되면 RSI, RDI는 데이터 크기만큼 자동으로 변함
-
반복하기
rep(Repeat): RCX가 0이 될 때까지 반복 (주로movs,stos와 함께 사용)repe/repz: RCX가 0이 아니면서, 비교 결과가 같을 동안 반복 (주로cmps,scas와 함께 사용)repne/repnz: RCX가 0이 아니면서, 비교 결과가 다를 동안 반복
-
예제: memset과 같이 0으로 채우기
mov rdi, buffer ; 목적지 주소
mov rax, 0 ; 채울 값 (0)
mov rcx, 100 ; 100번 반복
cld ; 방향은 정방향(+)
rep stosq ; RAX(0)를 [RDI]에 저장하고 RDI를 8바이트씩 증가시키기를 100번 반복
- 예제: 문자열 길이 찾기
mov rdi, string ; 스캔할 주소
mov al, 0 ; 찾을 값 (\0)
mov rcx, -1 ; 무한대(최댓값) 설정
cld
repne scasb ; AL(0)과 [RDI]가 다를 동안 계속 스캔
; 결과적으로 RDI는 널 문자 다음 위치를 가리키게 됨
- 예제: 문자열 뒤집기 (reverse)
section .data
SYS_WRITE equ 1 ; `sys_write` 시스템 콜 번호.
SYS_EXIT equ 60 ; `sys_exit` 시스템 콜 번호
STD_OUT equ 1 ; 표준 출력 file descriptor 번호
EXIT_CODE equ 0 ; 프로그램 종료 코드. 정상인 경우 0.
NEW_LINE_LEN equ 1 ; new line 심볼만 있는 경우의 문자열 길이
INPUT_LEN equ 12 ; INPUT 문자열 길이
NEW_LINE db 0xa ; new line 심볼('\n')의 ASCII 코드
INPUT db "Hello world!" ; 예제용 입력 문자열
section .bss
OUTPUT resb INPUT_LEN ; 뒤집은 문자열을 저장할 출력 버퍼
section .text
global _start
; 프로그램 엔트리 포인트
_start:
xor rcx, rcx ; rcx 값을 0으로 초기화. 입력 문자열 길이 값을 저장하기 위해 사용될 것임.
mov rsi, INPUT ; 입력 문자열 주소를 rsi 레지스터에 저장
mov rdi, OUTPUT ; 출력 버퍼 주소를 rdi 레지스터에 저장
call reverseStringAndPrint ; reverseStringAndPrint 프로시저 호출.
; 입력 문자열 길이를 계산하고 뒤집을 준비를 함
reverseStringAndPrint:
cmp byte [rsi], 0 ; 주어진 문자열의 1번째 요소와 `NUL` terminator를 비교
mov rdx, rcx ; 뒤집힌 문자열의 길이를 rdx 레지스터에 저장
je reverseString ; 입력 문자열 끝에 도달하면 뒤집을 것
lodsb ; rsi로부터 바이트 하나를 al 레지스터에 로드하고, 문자열의 다음 문자로 포인터를 이동시킴
push rax ; 스택에 입력 문자열의 문자를 저장
inc rcx ; 입력 문자열 길이를 저장하는 카운터를 1 증가시킴
jmp reverseStringAndPrint ; 입력 문자열의 끝에 이를 때까지 반복
; 문자열을 뒤집은 후에 출력 버퍼에 저장
reverseString:
cmp rcx, 0 ; 문자열 길이를 저장하는 카운터를 확인
je printResult ; 만약 `0`과 같다면 뒤집힌 문자열을 출력
pop rax ; 스택으로부터 문자 하나를 Pop
mov [rdi], rax ; 출력 버퍼에 가져온 문자 하나를 넣음
inc rdi ; 출력 버퍼의 다음 문자로 포인터를 이동시킴
dec rcx ; 문자열 길이에 대한 카운터를 1 감소시킴
jmp reverseString ; 문자열 끝에 이를 때까지 다음 문자로 이동
; 뒤집힌 문자열을 표준 출력으로 출력
printResult:
mov rax, SYS_WRITE ; 시스템 콜 번호 지정 (1: `sys_write`)
mov rdi, STD_OUT ; `sys_write`의 1번째 인자 지정 (`stdout`)
mov rsi, OUTPUT ; `sys_write`의 2번째 인자 지정 (출력할 결과 문자열에 대한 참조)
syscall ; `sys_write` 시스템 콜 호출
mov rdx, NEW_LINE_LEN ; 출력할 결과 문자열의 길이를 설정
mov rax, SYS_WRITE ; 시스템 콜 번호 지점 (1: `sys_write`)
mov rdi, STD_OUT ; `sys_write`의 1번째 인자 지정 (`stdout`)
mov rsi, NEW_LINE ; `sys_write`의 2번째 인자 지정 (출력할 결과 문자열에 대한 참조)
syscall ; `sys_write` 시스템 콜 호출
mov rax, SYS_EXIT ; 시스템 콜 번호 지정 (60: `sys_exit`)
mov rdi, EXIT_CODE ; `sys_exit`의 1번째 인자를 0으로 지정. (성공한 경우 0)
syscall ; `sys_exit` 시스템 콜 호출
- 매크로는 기계어 명령이 아님, 어셈블 할 때 미리 정의된 코드로 대체됨
- 함수와 비교해서 점프 오버헤드가 없지만, 매크로를 많이 사용할수록 바이너리 크기가 커진다는 단점이 있음
- 간단한 것은 괜찮지만 남용하지 않는 편이 좋음
%define 매크로이름 값: 매크로이름을 오른쪽의 값으로 치환함
- 선언한 "인자개수"만큼 호출할 때 인자를 순서대로 넣으면 마치 어셈블리어 명령어처럼 작동함
- 주의사항: 매크로 안에서 라벨을 사용할 경우 라벨 앞에
%%접두사가 있어야 함
- 주의사항: 매크로 안에서 라벨을 사용할 경우 라벨 앞에
%macro 매크로이름 인자개수
; 실행할 코드들
; %1, %2 순서로 인자에 접근
%endmacro
- 예제 코드는 다음과 같음
%macro SAFE_ADD 2
push rax ; 원래 rax 값 보존
mov rax, %1 ; 첫 번째 인자를 rax에
add rax, %2 ; 두 번째 인자를 더함
mov %1, rax ; 결과를 다시 첫 번째 인자에 저장
pop rax ; rax 값 복구
%endmacro
section .text
_start:
SAFE_ADD rbx, 10 ; rbx = rbx + 10 으로 확장됨
-
조건부 어셈블리를 사용하면 다음과 같은 이점이 있다.
- 플랫폼 대응: 윈도우용 코드와 리눅스용 코드를 한 파일에 짜두고, 빌드할 때 환경에 맞는 부분만 골라서 어셈블할 수 있음
- 디버깅: 개발 중에는 로그 출력 코드를 넣고, 배포할 때는 %ifdef 하나만 꺼서 로그 코드를 싹 제거해 성능을 높일 수 있음
- 최적화: CPU 사양에 따라 특정 명령어를 지원하는지 체크하여 최적의 코드를 선택하게 할 수 있음
-
%ifdef/%ifndef/%endif(Define 체크)- 특정 단어가 정의되어 있는지 확인함
- 주로 디버그 모드나 운영체제별 코드를 작성할 때 사용
%ifdef: 정의되어 있다면 포함%ifndef: 정의되어 있지 않다면 포함- 예제 코드
%define DEBUG_MODE %ifdef DEBUG_MODE ; 이 코드는 DEBUG_MODE가 정의되었을 때만 빌드에 포함됨 PRINT_MSG "Debug: Checkpoint 1", 20 %endif
-
%ifmacro/%ifnmacro/%endif(매크로 존재 체크)- 특정 이름을 가진 매크로가 이미 만들어져 있는지 확인함
- 다른 파일에서 가져온 매크로와 이름이 겹치지 않게 하거나, 특정 매크로가 있을 때만 코드를 짜고 싶을 때 유용함
-
%if/%elif/%else/%endif(수식 체크)- 숫자 크기를 비교하거나 계산 결과가 참(0이 아님)인지 확인함
- 예제 코드
%define VERSION 2 %if VERSION > 1 ; 버전이 1보다 클 때만 실행되는 최신 기능 코드 %else ; 구버전용 코드 %endif
-
%ifenv/%ifnenv(환경 변수 체크)- 컴퓨터의 **환경 변수(Environment Variable)**가 존재하는지 확인함
- 예를 들어 빌드하는 시스템의 사용자 이름이 무엇인지, 특정 경로가 설정되어 있는지에 따라 코드를 바꿀 수 있음
-
기타 특수 지시어
%ifempty: 매크로에 전달된 인자가 비어 있는지 확인함%ifid: 인자가 식별자(Identifier)(예: 레지스터 이름, 라벨 이름 등)인지 확인함%ifnum: 인자가 숫자인지 확인함%ifstr: 인자가 따옴표로 둘러싸인 문자열인지 확인함%iftoken: 특정 토큰(코드 조각)이 존재하는지 확인함
-
%include지시어%include "helpers.asm": 외부 .asm 파일을 include 시킴
-
%assign지시어- 파라미터 없이 숫자 값으로 확장되는 매크로를 정의함
- 예제
; 예: 데이터 테이블을 자동으로 생성하는 매크로 %assign i 1 %rep 5 db i ; 1, 2, 3, 4, 5 순서대로 바이트 생성 %assign i i+1 ; i 값을 1 증가 (재할당) %endrep ; 결과적으로 아래와 같이 코드가 생성됩니다: ; db 1 ; db 2 ; db 3 ; db 4 ; db 5
-
%defstr지시어- 파라미터 없이 문자열 값으로 확장되는 매크로를 정의함
%defstr hello_world_msg "Hello world!"
-
%!지시어- 환경 변수를 가져올 때 사용함
%defstr HOME %!HOME
-
%strlen지시어- 문자열 길이를 알려줌
- 예제
; PRINT 매크로 정의 %macro PRINT 1 mov rax, 1 ; 시스템 콜 번호 지정 (1: `sys_write`) mov rdi, 1 ; `sys_write`의 1번째 인자를 1로 설정 (`stdout`) mov rsi, %1 ; `sys_write`의 2번째 인자를 프린트 할 문자열에 대한 참조로 설정 (매크로의 1번째 인자로 대체될 것이다) mov rdx, %strlen(%1) ; `sys_write`의 3번째 인자를 프린트 할 문자열의 길이로 설정 syscall ; `sys_write` 시스템 콜 호출 %endmacro
-
%rotate,%rep지시어- 매크로 내부에서 반복 작업을 할 때 유용함
%rep: 특정 코드 블록을 지정한 횟수만큼 반복 생성%rep 5 db 0 ; 'db 0'이 5번 연속으로 써짐 %endrep%rotate: 매크로에 전달된 인자들(%1, %2, %3...)의 순서를 한 칸씩 민다. (1이면 오른쪽으로 한 칸 회전, -1이면 왼쪽으로 한 칸 회전)%macro MULTI_PUSH 1-* %rep %0 push %1 %rotate 1 ; 앞에서부터 차례대로 %endrep %endmacro %macro MULTI_POP 1-* %rep %0 %rotate -1 ; 뒤에서부터 거꾸로 pop %1 %endrep %endmacro section .text _start: MULTI_PUSH rax, rbx, rcx ; push rax, push rbx, push rcx ; ... 어떤 작업 수행 ... MULTI_POP rax, rbx, rcx ; pop rcx, pop rbx, pop rax (복구 완료!)
-
오류 메시지 출력을 위한 지시어
- 관련 지시어 종류는 다음과 같음
지시어 의미 빌드 중단 여부 특징 %warning경고 중단 안함 주의만 주고 계속 진행 %error오류 중단함 (지연) 발견된 모든 오류를 다 보여주고 종료 %fatal치명적인 오류 즉시 중단 발견 즉시 바로 종료 - 경고 예제
%if VERSION < 2 %warning "이 버전은 곧 지원이 중단될 예정입니다." %endif - 오류 예제
%macro MY_MACRO 1 %if %1 > 255 %error "인자값은 255를 넘을 수 없습니다!" %endif %endmacro - 치명적인 오류 예제
%ifndef REQUIRED_HEADER %fatal "필수 헤더 파일이 없습니다. 빌드를 중단합니다." %endif - 예제: 매크로 방어 코드
%macro PUSH_RAX_ONLY 1 %ifidni %1, rax ; 인자가 rax와 같다면 (대소문자 무시) push rax %else %error "이 매크로는 오직 rax만 허용합니다. 입력하신 것: %1" %endif %endmacro _start: PUSH_RAX_ONLY rbx ; 빌드 시 에러 발생!
- 관련 지시어 종류는 다음과 같음
-
struc매크로: C 언어의 구조체와 유사함- 기본 문법은 다음과 같다.
struc Person .name: resb 32 ; 32바이트 (이름) .age: resd 1 ; 4바이트 (나이) .id: resq 1 ; 8바이트 (ID) endstruc - 예제
section .data my_friend: istruc Person at Person.name, db "Alice", 0 at Person.age, dd 25 at Person.id, dq 12345678 iend ... mov eax, [rbx + Person.age] ; RBX가 구조체의 시작 주소라면, 32바이트(name) 뒤의 age 값을 읽어옴 (이름으로 참조하므로 오프셋 숫자를 매번 안 바꿔도 되서 편리함)
- 기본 문법은 다음과 같다.
-
그 외에도 상당히 많은 매크로가 있음
%abs(숫자): 숫자의 절대값을 반환%b2hs(데이터): Binary to Hex String 함수, 이진 데이터를 16진수 문자열로 변환 (예: 0xFF --> "FF")%hs2b(문자열): Hex String to Binary 함수, 16진수 문자열을 이진 데이터로 변환 (예: "4142" --> 0x41, 0x42 --> "AB")%chr(숫자): ASCII 코드 값을 문자로 변환%cond(조건, 참_결과, 거짓_결과): C언어의 3항 연산자와 비슷함%count(인자_목록): 인자의 개수를 반환%depend(파일명): 현재 어셈블리 파일이 특정 파일에 의존하고 있음을 알림%eval(수식): 수식을 계산한 결과값을 상수로 반환%find(검색어, 인자_목록): 인자 목록에서 특정 단어가 몇 번째 위치하는지 인덱스로 알려줌 (대소문자 구분함)%findi(검색어, 인자_목록): 인자 목록에서 특정 단어가 몇 번째 위치하는지 인덱스로 알려줌 (대소문자 구분 없음)%hex(숫자): 숫자를 16진수 형태 문자열로 변환 (예: 255 --> "0xFF")%isid(인자): 인자가 유효한 식별자인지 확인 (변수명, 라벨명, 매크로명으로 사용할 수 있으면 true)%isnum(인자): 숫자 상수이면 true%isstr(인자): 문자열이면 true%isreg(인자): 레지스터 이름이면 true%istoken(인자): 단일 토큰이면 true%map(인자_목록, 함수/매크로): 인자로 받은 목록의 각 항목에 대해 지정된 매크로를 실행하고 그 결과물들을 다시 모음%null(): 빈값 반환 (비어 있음을 명시할 때 사용)%num(값): 인자를 정수로 강제 변환 (타입 확인용으로도 사용 가능)%ord(문자열): 문자열의 첫 글자를 ASCII/유니코드 코드 값으로 반환 (%chr의 반대)%pathsearch(파일명): NASM의 include 경로(-I옵션)에서 해당 파일을 찾아 그 전체 경로를 반환%realpath(경로): 상대경로를 절대경로로 변환%sel(인덱스, 인자_목록): 여러 개의 인자 중 특정 위치(인덱스)에 있는 값을 추출함 (인덱스는 1부터 시작)%selbits(비트크기, 인자_목록): 데이터의 비트 크기(8, 16, 32, 64)에 따라 적절한 인자를 선택함%str(값): 숫자나 식별자를 따옴표가 붙은 문자열로 변환%strcat(문자열1, 문자열2, ...): 여러 개의 문자열을 하나로 합침%strlen(문자열): 문자열의 길이(글자 수)를 반환%substr(문자열, 시작위치, [길이]): 문자열의 일부분을 추출 (시작위치 인덱스는 1부터 시작)%tok(문자열): 따옴표가 붙은 문자열을 따옴표 없는 토큰으로 되돌림 (%str의 반대)__?FILE?__: 현재 파일 이름__?LINE?__: 현재 라인 번호- (그 외의 여러 가지 매크로들이 존재함)
- 앞에서 정수, 문자열을 다루었는데 문자도 실상 정수 데이터임을 감안한다면 지금까지 본 것은 전부 정수 연산/처리만 배운 것이다.
- 실수 연산은 앞에서 배운
mov,add같은 명령어로 수행하기에는 너무 복잡하고 효율적이지 않고 느림 (지수 맞추기, 정규화) - 현대 CPU에서는 FPU (Floating Point Unit)라는 전용 연산 장치, 더 나아가 SIMD (Single Instruction Multiple Data) 기술을 이용해 병렬 처리를 수행하여 부동 소수 연산을 고속으로 처리함
- FPU (Floating Point Unit)의 역할
- 지수 맞추기 (Alignment): 소수점 위치를 동일하게 맞춤
- 가수 더하기 (Add): 소수점 앞뒤의 실제 숫자들을 더함
- 정규화 (Normalization): 결과를 다시 표준 형태(예: 1.xxxx)로 만듦
- 반올림 (Rounding): 정해진 비트 수에 맞게 끝처리
- SIMD (Single Instruction Multiple Data) 기술 (FPU보다 더 빠름)
- SSE (Streaming SIMD Extensions): 128비트 레지스터를 사용하여 여러 개의 소수점 연산을 동시에 처리
- AVX (Advanced Vector Extensions): 256비트나 512비트 레지스터를 사용하여 한 번에 8~16개의 소수점 연산을 병렬로 처리
- FPU (Floating Point Unit)의 역할
- .data 섹션 예시를 통한 데이터 타입 정의 보기
section .data ; 1. Single Precision (FP32) - 32비트 (4바이트) ; "dd" 지시어를 사용하여 4바이트 상수를 정의 f32_pi: dd 3.141592 f32_one: dd 1.0 ; 2. Double Precision (FP64) - 64비트 (8바이트) ; "dq" 지시어를 사용하여 8바이트 상수를 정의 f64_pi: dq 3.141592653589793 f64_exp: dq 2.718281828459 ; 3. Extended Precision (80-bit) - 80비트 (10바이트) ; dt(Ten-byte) 지시어 사용 f80_exact: dt 1.234567890123456789 ; 4. Quadruple Precision (FP128) 또는 SIMD용 데이터 ; 보통 두 개의 dq나 do(octoword)를 사용하여 비트 단위로 정의 f128_val1: dq 0.0, 0.0 ; 16바이트 공간을 0으로 초기화 (2개의 공간) f128_val2: do 0.0 ; 16바이트 공간을 0으로 초기화 (1개의 공간) - .bss 섹션 예시를 통한 데이터 타입 정의 보기
section .bss ; 1. Half Precision (FP16 / Bfloat16) - 16비트 (2바이트) half_val: resw 1 ; 1개의 word(2바이트) 예약 ; 2. Single Precision (FP32) - 32비트 (4바이트) single_val: resd 1 ; 1개의 doubleword(4바이트) 예약 ; 3. Double Precision (FP64) - 64비트 (8바이트) double_val: resq 1 ; 1개의 quadword(8바이트) 예약 ; 4. Extended Precision (80-bit) - 80비트 (10바이트) extended_val: rest 1 ; 1개의 ten-byte(10바이트) 예약 ; 5. Quadruple Precision (FP128) - 128비트 (16바이트) quad_val: reso 1 ; 1개의 octoword(16바이트) 예약
- 실수 연산은 앞에서 배운
-
MXCSR 레지스터 (SSE/AVX 제어 및 상태)
- SSE와 AVX 연산의 전반적인 행동 지침을 결정하는 32비트 레지스터
비트 범위 이름 풀네임 설명 및 역할 Bit 0 IE Invalid Operation Exception 유효하지 않은 연산 예외 발생 Bit 1 DE Denormal Exception 비정규화된 피연산자 사용 (일부 환경) Bit 2 ZE Divide-by-zero Exception 0으로 나누기 예외 발생 Bit 3 OE Overflow Exception 오버플로우 예외 발생, 결과가 너무 커서 표현 불가능 Bit 4 UE Underflow Exception 언더플로우 예외 발생, 결과가 너무 작아서 표현 불가능 Bit 5 PE Precision Exception 결과가 정확하지 않아 반올림됨 Bit 6 DAZ Denormals Are Zeros [성능] 입력값이 너무 작은 수(Denormal)면 0으로 간주 Bit 7 IM Invalid Operation Mask 잘못된 연산 마스크 [1: 마스킹(무시), 0: 예외 발생(마스크 해제)] Bit 8 DM Denormal Exception Mask 비정규화 피연산자 마스크 [1: 마스킹(무시), 0: 예외 발생(마스크 해제)] Bit 9 ZM Divide-by-zero Exception Mask 0으로 나누기 마스크 [1: 마스킹(무시), 0: 예외 발생(마스크 해제)] Bit 10 OM Overflow Exception Mask 오버플로우(범위 초과) 마스크 [1: 마스킹(무시), 0: 예외 발생(마스크 해제)] Bit 11 UM Underflow Exception Mask 언더플로우(범위 미달) 마스크 [1: 마스킹(무시), 0: 예외 발생(마스크 해제)] Bit 12 PM Precision Mask 정밀도(반올림 오류) 마스크 [1: 마스킹(무시), 0: 예외 발생(마스크 해제)] Bit 13-14 RC Rounding Control 반올림 방식 결정 (00:반올림, 01:내림, 10:올림, 11:버림) Bit 15 FTZ Flush To Zero [성능] 연산 결과가 너무 작은 수면 강제로 0으로 처리 - 관련 명령어
ldmxcsr: 메모리 값을 레지스터로 로드stmxcsr: 레지스터 값을 메모리에 저장
- 팁: 실시간 연산이 중요한 환경에서는 MXCSR의 **DAZ(Bit 6)**와 FTZ(Bit 15) 비트를 1로 설정하는 것이 좋음. 아주 미세한 소수점 이하의 수(Denormal number)를 처리하느라 CPU 사이클을 낭비하는 것을 방지하여 연산 속도를 크게 향상시킬 수 있음
- SSE와 AVX 연산의 전반적인 행동 지침을 결정하는 32비트 레지스터
-
FPU (x87) 관련 레지스터 세트
- 고전적인 x87 부동소수점 장치의 상태와 제어를 담당하는 레지스터들
레지스터명 역할 주요 특징 FPSR Status Register 현재 FPU 상태 기록. 스택 TOP 위치, 비교 결과(C0~C3), 예외 발생 여부 포함 FPCR Control Register FPU 동작 제어. 연산 정밀도(24/53/64비트) 설정 및 반올림 방식 제어 FPTW Tag Word 8개 스택 레지스터(ST0~ST7) 각각의 상태(유효함, 0임, 비어있음 등)를 표시 FPIP / FPDP Instruction/Data Ptr 마지막으로 실행된 FPU 명령어의 주소와 데이터 주소를 저장 (디버깅용)
- 고전적인 x87 부동소수점 장치의 상태와 제어를 담당하는 레지스터들
- 실수 및 벡터 레지스터는 다음과 같다.
| 이름 | 크기 | 용도 |
|---|---|---|
| XMM0 ~ XMM15 | 16바이트 (128비트) | SSE 명령어, 실수 연산, 짧은 벡터 연산 |
| YMM0 ~ YMM15 | 32바이트 (256비트) | AVX 명령어 전용 (XMM을 포함하는 확장 형태) |
| ZMM0 ~ ZMM31 | 64바이트 (512비트) | AVX-512 전용 (최신 고성능 CPU) |
- 정수/실수 연산자를 대조해서 표로 다음과 같이 요약됨
- 접미어 ss: Scalar Single (32비트 float 하나)
- 접미어 sd: Scalar Double (64비트 double 하나)
- 접미어 ps: Packed Single (128비트 안에 float 4개를 동시에 연산)
- 접미어 pd: Packed Double (128비트 안에 double 2개를 동시에 연산)
- 접두사 f: x87 FPU (80비트 하나)
| 정수 명령어 | Single (32비트/4바이트) | Double (64비트/8바이트) | Extended (80비트/10바이트) | 설명 |
|---|---|---|---|---|
mov |
movss |
movsd |
fld (Load) / fst (Store) |
값을 레지스터/메모리로 복사 |
add |
addss |
addsd |
fadd |
덧셈 |
sub |
subss |
subsd |
fsub |
뺄셈 |
mul |
mulss |
mulsd |
fmul |
곱셈 |
div |
divss |
divsd |
fdiv |
나눗셈 |
cmp |
ucomiss / comiss |
ucomisd / comisd |
fcomi |
비교 (flag 업데이트) |
- 예제
section .data f_val1 dd 1.5 ; Single f_val2 dd 2.5 d_val1 dq 1.5 ; Double d_val2 dq 2.5 section .text _start: ; --- Single Precision 연산 (float) --- movss xmm0, [f_val1] ; xmm0 = 1.5 addss xmm0, [f_val2] ; xmm0 = 1.5 + 2.5 ucomiss xmm0, [f_val1] ; xmm0와 f_val1 비교 ja .greater ; xmm0 > f_val1 이면 점프 ; --- Double Precision 연산 (double) --- movsd xmm1, [d_val1] ; xmm1 = 1.5 mulsd xmm1, [d_val2] ; xmm1 = 1.5 * 2.5
-
FPU 관련 명령어: FPU 내부에는
st0,st1, ...st7까지의 레지스터가 있으며 push를 하면 숫자가 큰 레지스터 쪽으로 밀려나고, pop을 하면st0의 값을 가져오게 됨 (레지스터에 들어가면 무조건 80비트 사이즈가 됨)fld [mem](Load): 메모리에 있는 실수(float/double) 값을 FPU 스택 top(st0)에 pushfild [mem](Integer Load): 메모리에 있는 정수를 가져와서 실수로 변환하고 FPU 스택 top(st0)에 pushfldpi [mem]/fldz [mem]: pi(3.14...)나 0.0 같은 상수를 즉시st0에 로드fst [mem](Store):st0값을 메모리에 저장 (Pop 안함)fstp [mem](Store and Pop):st0값을 메모리에 저장하고 스택에서 제거fistp [mem](Integer Store):st0값을 실수에서 정수로 변환하고 메모리에 저장한 후에 스택에서 제거- 예제 코드
section .data a dd 1.5 b dd 2.5 res dd 0.0 section .text ; 1. 변수 a를 로드 fld dword [a] ; 스택: [ 1.5 ] (st0=1.5) ; 2. 변수 b를 로드 fld dword [b] ; 스택: [ 2.5, 1.5 ] (st0=2.5, st1=1.5) ; 3. 두 수 더하기 faddp st1, st0 ; st0와 st1을 더하고 pop. 결과는 st0에 남음 ; 스택: [ 4.0 ] (st0=4.0) ; 4. 결과를 메모리에 저장하고 스택 비우기 fstp dword [res] ; res = 4.0, 스택: [ ] (Empty) ```
-
XMM 레지스터에 저장하는 명령어 (SSE)
movss: 32비트 float 1개 저장 (레지스터 하위 32비트만 사용)movsd: 64비트 double 1개 저장 (레지스터 하위 64비트만 사용)movaps: 128비트 Packed Single 이동 (Aligned), 메모리 주소가 16배수여야 함 (빠름)movups: 128비트 Packed Single 이동 (Unaligned), 주소 제한 없음 (약간 느릴 수 있음)movapd: 128비트 Packed Double 이동 (Aligned), 메모리 주소가 16배수여야 함 (빠름)movupd: 128비트 Packed Double 이동 (Unaligned), 주소 제한 없음 (약간 느릴 수 있음)movd: 32비트 정수를 일반 레지스터 <--> XMM 이동movq: 64비트 정수를 일반 레지스터 <--> XMM 이동movdqa: 128비트 정수 벡터 이동 (Aligned)movdqu: 128비트 정수 벡터 이동 (Unaligned)- ...
-
YMM 레지스터에 저장하는 명령어 (AVX)
vmovss/vmovsd: AVX 버전의 스칼라 실수 이동 (기존 SSE와 유사하나 v 접두사 사용)vmovaps: 256비트 정렬된 실수 벡터 이동 (메모리 주소가 32배수여야 함)vmovups: 256비트 정렬되지 않은 실수 벡터 이동 (주소 제한 없음)vmovdqa: 256비트 정렬된 정수 벡터 이동 (YMM 레지스터 전체(32바이트) 사용)vmovdqu: 256비트 정렬되지 않은 정수 벡터 이동 (정수 데이터 전용)- ...
-
예제
section .data float_val dd 3.14 ; 32비트 실수 double_val dq 2.71828 ; 64비트 실수 align 32 vector_val dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 ; 256비트용 section .text global _start _start: ; 1. SSE Scalar Load movss xmm0, [float_val] ; float 로드 movsd xmm1, [double_val] ; double 로드 ; 2. AVX Packed Load (Aligned) ; 메모리 주소가 32바이트로 정렬되어 있어야 vmovaps 사용 가능 vmovaps ymm0, [vector_val] ; 8개의 float을 한 번에 YMM0에 로드 ; 3. Store vmovups [rsp-32], ymm0 ; 스택에 256비트 데이터 저장 (정렬 보장 안될 때 u 사용) ; 종료 (Linux syscall) mov eax, 60 xor edi, edi syscall
-
SSE/AVX 부동소수점 산술 연산
명령어 설명 피연산자 특징 addss/subssSingle Scalar 더하기/빼기 xmm1(하위32bit) + xmm2/mem32 addsd/subsdDouble Scalar 더하기/빼기 xmm1(하위64bit) + xmm2/mem64 addps/subpsPacked Single 더하기/빼기 xmm1(128bit 전체, 4개) + xmm2/mem128 addpd/subpdPacked Double 더하기/빼기 xmm1(128bit 전체, 2개) + xmm2/mem128 mulss/divssSingle Scalar 곱하기/나누기 위와 동일, 결과는 dest에 저장 mulsd/divsdDouble Scalar 곱하기/나누기 위와 동일 sqrtss/sqrtsd제곱근 (Root) 계산 dest = sqrt(src) maxss/minss최대값/최소값 선택 dest = (dest > src) ? dest : src vaddps/vaddpdAVX 버전 벡터 더하기 ymm1 = ymm2 + ymm3/mem256 (3개 피연산자) -
SSE/AVX 정수 벡터 연산
명령어 설명 피연산자 특징 padddPacked Add Doubleword 32비트 정수 4개를 동시에 더함 paddbPacked Add Byte 8비트 정수 16개를 동시에 더함 pmaxs(q/d/w/b)Packed Max Signed 부호 있는 정수 중 큰 값 선택 (q=64, d=32, w=16, b=8 bit) pmaxu(q/d/w/b)Packed Max Unsigned 부호 없는 정수 중 큰 값 선택 pmins/pminuPacked Min 위와 동일하게 최소값 선택 -
반올림 및 정밀도 제어
roundss,roundsd,roundps,roundpd- 예제
roundss xmm1, xmm2, imm8: xmm2의 실수값을 imm8에 지정된 방식(반올림, 내림, 올림 등)으로 처리하여 xmm1에 저장합니다.- 주요 모드(imm8)
- 0: 가장 가까운 정수 (반올림)
- 1: 내림 (Floor)
- 2: 올림 (Ceiling)
- 3: 버림 (Truncate)
- 주요 모드(imm8)
-
FPU (x87) 산술 연산
명령어 설명 피연산자 특징 fadd부동소수점 더하기 fadd st0, st1 또는 fadd [mem] fiaddInteger 더하기 메모리의 정수를 가져와서 st0에 더함 fsub부동소수점 빼기 st0 = st0 - st1 fisubInteger 빼기 st0 = st0 - (int)mem fimulInteger 곱하기 st0 = st0 * (int)mem fidivInteger 나누기 st0 = st0 / (int)mem fabs절대값 (Absolute) st0의 부호 비트를 강제로 0으로 바꿈
-
SSE/AVX 스칼라 비교 (조건문용)
명령어 대상 설명 ucomissSingle xmm1과 xmm2/mem32 비교 (Unordered 비교) ucomisdDouble xmm1과 xmm2/mem64 비교 comiss/comisdS/D위와 같으나, 숫자가 아닌 값(NaN)이 있을 때 예외를 발생시킴 - 예제
ucomiss xmm0, xmm1 ; xmm0와 xmm1 비교 ja .greater ; xmm0 > xmm1 이면 점프
- 예제
-
SSE/AVX 벡터 비교 (마스크 생성용)
- 여러 개의 데이터를 한꺼번에 비교할 때 사용하며, 결과로 비트 마스크를 생성합니다. (참이면 모든 비트가 1, 거짓이면 0)
명령어 의미 설명 cmpss/cmpsdScalar 단일 값 비교 후 결과를 dest 하위에 마스크로 저장 cmpps/cmppdPacked 벡터 전체를 비교 후 각 칸에 마스크 저장 - 비교 조건(Predicate): 명령어 뒤에 숫자를 붙여 조건을 정함
- 0: Equal (==)
- 1: Less Than (<)
- 2: Less or Equal (<=)
- 3: Unordered (unord) - NaN 여부 확인
- 4: Not Equal (!=)
- 5: Greater or Equal (>=)
- 6: Greater (>)
- 7: 두 피연산자 모두 NaN
- 여러 개의 데이터를 한꺼번에 비교할 때 사용하며, 결과로 비트 마스크를 생성합니다. (참이면 모든 비트가 1, 거짓이면 0)
-
AVX 전용 비교 (
vcmpps등)- AVX에서는 3개 이상의 피연산자를 지원하며, 비교 조건을 숫자가 아닌 문자로 명시할 수 있어 훨씬 직관적임
vcmpps ymm1, ymm2, ymm3, 0: ymm2와 ymm3를 비교해서 결과를 ymm1에 마스크로 저장
-
FPU (x87) 비교 연산
- FPU 스택 내의 값을 비교
명령어 설명 fcom/fcompst0와 st1을 비교 (p가 붙으면 비교 후 pop) ficomst0와 메모리의 정수를 비교 fcomi비교 결과를 즉시 CPU 플래그(EFLAGS)에 반영 (현대적 방식) ftstst0가 0.0인지 확인 (Test)
- FPU 스택 내의 값을 비교
- 고급 언어와 어셈블리를 혼합해서 사용하는 방법은 성능 면에서 강력한 장점을 가질 수 있음
- C/C++의 경우
__asm,asm과 같은 키워드를 이용해 인라인 어셈블리 사용 가능 - 반면, Java나 C# 같은 Managed 언어의 경우 인라인 어셈블리는 지원하지 않으나 다른 우회 방법이 다 존재함
- C/C++의 경우
- 예제: C++ x86 기준
- x64 환경에서는 인라인 어셈블리를 쓸 수 없으며, 별도의 .asm 파일을 만들거나 Intrinsic 함수를 써야 함
#include <iostream>
int main() {
int a = 10, b = 20, result = 0;
// MSVC x86 인라인 어셈블리
__asm {
mov eax, a ; a의 값을 eax 레지스터로
add eax, b ; eax에 b를 더함
mov result, eax ; 결과를 result 변수에 저장
}
std::cout << "결과: " << result << std::endl;
return 0;
}
- 예제: C++ Intrinsic 함수 사용
#include <iostream>
#include <immintrin.h> // SIMD Intrinsic 헤더
int main() {
// 128비트 레지스터(XMM)에 4개의 float 값을 로드 (SSE 명령어)
// NASM의 movups와 대응
__m128 a = _mm_set_ps(4.0f, 3.0f, 2.0f, 1.0f);
__m128 b = _mm_set_ps(10.0f, 10.0f, 10.0f, 10.0f);
// 벡터 덧셈 실행 (NASM의 addps와 대응)
__m128 result = _mm_add_ps(a, b);
// 결과 확인을 위해 배열로 변환
float f[4];
_mm_storeu_ps(f, result);
printf("Result: %f %f %f %f\n", f[0], f[1], f[2], f[3]);
return 0;
}
- 예제: C# P/Invoke (외부 어셈블리 호출) 방식
- 다음 코드를
nasm -f win64 my_lib.asm -o my_lib.obj로 어셈블 하여 DLL 생성
- 다음 코드를
; 64비트 리눅스/윈도우 호출 규약 (윈도우는 RCX, RDX 사용)
section .text
global add_numbers
add_numbers:
mov rax, rcx ; 첫 번째 인자 (a)
add rax, rdx ; 두 번째 인자 (b)를 더함
ret ; 결과는 rax에 담겨 반환됨
using System;
using System.Runtime.InteropServices;
class Program
{
// DLL에서 함수 호출을 선언 (P/Invoke)
[DllImport("my_lib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern long add_numbers(long a, long b);
static void Main()
{
long result = add_numbers(15, 25);
Console.WriteLine($"어셈블리 호출 결과: {result}");
}
}
- 예제: C# Hardware Intrinsics (하드웨어 내장 함수) 방식 (추천하는 방식)
using System;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86; // x86/x64 전용 명령어 모음
class Program
{
static void Main()
{
// SSE를 사용한 4개 숫자 동시 덧셈 (SIMD)
if (Sse2.IsSupported)
{
// 4개의 32비트 정수 벡터 생성
Vector128<int> vecA = Vector128.Create(1, 2, 3, 4);
Vector128<int> vecB = Vector128.Create(10, 20, 30, 40);
// paddd (Packed Add Doubleword) 명령어와 1:1 대응
Vector128<int> resultVec = Sse2.Add(vecA, vecB);
Console.WriteLine($"벡터 덧셈 결과: {resultVec}");
}
else
{
Console.WriteLine("이 CPU는 SSE2를 지원하지 않습니다.");
}
}
}
- C#의 Hardware Intrinsics는 NASM에서 사용하는 기계어 명령어와 거의 1:1로 매핑되도록 설계되어 있음
- C# 개발자는 별도의 DLL 호출(P/Invoke) 없이도 소스 코드 내에서 직접 CPU의 특수 기능을 활용할 수 있음
-
부동소수점 산술 연산 (Floating Point)
- 이 명령어들은 System.Runtime.Intrinsics.X86 네임스페이스 아래의 Sse, Sse2, Avx 클래스에 포함되어 있음
기능 NASM 명령어 C# Intrinsic 메서드 레지스터 타입 단정밀도 덧셈 addss(Scalar)Sse.AddScalar(v1, v2)Vector128<float>배정밀도 덧셈 addsd(Scalar)Sse2.AddScalar(v1, v2)Vector128<double>벡터 단정밀도 합 addps(Packed)Sse.Add(v1, v2)Vector128<float>벡터 배정밀도 합 vaddpd(AVX)Avx.Add(v1, v2)Vector256<double>제곱근 계산 sqrtpsSse.Sqrt(v1)Vector128<float>최대값 선택 maxpsSse.Max(v1, v2)Vector128<float>
- 이 명령어들은 System.Runtime.Intrinsics.X86 네임스페이스 아래의 Sse, Sse2, Avx 클래스에 포함되어 있음
-
정수 벡터 연산 (Integer Vector)
- 정수 연산은 주로 Sse2와 Avx2 클래스에서 담당함
기능 NASM 명령어 C# Intrinsic 메서드 요소 크기 8비트 정수 합 paddbSse2.Add(v1, v2)Vector128<sbyte/byte>32비트 정수 합 padddSse2.Add(v1, v2)Vector128<int/uint>64비트 정수 합 vpaddq(AVX2)Avx2.Add(v1, v2)Vector256<long/ulong>비트 논리곱 (AND) pandSse2.And(v1, v2)공통 비트 시프트 (Left) pslldSse2.ShiftLeftLogical(v, count)int기준
- 정수 연산은 주로 Sse2와 Avx2 클래스에서 담당함
-
데이터 로드 및 이동 (Load & Move)
- 메모리에서 데이터를 가져오거나 레지스터 간에 데이터를 옮기는 작업
기능 NASM 명령어 C# Intrinsic 메서드 특징 정렬된 로드 movapsSse.LoadAlignedVector128(ptr)16/32바이트 정렬 필수 비정렬 로드 movupsSse.LoadVector128(ptr)정렬 제한 없음 스칼라 정수 이동 movd/movqSse2.ConvertToInt32(v)레지스터 --> 일반 변수 상수 벡터 생성 (여러 명령 조합) Vector128.Create(val)모든 요소 동일 값 채우기
- 메모리에서 데이터를 가져오거나 레지스터 간에 데이터를 옮기는 작업
-
실제 코드 비교 예시
-
두 개의 float 배열(4개 요소)을 더하는 작업을 NASM과 C# Intrinsics로 비교
- NASM 예시
movups xmm0, [rsi] ; 첫 번째 배열 로드 (비정렬 가능) movups xmm1, [rdi] ; 두 번째 배열 로드 addps xmm0, xmm1 ; 4개의 float 동시 덧셈 movups [rdx], xmm0 ; 결과 저장- C# Intrinsics 예시
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; // unsafe 문맥 내에서 포인터 사용 가능 public unsafe void VectorAdd(float* a, float* b, float* res) { // movups 대응 Vector128<float> v1 = Sse.LoadVector128(a); Vector128<float> v2 = Sse.LoadVector128(b); // addps 대응 Vector128<float> result = Sse.Add(v1, v2); // movups (store) 대응 Sse.Store(res, result); }