openlang

호출 규약은 -O0 출력부터 읽는 게 맞을까

호출 규약은 레지스터 표를 외울 때보다 실제 컴파일러 출력에서 읽어 낼 때 더 잘 남는다. 좋은 연습은 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. 패턴 안의 이름은 무엇을 뜻할까요?

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