지난 1년 반 동안 Miri에 많은 변화가 있었고, 그 성과를 널리 알리기 위해 정리했습니다. 또한 일부 CI 설정에 영향을 주는 호환성 파괴 변경 사항도 안내합니다.
지난 1년 반 동안 Miri에는 많은 변화가 있었고, 이 진전을 더 널리 알리는 것이 좋겠다고 생각해 이렇게 글을 씁니다. 또한 최근 일부 CI 설정에 영향을 주는 호환성 파괴 변경을 적용했으니, 필요한 경우 CI 설정을 업데이트하시라는 공지의 의미도 있습니다.
Miri를 처음 접하셨다면, Miri는 여러분의 Rust 코드를 실행하면서 Undefined Behavior(정의되지 않은 동작)이 발생하는지 검사하는 인터프리터입니다. 아주 면밀한(그리고 매우 느린) valgrind 버전이라고 생각하셔도 됩니다. 프로그램이 초기화되지 않은 메모리를 잘못 사용하거나, 경계를 벗어난 메모리 접근이나 포인터 연산을 수행하거나, 핵심 언어 불변식을 위반하거나, 올바른 포인터 정렬을 보장하지 않거나, 잘못된 별칭(aliasing)을 야기할 때 Miri가 이를 감지합니다. 따라서 특히 unsafe 코드를 작성할 때 유용하며, unsafe 코드가 올바르고 안전하도록 요구되는 모든 규칙을 따르고 있는지 확인하는 데 도움을 줍니다. Miri는 메모리 누수도 감지합니다. 즉, 프로그램 실행이 끝나면 제대로 해제되지 않은 메모리가 있는지 알려줍니다.
다만 인터프리터라는 특성상, Miri가 실행할 수 있는 코드의 종류에는 제약이 있습니다. 보통 C 라이브러리나 운영체제와 상호작용하는 부분은 별도 지원이 필요합니다. C 코드는 Miri가 해석할 수 없기 때문입니다. 또한 해석하기 어려운 일부 Rust 기능은 아직 지원하지 않지만, 이 격차는 서서히 줄여가고 있습니다.
지난 1.5년 동안 훌륭한 기여자들의 연이은 노력 덕분에, Miri에서 실행할 수 있는 Rust 코드의 범위를 크게 넓혔습니다. 그중 몇 가지 하이라이트를 아래에 정리합니다.
직접 Miri를 사용하는 법을 알고 싶으시다면 글의 끝부분을 보세요. 이미 Miri를 사용 중이시라면, 여전히 --exclude-should-panic 같은 플래그를 넘기거나 동시성이 필요한 테스트를 비활성화하고 있을 수도 있습니다. 이제는 그런 플래그를 업데이트할 수 있을 것입니다. 또한 아래에 cargo miri가 CLI 인자를 해석하는 방식의 호환성 파괴 변경 사항이 있으니 참고하세요!
HashMapRust의 HashMap은 실행할 때마다 새로운 무작위 시드를 선택합니다. 이 시드는 운영체제에서 얻는데, Miri는 @Aaron1011이 getrandom을 구현하기 전까지는 이를 지원하지 못했습니다(#683). 동일한 프로그램을 Miri로 실행할 때마다 같은 방식으로 동작하도록, Miri는 내부적으로 결정적 RNG(기본 시드 0, -Zmiri-seed로 변경 가능)를 사용해 getrandom을 구현합니다. 이 PR 덕분에 무작위성을 위해 rand 크레이트를 사용하는 프로젝트도 Miri에서 실행할 수 있게 되었습니다.
하지만 이는 Miri에서의 무작위성이 실제로는 무작위가 아님을 의미합니다. 그러므로 중요한 암호화 작업을 수행하는 데 Miri를 사용하지 마세요.
예전의 Miri는 패닉이 발생하면 프로그램 실행을 그냥 중단(중절)했습니다. 실제 Rust 프로그램의 동작에 더 가깝게 맞추기 위해, @Aaron1011이 Miri에 올바른 언와인딩 지원을 구현했습니다(#693). 심지어 패닉 포획(catch)도 다시 구현했는데, 이를 위해 rustc, 표준 라이브러리, Miri 자체 전반에 걸친 여러 요소의 정렬(정합)을 맞춰야 했습니다. 이제 Miri는 #[should_panic] 테스트도 실행할 수 있습니다. 최근에는 Windows 타깃에 대해서도 지원됩니다.
@christianpoveda 덕분에, 이제 Miri는 임의의 포인터를 정수로, 그리고 다시 포인터로 캐스팅하는 것을 올바르게 지원합니다(#779).
최근 저는 정렬 검사도 이 정보를 온전히 반영하도록 조정하여, 이제 Miri가 자체 정렬 로직을 수행하는 코드도 실행할 수 있게 했습니다(#1513). 다만 이런 코드는 순전히 우연으로 동작하는 경우가 있을 수 있으니, 제대로 테스트하려면 최소한 10회 이상 실행하는 것이 좋습니다.
@christianpoveda가 파일 시스템 접근을 구현하기 시작했습니다(해당 PR 시리즈의 시작은 #962). 이후 @divergentdave가 디렉터리 나열과 관련 연산을 추가로 개선했습니다(시작 PR: #1152). 이제 Miri에서 실행되는 프로그램이 호스트 컴퓨터의 파일을 읽고 쓸 수 있습니다. 이는 해석 중인 프로그램과 외부 세계 간의 첫 번째 형태의 통신입니다. 이런 통신은 -Zmiri-disable-isolation 옵션으로 명시적으로 요청해야 합니다. 기본적으로 Miri는 각 실행이 완벽히 재현 가능하도록 프로그램을 격리합니다.
파일 시스템 접근은 Linux와 macOS 타깃에서만 지원됩니다. 하지만 크로스 인터프리테이션 덕분에 Windows 사용자에게도 문제되지 않습니다. 아래 항목을 참고하세요.
표준 라이브러리와 해석 대상 크레이트 모두에 대해 check-only 빌드를 사용하도록 한 @Aaron1011의 선행 작업을 바탕으로(#1136), 저는 Miri에 “크로스 인터프리테이션”을 지원하도록 했습니다(#1249). 즉, Windows 호스트에서라도 --target x86_64-unknown-linux-gnu를 지정하면, Miri가 프로그램을 리눅스에서 실행되는 것처럼 해석합니다. 특히 운영체제와 상호작용할 때 리눅스용 표준 라이브러리 경로를 사용합니다. Miri가 파일 시스템 접근을 위한 리눅스 API를 지원하므로, Windows 호스트에서 실행하더라도 이런 프로그램을 해석할 수 있습니다.
이는 호스트 플랫폼과 다른 타깃 특성을 시험할 때 특히 유용합니다. 예를 들어 64비트 macOS 호스트에서도 32비트 Linux 타깃(--target i686-unknown-linux-gnu)용 프로그램을 실행하여 포인터 크기가 다른 경우에도 로직이 올바른지 확인할 수 있습니다. Miri는 --target mips64-unknown-linux-gnuabi64 같은 빅엔디안 타깃도 지원하므로, 코드가 엔디안에 민감하다면 빅엔디안 시스템에서 올바르게 동작하는지 시험할 수 있습니다. 마지막으로, 크로스 인터프리테이션은 Miri 자체를 개발하는 데도 엄청나게 도움이 되었습니다. 예컨대 Windows 타깃에서의 패닉과 언와인딩 지원을 손볼 때도 이 기능에 의존했습니다.
올해 초, @vakaras가 갑자기 일련의 패치를 들고 나타나 Miri에 동시성 지원을 추가해 주었습니다(#1284). 이는 그가 Amazon 인턴십 동안 진행한 작업이라고 하니, 이 일을 후원해 준 Amazon에도 감사드립니다! 이제 Miri 프로그램은 스레드를 생성하고 락이나 원자적 연산으로 상호작용할 수 있습니다. 다만 몇 가지 주의할 점이 있습니다. Miri는 데이터 레이스를 탐지하지 않기 때문에, 잘못된 동기화로 데이터 레이스를 일으키는 프로그램은 Miri가 눈치채지 못한 채 Undefined Behavior를 유발할 수 있습니다. 또한 Miri의 스케줄러는 꽤 조악해서, 특정 상황에서는 프로그램이 무한 루프에 빠질 수도 있습니다.
cargo 호환성(호환성 파괴 변경!)최근 저는 사용자가 Miri에서 프로그램을 실행하는 주 진입점인 cargo miri를 거의 전면 재작성했습니다(#1540). 이제 cargo 자체와 더 잘 호환됩니다. cargo test와 cargo miri test는 완전히 동일한 플래그를 지원하고, cargo run과 cargo miri run도 마찬가지입니다.
하지만 이를 위해 호환성 파괴 변경이 필요했습니다. 이전에는 테스트 슈트를 실행하면서 Miri 자체와 프로그램에 플래그를 전달하려면 cargo miri test -- <miri flags> -- <test suite flags> 방식이었습니다. 이제는 cargo test와 동일하게 cargo miri test -- <test suite flags>로 플래그를 전달합니다. Miri에 전달할 플래그가 필요하다면, RUSTFLAGS처럼 동작하는 MIRIFLAGS 환경변수를 설정하면 됩니다. 또한 추가 인자 없이 cargo miri를 실행하는 것에 대한 지원을 제거했습니다. 이전에는 이것이 cargo miri run의 별칭이었는데, (a) 실제로는 cargo miri test가 훨씬 더 자주 사용되고, (b) 임의의 플래그를 지원하면서도 이러한 옵션을 구분하기가 까다롭기 때문입니다.
CI에서 Miri로 테스트를 실행하도록 설정해 두셨다면, 새 형식에 맞게 구성을 조정해 주세요. 당분간 Miri는 예전 방식도 지원하며 적절한 경고를 출력합니다. 하지만 언젠가는 해당 지원 코드를 제거할 계획입니다. GitHub에 호스팅된 프로젝트가 이 변경의 영향을 받는 경우, 이미 제가 알림을 보냈을 것입니다. 다만 일부 프로젝트는 놓쳤을 수도 있고, 모든 프로젝트가 GitHub에 있는 것도 아닙니다. 이 참에 CI 스크립트에서 cargo miri setup도 제거하세요. @dtolnay 덕분에, 이제 Miri는 CI 환경에서 실행 중임을 자동으로 감지하고 비대화형 모드로 들어가기 때문에 더는 필요하지 않습니다.
이 목록은 결코 완전하지 않습니다. 지난 몇 달 동안 삼각함수부터 환경 변수 접근, 시간 측정에 이르기까지 수많은 작은 함수가 구현되어, Miri가 실행할 수 있는 프로그램의 범위는 계속 넓어졌습니다. @Aaron1011, @christianpoveda, @divergentdave, @JOE1994, @samrat 여러분 모두 감사합니다! 혹여 누구를 빠뜨렸다면 죄송합니다…
이 글을 읽고 호기심이 생겨 Miri를 써보고 싶다면, 이렇게 해보세요. unsafe 코드가 일부 포함된 크레이트가 있고, 이미 테스트 슈트가 있다고 가정하겠습니다(unsafe 코드는 테스트하고 계시죠?). Miri를 설치하고(rustup +nightly component add miri), cargo +nightly miri test를 실행하면 Miri에서 모든 테스트를 실행합니다(아직 지원되지 않는 doctest는 제외). Miri는 아직 실험적 도구이므로 nightly 툴체인이 필요합니다.
Miri는 매우 느리므로, 어떤 테스트는 실행 시간이 너무 길어 실용적이지 않을 수 있습니다. 다음과 같이 Miri에서만 반복 횟수를 조정하고, Miri가 아닐 때는 영향을 주지 않게 할 수 있습니다:
let limit = if cfg!(miri) { 10 } else { 10_000 };
테스트 슈트가 타이머나 파일 시스템 같은 OS 기능에 접근해야 한다면, MIRIFLAGS=-Zmiri-disable-isolation을 설정해 이를 활성화하세요(필요할 경우 Miri가 알려줍니다). 테스트 슈트가 지원되지 않는 동작에 부딪힌다면, 이슈를 보고해 주세요.
테스트 슈트가 Miri에서 계속 동작하도록 CI에 Miri를 추가하고 싶다면, README를 참고하세요. 다른 궁금증이 있다면 이 문서가 좋은 출발점이 될 것입니다.
Miri는 Rust Playground에도 통합되어 있습니다. “Tools” 메뉴에서 Miri를 선택하면 코드에 Undefined Behavior가 있는지 확인할 수 있습니다.
Miri가 여러분의 코드에 대해 불평하는데 이유를 모르겠다면, 기꺼이 도와드리겠습니다! 아마도 질문하기에 가장 좋은 곳은 Zulip(#general 스트림이면 충분합니다)과 Miri 이슈 트래커일 것입니다. 공개적으로 질문하는 것을 강력히 권장합니다. 그래야 다른 사람도 답변을 도울 수 있고, 모두가 응답으로부터 배울 수 있습니다. 작은 자립형 예제 코드(가능하면 플레이그라운드에서)로 문제를 재현할 수 있다면 답변이 훨씬 쉬워집니다. 그래도 줄이는 방법을 모르겠다면 편하게 질문해 주세요.
Miri 개선을 돕고 싶다면 정말 환영합니다! 시작점으로는 이슈 트래커가 좋습니다. 이슈 목록이 충분히 짧아서, 빠르게 훑어보면서 흥미를 끄는 것이 있는지 찾을 수 있습니다. 또 다른 좋은 출발점은 여러분의 테스트 슈트가 동작하지 못하게 막는 기능의 빈틈을 직접 구현해 보는 것입니다. 멘토링이 필요하면 언제든지 연락하세요. :)
오늘은 여기까지입니다. 이미 많은 분이 Miri를 사용하고 있다는 사실에 정말 놀랐습니다. unsafe 코드의 정확성에 접근하는 방식을 바꾸려는 이 시도는 제 기대보다 훨씬 성공적이었습니다. 여러분의 unsafe 코드의 정확성을 보장하는 데 Miri가 도움이 되길 바라며, 내년에는 Miri가 어떤 발전을 이룰지 무척 기대됩니다. :D