Skip to content

Instantly share code, notes, and snippets.

@dittos
Last active August 14, 2016 17:38
Show Gist options
  • Save dittos/a654b7aa56b56575726bbfee022fd6ee to your computer and use it in GitHub Desktop.
Save dittos/a654b7aa56b56575726bbfee022fd6ee to your computer and use it in GitHub Desktop.
import sys
from ctypes import *
from math import isfinite
'''
1) 수식을 파이썬 바이트코드로 컴파일
수식이 바이트코드로 컴파일 될 때 constant folding이 돼서 너무 간단해져 버리지 않도록 함
lineno table에서 연속된 두 바이트코드의 실제 코드상 라인 번호가 255 이상 차이나면 최적화하지 않는 것을 이용
참고: https://github.com/python/cpython/blob/e4091c77026088cb0611b6e896b1579805253f5b/Python/peephole.c#L403
'''
c = compile('0+({}{})'.format('\\\n' * 255, sys.argv[1]), '', 'eval')
code = c.co_code
consts = c.co_consts
'''
파이썬 바이트코드는 스택 기반으로, 일반적인 연산은 값들을 스택에 로드 -> 스택에 있는 값을 뽑아 연산 실행 -> 스택에 결과값을 다시 넣는 순서로 이루어진다.
인코딩은 1바이트의 opcode와 2바이트 * N개의 인자로 구성된다.
예를 들어 1 + 2는 다음과 같이 컴파일된다.
상수 테이블: (1, 2)
LOAD_CONST 0 # 64 00 00
LOAD_CONST 1 # 64 01 00
BINARY_ADD # 17
'''
'''
2) 파이썬 바이트코드를 x86-64 기계어로 컴파일
C 호출 컨벤션에 맞추기 위해서 상수 테이블을 인자로 받아 결과를 리턴하는 함수로 컴파일한다. (`double f(double[] consts)`의 시그니쳐를 가짐)
또한 파이썬 바이트코드의 스택 기반 연산을 거의 그대로 번역하기 위해 역시 스택 기반인 x87 부동소수점 연산을 사용할 것이다.
따라서 어셈블리어의 뼈대는 다음과 같다.
push %rbp # 55
mov %rsp,%rbp # 48 89 e5
mov %rdi,-8(%rbp) # 48 89 7d f8
... function body ...
fstpl -16(%rbp) # dd 5d f0
movsd -16(%rbp),%xmm0 # f2 0f 10 45 f0
pop %rbp # 5d
retq # c3
주: 사실 어셈블리 잘 모릅니다. 다음 명령어로 디스어셈블 하면서 알아냈습니다.
$ gcc -S test.c -mfpmath=387 && as test.s && objdump -D a.out
이제 몸통 부분을 번역할 차례다. 구현해야 할 연산은 크게 LOAD_CONST와 UNARY_*/BINARY_* 두가지 계열로 나눌 수 있다.
LOAD_CONST는 다음과 같이 번역된다.
# consts 배열의 시작 주소를 rax 레지스터에 저장
mov -8(%rbp),%rax # 48 8b 45 f8
# i번째 상수의 주소 계산 (double은 8바이트이므로 + i*8)
# 기계어에서는 i에 따라 마지막 바이트가 바뀌면 된다.
add $8,%rax # 48 83 c0 08
# 계산한 주소에 들어있는 값을 참조하여 x87 부동소수점 레지스터 스택에 푸시
fldl (%rax) # dd 00
UNARY_*/BINARY_* 연산은 fadd, fsubrp, fmulp 등의 명령에 1:1 대응된다.
'''
ops = {
10: (0, b''),
11: (0, b'\xd9\xe0'),
23: (0, b'\xde\xc1'),
27: (0, b'\xde\xf9'),
24: (0, b'\xde\xe9'),
20: (0, b'\xde\xc9'),
83: (0, b'\xdd\x5d\xf0\xf2\x0f\x10\x45\xf0\x5d\xc3'),
100: (1, lambda x: b'\x48\x8b\x45\xf8\x48\x83\xc0' + bytes([x * 8]) + b'\xdd\x00'),
}
buf = b'\x55\x48\x89\xe5\x48\x89\x7d\xf8'
while code:
# 모르는 opcode가 나오면 ops 사전을 참조하다가 에러가 나므로 올바르지 않은 수식을 실행하지 않을 수 있다.
arity, op = ops[code[0]]
code = code[1:]
if arity:
op = op(code[0] + (code[1] << 8))
code = code[2:]
buf += op
'''
3) 컴파일한 기계어를 실행 가능한 메모리 영역에 넣고 호출
메모리에 있는 명령어를 실행시키려면 시작 위치가 페이지 크기에 정렬되어 있어야 하므로 valloc 함수로 메모리를 할당하고 컴파일한 기계어를 이동시킨다.
그리고 mprotect 함수로 실행 권한을 부여한다. (7 = PROT_READ | PROT_WRITE | PROT_EXEC)
'''
libc = CDLL('libc.so.6')
size = len(buf)
addr = memmove(libc.valloc(size), buf, size)
libc.mprotect(addr, size, 7)
consts_t = c_double * len(consts)
f = cast(addr, CFUNCTYPE(c_double, consts_t))
n = f(consts_t(*consts))
assert isfinite(n) # inf나 nan이 나오면 assertion fail
print(n)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment