80386 마이크로코드 역어셈블리
Source: Hacker News
8086 마이크로코드 디스어셈블을 올린 뒤, Ken Shirriff가 80386 마이크로코드 ROM의 고해상도 이미지를 보내왔습니다. 몇 가지 이유 때문에 이걸 가지고 뭘 할 거라고는 생각하지 못했는데, 첫 번째는 8086(10 752 비트)과 비교했을 때 80386은 무려 94 720 비트라서 (bitract 같은 도구를 쓰더라도) 전환하고 검증하는 작업이 매우 번거로울 것이라는 점이었습니다. 두 번째 이유는 어디서 시작해야 할지 몰랐다는 점인데, 8086은 특허 덕분에 전체적인 개요와 검색할 수 있는 코드 조각들을 알 수 있었지만 80386은 완전한 블랙 박스였습니다. 무엇을 하는지는 알았고 대략적인 동작 방식도 짐작했지만, 그 정보를 거대한 바이너리 덩어리 안에서 찾을 수 있는 형태로 바꾸는 일은 불가능에 가까운 도전처럼 보였습니다.
몇 년 뒤, Discord에서 GloriousCow와 Smartest Blob(아마도 다른 사람들도)과 이야기를 나누던 중, 80386 다이의 고해상도 이미지를 얻어 마이크로코드를 추출하면 재미있겠다는 이야기가 나왔습니다. 저는 첫 번째 단계는 이미 진행됐지만 이미지를 바이너리 덩어리로, 그 바이너리 덩어리를 이해 가능한 마이크로코드로 바꾸는 일은 너무 어렵다고 했습니다. 그들은 이를 일종의 도전 과제로 받아들였고, 이미지 처리, AI, 인간이 돕는 자동화 등을 조합해 며칠 만에 이미지에서 바이너리 덩어리를 추출하고 교차 검증했습니다.
하지만 디스어셈블은 여전히 큰 도전이었습니다! 우리는 여러 패턴을 찾아내고, 점차 μ‑ops를 한 축에, μ‑op 비트를 다른 축에 배치하는 방법을 알아냈습니다. 그리고 μ‑ops를 읽어 들이는 순서(끝쪽에 사용되지 않는 μ‑ops 블록이 힌트가 됨)와 μ‑op 비트를 필드로 나누는 방법을 파악했습니다. 8086 마이크로코드 작업을 기반으로 두 개의 필드는 복사할 소스와 목적 레지스터가 될 것이라고 추정했습니다. 또한 80386은 ALU 연산을 2 사이클에 수행할 수 있다는 점에서, 두 번째 입력을 지정하는 필드가 있어야 첫 사이클에 두 피연산자를 로드하고 두 번째 사이클에 결과를 목적지에 기록할 수 있을 것이라고 생각했습니다. 또 일정한 규칙성을 보이는 패턴이 있었는데, 이는 명령어의 끝을 나타내는 것일 가능성이 높았습니다(우리는 맞았습니다).
Ken은 80386 다이 위의 여러 라인과 로직 비트를 추적해 연결 구조를 파악하도록 도와주었습니다. 점점 그림이 명확해졌고, 새로운 것을 알아낼 때마다 같은 구조를 사용하는 다른 마이크로코드 조각의 의미를 추론할 수 있었습니다. 동시에 우리는 명령어 디코더(여러 작은 PLA로 구성)와 보호 테스트 PLA를 해독하고 있었습니다. 결국 386 명령어와 마이크로코드 조각을 연결시킬 수 있게 되었고, 전체적인 그림이 크게 선명해졌습니다.
80386은 대부분의 명령어에 대해 사이클당 실행 속도가 8086보다 훨씬 빠릅니다. 이는 훨씬 많은 트랜지스터를 투입했기 때문인데, 8086에서 마이크로코드로 구현된 많은 알고리즘이 80386에서는 “하드웨어 가속” 형태로 구현됩니다. 그래서 80386 마이크로코드의 대부분은 이러한 가속기들을 설정하는 데 쓰이고, 직접 알고리즘을 구현하는 비중은 적다는 것을 초기에 깨달았습니다. 곱셈·나눗셈 하드웨어, 배럴 쉬프터, 보호 테스트 유닛 등 가속기와 마이크로코드 사이의 인터페이스를 규명하는 것이 큰 작업이었습니다.
How many different instructions does the 80386 have, according to the microcode? What are they?
마이크로코드 디코딩 ROM에는 215개의 진입점이 있습니다. 이는 8086의 60개에 비해 크게 증가한 수치입니다! 이 중 일부는 새로운 명령어이고, 나머지는 피연산자가 레지스터인지 메모리인지, CPU가 실모드인지 보호모드인지, REP 접두어가 적용되는지 등에 따라 서로 다른 루틴으로 처리되기 때문입니다. 모든 항목을 여기서 일일이 나열하지는 않겠지만, 관심이 있다면 fields.txt 파일에서 확인할 수 있습니다(하위 루틴과 공유 코드도 포함). 상위 레벨 마이크로코드 루틴의 크기를 나열하는 것은 의미가 별로 없습니다. 많은 루틴이 작은 작업만 수행하고 다른 진입점과 공유 루틴으로 점프하기 때문이죠. 또한 각 진입점이 처리하는 opcode 수를 나열하는 것도 의미가 없습니다. 명령어 디코더는 opcode 외에도 여러 정보를 사용해 어떤 루틴을 호출할지 결정합니다.
Are there any instructions not handled by the microcode?
놀랍게도 없습니다! 8086(그리고 현대 CPU)와 달리 80386은 항상 μ‑op를 실행하며, 모든 명령어에 대해 마이크로코드가 존재합니다.
Does the microcode contain any “junk code” that doesn’t do anything?
주소 0x849‑0x856(“unused?”라고 표시된) 구간은 진입점이 연결되지 않은 것으로 보입니다. 정확히 무슨 일을 하는지는 확실히 알 수 없지만, 0x8e9‑0x8f5 구간의 #PF(PAGE_FAULT) 루틴과 많은 공통점을 가지고 있습니다. 두 루틴 모두 인터럽트 0x0e를 발생시키고 오류 코드를 페이지 유닛의 마지막 오류 코드로 설정합니다. 차이점은 이 루틴이 CR2 레지스터에 페이지 유닛에서 가져온 미스테리한 값을 넣는다는 점입니다. 다른 마이크로코드들은 CPU의 문서화된 동작(또는 ICE(In‑Circuit Emulator) 하드웨어와의 상호작용을 구현하는 비공식 동작)을 구현하도록 설계되어 있습니다.
Does the microcode have any hidden features, opcodes or easter eggs that have not yet been documented?
실제 386 머신을 가지고 테스트해 본 적이 없기 때문에 확신할 수는 없지만, 보호 모드 OS가 사용자 모드 프로세스에 제한된 I/O 포트 접근 권한을 부여하기 위해 사용하던 I/O 권한 비트맵 처리에 결함을 발견했을 가능성이 있습니다. 4바이트 포트 접근이 발생할 때 마이크로코드는 첫 3바이트 주소에 대해서만 권한 비트를 검사하는 것으로 보입니다. 따라서 프로세스가 권한을 가진 I/O 포트 영역의 가장자리에서 4바이트 접근을 시도하면 마지막 바이트만 잘못 허용되어, OS가 예상하지 못한 하드웨어 레지스터에 접근할 수 있습니다. 이 버그는 매우 희귀하고, 마이크로코드 디스어셈블 없이는 발견하기 어려운 점도 이해됩니다. 40년 넘게 이런 하드웨어 버그가 unnoticed 상태였다는 점도 놀랍습니다. 다만 이 마이크로코드는 초기 80386 버전이 아닌 것으로 보이며, XBTS/IBTS 명령은 디코더에만 등장합니다.
How can I learn to understand the microcode disassembly?
Where can I download the disassembly?
디스어셈블은 x86 microcode repository on github에서 확인할 수 있습니다. 먼저 parts.txt 파일을 읽어 다른 파일들의 역할을 파악하거나, 바로 microcode_10.txt를 열어 디스어셈블을 살펴보세요.
Credits
감사합니다: Daniel Balsom (gloriouscow), Smartest Blob, nand2mario, 그리고 Ken Shirriff.
이 글은 2026년 5월 23일 토요일 오전 11시 06분에 게시되었으며, computer, hardware 카테고리에 포함됩니다.
응답은 RSS 2.0 피드를 통해 확인할 수 있습니다.