2014. február 20., csütörtök

Python disassembly trükk

Amióta pythonnal foglalkozom, mindig hiányoltam annak a lehetőségét, hogy megnézzem, egy programkód pontosan milyen utasításokra fordul le végrehajtás előtt. Ez ugye egy C nyelven nevelkedett fejlesztő-palántának teljesen általános elvárás, az ember könnyen hozzászokik, hogy bármely olyan programrész esetén, ahol nem vagyunk biztosak benne, hogy adott architectúrán, adott fordítóval és opciókkal milyen gépi kód fog keletkezni, könnyen bele lehet nézni az assembly kódba. Ez sokszor debuggolás közben is jól jön.
Talán más python fejlesztők körében köztudott, hogy erre python esetében is lehetőség van, én csak most fedeztem fel a trükkjét. Mivel a python program általában interpretált módon fut (én is legtöbbször így használom), ezért sokszor nincs lehetőségünk a C nyelv esetében használt módon disassembly-t készíteni. Ha natívan futtatható állományt készítünk belőle, akkor a CPython réteg miatt lesz nehezen értelmezhető az assembly kód. Szerencsére van mód arra, hogy az interpreter által előállított byte-kódot kiírassuk olvasható formában, amit lentebb be is mutatok.
Felmerülhet, hogy mi szükség van a disassembly-re, ha egyszer a python interpreter VM-ben futtat, így a generált byte-kód úgyis minden architectúrán ugyanolyan (a kauzalitási sorrend persze fordított :) ). Ettől függetlenül egy olyan fejlesztőnek, aki még csak most ismerkedik a nyelv nyalánkságaival (mint én is), hasznos lehet a mélyebb összefüggések megértéséhez.

Példaként először nézzünk meg, hogyan történik a disassemblálás (van erre szép magyar szó egyáltalán?) C nyelvnél, GCC használatával. Vegyünk példának egy aprócska programot (add.c):

int main(void)
{
    int a, b;
    a = 1;
    b = a + 2;
    return 0;
}


Ezután assembly nyelvre fordítjuk (a példa kedvéért expliciten kikapcsolt optimalizációval):

$> gcc -S -fverbose-asm -o add.asm -O0 add.c

Megjegyzendő, hogy az assembly kódba GDB-n belül is belenézhetünk, sőt akár egyenként hajthatjuk végre az utasításokat a stepi/nexti parancsokkal.
A keletkezett kód a szokásos GCC-assembleres sallangokat figyelmen kívül hagyva tiszta és lényegre törő:

        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp    #
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp      #,
        .cfi_def_cfa_register 6
        movl    $1, -8(%rbp)    #, a
        movl    -8(%rbp), %eax  # a, tmp62
        addl    $2, %eax        #, tmp61
        movl    %eax, -4(%rbp)  # tmp61, b

        movl    $0, %eax        #, D.1590
        popq    %rbp    #
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
        .section        .note.GNU-stack,"",@progbits



Hogy néz ki ugyanez pythonban? A pythonnak ehhez külön modulja van, amit berántva könnyen megnézhetjük a generált byte-kódot olvasható formában. Legegyszerűbben az interaktív shell-ben tehetjük ezt meg:

>>> from dis import dis
>>> co = compile("a = 1; b = a + 2", "<string>", "exec")
>>> dis(co)
  1           0 LOAD_CONST               0 (1)
              3 STORE_NAME               0 (a)
              6 LOAD_NAME                0 (a)
              9 LOAD_CONST               1 (2)
             12 BINARY_ADD
             13 STORE_NAME               1 (b)
             16 LOAD_CONST               2 (None)
             19 RETURN_VALUE



Érdemes ezt kipróbálni a fontosabb vezérlési szerkezetekre (for, try, with, stb.), és megnézni, mit hogyan valósít meg a python a motorháztető alatt. Remélem segíthet ez a trükk néhány hozzám hasonló kezdő python fejlesztőnek, hogy mélyebben megismerje a nyelvet.