openlang

RISC-V 호출 규약, -O0으로 읽을까 -O2로 읽을까

호출 규약은 레지스터 표를 외울 때보다 실제 컴파일러 출력에서 읽어 낼 때 더 잘 남는다. 좋은 연습은 int 매개변수 세 개를 받아 합을 돌려주는 C 함수를 RV32용으로 -O0에서 한 번, -O2에서 한 번 컴파일해 보는 것이다.

-O0 리스트는 시끄럽다. 함수가 프레임을 만들고, a0, a1, a2를 스택에 저장했다가 다시 읽고, 더한 뒤 결과를 a0에 두고 ret 한다. -O2에서는 add 명령 두 개와 ret으로 줄어든다. 규약은 같고 리스트는 전혀 다르며, 그 비교가 핵심이다. 인자는 어느 쪽이든 인자 레지스터로 들어오고 결과는 어느 쪽이든 a0로 나간다. 나머지는 전부 컴파일러 자신의 장부 정리였다.

다음에는 함수가 다른 함수를 호출하게 바꾸고 왜 이제 ra를 저장해야 하는지 묻는다. ret은 그냥 jalr x0, 0(ra)이고 안쪽 call이 ra를 덮어쓴다.

최적화하지 않은 단계를 건너뛰면 스택 왕래가 규약의 일부라고 믿기 쉽다. 그렇지 않다. 확신이 없는 건 순서다. -O2부터 시작하면 깔끔하지만 프레임 설정이 나중에 뜻밖의 일로 나타난다.

이어지는 대화 3

서로의 근거를 따라
  1. 고유 링크

    디스어셈블러 없이 만져 볼 수 있는 규약이 필요하면 Lua C API가 좋은 준비 운동이에요. 함수를 먼저, 그다음 인자를 순서대로 가상 스택에 올리고 lua_call(L, nargs, nresults)를 부르면 결과가 그 자리를 대신해요. 라이브러리 호출로 풀어 쓴 호출 규약인 셈이고, 개수를 틀리면 lua_gettop(L)을 찍어 보는 순간 바로 드러나요. 신기하게도 그다음엔 레지스터 버전이 덜 신비롭게 느껴지더라고요.

  2. 고유 링크

    처음부터 밝혀 둘 한계가 하나 있습니다. C 표준은 레지스터도 스택 프레임도 전혀 규정하지 않습니다. 호출 규약은 플랫폼 ABI에서 오므로, 그 리스트가 가르치는 것은 C가 아니라 RISC-V psABI입니다. 누군가 같은 함수를 다른 타깃으로 컴파일해 보고 언어가 바뀌었다고 결론 내리는 순간 이 구분이 중요해집니다.

같은 질문의 다른 갈래

  1. (x, x)는 두 값이 같다는 뜻이 아니다

    OCaml의 (x, x)를 "두 값이 같다"로 읽는 학습자가 계속 나와요. 그런 뜻도 아니고, 애초에 허용되지도 않아요. 처음 배우는 사람은 대개 두 설명 중 하나를 떠올려요. 하나는 앞의 x가 구성 요소에 이름을 붙이고 뒤…