Reverse Engineering σε περιβάλλον Linux, Μέρος 3: Hands-on παράδειγμα: Java vs C
Description: This is issue 35 of Magaz, the Hellenic LinuxMagazine
Keywords: magaz hellenic greek hellas greece hellug linux magazine

Magaz, The Greek Linux Magazine
Τεύχος #35
Περιληπτικά Περιεχόμενα Αναλυτικά Περιεχόμενα Mirrors Links Πληροφορίες Συντάκτες Αναζήτηση Επικοινωνία
Επόμενο Προηγούμενο Περιεχόμενα ### 4. Hands-on παράδειγμα: Java vs C ...και την 42η μέρα ο Θεός δημιούργησε την C. Βλέποντας την απειλή οι δυνάμεις του Κακού αποφάσισαν να αντεπιτεθούν... και έτσι γεννήθηκε η Java. Η Java από τότε που δημιουργήθηκε κλήθηκε να αντιμετωπίσει τις κυρίαρχες τότε (αλλά και σήμερα) C και C++. Η σύγκριση ήταν αναπόφευκτη΄ η Java διαφημιζόταν ως μια πιο κομψή και ασφαλής C++, μια γλώσσα ικανή να χρησιμοποιηθεί σε ένα ευρύ πεδίο εφαρμογών, από ιστοσελίδες μέχρι ενσωματωμένες συσκευές. Όπως συνηθίζεται στον κόσμο των υπολογιστών, ένας "ιερός" πόλεμος ξέσπασε. Οι πολέμιοι της Java είχαν κυρίως δύο λόγους για τους οποίους ήθελαν να την κάψουν στην πυρά. Καταρχάς υπήρχαν εκείνοι που στο άκουσμα της λέξης "αντικειμενοστρεφής" έβγαζαν σκόρδα και προσπαθούσαν να ξορκίσουν τα δαιμόνια. Σε αυτούς, βέβαια, δεν άρεσε ούτε η C++, η οποία όμως έπαιρνε άφεση διότι μπορούσε απλώς να χρησιμοποιηθεί ως βελτιωμένη C. Ο δεύτερος και πιο καταδικαστικός λόγος ενάντια στην Java ήταν ότι ήταν αργή. Ειδικά σε ότι είχε σχέση με γραφικές διεπαφές (GUI) ήταν εκνευριστικά αργή και επιπλέον είχε μια μέτριας ποιότητας (εμφανισιακά, τουλάχιστον) βιβλιοθήκη χειριστηριών. Σήμερα, αρκετό καιρό ύστερα από την έναρξη της διαμάχης, τα πράγματα φαίνεται να έχουν μπει σε μια τάξη. Η Java έχει καταφέρει να βελτιωθεί θεαματικά στο φλέγον θέμα της απόδοσης και έτσι έχει κατακτήσει μια αρκετά υψηλή θέση στις καρδιές των προγραμματιστών αλλά και των διοικητικών στελεχών. Η C παραμένει κυρίαρχη στον τομέα για τον οποίο σχεδιάστηκε, τον προγραμματισμό συστημάτων, ενώ η C++ απολαμβάνει μια σταθερή θέση σε εφαρμογές που επωφελούνται από την αντικειμενοστρεφή προσέγγιση και ταυτόχρονα έχουν αυξημένες απαιτήσεις απόδοσης. Δυστυχώς στις απόψεις πολλών η Java και γενικά οι διερμηνευόμενες (interpreted) γλώσσες έχουν στιγματιστεί από την αργοπορία που τις χαρακτήριζε στα πρώτα τους βήματα. Στο πείραμα που ακολουθεί θα εξετάσουμε και θα εξηγήσουμε την απρόσμενα καλή απόδοση ενός προγράμματος σε Java. ### 4.1 Το πείραμα Στο πείραμα που ακολουθεί θα μετρήσουμε τους χρόνους εκτέλεσης του ίδιου προγράμματος σε C (gcc 3.2.3) και σε Java (Sun J2RE 1.4.2_01). Το πρόγραμμα είναι η αναδρομική συνάρτηση υπολογισμού των όρων της σειράς fibonacci fn = fn-1 + fn-2, με f0 = 0 και f1 = 1. Σε C:
    #include <stdio.h>
    #include <stdlib.h>

    unsigned long fib(unsigned long n)
    {
       if (n < 2)
            return n;
       else
            return fib(n-1) + fib(n-2);
    }

    int main(int argc, char *argv[])
    {
        int n;

        if (argc < 2)
            n = 1;
        else
            n = atoi(argv[1]);

        printf("%lu\n", fib(n));

        return 0;
    }
Σε Java:
    public class fib {

        public static void main(String args[]) {
            int n;

            if (args.length < 1)
                n = 1;
            else
                n = Integer.parseInt(args[0]);

            System.out.println(fib(n));
        }

        public static int fib(int n) {
            if (n < 2)
                return n;
            else
                return fib(n-1) + fib(n-2);
        }
    }
Μεταγλωττίζουμε τα δύο προγράμματα και τα εκτελούμε 3 φορές το καθένα. Το J2RE της Sun προσφέρει δύο εικονικές μηχανές, την server και την client (default). Η πρώτη έχει μεγαλύτερες απαιτήσεις μνήμης αλλά έχει ως αποτέλεσμα καλύτερη ταχύτητα εκτέλεσης ενώ η δεύτερη έχει πιο μικρές απαιτήσεις αλλά η ταχύτητα εκτέλεσης δεν είναι η βέλτιστη. Εδώ χρησιμοποιούμε την παράμετρο "-server" που επιλέγει την server VM (εικονική μηχανή).
    $ gcc -O3 -fomit-frame-pointer -o fib.c.exe fib.c
    $ time fib.c.exe 39
    63245986

    real    0m7.479s
    user    0m7.361s
    sys     0m0.017s
    $ time fib.c.exe 39
    63245986

    real    0m7.478s
    user    0m7.368s
    sys     0m0.013s
    $ time fib.c.exe 39
    63245986

    real    0m7.471s
    user    0m7.366s
    sys     0m0.016s
    $ javac fib.java
    $ time java -server fib 39
    63245986

    real    0m5.853s
    user    0m5.618s
    sys     0m0.049s
    $ time java -server fib 39
    63245986

    real    0m5.848s
    user    0m5.625s
    sys     0m0.043s
    $ time java -server fib 39
    63245986

    real    0m5.847s
    user    0m5.622s
    sys     0m0.044s
Η Java φαίνεται να έχει αρκετά καλύτερες επιδόσεις από τη C!!! Είναι δυνατόν; Ευτυχώς ή δυστυχώς είναι! ### 4.2 Η αναζήτηση Αφού περάσει το πρώτο σοκ, ανασυντασσόμαστε και προσπαθούμε να φερθούμε όσο πιο επιστημονικά γίνεται. Έχουμε ένα πείραμα με απροσδόκητα αποτελέσματα και καλούμαστε να τα εξηγήσουμε. Καταρχάς, γνωρίζουμε ότι o διερμηνευτής της Java περιέχει JIT μεταγλωττιστή οπότε γενικά θα περιμέναμε μια καλή απόδοση. Το βασικό ερώτημα είναι το τι έκανε ο JIT μεταγλωττιστής, που δε μπόρεσε να κάνει ο gcc, ώστε να βελτιώσει την απόδοση κατά 20% περίπου. Για να απαντήσουμε σε αυτό το ερώτημα το καλύτερο που μπορούμε να κάνουμε είναι να συγκρίνουμε τον κώδικα μηχανής που παρήγαγε καθένας από τους δύο αντιπάλους. Τον κώδικα μηχανής που παρήγαγε ο gcc είναι πολύ απλο να τον εξετάσουμε χρησιμοποιώντας τον HT Editor. Βεβαίως, μπορείτε να χρησιμοποιήσετε όποιο εργαλείο σας βολεύει.
    || ....... ! ;********************************************************
    || ....... ! ; function fib (global)
    || ....... ! ;********************************************************
    || ....... ! fib:                            ;xref c80483d7 c80483e4 c8048427
    || ....... !                                 ;xref c8048434
    || ....... !   push    esi
    || 8048411 !   push    ebx
    || 8048412 !   mov     esi, [esp+0ch]       ;; esi=n
    || 8048416 !   cmp     esi, 1
    || 8048419 !   mov     eax, esi
    || 804841b !   ja      loc_8048420
    || 804841d !
    || ....... ! loc_804841d:                    ;xref j804843f
    || ....... !   pop     ebx
    || 804841e !   pop     esi
    || 804841f !   ret
    || 8048420 !
    || ....... ! loc_8048420:                    ;xref j804841b
    || ....... !   sub     esp, 0ch
    || 8048423 !   lea     edx, [esi-1]
    || 8048426 !   push    edx
    || 8048427 !   call    fib                  ;; fib(n-1)
    || 804842c !   lea     edx, [esi-2]
    || 804842f !   mov     ebx, eax
    || 8048431 !   mov     [esp], edx
    || 8048434 !   call    fib                  ;; fib(n-2)
    || 8048439 !   lea     eax, [eax+ebx]       ;; eax = fib(n-1) + fib(n-2)
    || 804843c !   add     esp, 10h
    || 804843f !   jmp     loc_804841d
Ένα πολύ χρήσιμο χαρακτηριστικό του HT editor είναι ότι υποστηρίζει πληθώρα εκτελέσιμων αρχείων, μεταξύ αυτών και τα .class αρχεία της Java! Έτσι είναι απλό να εξετάσουμε και τα bytecodes στα οποία μεταφράστηκε η συνάρτηση fib:
    ||     ... ! ;----------------------------------------------
    ||     ... ! ; public static int fib::fib(int)
    ||     ... ! ;----------------------------------------------
    ||     ... ! fib_1fa:
    ||     ... !   iload_0                                ;; Σωρός: [n]
    ||     1fb !   iconst_2                               ;; Σωρός: [n] [2]
    ||     1fc !   if_icmpge       loc_201                ;; Σωρός: -, if (n >= 2) goto loc_201
    ||     1ff !   iload_0                                ;; Σωρός: [n]
    ||     200 !   ireturn
    ||     201 !
    ||     ... ! loc_201:                        ;xref j1fc
    ||     ... !   iload_0                                ;; Σωρός: [n]
    ||     202 !   iconst_1                               ;; Σωρός: [n] [1]
    ||     203 !   isub                                   ;; Σωρός: [n-1]
    ||     204 !   invokestatic    <int fib::fib(int)> 4  ;; Σωρός: [fib(n-1)]
    ||     207 !   iload_0                                ;; Σωρός: [fib(n-1)] [n]
    ||     208 !   iconst_2                               ;; Σωρός: [fib(n-1)] [n] [2]
    ||     209 !   isub                                   ;; Σωρός: [fib(n-1)] [n-2]
    ||     20a !   invokestatic    <int fib::fib(int)> 4  ;; Σωρός: [fib(n-1)] [fib(n-2)]
    ||     20d !   iadd                                   ;; Σωρός: [fib(n-1)+fib(n-2)]
    ||     20e !   ireturn
#### Φως στο τούνελ του Java JIT μεταγλωττιστή Η αποστολή μας, αν τη δεχθούμε, είναι να βρούμε και να αναλύσουμε τον κώδικα μηχανής που παρήγαγε ο JIT μεταγλωττιστής (native κώδικας). Το πρόβλημα είναι ότι δε γνωρίζουμε καθόλου την εσωτερική λειτουργία του Sun Java διερμηνευτή (αφού ο κώδικας είναι κλειστός). Το μόνο σίγουρο είναι ότι ο εκτελέσιμος κώδικας μηχανής δημιουργείται δυναμικά κάπου στο χώρο διευθύνσεων της Java και ο έλεγχος κάποια στιγμή περνάει στο σημείο αυτό. Αρχίζοντας την εξερεύνηση μας εκτελούμε την ltrace -i -o fib.ltr java -server fib 10 (βλέπε προηγούμενα τεύχη). Εξετάζοντας το fib.ltr τα αποτελέσματα δεν είναι ιδιαίτερα ενθαρρυντικά, οπότε συνεχίζουμε με την strace -i -o fib.str java -server fib 10. Προς το τέλος του fib.str βρίσκουμε τα εξής:
    [400379db] open("/home/alf/projects/magaz/issue3/src/fib.class", O_RDONLY|O_LARGEFILE) = 5
    [401514e7] fstat64(5, {st_mode=S_IFREG|0644, st_size=561, ...}) = 0
    [401512f7] stat64("/home/alf/projects/magaz/issue3/src/fib.class", ...
    [40036eeb] read(5, "\312\376\272\276\0\0\0.\0$\n\0\7\0\22\n\0\23\0\24\t\0\25"..., 561) = 561
    [40036f5f] close(5)                     = 0
    [40118b71] gettimeofday({1081166745, 55282}, NULL) = 0
    [40118b71] gettimeofday({1081166745, 55460}, NULL) = 0
    [40118b71] gettimeofday({1081166745, 55594}, NULL) = 0
    [40118b71] gettimeofday({1081166745, 56101}, NULL) = 0
    [40118b71] gettimeofday({1081166745, 56241}, NULL) = 0

    ...

    [401516d7] lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
    [401516d7] lstat64("/home/alf", {st_mode=S_IFDIR|0711, st_size=4096, ...}) = 0
    [401516d7] lstat64("/home/alf/projects", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
    [401516d7] lstat64("/home/alf/projects/magaz", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
    [401516d7] lstat64("/home/alf/projects/magaz/issue3", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
    [401516d7] lstat64("/home/alf/projects/magaz/issue3/src", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
    [40118b71] gettimeofday({1081166745, 82175}, NULL) = 0
    [40118b71] gettimeofday({1081166745, 82915}, NULL) = 0
    [40118b71] gettimeofday({1081166745, 83546}, NULL) = 0
    [40118b71] gettimeofday({1081166745, 83719}, NULL) = 0
    [40118b71] gettimeofday({1081166745, 83851}, NULL) = 0

    ...

    [40159ac5] brk(0)                       = 0x80c4000
    [40159ac5] brk(0x80c5000)               = 0x80c5000
    [40159ac5] brk(0)                       = 0x80c5000
    [40159ac5] brk(0x80c6000)               = 0x80c6000
    [40036e5b] write(1, "89", 2)            = 2
    [40036e5b] write(1, "\n", 1)            = 1

    ...

    [400a8ac1] kill(789, SIGRTMIN)          = 0
    [400a8ac1] kill(789, SIGRTMIN)          = 0
    [40154c7d] unlink("/tmp/hsperfdata_alf/787") = 0

    ...
Παρατηρούμε πως η Java διαβάζει το .class αρχείο που περιέχει τα bytecodes της εφαρμογής μας. Ύστερα αρχίζει να διαβάζει συνεχώς την ώρα της ημέρας, μαθαίνει κάποια πράγματα για το τρέχον directory, συνεχίζει να δειγματοληπτεί την ώρα, αυξάνει το μέγεθος του data segment κατά 0x2000 bytes και τελικά τυπώνει το αποτέλεσμα (89). Ομολογουμένως οι πληροφορίες δεν είναι ιδιαίτερα χρήσιμες. Ένα ενδιαφέρον σημείο είναι το αρχείο /tmp/hsperfdata_alf/787 το οποίο βλέπουμε να διαγράφεται. Παραπάνω στο fib.str υπάρχει το σημείο που ανοίγει:
    [400379b8] open("/tmp/hsperfdata_alf/787", O_RDWR|O_CREAT|O_TRUNC, 0600) = 3
    [4015bd61] ftruncate(3, 16384)          = 0
    [4015d8ed] old_mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x4081e000
    [40036f41] close(3)                     = 0
Το /tmp/hsperfdata_alf/787 αντιστοιχείται (mapped) στις διευθύνσεις μνήμης 0x4081e000-0x40822000 και περαιτέρω προσπελάσεις γίνονται μέσα από αυτές τις διευθύνσεις. Μπορούμε να δούμε τα περιεχόμενα του αρχείου αν φροντίσουμε ώστε να αργήσει να τελειώσει το πρόγραμμα μας (πχ με java -server fib 100), για να μην προλάβει να σβηστεί το προσωρινό αυτό αρχείο. Μια σύντομη εξέταση του αρχείου μας δείχνει πως πρόκειται για πληροφορίες που χρησιμοποιεί ο HotSpot(TM) JIT μεταγλωττιστής της Sun. Έτσι εξηγείται και το "hsperfdata_alf" που μάλλον σημαίνει HotSpot(TM) Performance Data για τον χρήστη alf. Επίσης, το αρχείο αρχίζει με τα bytes 0xCA 0xFE 0xC0 0xC0. Η αναζήτηση στο internet για το νόημα των bytes ήταν άκαρπη, πάντως υποψιάζομαι ότι είναι το signature των αρχείων δεδομένων του JIT μεταγλωττιστή. Αν και (ελπίζω) ενδιαφέρουσα, η ως τώρα περιήγηση στον JIT μεταγλωττιστή δε μας οδήγησε πιο κοντά στη λύση. Παραμένει το καίριο ερώτημα για τη θέση του native κώδικα. Ένας συλλογισμός που ίσως μας οδηγήσει στη λύση είναι ο παρακάτω: Ο java διερμηνευτής αφού δημιουργήσει τον native κώδικα περνάει τον έλεγχο σε αυτόν. Ο κώδικας μας είναι αμιγώς cpu-intensive, δηλαδή χρησιμοποιεί πολύ τον επεξεργαστή και δεν έχει I/O που μπορούν να διακόψουν τη λειτουργία του. Αυτό σημαίνει πως αν σε μια τυχαία χρονική στιγμή εξετάσουμε σε ποια διεύθυνση δείχνει ο instruction pointer (IP) της διεργασίας, αυτή με πολύ μεγάλη πιθανότητα θα βρίσκεται μέσα στις διευθύνσεις που καταλαμβάνει ο native κώδικας. Επομένως, το πρόβλημα μας μετασχηματίστηκε στην εύρεση ενός τρόπου να παρακολουθούμε σε ποια διεύθυνση βρίσκεται η εκτέλεση κάποια διεργασίας! #### ...Σαν να ψάχνεις διεύθυνση στα άχυρα Μια πρώτη σκέψη είναι να εκτελέσουμε την java στο GDB, να διακόπτουμε το πρόγραμμα κάθε λίγο και να καταγράφουμε τις τιμές του IP. Απλό και αποτελεσματικό... μόνο που δε φαίνεται να λειτουργεί στη δική μας περίπτωση :(
    $ gdb -q java
    (no debugging symbols found)...(gdb) r -server fib 10
    Starting program: /usr/lib/j2sdk1.4.2_01/bin/java -server fib 10
    (no debugging symbols found)...[New Thread 16384 (LWP 1536)]
    (no debugging symbols found)...
    (no debugging symbols found)...Cannot find user-level thread for LWP 1536: generic error
    (gdb) info reg
    No selected frame.
    (gdb) q
    The program is running.  Exit anyway? (y or n) y
    Cannot find thread 16384: generic error
    (gdb) q
    The program is running.  Exit anyway? (y or n) y
    Cannot find thread 16384: generic error
Όχι μόνο δεν καταφέραμε να τρέξουμε τη Java αλλά "τα πήρε" και ο GDB. Αααργκ!!! Αν κάποιος ξέρει τι συμβαίνει παρακαλώ να μου γράψει... Μην απελπίζεστε! Ευτυχώς για εμάς, το πάντα χρήσιμο /proc προσφέρει την υπηρεσία που χρειαζόμαστε (τον τρέχων IP μιας διεργασίας). Η πληροφορία βρίσκεται καλά κρυμμένη μέσα στο /proc/#/stat και θα χρησιμοποιήσουμε την εντολή ps για να τη φέρουμε στην επιφάνεια. Συγκεκριμένα θα χρησιμοποιήσουμε τη παράμετρο -ο για να ορίσουμε τις πληροφορίες που θέλουμε να εμφανίζει η ps (βλέπε man page). Τώρα, λοιπόν, έχουμε όλα τα εργαλεία στα χέρια μας και μπορούμε να αρχίσουμε δουλειά! Με τις παρακάτω εντολές τρέχουμε τη java και κάθε 0.2 δευτερόλεπτα τυπώνουμε τον IP της (30 δείγματα).
    $ java -server fib 100 &
    [1] 1390
    $ for ((i=0;$i<30;i++)); do ps --pid=1390 -o eip; sleep 0.2; done
         EIP
    42900340
         EIP
    429003b8
         EIP
    4290033b
         EIP
    4290031c
         EIP
    4290030f
         EIP
    42900302
         EIP
    42900372
         EIP
    429002e0
         EIP
    429002e0
         EIP
    42900414
         EIP
    42900386
         EIP
    429003dc
         EIP
    429003d3
         EIP
    429002e0
         EIP
    4290044d
         EIP
    42900455
         EIP
    429003dc
         EIP
    429003b4
         EIP
    42900369
         EIP
    42900453
         EIP
    42900409
         EIP
    42900350
         EIP
    42900441
         EIP
    429003c4
         EIP
    429002e7
         EIP
    42900372
         EIP
    42900390
         EIP
    42900311
         EIP
    429003b4
         EIP
    429003b4
Παρατηρούμε πως ο κώδικας βρίσκεται σε ένα βρόχο με μικρότερη διεύθυνση την 0x429002e0 και μέγιστη 0x42900453. Οι διευθύνσεις αυτές μπορεί να μην είναι τα ακριβή άκρα του βρόχου, αφού έχουμε κάνει τυχαία δειγματοληψία, αλλά με μεγάλη πιθανότητα είναι κάπου εκεί κοντά. Το μόνο που μένει τώρα είναι να διαβάσουμε τον κώδικα που βρίσκεται σε αυτές τις θέσεις μνήμης. Για το σκοπό αυτό θα χρησιμοποιήσουμε και πάλι το /proc και συγκεκριμένα το "αρχείο" /proc/#/mem. Η πρόσβαση στο αρχείο γίνεται με τις γνωστές εντολές (open/fopen, lseek/fseek, read/fread) αλλά υπάρχουν δύο σημεία που πρέπει να προσεχθούν. Καταρχάς, για να μας επιτραπεί να διαβάσουμε από το mem αρχείο θα πρέπει η δική μας διεργασία να έχει "δεθεί" με τη διεργασία της οποίας τη μνήμη σκοπεύουμε να προσπελάσουμε. Αυτό το "δέσιμο" γίνεται μέσω της ptrace, η οποία μας δίνει το δικαίωμα να παρακολουθούμε τη διεργασία. Ένα δεύτερο σημείο προσοχής είναι ότι θα πρέπει η διεργασία που μας ενδιαφέρει να μην είναι σε κατάσταση εκτέλεσης. Αυτό επιτυγχάνεται στέλνοντάς της το σήμα SIGSTOP.
    $ kill -SIGSTOP 1390
    $ memread 1390 0x429002e0 0x200 < fib.bin
    Reading from Process: 1390 Address: 0x429002e0 Size: 512
    Successfully read 512 bytes!
Ο αριθμός των 512 bytes επιλέχθηκε αυθαίρετα, με μόνη προϋπόθεση να είναι μεγαλύτερος από τα όρια του βρόχου που βρήκαμε προηγουμένως. Ο κώδικας του memread: memread.c.gz. ### 4.3 Η ανάλυση Τώρα που επιτέλους έχουμε στα χέρια μας τον native κώδικα μπορούμε να αρχίσουμε την ανάλυση. Τι μυστικά κρύβει, άραγε, αυτό το μικρό αρχείο των 512 bytes; Φορτώνουμε το αρχείο στον ht με ht fib.bin και επιλέγουμε disasm/x86 mode (πατώντας space ή F6). Παρακάτω φαίνεται ο κώδικας με κάποια σχόλια:
    00000000 89842400d0ffff    mov     [esp-00003000], eax
    00000007 81ec24000000      sub     esp, 0x24
    0000000d 83f902            cmp     ecx, 0x2           ;; ecx=n
    00000010 0f8c5d010000      jl      0x173              ;; n < 2 ?
    00000016 89742414          mov     [esp+0x14], esi
    0000001a 896c2418          mov     [esp+0x18], ebp
    0000001e 897c241c          mov     [esp+0x1c], edi
    00000022 8be9              mov     ebp, ecx
    00000024 4d                dec     ebp                ;; ebp=n-1
    00000025 8bd9              mov     ebx, ecx
    00000027 83c3fb            add     ebx, fffffffb      ;; ebx=n-5
    0000002a 8bf1              mov     esi, ecx
    0000002c 83c6fe            add     esi, fffffffe      ;; esi=n-2
    0000002f 8bc1              mov     eax, ecx
    00000031 83c0fc            add     eax, fffffffc      ;; eax=n-4
    00000034 8bd1              mov     edx, ecx
    00000036 83c2fd            add     edx, fffffffd      ;; edx=n-3

    00000039 83fd02            cmp     ebp, 0x2
    0000003c 0f8c96000000      jl      0xd8               ;; n-1 < 2 => n < 3 ?
    00000042 83fe02            cmp     esi, 0x2
    00000045 7c40              jl      0x87               ;; n-2 < 2 => n < 4 ?
    00000047 8954240c          mov     [esp+0xc], edx     ;;
    0000004b 89442408          mov     [esp+0x8], eax     ;; Save registers (1)
    0000004f 895c2404          mov     [esp+0x4], ebx     ;;
    00000053 890c24            mov     [esp], ecx         ;;
    00000056 8bca              mov     ecx, edx
    00000058 90                nop
    00000059 90                nop
    0000005a 90                nop
    0000005b e8a0ffffff        call    0x0                ;; call fib(n-3)
    00000060 89442410          mov     [esp+0x10], eax
    00000064 8b4c2408          mov     ecx, [esp+0x8]
    00000068 90                nop
    00000069 90                nop
    0000006a 90                nop
    0000006b e890ffffff        call    0x0                ;; call fib(n-4)
    00000070 8bf8              mov     edi, eax
    00000072 037c2410          add     edi, [esp+0x10]    ;; edi = fib(n-3)+fib(n-4)
    00000076 8b0c24            mov     ecx, [esp]         ;;
    00000079 8b5c2404          mov     ebx, [esp+0x4]     ;; Restore registers (1)
    0000007d 8b442408          mov     eax, [esp+0x8]     ;;
    00000081 8b54240c          mov     edx, [esp+0xc]     ;;
    00000085 eb02              jmp     0x89

    00000087 8bfe              mov     edi, esi           ;; edi = n-2
    00000089 83fa02            cmp     edx, 0x2
    0000008c 7c26              jl      0xb4               ;; n-3 < 2 => n < 5 ?
    0000008e 8954240c          mov     [esp+0xc], edx     ;;
    00000092 89442408          mov     [esp+0x8], eax     ;; Save registers (2)
    00000096 895c2404          mov     [esp+0x4], ebx     ;;
    0000009a 890c24            mov     [esp], ecx         ;;
    0000009d 8bc8              mov     ecx, eax
    0000009f e85cffffff        call    0x0                ;; call fib(n-4)
    000000a4 8be8              mov     ebp, eax
    000000a6 8b4c2404          mov     ecx, [esp+0x4]
    000000aa 90                nop
    000000ab e850ffffff        call    0x0                ;; call fib(n-5)
    000000b0 03c5              add     eax, ebp           ;; eax = fib(n-4) + fib(n-5)
    000000b2 eb11              jmp     0xc5

    000000b4 8954240c          mov     [esp+0xc], edx     ;;
    000000b8 89442408          mov     [esp+0x8], eax     ;; Save registers (3)
    000000bc 895c2404          mov     [esp+0x4], ebx     ;;
    000000c0 890c24            mov     [esp], ecx         ;;
    000000c3 8bc2              mov     eax, edx
    000000c5 8be8              mov     ebp, eax
    000000c7 03ef              add     ebp, edi
    000000c9 8b0c24            mov     ecx, [esp]         ;;
    000000cc 8b5c2404          mov     ebx, [esp+0x4]     ;; Restore registers (2,3)
    000000d0 8b442408          mov     eax, [esp+0x8]     ;;
    000000d4 8b54240c          mov     edx, [esp+0xc]     ;;

    000000d8 83fe02            cmp     esi, 0x2
    000000db 0f8c80000000      jl      0x161              ;; n-2 < 2 => n < 4
    000000e1 83fa02            cmp     edx, 0x2
    000000e4 7c39              jl      0x11f              ;; n-3 < 2 => n < 5
    000000e6 8bfa              mov     edi, edx
    000000e8 89442408          mov     [esp+0x8], eax     ;;
    000000ec 895c2404          mov     [esp+0x4], ebx     ;; Save registers (4)
    000000f0 890c24            mov     [esp], ecx         ;;
    000000f3 8bc8              mov     ecx, eax
    000000f5 90                nop
    000000f6 90                nop
    000000f7 e804ffffff        call    0x0                ;; call fib(n-4)
    000000fc 8944240c          mov     [esp+0xc], eax
    00000100 8b4c2404          mov     ecx, [esp+0x4]
    00000104 90                nop
    00000105 90                nop
    00000106 90                nop
    00000107 e8f4feffff        call    0x0                ;; call fib(n-5)
    0000010c 8bf8              mov     edi, eax
    0000010e 037c240c          add     edi, [esp+0xc]     ;; edi = fib(n-4) + fib(n-5)
    00000112 8b0c24            mov     ecx, [esp]         ;;
    00000115 8b5c2404          mov     ebx, [esp+0x4]     ;; Restore registers (4)
    00000119 8b442408          mov     eax, [esp+0x8]     ;;
    0000011d eb02              jmp     0x121

    0000011f 8bfa              mov     edi, edx
    00000121 83f802            cmp     eax, 0x2
    00000124 7c37              jl      0x15d              ;; n-4 < 2 => n < 6
    00000126 890424            mov     [esp], eax
    00000129 8bf1              mov     esi, ecx
    0000012b 8bcb              mov     ecx, ebx
    0000012d 90                nop
    0000012e 90                nop
    0000012f e8ccfeffff        call    0x0                ;; call fib(n-5)
    00000134 890424            mov     [esp], eax
    00000137 8bce              mov     ecx, esi
    00000139 83c1fa            add     ecx, fffffffa      ;; n' = n - 6
    0000013c 90                nop
    0000013d 90                nop
    0000013e 90                nop
    0000013f e8bcfeffff        call    0x0                ;; call fib(n-6)
    00000144 030424            add     eax, [esp]         ;; eax = fib(n-5) + fib(n-6)
    00000147 eb14              jmp     0x15d
    00000149 8b6c2418          mov     ebp, [esp+0x18]
    0000014d 8b7c241c          mov     edi, [esp+0x1c]
    00000151 8b742414          mov     esi, [esp+0x14]
    00000155 83c424            add     esp, 0x24
    00000158 e9a377ffff        jmp     ffff7900
    0000015d 03c7              add     eax, edi
    0000015f eb02              jmp     0x163
    00000161 8bc6              mov     eax, esi
    00000163 03c5              add     eax, ebp
    00000165 8b6c2418          mov     ebp, [esp+0x18]
    00000169 8b7c241c          mov     edi, [esp+0x1c]
    0000016d 8b742414          mov     esi, [esp+0x14]
    00000171 eb02              jmp     0x175
    00000173 8bc1              mov     eax, ecx
    00000175 83c424            add     esp, 0x24
    00000178 c3                ret

    00000179 90                nop                        ;; Από εδώ και κάτω είναι άχρηστος (για τους σκοπούς μας) κώδικας
    0000017a 90                nop
    0000017b 8bc8              mov     ecx, eax
    0000017d ebca              jmp     0x149
    0000017f 8bc8              mov     ecx, eax
    00000181 ebc6              jmp     0x149
    00000183 8bc8              mov     ecx, eax
    00000185 ebc2              jmp     0x149
    00000187 8bc8              mov     ecx, eax
    00000189 ebbe              jmp     0x149
    0000018b 8bc8              mov     ecx, eax
    0000018d ebba              jmp     0x149
    0000018f 8bc8              mov     ecx, eax
    00000191 ebb6              jmp     0x149
    00000193 8bc8              mov     ecx, eax
    00000195 ebb2              jmp     0x149
    00000197 8bc8              mov     ecx, eax
    00000199 ebae              jmp     0x149
Το παραπάνω listing αν και μεγάλο σε μήκος είναι στην ουσία αρκετά απλό. Οι βασικοί λόγοι για τους οποίους φαίνεται μπερδεμένο είναι οι συνεχείς μετακινήσεις προς και από το σωρό και οι επαναχρησιμοποίηση των καταχωρητών για εντελώς διαφορετικό σκοπό (πχ ο ebp αρχικά εκφράζει το n-1 ενώ αργότερα, από τη διεύθυνση 0xc7 και πέρα περιέχει κάποιο άλλο άθροισμα). Το πρώτο βήμα για την ανάλυση κώδικα σε assembly είναι συνήθως η επισύναψη σχολίων πάνω στον κώδικα όπως παραπάνω. Ένας καλός τρόπος για να συνεχίσουμε είναι η σταδιακή μετατροπή του κώδικα σε κάποια πιο υψηλή και γνώριμη μορφή, αγνοώντας περιττές λεπτομέρειες. Για παράδειγμα, το κομμάτι ανάμεσα στις διευθύνσεις 0x39 και 0x87 θα μπορούσε ως επόμενο βήμα να γραφτεί:
        if (n-1 < 2) goto 0xd8
        if (n-4 < 2) goto 0x87
        edi = fib(n-3) + fib(n-4)
        goto 89
    87: edi = n-2
    89:
και ύστερα, αναγνωρίζοντας τη δομή if-else που υπάρχει:
        if (n-1 < 2) goto 0xd8
        if (n-4 >= 2)
            edi = fib(n-3) + fib(n-4);
        else
            edi = n-2;
    89:
Ακολουθώντας μια τέτοια διαδικασία τελικά καταλήγουμε στον παρακάτω Java/C κώδικα που εκφράζει πιο ξεκάθαρα την λειτουργία του assembly κώδικα:
    int fib_jit(int n)
    {
            int C,D;

            if (n < 2)
            return n;

            if (n >= 3) {
                    int A,B;

                    if (n >= 4)
                            A=fib_jit(n-3)+fib_jit(n-4);
                    else
                            A=n-2;

                    if (n >= 5)
                            B=fib_jit(n-4)+fib_jit(n-5);
                    else
                            B=n-3;

                    C=A+B;
            }
            else
                    C=n-1;

            if (n >= 4) {
                    int A,B;

                    if (n >= 5)
                            A=fib_jit(n-4)+fib_jit(n-5);
                    else
                            A=n-3;

                    if (n >= 6)
                            B=fib_jit(n-5)+fib_jit(n-6);
                    else
                            B=n-4;

                    D=A+B;
            }
            else
                    D=n-2;

            return D+C;
    }
Βέβαια, ακόμη και με την παραπάνω μορφή το τι ακριβώς συμβαίνει και γιατί λειτουργεί το κατασκεύασμα του JIT μεταγλωττιστή παραμένει κάπως μυστήριο. Για να ανακαλύψουμε όλη την αλήθεια θα πρέπει να θυμηθούμε την αρχική συνάρτηση fib. Αυτή μπορεί να ξαναγραφτεί ως:
    public static int fib(int n) {
        int R;

        if (n >= 2)
            R=fib(n-1) + fib(n-2);
        else
            R=n;

        return R;
    }
Σας θυμίζει τίποτα; Η δομή if-else που υπάρχει στη αρχική fib φαίνεται να μοιάζει πολύ με τις if-else που υπάρχουν διάσπαρτες στην fib_jit. Για την ακρίβεια, οι δομές που υπάρχουν στην fib_jit είναι η fib() για διάφορες παραστάσεις του n!!! Για να γίνει πιο κατανοητό το παραπάνω θεωρήστε το:
    if (n >= 4)
        A = fib_jit(n-3)+fib_jit(n-4);
    else
        A = n-2;
το οποίο πρακτικά είναι το:
    Α=fib(n-2);
Κάνοντας τις παραπάνω αντικαταστάσεις και κάποιες απλοποιήσεις προκύπτει η εξής fib_jit1:
    int fib_jit1(int n)
    {
            int C,D;

            if (n < 2)
            return n;

            if (n >= 3)
                    C = fib(n-2) + fib(n-3);
            else
                    C = n-1;

            if (n >= 4)
                    D = fib(n-3) + fib(n-4);
            else
                    D = n-2;

            return D+C;
    }
Χμμ, εμφανίστηκαν πάλι οι γνωστές δομές if-else. Είναι σαν τον κώδικα της μαρμότας... ξαναζείς τον ίδιο κάθε φορά :) Αν αντικαταστήσουμε και αυτές τις δομές με την αντίστοιχη fib προκύπτει:
    int fib_jit2(int n)
    {

            if (n < 2)
            return n;

            return fib(n-1) + fib(n-2);
    }
H παραπάνω συνάρτηση δεν είναι άλλη από τη αυθεντική fib! Επομένως φτάσαμε στο συμπέρασμα πως fib=fib_jit και άρα όντως η fib_jit λειτουργεί σωστά, αφού στην ουσία είναι η ίδια η fib σε άλλη μορφή. Τώρα πιστεύω πως είναι σαφές τι "μαγικά" έκανε ο JIT μεταγλωττιστής της Java. Ουσιαστικά έκανε inlining των αναδρομικών κλήσεων της fib σε δύο επίπεδα βάθους! Αυτό είχε ως αποτέλεσμα να αποφευχθεί ένα μέρος του κόστους των κλήσεων συναρτήσεων και επομένως να αυξηθεί η ταχύτητα εκτέλεσης. ### 4.4 Το συμπέρασμα Τελικά είναι η Java πιο γρήγορη από τη C; Θέλω να ελπίζω πως θα συμφωνήσετε μαζί στο ότι αυτή είναι η λάθος ερώτηση! Αυτό που πρέπει να ρωτήσουμε είναι αν ο JIT μεταγλωττιστής παράγει πιο γρήγορο κώδικα από τον gcc. Η απάντηση είναι πως ειδικά σε αυτή τη περίπτωση ναι και σε γενικές γραμμές παράγει εξίσου καλό κώδικα. Αν συνεχίσουμε το πείραμα και μεταγλωττίσουμε τη συνάρτηση fib_jit() (σε C) με τον gcc, το τελικό εκτελέσιμο θα έχει ελαφρώς καλύτερη απόδοση από αυτό της Java. Το θέμα εδώ είναι ότι ο JIT μας απαλλάσσει από αυτή τη διαδικασία. Βέβαια, για να υπερασπιστούμε λίγο και τον gcc, o JIT μεταγλωττιστής έχει το πλεονέκτημα πως έχει πρόσβαση και στις πληροφορίες δυναμικής εκτέλεσης του προγράμματος ενώ ο gcc μπορεί μόνο στατικά να ελέγξει τον κώδικα. Ηθικό δίδαγμα: οι γλώσσες δε μπορούν να συγκριθούν με κριτήριο την ταχύτητα, μόνο οι μεταγλωττιστές τους μπορούν. Επόμενο Προηγούμενο Περιεχόμενα