컴파일러나 어셈블러를 사용하지 않고 AI 에이전트에게 기계어(machine code)를 직접 작성하도록 시도하는 것을 2026년 10월에 여러 번 진행했습니다. 작은 루틴 과제와 ISA 조합 8가지(x86-64에서 6개, AArch64에서 2개)를 각각 2회씩, 실행이나 시뮬레이션 없이 한 번에 작성하도록 했을 때, Claude Haiku 4.5는 16번 중 1번만 정답했습니다. 같은 과제와 같은 프롬프트로, Claude Haiku 5.5는 16번 중 13번을 정답했습니다. 상위 모델인 Sonnet 5.5나 Opus 5.5는 Haiku 5.5가 놓친 AArch64에서도 더욱 확실하게 작성했습니다.
기계어를 작성할 수 있는지 여부보다 더 궁금했던 것은 그 판정(judgment)이었습니다. 에이전트는 제출 시 자신감도(confidence score)를 보고했지만, 확인한 범위 내에서는 자신감도가 합격/불합격을 구분하지 못했습니다. 불합격이었던 2건은 각각 20%와 5%였고, 합격했던 건에도 10%나 30%가 있었습니다. 생성된 본인의 설명은 정확성의 근거가 될 수 없습니다.
이 시도에서 판정에 사용된 것이 CAPH TECH에서 개발한 Mukoz입니다. Mukoz는 바이너리가 작성된 계약(contract)을 충족하는지 소스 코드 없이 검사합니다. 여기서는 Mukoz가 어떤 도구인지, 시도에서 무엇이 관찰되었는지, 그리고 무엇을 보여주지 못했는지 순서대로 설명하겠습니다.
Mukoz는 무엇을 검사하는가
Mukoz의 입력은 실행 가능한 아티팩트(바이너리 또는 진입점을 명시한 기계어 루틴)와 세 가지 TOML 파일로 구성됩니다.
contract.toml: 무엇이 성립해야 하는지 (입력, 결과, requires/ensures, 허용되는 부작용, 종료)
binding.toml: 계약을 바이너리에 어떻게 대응시킬지 (진입점, 레지스터, 메모리 영역, 파일)
suite.toml: 어떤 케이스를 실행할지 (경계값, 랜덤 케이스, 상한, 실행기)
...
계약(contract)과 바인딩(binding)을 분리한 것이 설계의 핵심입니다. 계약은
입니다. Mukoz가 내놓는 것은 증거이며, 출하해도 되는지 판단은 사람이 하는, 이라는 선을 포맷에 심어 넣고 있습니다.
설계 원칙 중, 이 기사와 관련된 것은 다음 하나입니다.
생성자와 판별자를 동일시하지 않는다. AI의 자체 신고를 합격 조건으로 삼지 않는다.
서두에서 작성한 자신도에 대한 관찰은 이 원칙을 뒷받침하는 작은 예였습니다.
최근 활용 사례
Mukoz는 바이너리가 무엇으로 만들어졌는지 묻지 않습니다. 컴파일러로든, 사람의 손으로 직접 쓰든, 기계어를 직접 쓰는 AI 에이전트로든 같은 계약으로 판별합니다. 시도를 통해 구체적으로 보이게 된 활용 사례는 네 가지입니다.
에이전트가 빌드한 바이너리 검사. 에이전트가 C나 어셈블리로 작성하고 툴체인이 빌드한, 출하될 그 자체를 검사합니다. 생성 루프의 시도에서는, 6가지 결함이 심어진 C의 to-do 프로그램을 Claude Sonnet의 서브 에이전트가 수정했습니다. 사용해도 되는 것은 작업 디렉토리와 Mukoz의 출력뿐이라고 지시했습니다(디렉토리 외부를 읽고 있지는 않은지 검증하지는 않았습니다). 8번의 검사(그중 6번은 편집 후)로 REJECT에서 ACCEPT_WITHIN_SCOPE에 도달했고, 차이점은 정확히 6가지 결함을 고치는 것이었습니다.
소스가 없는 바이너리 수정. 레거시나 서드파티의 바이너리에 패치를 적용하여, 계약을 계속 충족하고 있는지, 오래된 반례가 고쳐졌는지를 확인합니다. gcc -O3 -funroll-all-loops로 생성한 base64 루틴(512바이트)의 1바이트를 수정한 과제에서, 에이전트가 깨진 속성의 ID(char2)와 반례의 입출력으로부터 '홀수 그룹의 세 번째 글자만 틀린다'고 좁혀내고, 전개된 두 개의 그룹의 마스크를 비교하여 오류 바이트를 찾았습니다(에이전트 자체 보고에 의한 것. 검사는 2회).
호스트에서 작동하지 않는 ISA 코드 검사. x86-64 호스트에서 AArch64 루틴이나 프로세스를 에뮬레이터로 검사할 수 있습니다. 시도에서는 이것이 가장 명확한 장점이었습니다. Mukoz가 없는 조건의 에이전트는, AArch64 코드를 확인하기 위해 자체 시뮬레이터를 작성하고 있었습니다. 그 경우, 인코더와 시뮬레이터가 같은 오독을 공유할 수 있다는 약점을 에이전트 자신이 보고했습니다.
저수준 코드 평가에서의 채점. 계약을 한 번 쓰면, 어떤 제출물도 같은 계약으로 채점할 수 있습니다. 생성자가 채점자가 되는 일은 없습니다.
반대로 안 되는 것도 명확합니다. 정당성의 증명, 의도적으로 숨겨진 백도어(특정 64비트 값에서만 오작동하는 코드 등)의 발견, 부동 소수점 코드, 상수 시간성과 같은 타이밍 검사, OS나 GUI 애플리케이션 전체입니다.
시도의 설계
과제는 작은 루틴(memmove, 16진/base64 인코딩, UTF-8 검증 및 UTF-16으로의 변환, 네 개의 변환기를 하나의 입구로 모은 codec 등)과, Linux 프로세스로 작동하는 to-do 커맨드라인 프로그램입니다. 에이전트는 Claude의 서브 에이전트로 다음 중 어느 조건에서 작업했습니다.
| 조건 | 에이전트가 할 수 있는 것 |
|---|
A | Mukoz로 자신의 바이너리를 검사하고, 반례를 읽는다 (큰 규모의 시도에서는 20~40회까지) |
B | 원하는 방법으로 테스트한다. x86-64는 실행할 수 있지만, AArch64는 자체 시뮬레이터가 필요 |
Z | 실행도 시뮬레이션도 하지 않는다. 한 번에 작성한다 (바이트를 배열하는 데 Python은 사용 가능) |
대부분의 시도에서, 어셈블러와 역어셈블러를 사용하지 않도록 지시했습니다(지시일 뿐 강제는 아닙니다). 에이전트는 명령을 손으로 인코딩하고, Python으로 바이트 배열을 조립합니다. 제출 후에는, Mukoz와 코드를 공유하지 않는 숨겨진 오라클이 판별했습니다. 루틴은 독립적인 참조 구현과 2000~3000의 랜덤 케이스로 비교했고, to-do 프로그램은 300개의 랜덤 커맨드 세션을 사양의 Python 모델과 비교했습니다. 각 과제를 사용하기 전에, 참조 구현이 오라클을 통과하고, 변이체의 대부분이 떨어지는 것도 확인했습니다.
관찰된 것
모델의 세대에 따라 풍경이 바뀌었다
서두의 결과를 표로 나타내면 다음과 같습니다. 태스크는 memmove, hex_encode, utf8_count, base64, utf8_to_utf16, codec (x86-64)와, utf8_to_utf16, codec (AArch64)이며, 모두 조건 Z입니다.
⚠️ [IMG:N] 형식 토큰은 이미지 placeholder 입니다. 번역하지 말고 원래 위치에 그대로 유지하세요.
| Haiku 4.5 | Haiku 5.5 | |
|---|
| x86-64(12회) | 1회 합격 | 12회 합격 |
| ... | | |
이 표와는 별개로, 그 이전에 시도했던 (36건) 불합격 사례 중 Haiku 4.5 제출물 28건을 역어셈블하여 주원인을 분류했을 때, 13건은 명령어 인코딩 오류였습니다(REX.R과 REX.B 혼동, SIB의 인덱스 오류, ModRM 방향 반전, 미해결 점프 변위 등). 부호 비교에서 0x80과 비교하여 ASCII 바이트를 시작 바이트로 오인한 경우가 4건 있었습니다. 분류는 저희의 해석이며, 다른 사람이 읽으면 일부가 다르게 분류할 수도 있습니다. 조건 A | | |
검사 기록에서도 Haiku 4.5에서 합격하지 못한 327회의 검사 중 259회가 영역 외 접근 또는 잘못된 결과를 포함하고 있었습니다. Haiku 4.5 루틴에서 Mukoz가 포착한 것들 중 다수는 저희 분류상 알고리즘보다 낮은 계층의 오류였습니다.
반면, Haiku 5.5의 실패는 모두 AArch64였습니다. 유효 입력을 거부한 경우, 허용 영역을 읽은 경우, 제출에 이르지 못한 경우가 각각 1건씩 있었습니다.
다만, Haiku 5.5가 상위 모델을 따라잡았다고 할 수는 없습니다. 위 16회에 포함된 utf8_to_utf16과 codec은 Sonnet 5.5와 Opus 5.5에서도 동일한 조건 Z로 ISA별로 각 2회씩 작성하게 했습니다(실행 시기는 다르며, 프롬프트가 완전히 동일했을 것이라고는 할 수 없습니다). 이 공통 부분만 비교하면 다음과 같습니다.
| 한 번에 작성 (utf8_to_utf16과 codec) | x86-64 (4회) | AArch64 (4회) | 합계 |
|---|
| Haiku 4.5 | 0회 합격 | 0회 합격 | 0/8 |
| ... | | | |
차이가 난 것은 AArch64였습니다. Sonnet 5.5는 4회 모두, Opus 5.5는 4회 중 3회는 올바르게 작성했지만, Haiku 5.5는 1회였습니다. Opus 5.5는 utf8_count와 utf8_to_utf16_fast를 더한 16회 전체에서도 15회 합격했습니다. 유일하게 불합격한 경우는 AArch64의 utf8_to_utf16에서, 사로게이트 범위 체크 위치를 잘못하여 ED A0 80을 수락하고 그 바로 아래의 유효한 ED 9F BF를 거부했기 때문이었습니다.
더 큰 to-do 프로그램 과제(Haiku 5.5에서는 미실시)에서는 Sonnet 5.5와 Haiku 4.5에서 결과가 명확히 갈렸습니다. Claude Sonnet 5.5는 우선순위, 검색, 임포트를 가진 to-do 프로그램을 (gcc의 참조 구현으로 3435바이트) 한 번도 실행하지 않고 세 번 모두 올바르게 작성했습니다. 손수 작성한 인코딩 제출물은 29693619바이트입니다. Haiku 4.5는 그보다 기능이 적은 기본 to-do 프로그램을 18회 시도했으나, 모두 9608바이트의 스텁에서 멈췄습니다. 잘못 작성한 것이 아니라, 끝까지 작성하지 못한 것입니다. 모듈로 분할해도 main을 작성하지 못하니 변함이 없었습니다.
검사된 실행은 한 번에 작성하는 것보다 좋았다. 다만 Mukoz가 우월한 것은 아니다
Haiku 5.5는 AArch64에서 이러한 프로그램을 작성할 수 있지만, Sonnet 5.5나 Opus 5.5만큼 확실하게 작성하지는 못하는 중간 단계에 있습니다. 그래서 AArch64의 utf8_to_utf16과 codec으로 과제와 조건별로 4회씩(조건당 총 8회) 실행했습니다.
| 조건 | utf8_to_utf16 | codec | 합계 |
|---|
A : Mukoz | 4/4 | 4/4 | 8/8 |
B : 자체 시뮬레이터 | 4/4 | 4/4 | 8/8 |
Z : 한 번에 작성 | 1/4 | 1/4 | 2/8 |
A의 8회 중 3회는 첫 검사가 REJECT로, 수정 후 수락되었습니다. 에이전트 보고에 따르면, 수정한 내용은 범위 상수 오류와 인코딩을 잘못한 즉치값, bl로 덮어쓴 리턴 주소와 1바이트 초과, 반전된 길이 체크였습니다.
다만 B
모든 8회에서 합격했습니다. 어떤 에이전트도 AArch64 디코더와 인터프리터를 Python으로 작성했으며, 자신의 테스트가 오류를 포착할 수 있는지 확인하는 폴트 인젝션(fault injection)까지 작성한 경우도 있었습니다. 관찰된 것은 '검사 수단을 갖춘 실행이 한 번에 작성하는 것보다 좋았다'는 것이었지, 'Mukoz가 다른 방법보다 우수했다'는 것은 아니었습니다. 공유 도구가 절약할 수 있는 비용은 각 에이전트가 작성했을 시뮬레이터의 수고로움이지만, 그 비용은 측정하지 않았습니다.
Mukoz와 숨겨진 오라클은 일치했다
기록된 모든 최종 판정에서 Mukoz는 오라클이 놓친 제출물을 수용하지 않았습니다. 첫 번째 시도에서는 36건의 모든 최종 제출물에서 일치했고, 더 큰 규모의 시도에서는 63회의 판정 모두에서 일치했습니다. Haiku 5.5의 AArch64 비교 시도에서 판정한 30건(폐기된 실행 포함)의 내역은 수용하여 합격이 23건, 거부하여 불합격이 6건, HOLD (에뮬레이터가 미지원 명령)으로 불합격이 1건입니다.
그럼에도 불구하고, 판정의 강도는 스위트(suite)의 강도를 능가하지 못했습니다. 특정 to-do 변이체를 노출시킨 것은 생성된 1570케이스 중 단 한 케이스뿐이었습니다. 일치했다는 관찰은 보장되지 않습니다.
직접 생성된 바이너리 형태
후술할 토큰 비교와 그 이전에 얻은 Opus 5.5의 제출물 21건(x86-64, C에서 4건, 컴파일러를 사용하지 않고 17건)을 역어셈블하자 컴파일러 출력과는 다른 특징이 나타났습니다. 컴파일러를 사용하지 않고 작성된 17개의 루틴에는 call이 하나도 없었습니다. 17건 중 15건은 callee-saved 레지스터를 전혀 사용하지 않았으며, ABI 규칙을 '준수'하기보다는 '회피'하고 있었습니다. 컴파일러를 사용하지 않고 작성된 codec의 5건은 모두 64바이트와 256바이트의 base64 테이블을 가지고 있었습니다.
요청 문구 한 줄만으로 설계가 바뀌었습니다. Sonnet 5.5에 우선순위 없음, '최대한 빠르게', '최대한 작게'(모두 정확성이 전제)의 세 가지 방식으로 작성하게 하자, 12건 모두 합격했고 크기는 다음과 같았습니다.
| utf8_to_utf16 | codec | |
|---|
| 우선순위 없음 | 394〜418바이트 | 1551〜1675바이트 |
| ... | | |
속도 우선의 4건은 모두 ASCII 연속 부분을 SSE2로 일괄 변환하고 있었습니다. 다른 8건과 앞서 언급된 21건 역시 SIMD를 사용하지 않았습니다. 작게 만들기 우선 실행은 lodsb나 stosw 같은 문자열 명령과 짧은 점프를 사용하여 base64 알파벳을 테이블이 아닌 계산으로 구했습니다. Mukoz는 속도를 검사하지 않기 때문에, 속도는 별도로 측정했습니다. 한 대의 기계에서 측정한 범위에서는, 우선순위 없음 실행에 비해 속도 우선 실행은 ASCII 입력에서는 빠르지만 멀티바이트 문자가 많은 입력에서는 대체로 느려졌습니다.
토큰의 대부분은 사고 과정에 사용되었다
Claude Opus 5.5를 사용하여 동일한 루틴을 C, 어셈블리, 손으로 작성된 인코딩, 16진수 직접 쓰기로 작성하게 하고 작업에 소모된 토큰을 비교했습니다(각 2회). x86-64의 utf8_to_utf16의 경우, 평균적으로 C가 8.4k, 어셈블리가 11.0k, 손으로 작성된 인코딩이 20.7k 토큰이었습니다. 주어진 빌드 스크립트 외에 프로그램을 작성하거나 실행하지 않고, 16진수를 직접 쓰는 조건은 저렴해지지 않아 각각 59.0k와 23.4k였습니다.
추정컨대, 작업 토큰의 7496%가 사고 과정에 사용되었고, 소스나 16진수를 포함한 도구 호출에 대한 작성은 14k 토큰이었습니다(끝까지 완료된 손으로 작성된 인코딩 codec의 1회만 9k. 이 실행은 중간부터 Opus 4.8로 이어지고 있습니다). 이 추정이 맞다면, 바이트열 표현을 밀집시켜도 큰 절약은 되지 않습니다.
보여주지 못한 것
이 시도의 출발점 질문은 'Mukoz를 사용하면 에이전트는 올바른 바이너리에 확실히 도달할 수 있는가'였습니다. 답은, 확인되지 않았습니다. 규모가 큰 시도에서는 과제가 '작성할 수는 있지만 틀리는' 영역에 들어가지 못했습니다. Sonnet에는 너무 작아 한 번에 정확하게 작성되었고, Haiku 4.5에는 너무 커서 다 쓰지 못했습니다. Haiku 5.5의 AArch64 시도에서 처음 그 영역에 들어갔지만, 거기에서도 자체 시뮬레이터와 차이가 나지 않았습니다.
시행 운영에서도 문제가 있었습니다. x86-64 수기 인코딩의 codec(Opus 5.5)에서는 안전 분류기가 응답을 중단하는 경우가 있었습니다. 트랜스크립트가 남아있는 네 번 모두에서 중단되었고, 세 번은 다른 모델로 계속 진행했으며, 한 번은 제출 없이 끝났습니다. 다른 트랜스크립트에서는 거부된 사례를 찾지 못했고, 무엇이 방아쇠인지 알 수 없습니다. 또한, Haiku 5.5의 AArch64 시도에서 처음에 시작한 A 조건의 여덟 번은 에뮬레이터가 mukoz 옆에 설치되기 전에 시작되었기 때문에 모두 폐기하고 다시 했습니다.
가능성으로서: 일회용 소프트웨어
최근 용도는 소스 코드가 없는 바이너리 수정이나, 에이전트가 빌드한 결과물 검사입니다. 그 너머의 가능성으로, 에이전트가 필요할 때 필요한 컴포넌트를 바이너리로 생성하고 사용 후 버리는 형태도 생각할 수 있습니다. 더 나아가면, 소프트웨어 전체를 현장 환경에 맞춰 생성하는 경우도 있을 수 있습니다. 유지보수되는 결과물이 아니라, 환경 속에서 필요에 따라 나타나는 현상으로서의 소프트웨어입니다.
그 세계에서 남는 것은 바이트나 소스 코드가 아니라, 계약(contract)과 그것이 축적한 반례(counterexample)일 것입니다. Mukoz의 계약은 ISA에 의존하지 않으며, 회귀 사례는 바이너리가 아닌 계약과 타겟의 쌍에 연결됩니다. 과거의 반례는 바이너리를 다시 만들더라도 다음 검사에서 가장 먼저 실행됩니다. 오래된 판정은 다른 아티팩트(artifact)에 재활용되지 않습니다. 링크 파일로 모듈을 교체하여 검사하는 기능은 컴포넌트 단위 생성의 작은 원형이라고 할 수 있습니다. 이 형태와 Mukoz의 설계는 잘 맞을 것입니다.
다만, 이번 시도에서 작성할 수 있었던 것은 몇 KB 규모의 프로그램까지였고, 판정의 강도는 스위트(suite)의 강도를 넘지 못했습니다. 소스를 생성하여 컴파일하는 경로에서도 같은 일이 가능하며, 토큰 비용 면에서는 오히려 그쪽이 저렴하다는 결과도 나왔습니다. 재생성으로 인한 유지보수는 검증하지 않았습니다. 현재 시점에서 말할 수 있는 것은, 검사기(checker)의 설계가 그 방향을 방해하지 않는다는 정도입니다.
시도해 보기
Mukoz는 v0.1.0의 실험적인 프로젝트입니다. 인터페이스나 포맷은 변경될 가능성이 있으며, 배포하는 소프트웨어의 리뷰나 테스트를 대체할 수 없습니다. 동작을 확인한 호스트는 Linux x86-64만 있습니다.
git clone https://github.com/CAPHTECH/mukoz
cd mukoz
cargo build --release # mukoz와 mukoz-emu-icicle을 빌드합니다 (Rust 1.97.1, C 컴파일러, 첫 실행 시 네트워크 필요)
...
반례는 mukoz show <id>로 자세히 볼 수 있고, mukoz shrink으로 입력을 작게 만들고, mukoz replay로 수정된 바이너리에 대해 재실행할 수 있습니다. CI에서는 --gate를 붙이면 판정이 종료 코드가 됩니다.
- 리포지토리: github.com/CAPHTECH/mukoz (MIT 또는 Apache-2.0. 임의로 사용할
emu/의 Unicorn 백엔드는 GPL)
- 시도 집계: eval/RESULTS.md
- 에이전트용 가이드: docs/11
- 영어 보고서: We asked AI agents to write machine code directly
토론(Discussion)