과거에는 드물었던 CPU 버그가 오늘날 흔해진 이유와, 그것이 어떻게 발생·발현되며 어떤 방식으로 완화 또는 수정될 수 있는지를 복잡성, 마이크로코드, 전압/타이밍 문제까지 포함해 설명한다.
·1/22/2026, 4:00:44 PM 개인용 컴퓨팅 초창기에는 CPU 버그가 너무 드물어서 뉴스거리가 될 정도였습니다. 악명 높은 Pentium FDIV 버그는 많은 사람이 기억하고, 그보다 더 이른 CPU들도 나름의 문제가 있었습니다(6502가 떠오르네요). 하지만 요즘은 너무 흔해져서 Firefox 사용자가 보내는 크래시 리포트를 분류(triage)하다 보면 정기적으로 마주칩니다. CPU라는 물건의 특성상 이런 버그가 어떻게 생기고, 어떻게 나타나며, 무엇을 할 수 있고 무엇은 할 수 없는지 궁금할 수 있습니다. 🧵 1/31
이 모든 문제의 근본 원인은 결국 하나입니다. 바로 복잡성입니다. 현대 코어는 너무 복잡해져서 설계 시점에 “모든 가능한 조건에서 신뢰성 있게 동작한다”는 것을 증명하는 게 불가능하고, 철저한 테스트 역시 현실적으로 불가능합니다. 계속 증가하는 논리 복잡성 외에도, 동작 조건 자체도 변했습니다. 고정 전압과 고정 주파수는 이제 과거의 유물이 되었고, 이는 물리 설계를 더 어렵게 만듭니다. 2/31
먼저 논리(로직) 버그부터 시작해 봅시다. 아시다시피 CPU에는 어느 정도의 ‘보이는 상태(visible state)’가 있습니다. 데이터가 들어 있고 명령어에 의해 조작되는 레지스터 집합, 실행 중인 명령어의 주소를 가리키는(보유하는) 명령어 포인터(instruction pointer), 그리고 예컨대 부동소수점 연산의 반올림 방식을 바꾸는 것처럼 코어의 동작을 바꾸는 특수 레지스터들이죠. 3/31
집적 CPU 초기에는 이 상태가 사용자에게 보일 뿐만 아니라 코어 내부의 물리적 실체와도 일치했습니다. 레지스터는 코어 내부 SRAM 뱅크의 실제 엔트리에 대응했고, 명령어 포인터는 매 사이클 읽혀 메모리에서 명령어를 가져오는 물리 레지스터였습니다. 오늘날의 CPU에서 이런 것들은 그저 추상화일 뿐이고, 그 아래의 물리적 현실은 극적으로 더 복잡합니다. 4/31
현대 CPU는 추적해야 하는 상태(state)가 엄청나게 많습니다. 어느 순간이든 수백 개의 명령어가 ‘비행 중(in-flight)’일 수 있고, 각 명령어는 물리 레지스터에서 동작하는데, 이는 ISA의 레지스터를 매우 큰 뱅크에 있는 여분의 물리 슬롯에 즉시(Just-in-time) 매핑하는 메커니즘을 통해 할당됩니다. 각 명령어는 오랫동안 전적으로 추측(speculative) 상태인 데이터 집합과 연관되며, 여기에는 명령어 자체도 포함됩니다. 5/31
CPU는 분기 예측에 따라 실행해야 할 수도, 아닐 수도 있는 명령어 스트림을 가져오고 실행하면서 명령어 주소, 피연산자, 의존성을 추적합니다. 잘못 예측한 경우(misprediction) “틀린” 명령어들의 상태는 버려야 합니다. 마찬가지로 명령어 타이밍도 더 이상 예측 가능하지 않습니다. 메모리 접근은 몇 사이클부터 수백 사이클까지 걸릴 수 있고, 코어 안팎의 서로 다른 구조를 통해 데이터를 가져옵니다. 6/31
명령어 폴트(예외)도 예측할 수 없습니다. 보호된 메모리 영역에 접근해 메모리 접근이 실패하면 명령어 흐름을 멈춰야 하고, 폴트가 발생한 명령어 이전에 “일어나야 했던” 모든 것을 되돌린 뒤, 그런 상황을 처리하기 위해 운영체제가 제공한 코드를 실행하도록 코어를 유도해야 합니다. 그 결과 실행이 정확히 그 지점에서, 완벽히 순차적인 방식으로 멈춘 것처럼 보이게 해야 하죠. 7/31
그리고 성능만을 위해 CPU가 들고 다니는 막대한 양의 ‘숨은 상태(hidden state)’도 있습니다. 물리→메모리 주소 변환은 메모리에 저장된 테이블을 통해 이뤄지지만, 이 데이터는 코어 내부의 TLB(translation lookaside buffer)에 캐시되어야 합니다. 캐시 라인은 여러 코어가 공유할 수 있으며, 상태를 추적해야 합니다. 어떤 코어가 소유(owner)하고 있나? 공유(shared) 상태인가? 로컬에서 더티(dirty)한 데이터라 원격에서 가져와야 하나? 8/31
모든 단일 명령어의 실행은 이 방대한 상태의 상당 부분을 바꿀 수 있으며, 그 변경은 신뢰성 있게 이뤄져야 합니다. 하지만 앞서 말했듯 가능한 모든 조합을 테스트하는 건 불가능하고, 어떤 특정 시퀀스는 불일치하거나 손상된 상태로 이어질 수 있습니다. 그리고 그것은 결국 소프트웨어 버그처럼 드러납니다. 9/31
제가 실제로 마주친 예를 몇 가지 들면: 함수 호출에서 리턴할 때 스택에서 명령어 포인터를 가져오는데 값이 잘못된 것처럼 보입니다. 아마도 잘못된 명령어 포인터가 명령어 페치 파이프라인으로 전달된 탓일 수 있습니다: https://bugzilla.mozilla.org/show_bug.cgi?id=1746270 10/31
코드는 메모리에서 어떤 데이터를 로드할 것으로 기대했지만, 로드/스토어 유닛이 이전 페치에서의 오래된(stale) 데이터를 반환했습니다: https://bugzilla.mozilla.org/show_bug.cgi?id=1687914 11/31
명령어에 연관된 명령어 포인터가 손상되어, 겉보기에는 현재 실행 중인 명령어가 아닌 것처럼 보입니다. 로드가 스토어 예외를 유발한다거나, 점프가 접근 예외를 유발한다는 점에서 알 수 있습니다: https://bugzilla.mozilla.org/show_bug.cgi?id=1820832 12/31
다른 버그는 훨씬 더 나쁜 영향을 줄 수도 있습니다. 예컨대 AMD의 악명 높은 Barcelona TLB 버그는 코어를 복구 불가능한 상태로 만들었고, 사실상 실행을 멈춰 버리게 했습니다: https://arstechnica.com/gadgets/2007/12/linux-patch-sheds-light-on-amds-tlb-errata/ 13/31
이 모든 경우에서 유력한 원인은, 드물게 발생하는 사건 시퀀스—예: 빠른 연속 인터럽트나 컨텍스트 스위치, 특정 실행 모드에서 빠져나오거나 진입하는 동안 특정 명령어들의 타이밍—가 일어날 때 내부 CPU 상태를 추적하는 장치에 버그가 있는 것입니다. 이는 “특정 시점에 특정 조건을 체크하는 걸 빼먹었다” 같은 흔한 소프트웨어 버그가 아닙니다. 대부분의 시간엔 중요하지 않다가, 딱 그 한 번 중요해지는 종류죠. 14/31
상대적으로 최근 CPU의 에라타(errata)를 읽어보면, 알려진 모든 이슈에 비슷한 표현이 붙는 걸 볼 수 있습니다: “복잡한 마이크로아키텍처 조건에서(Under complex microarchitectural conditions...)”. 하드웨어식 표현으로는 “우리가 예상하지 못한 상태에 도달할 수 있었다”는 뜻입니다. 직접 이런 문서를 찾아보세요. 예를 들어 이런 문서에서요: https://edc.intel.com/content/www/us/en/secure/design/confidential/products-and-solutions/processors-and-chipsets/tiger-lake/11th-generation-intel-core-processor-family-specification-update/errata-details/ 15/31
이제 이런 종류의 버그를 사후에 고칠 수 있느냐가 궁금할 수 있습니다. 결론부터 말하면, 될 때도 있고 안 될 때도 있습니다. CPU는 순수한 하드코딩 덩어리가 아니고, 동작의 일부를 마이크로코드(microcode)에 의존합니다. 전통적으로 마이크로코드는 외부 명령어를 실행하기 위해 CPU가 내부적으로 수행하던 명령어 집합이었습니다. 요즘은 대부분 그렇지 않고, 현대 마이크로코드는 복잡한 명령어 구현뿐 아니라 상당한 양의 설정(configuration)도 함께 제공합니다. 16/31
예를 들어 마이크로코드는 특정 회로를 비활성화하는 데 사용될 수 있습니다. ‘루프 버퍼(loop buffer)’ 같은 구조를 떠올려 보세요. 디코드된 명령어를 캡처해 루프에서 다시 실행하며 명령어 페치를 우회하는 구조입니다. 이것이 버그로 판명되면 마이크로코드 업데이트로 이를 완전히 꺼버릴 수 있고, 안정성을 위해 최적화를 희생하는 셈이 됩니다. 17/31
새 코어를 구현할 때는 새로운 구조, 특히 더 공격적인 성능 기능을 마이크로코드로 비활성화할 수 있게 만들어 두는 것이 흔합니다. 그러면 설계 팀은 기능이 충분히 신뢰할 만하다고 입증될 때만 출시하거나, 다음 이터레이션으로 미룰 유연성을 갖게 됩니다. 18/31
마이크로코드는 데이터 레이스로 인해 발생하는 조건을 우회하기 위해, 특정 조건에서 파이프라인에 버블(bubble)을 주입하는 데도 사용될 수 있습니다. 연달아 수행되는 두 연산의 실행이 문제를 일으키는 것으로 알려져 있다면, 두 번째 연산의 실행을 한 사이클 지연시키는 방식으로 피할 수 있을지도 모릅니다. 다시 말해 성능을 안정성과 맞바꾸는 것이죠. 19/31
하지만 모든 버그를 이런 방식으로 고칠 수는 없습니다. 크리티컬 패스(critical path)에 놓인 로직 버그는 대개 수정이 어렵습니다. 또한 어떤 마이크로코드 수정은 부팅 시점, 즉 CPU 초기화 직후에 마이크로코드를 로드해야만 동작하는 경우도 있습니다. 운영체제가 업데이트된 마이크로코드를 로드하면 코어의 동작을 재구성하기엔 너무 늦을 수 있으며, 어떤 수정은 UEFI 펌웨어 업데이트가 필요합니다. 20/31
하지만 이건 로직 버그 이야기일 뿐이고, 안타깝게도 요즘은 그것보다 더 많은 문제가 있습니다. Intel 1세대 Raptor Lake CPU를 둘러싼 논란을 따라갔다면, 겉보기엔 랜덤한 실패가 일어나는 문제들이 있었다는 걸 알 겁니다. 이런 버그는 특정 조건에서 코어에 잘못된 전압이 공급되었고, 그 결과 특정 회로 내에서 레이스 컨디션이 발생해 잘못된 결과가 나오는 경우가 많았습니다. 21/31
이를 이해하려면 다음을 기억해야 합니다. CPU가 동작할 수 있는 최대 주파수는 파이프라인 스테이지를 구성하는 회로들에서 ‘가장 긴 경로(longest path)’에 의해 결정됩니다. 배선을 따라 신호가 전파되고 트랜지스터가 켜지고 꺼지는 데는 시간이 걸립니다. 그리고 현대 회로 설계는 엄격히 동기식이기 때문에, 모든 신호는 한 클록 사이클이 끝나기 전에 스테이지의 끝까지 도달해야 합니다. 22/31
클록 사이클이 끝나면, 해당 파이프라인 스테이지의 결과 신호는 파이프라인 레지스터에 저장됩니다. 이는 사용자에게 보이지 않는 저장 요소로, 파이프라인 스테이지들을 분리합니다. 그래서 예를 들어 어떤 스테이지가 두 숫자를 더한다면, 파이프라인 레지스터는 그 덧셈 결과를 보관합니다. 다음 사이클에서 이 결과는 다음 파이프라인 스테이지를 구성하는 회로들에 입력됩니다. 앞서 말한 덧셈 결과가 예컨대 주소라면, 캐시에 접근하는 데 쓰일 수도 있겠죠. 23/31
회로에서 신호가 전파되는 속도는 인가되는 전압의 크기에 비례합니다. 예전 CPU에서는 전압이 고정이었지만, 현대 CPU에서는 전력을 절약하기 위해 초당 수천 번씩 전압이 바뀝니다. 특정 클록 주파수에 필요한 만큼만(‘딱 충분한’ 만큼) 전압을 제공하면 전력 소모를 크게 줄일 수 있지만, 전압이 너무 낮으면 신호가 늦게 도착하거나 잘못된 신호가 파이프라인 레지스터에 저장되어 연쇄적인 실패가 발생할 수 있습니다. 24/31
Raptor Lake의 경우, 저와 다른 사람들이 공통적으로 관찰한 매우 흔한 패턴은 가끔 잘못된 8비트 값이 나온다는 것입니다. 이는 AH나 AL 같은 8비트 레지스터를 읽을 때 발생하는데, 이런 레지스터는 더 큰 정수 레지스터의 일부(slice)일 뿐이라 전용 물리 저장소가 없습니다. 일반 레지스터의 하위 16비트에서 상위/하위 8비트를 뽑아내는 연산은 보통 멀티플렉서(MUX)로 수행됩니다. 25/31
MUX는 두 개의 8-와이어 입력 묶음과, 어떤 입력을 출력으로 보낼지 선택하는 1개의 선택 신호 와이어, 그리고 8-와이어 출력 묶음으로 이루어진 회로입니다. 선택 신호 값에 따라 한쪽 입력 또는 다른 쪽 입력이 출력으로 나갑니다. 그런데 선택 신호가 너무 늦게 도착한다면—예를 들어 클록 사이클이 끝난 직후에 도착한다면—무슨 일이 벌어질까요? 출력에 잘못된 비트 묶음이 나오게 됩니다. 26/31
Raptor Lake CPU에서 정확히 이 일이 벌어진다고 확신할 수는 없습니다. 그저 하나의 가설일 뿐입니다. 하지만 현대 CPU 코어에는 이런 유형의 회로가 수백만, 수천만 개나 있고, 그중 어느 하나에서 타이밍 문제가 생겨도 이런 문제로 이어질 수 있습니다. 게다가 코어 전체에 전압을 전달하는 일은 극도로 아날로그적인 문제로, 전압 변동은 실행되는 명령어, 온도 등 온갖 이벤트로 인해 발생할 수 있습니다… 27/31
또 Raptor Lake CPU 문제는 시간이 지날수록 더 악화된다는 점도 기억할 겁니다. 이는 회로가 열화(degrade)되기 때문이고, 잘못된 전압을 인가하면 더 빨리 열화될 수 있습니다. 회로 열화는 그 자체로 연구 분야지만, 그 영향은 대체로 비슷합니다. 배선 저항은 증가하고, 트렌치 커패시터(trench capacitor)의 용량은 감소하는 등… 이런 변화가 합쳐지면 회로는 느려지고, 같은 주파수로 동작하려면 더 높은 전압이 필요해집니다. 28/31
CPU가 출하될 때는 가장 성능이 중요한 회로들이 이런 영향을 보상할 수 있도록 일정한 타이밍 여유(timing slack)를 갖도록 설계되어 있어야 합니다. 시간이 지남에 따라 이 타이밍 여유는 줄어듭니다. CPU가 이미 한계에 가깝게 동작하고 있다면, 노화(aging)로 인해 이 여유가 0이 되어 코어가 지속적으로 실패하게 될 수 있습니다. 29/31
그리고 관련 변수는 정말 많습니다. 타이밍은 대체로 트랜지스터 크기와 배선 저항에 좌우됩니다. 전압이 높으면 트랜지스터 성능은 좋아지지만 전력 소모가 늘고 따라서 온도가 올라갑니다. 온도가 올라가면 저항이 증가해 배선에서의 전파 속도가 떨어집니다. 최적의 전력 소비, 충분한 성능, 신뢰성 사이에서 동적 평형을 유지하는 것은 매우 섬세한 춤과 같습니다. 30/31
결국 현대 CPU는 엄청난 복잡성을 지닌 괴물이고, 버그는 피할 수 없는 존재가 되었습니다. 업계가 이런 문제를 해결하는 데 더 많은 자원을 투입해, 사용자에게 출하되기 전에 설계와 테스트를 개선했으면 좋겠지만, 유감스럽게도 기술 업계의 대부분은 사용자가 큰돈을 주고 사는 하드웨어가 제대로 동작하도록 보장하는 것보다, 신뢰하기 어려운 통계적 장난감으로 노는 데 더 관심이 있어 보입니다. 31/31
보너스: 스레드 마지막 글 이런 버그를 마주쳤을 때 하드웨어 설계자들에게 너무 가혹하지 않았으면 합니다. 그들은 점점 더 복잡한 것들을, 점점 더 촉박한 마감 속에서, 자신들이 하는 일을 거의 이해하지 못하는 상부 경영진 아래에서 만들어야 합니다. 이런 버그의 책임은 마땅히 책임질 곳에 두세요. 품질 좋은 제품을 만들 시간, 사람, 자원을 충분히 배정하지 않은 임원들입니다.