gcc を途中で止め、電卓を二度作る
한국어 원문으로 표시합니다.
목표
gcc 를 전처리·컴파일·어셈블·링크 단계마다 멈춰 산출물을 확인하고, 같은 RPN 계산을 바로 계산하는 인터프리터와 C 로 옮겨 굽는 컴파일러 두 가지로 만들어 둘이 같은 답을 내는지 대조합니다.
왜 중요한가
컴파일러와 인터프리터는 같은 앞단을 쓰고 마지막 한 걸음이 다를 뿐입니다. 그 한 걸음 때문에 오류를 잡는 시점(실행 전인가 중인가), 속도(번역 비용을 몇 번에 나눠 내는가), 그리고 뜻이 어긋날 위험이 생깁니다. 파이썬으로 짠 인터프리터가 파이썬의 나눗셈 규칙을 그대로 쓰면, C 로 구운 쪽과 음수에서 답이 갈립니다. 이 실습은 그 차이를 작은 계산기에서 손으로 겪게 합니다.
RPN 계산기의 말
낱말 정수(부호가 붙을 수 있음, 예: 7, -3) · x(실행할 때 받는 값) · + - * / %
뜻 낱말을 왼쪽부터 읽는다. 수는 스택에 올리고, 연산자는 두 값을 내려(먼저 내린 것이 오른쪽)
계산한 결과를 올린다. 끝에 값이 정확히 하나 남아야 한다.
나눗셈 C 처럼 0 쪽으로 자른다: -7 / 2 = -3, 나머지의 부호는 나눠지는 수를 따른다: -7 % 2 = -1
예 "x 1 + 2 *" 는 (x + 1) * 2
오류 글자 bad token 'foo' at 3 (1부터 센 낱말 자리)
stack underflow at 2 (그 연산자의 자리)
expected one value, got 2 (끝에 남은 값의 수)
division by zero
단계
/opt/fixtures/mini/c/hello.c를 단계마다 멈춰/root/mini/stages/hello.i(전처리),hello.s(어셈블리),hello.o(목적 파일),hello(실행 파일)를 만듭니다.nm hello.o로 아직 풀리지 않은 이름(U)을 확인해 보세요./root/mini/rpn.py의tokenize(text)를 채웁니다 — 공백으로 자른 낱말 목록, 모르는 낱말은RpnError("bad token 'foo' at 3").evaluate(tokens, x)를 채웁니다 — 스택으로 바로 계산하는 인터프리터. 오류는 위 글자 그대로RpnError.check(tokens)를 채웁니다 — 실행하지 않고 스택 깊이만 따라가, 문제가 없으면 가장 깊을 때의 칸 수를, 있으면 evaluate 와 같은 글자의RpnError를 냅니다. 0 나누기는 x 에 달려 있으니 여기서 판단하지 않습니다.to_c(tokens)를 채웁니다 — 같은 계산을 하는 C 프로그램 글자. 먼저check로 검사해 틀린 프로그램이면 C 를 쓰지 않고RpnError를 냅니다. x 는argv[1], 결과는 한 줄로 찍고, 0 으로 나누면runtime error: division by zero를 찍고 1 로 끝납니다.build(tokens, out)를 채웁니다 —out.c→out.s→out.o→out을 gcc 로 차례로 만들고 중간 산출물을 지우지 않습니다.- 여러분의 인터프리터와 여러분의 컴파일러를 서로 대조합니다. 채점기가 무작위 프로그램 15편을 x 네 값으로 두 길에 모두 돌립니다 — 음수 나눗셈·나머지에서 갈리지 않는지 확인하세요.
bench(tokens, xs, out)를 채우고,/opt/fixtures/mini/rpn/big.rpn을 x = 0부터 199까지로 잰 결과를/root/mini/rpn_report.json에 그대로 적습니다.
참고
- 작업 자리는
/root/mini이고, 실습을 시작하면rpn.py뼈대가 깔려 있습니다(함수 이름과 인자는 그대로 두세요 — 채점기가 이름으로 부릅니다). 지웠다면bash /opt/lab/minic/prepare.sh cc-pipeline-lab으로 되살립니다. - 흔한 실수: 나눗셈에 파이썬
//·%를 그대로 쓰는 것, 연산자 자리를 0부터 세는 것, check 에서 실제로 계산해 0 나누기를 오류로 내는 것. - 세션은 60분에 시작해 +시간으로 늘릴 수 있고, 끝나면
/root/mini가 사라집니다. 남기고 싶은 코드는 따로 옮겨 두세요.
gcc 를 네 번 멈춰 세운다
gcc 는 -E(전처리)·-S(컴파일)·-c(어셈블)에서 멈출 수 있고, 옵션이 없으면 링크까지 갑니다. 앞 단계의 산출물을 다음 단계의 입력으로 주면 한 단계씩 나눠 볼 수 있습니다. .o 에는 printf 의 주소가 아직 없어서 nm 이 U 로 표시합니다.
낱말로 자른다
text.split() 으로 자른 뒤 낱말마다 x·연산자·정수인지 봅니다. 정수는 부호가 하나 붙을 수 있는 숫자열입니다('--3' 은 정수가 아닙니다). 자리는 1부터 셉니다.
스택으로 바로 계산한다 — 인터프리터
연산자를 만나면 두 값을 내립니다. 먼저 내린 것이 오른쪽 피연산자입니다. 나눗셈은 abs 로 몫을 구한 뒤 부호를 붙여 0 쪽으로 자르고, 나머지는 a - 몫*b 로 구하면 C 와 같아집니다.
실행하지 않고 검사한다
값 대신 스택의 깊이만 셉니다. 수는 +1, 연산자는 두 개를 내려 하나를 올리니 -1 이고, 그 전에 깊이가 2 이상인지 봅니다. 가장 깊었던 값이 C 배열의 크기가 됩니다.
같은 계산을 C 로 옮긴다 — 컴파일러
C 에도 스택 배열 하나와 칸 번호(sp)를 두면 낱말마다 한 줄씩 그대로 옮겨집니다. 배열 크기는 check 가 잰 깊이입니다. C 의 / 와 % 는 이미 0 쪽으로 자르므로 따로 할 일이 없지만, 0 으로 나누면 프로세스가 죽으니 그 앞에서 검사합니다.
C → 어셈블리 → 목적 파일 → 실행 파일
1단계에서 손으로 한 네 단계를 subprocess 로 차례로 부릅니다(gcc -S, gcc -c, gcc). 한 단계라도 실패하면 gcc 오류의 마지막 줄을 RpnError 에 담아 냅니다.
두 길이 같은 답을 내는지 대조한다
이 단계는 새 코드를 쓰지 않습니다. 채점기가 무작위 프로그램을 여러분의 evaluate 와 build 결과 양쪽에 돌립니다. 떨어지면 음수 나눗셈·나머지의 부호부터 확인하세요 — 인터프리터가 파이썬 규칙을 따르면 C 와 갈립니다.
언제 컴파일이 이득인가 잰다
time.perf_counter 로 세 구간을 잽니다 — 값마다 evaluate, build 한 번, 값마다 실행 파일 띄우기. 프로세스를 띄우는 비용이 계산보다 클 수 있다는 것도 숫자로 보입니다. same 은 두 결과 목록이 같은지입니다.