Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

LLVM 디버깅하기

참고: 코드 생성에 관한 정보를 찾고 있다면 대신 이 장을 참조하십시오.

이 절에서는 코드 생성 과정에서 발생하는 컴파일러 버그를 디버깅하는 방법을 다룹니다(예: 컴파일러가 특정 코드를 생성한 이유나 LLVM에서 크래시가 발생한 이유). LLVM은 별도의 디버깅 문서가 필요할 만큼 큰 프로젝트이지만, 다음은 rustc 맥락에서 중요한 몇 가지 팁입니다.

예제 최소화하기

일반적으로 컴파일러는 코드를 분석하면서 많은 정보를 생성합니다. 따라서 보통은 최소한의 예제를 찾는 것이 유용한 첫 단계입니다. 이를 위한 한 가지 방법은 다음과 같습니다.

  1. 문제를 재현하는 새 크레이트를 만드십시오(예: 문제가 되는 크레이트를 의존성으로 추가하고 새 크레이트에서 사용합니다).

  2. 외부 의존성을 제거하여 크레이트를 최소화하십시오. 즉, 관련된 모든 것을 새 크레이트로 옮기십시오.

  3. 코드를 더 짧게 만들어 문제를 더욱 최소화하십시오(creduce처럼 이 작업을 도와주는 도구가 있습니다).

위의 2단계와 3단계 방법론에 대한 더 자세한 논의는 pnkfelix가 Rust 프로그램 최소화에 관해 특별히 작성한 훌륭한 블로그 글을 참조하십시오.

LLVM 내부 검사 활성화하기

공식 컴파일러(nightly 포함)는 LLVM 단언이 비활성화되어 있으므로, LLVM 단언 실패가 컴파일러 크래시(ICE가 아닌 “진짜” 크래시)나 그 밖의 이상한 동작으로 나타날 수 있습니다. 이러한 문제를 겪고 있다면, LLVM 단언이 활성화된 컴파일러(“alt” nightly 또는 bootstrap.toml에 llvm.assertions = true를 설정하여 직접 빌드한 컴파일러)를 사용해 보고 무언가 드러나는지 확인하는 것이 좋습니다.

rustc 빌드 과정에서는 LLVM 도구들을 build/host/llvm/bin에 빌드합니다. 이 도구들은 직접 호출할 수 있습니다. 이러한 도구는 다음과 같습니다.

  • llc: 비트코드(.bc 파일)를 실행 가능한 코드로 컴파일합니다. LLVM 백엔드 버그를 재현하는 데 사용할 수 있습니다.
  • opt: LLVM 최적화 패스를 실행하는 비트코드 변환기입니다.
  • bugpoint: 큰 테스트 케이스를 작고 유용한 테스트 케이스로 줄여 줍니다.
  • 그 밖에도 많은 도구가 있으며, 그중 일부는 아래 본문에서 언급합니다.

기본적으로 Rust 빌드 시스템은 LLVM 소스 코드나 LLVM 빌드 구성 설정의 변경 여부를 확인하지 않습니다. 따라서 rustc에 링크되는 LLVM을 다시 빌드해야 한다면, 먼저 build/host/llvm/에 있을 .llvm-stamp 파일을 삭제하십시오.

기본 rustc 컴파일 파이프라인에는 여러 개의 코드 생성 단위가 있는데, 이는 수동으로 재현하기 어렵고 LLVM이 병렬로 여러 번 호출된다는 뜻입니다. 가능하다면(즉, 그렇게 해도 버그가 사라지지 않는다면) rustc에 -C codegen-units=1을 전달하면 디버깅이 더 쉬워집니다.

원시 LLVM 입력 직접 확보하기

rustc가 LLVM IR을 생성하게 하려면 --emit=llvm-ir 플래그를 전달해야 합니다. cargo로 빌드하는 경우 RUSTFLAGS 환경 변수를 사용하십시오(예: RUSTFLAGS='--emit=llvm-ir'). 이렇게 하면 rustc가 대상 디렉터리에 LLVM IR을 출력합니다.

cargo llvm-ir [options] path는 path에 있는 특정 함수의 LLVM IR을 출력합니다. (cargo install --locked cargo-asm을 실행하면 cargo asm과 cargo llvm-ir이 설치됩니다.) --build-type=debug는 디버그 빌드용 코드를 내보냅니다. 이 밖에도 유용한 옵션들이 있습니다. 또한 LLVM IR에 포함된 디버그 정보는 출력을 상당히 어지럽힐 수 있으므로, RUSTFLAGS="-C debuginfo=0"이 매우 유용합니다.

RUSTFLAGS="-C save-temps"는 컴파일 중 여러 단계에서 LLVM 비트코드를 출력하며, 이는 때때로 유용합니다. 출력된 LLVM 비트코드는 컴파일러 출력 디렉터리의 .bc 파일에 저장되며, 이 디렉터리는 rustc에 --out-dir DIR 인자를 전달하여 설정합니다.

  • rustc 자체를 호출할 때 LLVM 백엔드에서 단언 실패나 세그멘테이션 오류가 발생한다면, 이 .bc 파일들을 각각 llc 명령에 전달해 보고 같은 실패가 발생하는지 확인하는 것이 좋습니다. (LLVM 개발자들은 최소화된 재현을 위해 Rust 크레이트를 사용하는 버그보다 .bc 파일로 축소된 버그를 선호하는 경우가 많습니다.)

  • 사람이 읽을 수 있는 형태의 LLVM 비트코드를 얻으려면 llvm-dis를 사용하여 비트코드(.bc) 파일을 .ll 파일로 변환하기만 하면 됩니다. llvm-dis는 rustc의 대상 로컬 컴파일 결과물에 포함되어 있을 것입니다.

rustc는 LLVM의 최적화가 없더라도 -O의 활성화 여부에 따라 서로 다른 IR을 내보낸다는 점에 유의하십시오. 따라서 rustc가 내보내는 IR을 다뤄 보고 싶다면 다음과 같이 해야 합니다:

$ rustc +local my-file.rs --emit=llvm-ir -O -C no-prepopulate-passes \
    -C codegen-units=1
$ OPT=build/$TRIPLE/llvm/bin/opt
$ $OPT -S -O2 < my-file.ll > my

예를 들어 어떤 IR이 최적화 시점의 단언 실패를 일으키는지 확인하거나 LLVM이 특정 최적화를 언제 수행하는지 확인하기 위해 LLVM 파이프라인 도중의 LLVM IR만 얻고 싶다면, rustc 플래그 -C llvm-args=-print-after-all을 전달하고, 필요하다면 -C llvm-args='-filter-print-funcs=EXACT_FUNCTION_NAME을 추가할 수 있습니다(예: -C llvm-args='-filter-print-funcs=_ZN11collections3str21_$LT$impl$u20$str$GT$\ 7replace17hbe10ea2e7c809b0bE').

이렇게 하면 표준 오류로 많은 내용이 출력되므로, 이를 파일로 파이프하는 것이 좋습니다. 또한 -filter-print-funcs와 -C codegen-units=1을 둘 다 사용하지 않는다면, 여러 코드 생성 단위가 병렬로 실행되기 때문에 출력 내용이 서로 뒤섞여 아무것도 읽을 수 없게 됩니다.

  • 앞서 설명한 방법에는 한 가지 주의할 점이 있습니다. LLVM의 -print 계열 옵션은 패스가 실행되는 IR 단위(예: 함수 하나)만 출력하며, 참조되는 선언, 전역 변수, 메타데이터 등은 포함하지 않습니다. 따라서 일반적으로 -print의 출력을 llc에 입력하여 특정 문제를 재현할 수는 없습니다.

  • LLVM 내부에서 SafeStackLegacyPass::runOnFunction의 시작 부분에서 F.getParent()->dump()를 호출하면 모듈 전체가 덤프되며, 이는 재현을 위한 더 나은 기반이 될 수 있습니다. (다만 -C save-temps로 덤프된 .bc 파일에서도 동일한 덤프를 얻을 수 있을 것입니다.)

특정 함수의 IR만 필요하다면(예를 들어 그 함수가 왜 단언 실패를 일으키는지 또는 왜 올바르게 최적화되지 않는지 확인하고 싶다면), 다음과 같이 llvm-extract를 사용할 수 있습니다.

$ ./build/$TRIPLE/llvm/bin/llvm-extract \
    -func='_ZN11collections3str21_$LT$impl$u20$str$GT$7replace17hbe10ea2e7c809b0bE' \
    -S \
    < unextracted.ll \
    > extracted.ll

LLVM 최적화 패스 조사하기

최적화 패스로 인해 잘못된 동작이 나타난다면 -opt-bisect-limit이라는 매우 유용한 LLVM 옵션을 사용할 수 있습니다. 이 옵션은 실행할 가장 높은 패스의 인덱스 값을 나타내는 정수를 받습니다. 실행되는 패스의 인덱스 값은 실행할 때마다 동일하게 유지됩니다. 따라서 이를 결과 프로그램을 기반으로 탐색 공간의 이분 탐색을 자동화하는 소프트웨어와 결합하면, 문제를 일으키는 패스를 빠르게 찾아낼 수 있습니다. -opt-bisect-limit이 지정되면 모든 실행 내역이 각각의 인덱스, 그리고 해당 패스가 실행되었는지 건너뛰었는지를 나타내는 출력과 함께 표준 오류에 표시됩니다. 제한 값을 인덱스 -1로 설정하면(예: RUSTFLAGS="-C llvm-args=-opt-bisect-limit=-1") 모든 패스와 그에 해당하는 인덱스 값이 표시됩니다.

최적화 파이프라인을 다뤄 보고 싶다면, rustc가 내보낸 LLVM IR에 ./build/host/llvm/bin/에 있는 opt 도구를 사용할 수 있습니다.

LLVM 자체의 구현을 조사할 때는 LLVM의 내부 디버그 인프라에 대해 알고 있어야 합니다. 이 기능은 LLVM 디버그 빌드에서 제공되며, bootstrap.toml에서 다음 설정을 변경하여 rustc의 LLVM 빌드에서 활성화할 수 있습니다:

# Indicates whether the LLVM assertions are enabled or not
llvm.assertions = true

# Indicates whether the LLVM build is a Release or Debug build
llvm.optimize = false

간단히 요약하면 다음과 같습니다:

  • assertions=true를 설정하면 대략적인 수준의 디버그 메시지가 활성화됩니다.
    • 그에 더해 optimize=false를 설정하면 세밀한 수준의 디버그 메시지가 활성화됩니다.
  • LLVM의 LLVM_DEBUG(dbgs() << msg)는 rustc의 debug!(msg)와 비슷합니다.
  • -debug 옵션은 모든 메시지를 활성화합니다. 이는 rustc에서 환경 변수 RUSTC_LOG=debug를 설정하는 것과 비슷합니다.
  • -debug-only=<pass1>,<pass2> 변형은 좀 더 선택적입니다. 이는 rustc에서 환경 변수 RUSTC_LOG=path1,path2를 설정하는 것과 비슷합니다.

도움 받기와 질문하기

궁금한 점이 있다면 rust-lang Zulip, 특히 #t-compiler/wg-llvm 채널을 방문하십시오.

알아 두면 좋은 컴파일러 옵션

-C help 및 -Z help 컴파일러 스위치는 유용하게 쓸 수 있는 다양하고 흥미로운 옵션을 나열합니다. 다음은 LLVM 개발과 관련하여 가장 자주 쓰이는 몇 가지 옵션입니다(일부는 위의 튜토리얼에서도 사용되었습니다).

  • --emit llvm-ir 옵션은 텍스트 형식의 LLVM IR이 담긴 <filename>.ll 파일을 생성합니다
    • --emit llvm-bc 옵션은 바이트코드 형식(<filename>.bc)으로 생성합니다
  • -C llvm-args=<foo>를 전달하면 llc나 opt 같은 도구가 받는 옵션을 거의 모두 전달할 수 있습니다. 예를 들어 -C llvm-args=-print-before-all을 전달하면 모든 LLVM 패스 전에 IR을 출력합니다.
  • -C no-prepopulate-passes는 LLVM 패스 관리자에 패스 목록을 미리 채워 넣지 않도록 합니다. 이를 통해 최적화를 거친 LLVM IR이 아니라 rustc가 생성하는 LLVM IR을 볼 수 있습니다.
  • -C passes=val 옵션을 사용하면 추가로 실행할 LLVM 패스 목록을 공백으로 구분하여 지정할 수 있습니다
  • -C save-temps 옵션은 컴파일 중에 생성되는 모든 임시 출력 파일을 저장합니다
  • -Z print-llvm-passes 옵션은 실행 중인 LLVM 최적화 패스를 출력합니다
  • -Z time-llvm-passes 옵션은 각 LLVM 패스의 소요 시간을 측정합니다
  • -Z verify-llvm-ir 옵션은 LLVM IR이 올바른지 검증합니다
  • --jobs-backend=1은 서로 다른 컴파일 단위의 병렬 컴파일을 비활성화합니다
  • -Z llvm-time-trace 옵션은 LLVM 패스의 세부 정보와 소요 시간이 담긴, Chrome 프로파일러와 호환되는 JSON 파일을 출력합니다.
  • -C llvm-args=-opt-bisect-limit=<index> 옵션을 사용하면 LLVM 최적화를 이분 탐색할 수 있습니다.

LLVM 버그 보고하기

LLVM 버그 보고서를 제출할 때는 문제를 보여 주는 최소한의 동작 예제가 필요할 것입니다. 이를 위해서는 Godbolt 컴파일러 탐색기가 매우 유용합니다.

  1. 문제가 되는 코드의 LLVM IR을 얻었다면(위 내용 참고), Godbolt로 최소한의 동작 예제를 만들 수 있습니다. llvm.godbolt.org에 접속하십시오.

  2. 프로그래밍 언어로 LLVM-IR을 선택하십시오.

  3. llc를 사용하여 IR을 그대로 특정 타깃으로 컴파일하십시오.

    • 유용한 플래그가 몇 가지 있습니다. -mattr은 타깃 기능을 활성화하고, -march=는 타깃을 선택하며, -mcpu=는 CPU를 선택하는 등의 역할을 합니다.
    • llc -march=help 같은 명령은 사용 가능한 모든 아키텍처를 출력합니다. Rust의 아키텍처 이름과 LLVM의 이름이 일치하지 않는 경우가 있으므로 이 명령이 유용합니다.
    • rustc를 어딘가에서 직접 컴파일했다면, target 디렉터리에 llc, opt 등의 바이너리가 있습니다.
  4. LLVM-IR을 최적화하려면 opt를 사용하여 LLVM 최적화가 IR을 어떻게 변환하는지 확인할 수 있습니다.

  5. 문제를 보여 주는 godbolt 링크를 확보했다면 LLVM 버그를 작성하기는 꽤 쉽습니다. LLVM의 GitHub 이슈 페이지를 방문하기만 하면 됩니다.

LLVM의 버그 수정 사항 이식하기

버그가 LLVM 버그임을 확인하고 나면, 해당 버그가 이미 LLVM에 보고되어 수정되었지만 아직 우리 쪽에는 그 수정 사항이 반영되지 않은 경우를 종종 발견하게 됩니다(또는 LLVM에 충분히 익숙하여 직접 수정할 수도 있습니다).

그런 경우에는 rustc가 수정 사항을 더 쉽게 사용할 수 있도록, 버그 수정을 우리의 자체 LLVM 포크에 직접 이식하는 방법을 택하기도 합니다. 우리의 LLVM 포크는 rust-lang/llvm-project에서 관리됩니다. 그곳에 수정 사항을 반영한 뒤에는 서브모듈 커밋을 변경하는 PR도 반영해야 합니다 – 도움이 필요하면 Zulip에 문의하십시오.