mir.pe (일반/밝은 화면)
최근 수정 시각 : 2024-11-03 16:05:43

x86


명령어 집합
CISC AMD64 x86 · M68K · 68xx · Z80 · 8080 · MOS 65xx · VAX
RISC AArch64 ARM · RISC-V · MIPS · DEC Alpha · POWER PowerPC · CELL-BE
LoongArch · OpenRISC · PA-RISC · SPARC · Blackfin · SuperH · AVR32 AVR
VLIW
EPIC
E2K · IA-64 · Crusoe

1. 개요2. 역사
2.1. 16비트: IA-16(x86-16)
2.1.1. 레지스터2.1.2. 주소 지정2.1.3. 80286
2.2. 32비트: IA-32(x86-32)
2.2.1. 버전2.2.2. 레지스터
2.3. 64비트: AMD64(x86-64)
2.3.1. 레벨2.3.2. X86S(구 x86-S)2.3.3. APX
3. 확장4. 명령어 목록5. ABI
5.1. 호출 규약
6. 문제점
6.1. 중구난방으로 확장된 명령어 세트6.2. 부족한 레지스터
7. 기타8. 관련 문서

1. 개요

인텔1978년에 출시한 인텔 8086에 적용된 16비트 아키텍처이자, 그 호환 프로세서와 후속작들( 32비트, 64비트)을 이르는 말이다. 1978년에 출시되어 40년이 지난 굉장히 오래된 아키텍처이지만, 이후에 출시된 프로세서들은 8086의 명령어 세트를 기반으로 하여 확장된 것이다. 이러한 이유로 32비트 CPU는 x32라고 표기하지 않고 x86이라고 표기한다.

' x64'와 대비하여 '32비트 아키텍처'의 관습적 명칭으로도 쓰인다. 일반인이 설명서에서 본 것은 보통 이 의미다. 그러나 아키텍처인 x86을 IBM PC 호환기종라고 부르는 것에는 문제가 있는데 이 관련해서는 해당 문서에 더 자세한 설명이 있다.

파일:newsroom-x86-ecosystem-advisory-group.webp
32비트와 64비트 시절에는 소위 윈텔로 불리는 인텔과 마이크로소프트 주도 아래 설계가 되었으나 파편화를 막기 위해 2024년 부터 AMD, 구글, 오라클, 메타 등 다른 대기업과 리누스 토발즈, 팀 스위니 등 전문가가 추가로 참여하는 x86 자문위원회를 구성해 개발 중 이다.

2. 역사

1970년 출시된 Computer Terminal Corporation(CTC)사의 Datapoint 2200이 기원이다. 이는 인텔 8080을 비롯하여 인텔의 명령어 집합과 레지스터 구조에 많은 영향을 주었다.

1976년 인텔은 32비트 프로세서인 iAPX 432가 개발되는 동안 경쟁사인 모토롤라, 자일로그 등에 맞서기 위해 기존의 성공작인 8080과 호환이 가능한 16비트 프로세서인 인텔 8086의 개발을 시작하게 된다.

1978년 출시된 인텔 8086으로 x86의 역사가 시작된다. 당시 8086은 너무 비싸기도 했지만 기존에 널리 사용되던 8비트 주변 장치와 호환성을 맞추기가 어려웠다. 그래서 원가절감형으로 8088이 출시되었고 이는 IBM의 개인용 컴퓨터의 프로세서로 쓰이면서 대박을 치게 된다. 그 후 80186/80188로 확장되었다.

1980년 부동소수점 연산을 위한 보조 프로세서인 인텔 8087이 출시된다.

1982년 인텔 80286이 출시된다. 실제 모드와 보호 모드가 추가되며 실제 모드에서는 이전 프로세서와 같은 환경을 제공했으나 보호 모드에서는 호환성을 일부 포기한 대신 메모리 보호와 24비트의 넓은 주소 공간을 지원했다. 명령어 세트 자체는 80186에 메모리 보호와 멀티태스킹을 위한 명령어들을 추가한 것이 전부지만 내부 구조가 개선되어 같은 클럭이어도 두 배는 빠르게 작동했다.

1985년 인텔 80386이 출시되었고 32비트 시대의 문이 열렸다. 명령어 세트와 내부 구조가 32비트로 확장되었고 더 많은 명령어를 제공했다. 메모리도 32비트로 확장되었다. i386으로 불린다.

1989년 인텔 80486이 출시되었다. 더 많은 명령어와 부동소수점 연산 장치(x87)의 통합이 이루어졌다. 이를 i486이라 한다.

1993년 펜티엄 1 출시와 함께 i586으로 업데이트 된다. 1994년 인텔 펜티엄 프로 출시와 함께 i686이 등장하는데, 펜티엄 2가 출시하며 개인용으로 사용할 수 있게 되는건 1997년 부터다.

1997년 인텔에서 정수 SIMD(벡터) 명령어 집합인 MMX를 지원하는 프로세서를 출시하였다. MMX 확장은 기존 x87 부동소수점 유닛의 80비트 레지스터의 하위 64비트를 활용한다.

1998년 경쟁사인 AMD에서 MMX를 확장한 부동소수점 SIMD 명령어 집합인 3DNow!를 지원하는 프로세서를 출시하였다.

1999년 인텔에서 새로운 SIMD 명령어 집합인 SSE를 발표한다. SSE 확장은 새로운 128비트 SIMD 레지스터를 사용하며 2000년 발표된 SSE2를 통해 MMX를 대체하게 된다.

1999년 AMD에서 x86의 64비트 확장인 AMD64를 발표한다. AMD64의 전체 사양은 2000년 8월에 공개되었다. 2003년 AMD64를 지원하는 옵테론이 출시되고 이어서 2004년에 인텔이 프레스캇으로 EM64T를 지원하면서 AMD64는 사실상 x86 계열 아키텍처의 표준이 된다.

2005년 인텔에서 가상화 기술인 VT-x를 발표하고 이어서 2006년 AMD에서 VT-x와 호환되지 않는 독자적인 가상화 기술인 AMD-V를 발표한다.

2007년 AMD에서 SSE5 확장을 발표되나 실제 구현되지는 않고 일부 명령어가 FMA, F16C, XOP 등 별도의 확장으로 구현되게 된다.

2008년 인텔에서 256비트 SIMD 명령어 집합인 AVX 및 FMA를 발표한다. 2011년 AVX를 지원하는 인텔 샌디 브릿지 프로세서 및 AMD 불도저 프로세서가 출시되었다. FMA는 3-operand 형식의 FMA3과 4-operand 형식의 FMA4로 나뉘는데, 2013년 인텔 하스웰 프로세서에서 FMA3이 AVX의 후속 버전인 AVX2와 함께 도입된 이후로 현재는 인텔과 AMD 모두 FMA3을 채택한 상태이다.

2015년 인텔에서 AVX 명령어를 512비트로 확장하고 여러 명령어 및 마스크 레지스터 등을 추가한 AVX-512를 발표하나, 여러 이유로 일부 프로그램에서만 제한적으로 사용하고 있다.

2017년 인텔의 x86 명령어 독점 라이센스[1]가 사실상 만료되었고 이때 ARM이 PC시장 진출을 발표함.

2023년 인텔에서 16비트 및 32비트 레거시 모드를 삭제하고 64비트 모드 및 링 3에 한해 32비트 호환 모드만 남긴 X86S를 제안하였다.

2023년 인텔에서 범용 레지스터(R16-R31)를 추가하고 정수 명령어를 보강한 APX 확장 및 기존 AVX/AVX-512 확장을 계승하는 AVX10 확장을 발표하였다.

2.1. 16비트: IA-16(x86-16)

8086/8088, 80186, 80286에 적용된 초기 16-bit 아키텍처. 8086은 8800(iAPX 432) 프로세서가 개발되는 동안 경쟁사의 제품에 대응하기 위한 제품이었기 때문에 추후 확장을 고려하지 않고, 어셈블리 수준에서 8080용 코드의 하위 호환이 가능하도록 설계되었다.

2.1.1. 레지스터

범용 레지스터
세그먼트 레지스터 (16-bit)
특수 레지스터
x86-16은 4개의 비트 범용 레지스터(AX, CX, DX, BX)를 가지고 있다. 이 레지스터들은 상위 8비트와 하위 8비트로 분할하여 사용할 수 있다.(AL, CL, DL, BL, AH, CH, DH, BH) 어떤 명령에서든지 제약없이 사용 가능하다. 하지만 각자 특수한 기능들을 가지고 있어 완전한 범용 레지스터라고 하기 힘들다. CX 레지스터는 LOOP 계열 명령어에서 남은 횟수를 기록하기 위해 사용되며, DX는 AX 레지스터의 확장, 보조 용도 또는 포트 번호로 사용될 수 있고 BX 레지스터는 주소용으로 사용된다. AX 레지스터는 즉시 연산(소스 레지스터나 메모리를 지정할 필요 없이 바로 뒤에 필요한 값이 뒤따라온다.)등에 사용되기 때문에 실질적으로는 범용 레지스터와 특수 목적 레지스터의 중간에 위치하는 레지스터이다.

주소용 인덱스 레지스터로 SI와 DI 두 개의 16비트 레지스터를 가지고 있다. 범용 레지스터 보다는 특수 목적 레지스터에 가깝다. 보통 주소를 담고 있으며 문자열 연산 명령어에 사용된다. 스택 포인터로 SP와 BP를 가지고 있다. 완전한 특수 목적 레지스터로 사용된다. 현재 스택의 꼭대기와 스택 프레임을 담고 있기 때문에 일반적인 용도로(계산 등) 사용할 수는 있지만 설계 목적상 실제 사용은 하지 않는다. 함수에서 지역 변수 공간을 만들기 위해 스택 포인터의 값을 증가 또는 감소시키거나 베이스 포인터는 이전의 스택 프레임의 위치를 기억하기 위해 PUSH, POP 하기는 한다.

20비트의 주소 지정을 위한 ES, CS, SS, DS 4개의 16비트 세그먼트 레지스터를 가지고 있다. 완전한 특수 목적 레지스터이며 연산은 물론이고 이동조차 자유롭지 못한다. 주소 지정에 대해서는 주소 지정 문단을 참고

프로세서의 상태나 연산 결과의 상태를 기록하기 위해 플래그 레지스터를 가지고 있다. 하위 8비트는 8080에서 사용되는 레지스터와 동일하며 상위 8비트로 새로 확장되었다.
각주 [ 펼치기 · 접기 ]

[1] x86-32bit [A*] 8080의 'A' 레지스터에 대응 [B*] 8080의 'B' 레지스터에 대응 [C*] 8080의 'C' 레지스터에 대응 [D*] 8080의 'D' 레지스터에 대응 [E*] 8080의 'E' 레지스터에 대응 [H*] 8080의 'H' 레지스터에 대응 [L*] 8080의 'L' 레지스터에 대응 [SP*] 8080의 'SP' 레지스터에 대응 [PC*] 8080의 'PC' 레지스터에 대응 [*] 하위 8비트는 8080의 플래그 레지스터와 동일 [O] Overflow Flag, 오버플로 플래그. 연산 결과가 연산 범위를 초과하였을 때 켜지는 플래그이다. 음수 플래그와는 다르다. 이 플래그는 덧셈을 할 때 연산 전의 결과가 양수고 연산 후의 결과가 음수일 때 켜진다. 또는 뺄셈을 할때 위와 반대 경우면 켜진다. 대신 0이면 켜지지 않는다. [D] Direction Flag, 방향 플래그. 문자열 연산(명령어 뒤에 S가 붙는 명령어들, 예를 들어 MOVS 같은 것)은 연산후에 주소값이 자동으로 더해지는데(더해지는 값은 사용하는 자료형의 바이트의 크기를 따른다.) 이 방향을 정하는 비트다. 1이면 역방향으로 더해진다. [I] Interrupt Mask Bit, 인터럽트 무시 비트. 외부 인터럽트를 받을지 말지 결정하는 플래그이다. 0이면 외부 인터럽트를 받지 않는다. 다만 무시 불가 인터럽트(NMI)까지 거부할 수 없다. [T] Trap Flag, 트랩 플래그. 이 플래그가 켜지면 명령어가 실행될 때마다 인터럽트가 걸린다. 디버거에서 주로 사용된다. [S] Sign Flag, 음수 플래그. 연산 결과가 음수일 때 켜진다. 오버플로 플래그와 다른 점은 연산 전의 값은 상관없다는 것이다. [Z] Zero Flag, 제로 플래그. 연산 결과가 0일 때 켜진다. 비교 후 결과가 서로 같음을 기록하는데 쓰인다. [A] Auxiliary Carry Flag, 보조 캐리(올림) 플래그. 8비트 중에서 하위 4비트의 올림을 기록하는 데 사용된다. 정확한 용도는 두 값의 연산 후에 BCD 결과로 보정하는 용도로 쓰인다. x86은 BCD 연산을 지원하지 않기 때문에 일단 계산하고 결과를 원래 BCD 연산의 결과에 맞게 보정한다. 8비트 연산에서만 효과가 있고 16비트 이상의 연산에서는 하위 8비트의 결과만 보정해 준다. [P] Parity Flag, 패리티 플래그. 8비트 연산 결과의 1의 개수가 짝수거나 0개면 켜진다. 예를 들어 0xb7, 10110111이 결과일 경우 켜지지 않지만 1이 증가하면 1의 개수가 짝수가 되므로 켜진다. [C] Carry Flag, 캐리(올림) 플래그. 올림 또는 빌림이 발생하면 켜진다. ADC, SBB 등의 올림(빌림)을 같이 더해 주는 연산을 해서 16비트라는 적은 크기의 자료형을 32, 64비트로 확장할 수 있다.)

2.1.2. 주소 지정

x86은 20비트의 주소 범위를 지원하며 최대 1 MB의 메모리에 접근할 수 있다. 하지만 x86-16은 레지스터의 최대 크기가 16비트 이므로 세그먼트 레지스터를 이용하여 16비트보다 많은 메모리에 접근할 수 있도록 했다. 이를 세그먼테이션이라고 하며 작동 원리는 다음과 같다.

먼저 세그먼트 레지스터를 16으로 곱하거나 왼쪽으로 4비트 시프팅한다. 즉 16진수 기준으로 한자리수가 올라가며 0x0b000 는 0xb0000이 된다. 이 값에 레지스터 또는 포인터의 값을 더한다. 만약 명령어 포인터의 값이 0x13ff라면 0xb13ff다. 이 주소값을 이용해 메모리에 접근한다.

세그먼테이션은 콜 스택의 데이터 입출력을 깔끔하게 했다는 장점이 있다. 스택은 현재 프로세서의 워드 단위를 따라야 하는데 이는 스택이라는 공간은 항상 정렬되어 있기 때문이다. 지역 변수는 스택에 할당되는데 1바이트의 변수를 선언하더라도 스택 포인터는 항상 2바이트(32비트 시스템에서는 4바이트) 단위로 증가하거나 감소한다. 만약 레지스터가 16비트가 아니고 20비트였다면 16비트 단위를 벗어나기 때문에 사용이 힘들었을 것이다.

하지만 이는 프로그래밍을 어렵게 했는데 포인터 관리가 힘들었기 때문이다. 16비트로는 20비트에 주소에 접근할 수 없으니 현재 16비트 주소로 바로 접근이 안 되는 코드나 데이터는 세그먼트 레지스터 값을 변경해야 했다. 바로 이 때문에 Near와 Far라는 구분이 생기게 된다.

2.1.3. 80286

80286에서는 메모리 관리(가상 메모리), 권한 수준(privilege level)을 통한 보호 메커니즘 등 멀티태스킹 운영체제를 위한 기능이 추가되었는데, 이에 따라 일부 코드의 동작이 변화하였다. 인텔은 이에 따라 프로세서의 동작 모드를 기존 8086/8088용 프로그램을 수정 없이 그대로 실행할 수 있는 '실제 모드'와 상기한 신기능을 지원하는 '보호 모드' 두 가지로 나누었다.

2.2. 32비트: IA-32(x86-32)

일반적으로 말하는 32비트 PC 아키텍처로, 인텔 80386 및 호환 프로세서부터 지원하며, 인텔 넷버스트 마이크로아키텍처 AMD K7 마이크로아키텍처를 마지막으로 더 이상 사용되지 않는다. 이후 나오는 아키텍처부터는 후술할 AMD64로 넘어가게 된다. 그러나 AMD64 역시 이 IA-32의 확장형이라 그 영향력이 아직까지 이어진다고 볼 수 있다.

레지스터는 32비트로 확장되었고 앞에 'E'가 붙는다. 32비트가 기본 연산 단위가 되었고 32비트의 주소 범위를 지원한다.(16비트 단위로 사용하려면 특수한 접두사를 이용해야 한다.)

x86의 역사에서 가장 중요한 버전 중 하나이며 후에 64비트까지 영향을 끼치게 된다.

2.2.1. 버전

총 4개의 버전이 나왔는데, i686 이후엔 SSE,SSE2 같은 SIMD 명령어도 나오나 그리 세부적으로 나누어지지 않았다. 일반적인 프로그램들은 보통 호환성을 위해 최초 버전인 i386을 타겟으로 잡고 컴파일하나 고성능을 요구하는 게임들은 MMX 등 필요한 명령어를 명시 하는 방법으로 i686을 요구하기도 했다. 또한 여러 리눅스들은 배포판 마다. i386, i586, i686 등을 붙여서 호환성과 성능 사이에서 선택하도록 했다.

현재는 성능문제를 떠나 보안 문제 때문에 구형 버전들을 드롭시키고 있다. 가령 리눅스를 봐도 최초 버전은 i386을 타겟으로 만들었으나, 커널 3.8 부터 지원을 중단한다.

2.2.2. 레지스터

16비트 ISA인 IA-16의 레지스터의 경우 범용 레지스터의 크기는 16비트였지만 8비트 단위로 상위비트 하위비트로 나뉘어 (0xFF00, 0x00FF) 개별적으로 접근하는것 또한 가능하였으나 32비트 부터는 한개의 레지스터만 제공된다.
접두사는 E이며 AX레지스터는 EAX인 식이며 기능은 동일하다.

범용 레지스터 (32-bit)
세그먼트 레지스터 (16-bit)
특수 레지스터

2.3. 64비트: AMD64(x86-64)

파일:상세 내용 아이콘.svg   자세한 내용은 AMD64 문서
번 문단을
부분을
참고하십시오.

파일:CC-white.svg 이 문단의 내용 중 전체 또는 일부는
문서의 r184
, 번 문단
에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문단의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r184 ( 이전 역사)
문서의 r ( 이전 역사)

AMD64는 AMD가 1999년에 발표한 x86의 64비트 확장 아키텍처[21]로, 현시점인 2020년대 기준으로 절대다수의 CPU가 채택하고 있는 아키텍처이다. 표준 명칭은 AMD64이지만 x86-64, x64, EM64T, Intel64 등 여러 이름으로도 불린다. 주요 변경점은 다음과 같다:
[21] 2022년 라이센스 만료 [22] AMD64 Architecture Programmer’s Manual: Volumes 1-5 | 40332 § 5.1 Page Translation Overview & § 2.2.1 Memory Addressing

2.3.1. 레벨

AMD에서 AMD64를 만들었지만 시장점유율은 인텔이 훨씬 앞섰고, 따라서 제안되는 명령어 채택율도 인텔이 크게 앞서면서 사실상 인텔이 주도해서 만드는 명령어가 되고 있다. AMD는 SSE5 같은 자신만의 명령어 셋이나 컴파일러 개발에 힘 썼지만 x86-64-v4이후 이러한 노력을 접고 인텔에 맞춰 가고 있다. 오히려 인텔이 2020년대 초반부터 중반까지 E코어 이슈로 개인용 CPU들을 AVX-512가 제거된 x86-64-v3을 시장에 내놔서 AMD보다 자사표준에 밀리는 일도 있었다.

자신의 CPU가 x86-64-v 몇 레벨 확인 하기 위해선 리눅스에선 #이 방법을 윈도우는 # 이 프로그램을 쓰면된다.

2.3.2. X86S(구 x86-S)

X86 Simplified
현 AMD64 아키텍처는 하위 호환을 위해 16비트 및 32비트 레거시 동작 모드들을 유지하고, 새로운 모드를 사용하기 위해서는 이전 모드를 거치도록 설계되어 있어 AMD64 프로세서를 64비트 모드로 사용하기 위해서는 전원을 켰을 때 기본적으로 진입하는 16비트 모드에서 32비트 모드를 거쳐 64비트 모드로 진입해야 한다. 2023년 인텔이 제안한 X86S는 더이상 사용되지 않는 16비트와 32비트 네이티브 (정확하게는 실제 모드와 보호 모드)지원을 삭제하고 처음부터 64비트 모드에서 동작하는 방식이다. 지금까지의 프로세서들은 메인보드 펌웨어가 UEFI여도 CSM을 통해 DOS나 32비트 운영체제를 실행하는 것이 가능하지만 x86S에서는 프로세서가 명령어를 아예 지원하지 않으므로 64비트 운영체제만 사용이 가능하게 된다.

삭제되는 모드들은 다음과 같다:
다만 32비트 지원 삭제는 링 0의 모드에서만 삭제되는 것으로 링 0에서 돌아가는 운영체제는 64비트 코드만 동작할 수 있으며 링 3에서 동작하는 유저랜드 32비트 프로그램들은 호환 모드를 사용해 그대로 사용이 가능하다. 다만 종래 롱 모드의 호환 모드는 16비트 어드레싱도 가능하였으나 X86S에서는 32비트 지원만 남는다.

2024년 현재 아직 시장에 나온제품은 없지만 벌서 1.2까지 개정되었다. 또한 첫 발표 당시 이름인 x86-S에서 2023년 11월 x86S 1.1을 발표하며 그냥 x86S로 바꾸었다.

2.3.3. APX

인텔이 2023년 제안한 AMD64 확장 표준. 표준 AMD64는 16개의 범용 레지스터를 가지고 있는데, 해당 확장을 지원하는 경우 16개의 범용 레지스터(R16-R31)가 추가돼 총 32개의 범용 레지스터를 사용할 수 있고, 일부 레거시 명령어에 New Data Destination 및 No Flags 옵션이 추가되었다.

EVEX 인코딩 또는 REX 접두사를 확장한 2바이트 크기(0xD5 + Payload)의 REX2 명령어 인코딩을 사용한다. AMD64의 메모리 점프는 32비트 immediate만 지원하고 있어 64비트의 어드레스 공간을 직접 점프하는것이 불가능해 값을 범용 레지스터에 먼저 올리고 레지스터의 값을 인덱스 포인터로 옮기는 indirect jump만 가능하였으나, APX의 REX2는 imm64를 직접 사용해 점프하는것이 가능해 졌다.

또한 2개의 레지스터를 동시에 스택에 PUSH 또는 POP할 수 있는 PUSH2, POP2 명령어 및 CMP, TEST 명령어를 조건부로 실행하는 CCMP, CTEST 명령어 등이 추가되었다.

3. 확장

파일:상세 내용 아이콘.svg   자세한 내용은 x86/확장 문서
번 문단을
부분을
참고하십시오.
<rowcolor=#fff> x86 · AMD64 확장 명령어 집합
인텔 주도 확장 명령어
범용 APX
SIMD MMX · SSE SSE2 · SSE3 · SSSE3 · SSE4.1 · SSE4.2 · AVX AVX2 · AVX-512 · AVX10 · AMX
AVX-512: F · CD · DQ · BW · VL · IFMA · VBMI · VBMI2 · VNNI · VAES · GFNI · BITALG
AVX[1]: AVX-VNNI · AVX-IFMA
AVX10: AVX10.1 · AVX10.2
비트 조작 BMI1 · BMI2 · ADX
보안 및 암호 AES-NI · CLMUL · RDRAND · RDSEED · SHA · MPX · SGX · TME · MKTME
가상화 및 기타 VT-x(VMX) · SMX · TSX
AMD 주도 확장 명령어
SIMD 및 비트 연산 3DNow! PREFETCHW · F16C · XOP · FMA FMA4 · FMA3
비트 조작 ABM
보안 및 암호 SME
가상화 및 기타 AMD-V

[1] 512-bit EVEX 인코딩된 AVX-512 명령어의 256-bit VEX 인코딩 버전

4. 명령어 목록

파일:상세 내용 아이콘.svg   자세한 내용은 x86/명령어 목록 문서
번 문단을
부분을
참고하십시오.

5. ABI

5.1. 호출 규약

아키텍처 호출 규약 인자 (parameters) 반환값
(return value)
Caller-saved
Registers
Callee-saved
Registers
Stack
Cleanup
비고
레지스터 스택 방향
16-bit cdecl (스택만 사용) High-to-Low AX(, DX)
32-bit Microsoft (Windows 환경)
cdecl (스택만 사용) High-to-Low EAX(, EDX)
[EAX][23]
ST(0)[x87]
EAX, ECX, EDX EBX, ESI, EDI
ESP, EBP
Caller
stdcall Callee
fastcall ECX, EDX Callee
thiscall (C++) ECX Callee [25]
vectorcall[SSE2] ECX, EDX, XMM0-5[27] EAX(, EDX)
[EAX]
XMM0(~XMM3)[28]
Callee
GNU (Linux 환경)
cdecl (스택만 사용) High-to-Low EAX(, EDX)
[EAX][29]
ST(0)[x87]
EAX, ECX, EDX EBX, ESI, EDI
ESP, EBP
Caller
thiscall (C++) [31]

용어 설명:
[예시]

다음과 같은 caller와 callee가 있다고 하자:
#!syntax cpp
int callee(int x, int y) {
    return x * y;
}

int caller(int x, int y) {
    return callee(x + y, x - y);
} 
cdecl과 같이 스택을 통해 함수 인자를 전달하는 경우 다음과 같이 호출이 이뤄진다:
caller(int,int):
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR [ebp+8]
        mov     edx, DWORD PTR [ebp+12]
        mov     ecx, eax
        sub     ecx, edx
        push    ecx
        add     eax, edx
        push    eax
        call    callee(int,int)
      ; (...) 
fastcall과 같이 일부(인자 수에 따라 전체) 인자를 레지스터를 통해 전달하는 경우 다음과 같이 호출이 이뤄진다:
caller(int,int):
        push    ebp
        mov     ebp, esp
        mov     eax, edx
        mov     edx, ecx
        sub     edx, eax
        add     ecx, eax
        call    callee(int,int)
      ; (...) 
cdecl과 같이 스택 정리를 caller에서 하는 경우 callee는 다음과 같이 값을 반환한다:
callee(int,int):
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR [ebp+8]
        imul    eax, DWORD PTR [ebp+12]
        pop     ebp
        ret 
한편 stdcall과 같이 스택 정리를 callee에서 하는 경우 callee는 다음과 같이 값을 반환하며 스택을 정리한다:
callee(int,int):
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR [ebp+8]
        imul    eax, DWORD PTR [ebp+12]
        pop     ebp
        ret     8
이때 caller는 스택 정리 없이 바로 return할 수 있다:
caller(int,int):
      ; (...)
        call    callee(int,int)
        mov     esp, ebp
        pop     ebp
        ret 
(예시에서는 인자 전달 외에는 스택을 사용하지 않으므로 다음과 같이 mov esp, ebp를 생략할 수 있다:)
caller(int,int):
      ; (...)
        call    callee(int,int)
        pop     ebp
        ret 
cdecl과 같이 caller에 stack 정리의 책임이 있는 경우 호출 직후 add 명령어를 사용해 인자 전달에 사용한 스택을 정리해 주어야 한다:
caller(int,int):
      ; (...)
        call    callee(int,int)
        add     esp, 8
      ; (...) 


[23] 반환 자료형의 크기가 큰 경우 스택에 공간을 할당하여 값을 저장하고 그 포인터를 EAX 레지스터에 반환함 [x87] float, double [25] C++ 클래스의 static이 아닌 멤버 함수에 사용됨. 함수의 첫 번째 인자는 this 포인터로, ECX 레지스터를 통해 전달되고 나머지 인자는 스택을 통해 전달됨. [SSE2] SSE2 또는 그 이상 지원되는 경우만 사용 가능 [27] 부동소수점(float, double) 또는 SIMD를 포함하는 '벡터' 인자 [28] 부동소수점(float, double) 또는 SIMD 등 벡터 값. 나머지 자료형의 경우 fastcall과 동일 [29] 반환 자료형의 크기가 큰 경우 스택에 공간을 할당하여 값을 저장하고 그 포인터를 EAX 레지스터에 반환함 [x87] [31] C++ 클래스의 static이 아닌 멤버 함수에 사용됨. 함수의 첫 번째 인자는 this 포인터로, 다른 인자와 마찬가지로 스택을 통해 전달됨

6. 문제점

전반적으로 마구잡이로 확장되고 쓸모없이 전력과 공간만 잡아먹는 낡은 레거시 등이 문제시 되고 있다.

그래서 위에 서술된 X86S 등에서 어느정도 정리정돈을 목표로 작업중이다.

6.1. 중구난방으로 확장된 명령어 세트

8080의 구조를 최대한 옮겨오면서도 16비트 명령어를 추가하려다 보니 8086 때부터도 복잡한 명령어가 많았다. 특히 아이태니엄이 실패하기 전까지 인텔은 꾸준히 x86을 포기하려고 시도하였기 때문에 AVX 이전까지 명령어 추가가 추후 확장을 고려하지 않고 무분별하게 이뤄져 명령어의 인코딩이 매우 복잡해졌다. 이런 명령어들은 고속화에 악영향을 준다. AX(AL)을 16비트 상수값 포인터가 가리키는 메모리 주소로 쓰거나 읽는 명령어가 있었다. 또 비슷한 기능을 하는 ADD, SUB 등 9개의 명령어가 있다. 이는 다른 명령어를 사용하는 것보다 고작 1바이트만 줄였을 뿐이다. AH 레지스터는 이용하지 못한다. 심지어 반복문을 처리하는 명령어(LOOP, LOOPZ, LOOPNZ)도 있을 정도였다.

가장 쓸데없던 명령어는 PUSH, POP, INC, DEC (레지스터 피연산자) 명령어로 다른 명령어와 고작 1바이트 차이였을 뿐 하는 기능은 완전히 같았다. 얼마나 쓸모없었는지 INC는 64비트 모드에서는 버려졌다. 왜 문제되냐 하면 확장성에 문제가 된다. 단독으로 32개의 1바이트 opcode를 잡아먹는다. 이제는 남은 공간이 얼마 없어 추가적인 명령어 확장은 POP CS 명령을 이용해서 확장한다. x86 디버거를 사용해서 바이너리를 읽어보면 대부분의 명령어가 0x0F로 시작하는 경우가 많은데 이는 사용하지 않는 명령어의 값을 접두사로 사용하는 것이다. AVX의 경우 거의 쓰이지 않아 64비트 모드 사양에서 비활성화된 특수한 레거시 명령어[32]를 사용하여 인코딩한다. 그래서 디버거가 오래되었을 경우 AVX 명령이 다른 명령어로 해석되지만 실행은 정상적으로 이루어지는 기괴한 상황이 발생한다. 다른 RISC 계열 프로세서는 고정된 크기의 명령어를 읽고 해석만 하면 되지만 x86은 이러한 복잡한 디코딩 과정을 전부 거쳐야만 한다.

1978년 당시에는 전반적인 CPU 성능과 메모리 크기가 열악했기 때문에 명령어 크기의 1바이트 차이가 큰 영향을 주었을지도 모른다. 특히 메모리 크기가 영향을 매우 심하게 줬다. 지금은 이해가 안 될지도 모르지만, 당시에는 64KB도 굉장히 큰 거였다. MS-DOS의 베이스 메모리 640KB가 이것 때문에 나왔으며, 1MB급 메모리만 해도 2020년 후반부터 시작된 이른바 그래픽카드 대란이 장난으로 보일 정도로 비쌌다. 애초에 2000년대를 앞두고 터진 이른바 밀레니엄 버그 역시 이것이 원인 중 하나다.[33] 하지만 64비트 시스템이 보편화된 오늘날에는 더 이상 1바이트의 차이가 성능을 결정짓지 않는다.

6.2. 부족한 레지스터

x86(IA-32)의 경우 8개의 레지스터가 제공되지만 스택 포인터(sp/esp, bp/ebp) 등 용도가 정해진 레지스터를 제외하면 자유롭게 사용할 수 있는 레지스터가 4개밖에 없다. 이로 인해 범용 레지스터 수가 많은 RISC 아키텍처 대비 더 많은 메모리 접근을 필요로 하기 때문에 속도상의 손해가 작지 않다.

현대 x86 CPU에서는 캐시 메모리 도입, Store-to-Load Forwarding 등으로 유효 메모리 레이턴시를 개선했지만 여전히 레지스터를 활용하는 경우에 비해 다소 느리다. (레이턴시가 가장 빠른 L1 캐시에 접근하는 경우에도 아키텍처에 따라 3-5사이클이 소요된다!)

AMD64에서는 이걸 염두에 뒀는지 범용 레지스터 수를 8개에서 16개로 늘렸다. (다만 기존 x86 명령어 인코딩과의 호환을 위해 새로 추가된 r8~r15 레지스터에 접근하는 경우 1바이트 접두사를 추가로 요구한다. 또한 AMD64를 처음 발표한 1999년 기준으론 좋은 선택이었으나, 2020년대 기준으론 부족하기 마찬가지다[34].) 그래서 2023년 인텔이 제안한 APX 확장의 경우 범용 레지스터 수를 추가로 늘려 총 32개의 범용 레지스터를 제공한다.

7. 기타

왜 이런 명령어 셋이 따로 있는가 하면 초기의 x86(8086~80386)에는 부동소수점 연산 유닛(FPU)이 없었고 정수형 ALU만 달려 있었기 때문이다. 필요시 별도로 부동소수점 연산 보조 프로세서인 x87을 설치하도록 되어 있는 설계였는데, 당시엔 FPU의 하드웨어 비용이 꽤 비싼데 비해 빠른 부동소수점 연산 기능은 과학기술 및 그래픽 쪽 용도가 아니면 그다지 필요가 없었기 때문에[35] x86 CPU의 단가를 낮추기 위해서 별도로 뺀 것이다. FPU가 없어도 소프트웨어적으로 느리고 정밀도가 떨어지지만 부동소수점 연산은 가능했으므로 크게 불편한 점은 없었다. 특정 응용 프로그램을 제외하면 자주 쓰는 기능도 아니었기 때문에, 당시에도 대부분의 일반사용자는 x87 보조 프로세서 장착 없이도 문제없이 잘만 사용했다.[36] 심지어 대부분의 사용자가 x87이라는 것의 존재를 모를 정도. 80286이면 80287, 80386이면 80387로 짝이 있었는데, 80386+80287처럼 하위 버전의 x87 보조 프로세서를 사용하는 것도 가능했다. 이 시기에는 서드파티 제조사에서도 x87 호환 명령어 집합을 사용하는 자체적인 보조 프로세서를 제작하기도 했다.
FPU가 x86 CPU 내부로 들어온 것은 80486DX부터였다. x87의 소형화에 성공하여 CPU 유닛에 집적이 가능해졌지만, 그 부작용으로 안 그래도 비쌌던 x86 프로세서의 가격이 확 뛰긴 했다. 그래서 보조 프로세서를 달지 않은 486SX와 그와 짝이 되는 487SX도 출시는 했지만, 의외로 시장에서는 그리 환영받지 못하고 다들 486DX를 구매했다. 아마도 386 시절에 SX와 DX의 차이로 받아들인 소비자들이 DX를 선호했기 때문이 아닐까 추측된다. 487SX는 순수한 FPU가 아닌 CPU까지 포함된 486DX의 재포장 버전이었다. 486SX 없이 단독 사용은 불가능하지만 486SX의 CPU를 끄고 모든 명령을 자기가 다 처리했다. 게다가 같은 클럭의 486SX와 486DX는 성능 차이가 그리 크지 않았다.

8. 관련 문서


[32] VEX 인코딩 [33] 이것은 MS-DOS 6.0 이전 기준으로 yyyy-mm-dd 라는 형식으로 연도 날짜를 정하는 기준이면 당시 시스템으로는 메모리에서 그 날짜 데이터를 다 받아들이지 못했다. 그래서 메모리를 아끼기 위해 yy-mm-dd라는 형태로 한 것. [34] 대표적인 예로 2011년 10월에 발표되어 ARM최초로 64비트를 지원하는 ARMv8-A가 31개의 범용 레지스터를 지원한다. [35] 실제로는 3D 관련 기술용으로 많이 쓰였다. 대표적으로 AutoCAD와 3DS MAX [36] AutoCAD와 3DS MAX는 코프로세서라 불리던 x87이 없어도 실행은 되었다. 물론 x87 없이 업무용으로 사용 가능한 수준의 성능은 얻을 수 없었다.