2015-06-28 17 views
6

Ho sperimentato un po 'con le funzioni e ho scoperto che l'ordine degli argomenti è invertito in memoria. Perché?Perché l'ordine degli argomenti delle funzioni è invertito?

pila-test.cpp:

#include <stdio.h> 

void test(int a, int b, int c) { 
    printf("%p %p %p\n", &a, &b, &c); 
    printf("%d %d\n", *(&b - 1), *(&b + 1)); 
} 

int main() { 
    test(1,2,3); 
    return 0; 
} 

clang:

$ clang++ stack-test.cpp && ./a.out 
0x7fffb9bb816c 0x7fffb9bb8168 0x7fffb9bb8164 
3 1 

GCC:

$ g++ stack-test.cpp && ./a.out 
0x7ffe0b983b3c 0x7ffe0b983b38 0x7ffe0b983b34 
3 1 

EDIT: Non duplicare: Ordine di valutazione può essere diverso da layout della memoria , quindi è una domanda diversa.

+0

possibile duplicato di [Compilatori e ordine di argomentazione di valutazione in C++] (http: // stackoverflow.it/questions/621542/compilers-and-argument-order-of-evaluation-in-c) – Steephen

+1

@Steephen Ordine di valutazione, davvero? – kravemir

+0

@Miro è specifico dell'implementazione e l'ordine degli argomenti viene elaborato dal compilatore – Steephen

risposta

7

la convenzione di chiamata dipende l'attuazione.

Ma per supportare le funzioni variadiche di C (in C++ espresse con l'ellissi ... nell'elenco di argomenti formali), gli argomenti vengono solitamente inseriti, oppure viene riservato uno spazio di stack per essi, nell'ordine da destra a sinistra. Questo è solitamente chiamato (1)C convenzione di chiamata. Con questa convenzione e la convenzione comune secondo la quale lo stack della macchina cresce verso il basso in memoria, il primo argomento dovrebbe finire con l'indirizzo più basso, opposto al risultato.

E quando compilo il tuo programma con MinGW g ++ 5.1, che è a 64 bit, ottengo

000000000023FE30 000000000023FE38 000000000023FE40 

E quando ho compilare il programma con 32 bit di Visual C++ 2015, ottengo

00BFFC5C 00BFFC60 00BFFC64 

Ed entrambi di questi risultati sono coerenti con la convenzione di chiamata C, contrariamente al risultato.

Quindi la conclusione sembra essere che il tuo compilatore ha un valore predefinito diverso dalla convenzione di chiamata C, almeno per le funzioni non-variadiche.

Si potrebbe verificare questo aggiungendo un ... alla fine dell'elenco degli argomenti formali.


1) La convenzione di chiamata C comprende anche che è il chiamante che regola lo stack pointer quando la funzione ritorna, ma questo è irrilevante.

+0

Prendere l'indirizzo degli argomenti probabilmente ha qualcosa a che fare con esso, poiché ciò significa che non possono essere (solo) memorizzati nei registri –

+0

Ho provato a testare la direzione di crescita dello stack usando la ricorsione, e cresce in diminuzione nella memoria. Quindi, è una convenzione di chiamata diversa da quella che hai affermato. Ora, indagherò sulle convenzioni di chiamata e perché ne sta usando una diversa :) – kravemir

+0

È proprio il fatto che la convenzione di chiamata in x86-64 è per definizione basata su registri (presumo che la macchina non sia Arm64 o Sparc64, o Itanium, Alpha64, ecc., poiché sono piuttosto rari). Non esiste una convenzione di tipo 'C' - spetta al progettista del compilatore creare qualcosa che funzioni - sebbene uno molto comune spinga effettivamente le cose da destra a sinistra e il chiamante lo aggiusta, non è NIENTE che lo dice dev'essere in questo modo (come mostrato ad esempio x86-64) –

7

Questo comportamento è specifico dell'implementazione.

Nel tuo caso, è perché gli argomenti sono messi in pila. Qui uno interesting article che mostra il tipico layout di memoria di un processo, che mostra come lo stack si riduce. Il primo argomento che viene inserito nello stack avrà quindi l'indirizzo più alto.

+0

Thx, ma questo non risponde * perché * :) È ovvio che il comportamento è specifico dell'implementazione. Tuttavia, sto chiedendo perché è invertito, perché non è il primo argomento prima nella memoria? – kravemir

+0

@Miro Ho modificato con un collegamento che spiega che lo stack si riduce (almeno su windows e linux) – Christophe

+0

+ per il collegamento, ma la risposta accettata è migliore :) – kravemir

1

Il codice C (e C++) utilizza lo stack del processore per passare argomenti a funzioni.

Il funzionamento dello stack dipende dal processore. Lo stack può (teoricamente) crescere verso il basso o verso l'alto. Quindi il tuo processore sta definendo, se gli indirizzi crescono o si riducono. Infine, non è responsabile solo dell'architettura del processore, ma ci sono calling conventions per il codice in esecuzione su un'architettura.

Le convenzioni di chiamata dicono, come gli argomenti devono essere messi in pila per una specifica architettura del processore. Le convenzioni sono necessarie, che le librerie di diversi compilatori possono essere collegate tra loro.

Fondamentalmente, per te come utente C normalmente non fa differenza, se gli indirizzi delle variabili nello stack crescono o si riducono.

Dettagli:

+0

C non fa nulla del genere. Alcune implementazioni * possono * farlo, ma non è richiesto dalla lingua. Ad esempio, alcuni processori hanno serie di registri molto grandi per facilitare il passaggio degli argomenti. –

+0

@AndrewMedico: Quello che intendevo, sono le convenzioni di chiamata per le architetture, che riguardano anche il codice C. Pertanto, lo standard C non lo definisce, ma è necessario che un'architettura definisca tali standard in modo che i diversi output del compilatore possano essere collegati tra loro. Vedi il mio link. Ho chiarito la mia frase. – Juergen

+0

Questo non è in effetti definito dall'architettura. È definito da ABI e può avere più ABI per un'architettura e il processore non ha alcuna idea sull'ABI, è solo un contratto per programmatori e compilatori. –

2

Lo standard C (e C++) non definisce l'ordine degli argomenti che vengono passati o il modo in cui devono essere organizzati in memoria. Spetta allo sviluppatore del compilatore (di solito in collaborazione con gli sviluppatori del sistema operativo) creare qualcosa che funzioni su una particolare architettura del processore.

Nelle architetture MOST, lo stack (ei registri) viene utilizzato per passare argomenti a una funzione, e ancora, per le architetture MOST, lo stack cresce da indirizzi "alto a basso" e nella maggior parte delle implementazioni C, l'ordine di essi vengono passati sono "di sinistra ultimo", quindi se abbiamo una funzione

void test(int a, int b, int c) 

poi argomenti sono passati nell'ordine:

c, b, a 

alla funzione.

Tuttavia, ciò che complica è quando il valore degli argomenti viene passato nei registri e il codice che utilizza gli argomenti sta prendendo l'indirizzo di quegli argomenti - i registri non hanno indirizzi, quindi non puoi prendere l'indirizzo di una variabile di registro. Quindi il compilatore genererà del codice per memorizzare l'indirizzo nello stack [da dove possiamo ottenere l'indirizzo del valore] localmente alla funzione. Questo dipende interamente dalla decisione del compilatore che ordina di farlo e sono abbastanza sicuro che questo è ciò che stai vedendo.

Se vi prendete il vostro codice e farlo passare attraverso clang, vediamo:

define void @test(i32 %a, i32 %b, i32 %c) #0 { 
entry: 
    %a.addr = alloca i32, align 4 
    %b.addr = alloca i32, align 4 
    %c.addr = alloca i32, align 4 
    store i32 %a, i32* %a.addr, align 4 
    store i32 %b, i32* %b.addr, align 4 
    store i32 %c, i32* %c.addr, align 4 
    %call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str, i32 0, i32 0), i32* %a.addr, i32* %b.addr, i32* %c.addr) 
    %add.ptr = getelementptr inbounds i32, i32* %b.addr, i64 -1 
    %0 = load i32, i32* %add.ptr, align 4 
    %add.ptr1 = getelementptr inbounds i32, i32* %b.addr, i64 1 
    %1 = load i32, i32* %add.ptr1, align 4 
    %call2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.1, i32 0, i32 0), i32 %0, i32 %1) 
    ret void 
} 

Anche se non può essere del tutto banale per leggere, si possono vedere le prime righe del test-funzione è:

%a.addr = alloca i32, align 4 
    %b.addr = alloca i32, align 4 
    %c.addr = alloca i32, align 4 
    store i32 %a, i32* %a.addr, align 4 
    store i32 %b, i32* %b.addr, align 4 
    store i32 %c, i32* %c.addr, align 4 

Questo sta creando essenzialmente spazio sulla pila (%alloca) e memorizzare le variabili a, b e c in quelle posizioni.

Ancora meno facile da leggere è il codice assembler che gcc genera, ma si può vedere una cosa simile accade qui:

subq $16, %rsp   ; <-- "alloca" for 4 integers. 
movl %edi, -4(%rbp)  ; Store a, b and c. 
movl %esi, -8(%rbp) 
movl %edx, -12(%rbp) 
leaq -12(%rbp), %rcx  ; Take address of ... 
leaq -8(%rbp), %rdx 
leaq -4(%rbp), %rax 
movq %rax, %rsi 
movl $.LC0, %edi 
movl $0, %eax 
call printf    ; Call printf. 

si potrebbe chiedere perché si alloca spazio per 4 interi - questo è perché lo stack dovrebbe essere sempre allineati a 16 byte in x86-64.

1

L'ABI definisce come passare i parametri.

Nell'esempio, è leggermente complicato poiché l'ABI x86_64 predefinito di gcc e clang passa i parametri sui registri (*), non c'era alcun indirizzo per essi.

Quindi si fa riferimento ai parametri, quindi il compilatore è obbligato ad allocare memoria locale per tali variabili e quell'ordinamento e il layout della memoria sono anche specifici dell'implementazione.

  • Nota: fino a 6 parametri banali, se ce ne sono di più, passano allo stack.
  • Riferimento: x86_64 ABI
0

Parlando a 32 bit x86 di Windows

Risposta breve: puntatore per gli argomenti della funzione non è necessario puntatore allo stack in cui sono stati spinti sulla chiamata di funzione reale, ma può essere ovunque il compilatore ha spostato la variabile.

Risposta lunga: incontrato lo stesso problema durante la conversione il mio codice da bcc32 (Embarcadero compilatore classico) per CLANG. Il codice RPC generato dal compilatore MIDL è stato interrotto perché gli argomenti della funzione RPC traducono gli argomenti serializzati portando il puntatore al primo argomento della funzione, assumendo che tutti gli argomenti seguenti seguiranno come ad es. Serializzare (& a).

chiamate di funzione CDECL debug generate dalla BCC32 e CLANG:

  • BCC32: argomenti della funzione sono passati nell'ordine corretto in pila, poi, quando è richiesta l'indirizzo argomentazione, l'indirizzo stack è dato direttamente.

  • CLANG: argomenti della funzione sono passati nell'ordine corretto in pila, tuttavia nella funzione attuale, una copia di tutti gli argomenti è fatta in memoria in ordine inverso della pila, e quando è necessaria indirizzo argomento della funzione, l'indirizzo di memoria è dato direttamente, con conseguente ordine ripristinato.

Altrimenti detto, non assumere come gli argomenti di funzione sono disposti in memoria dall'interno del codice funzione C/C++. Il suo compilatore dipende.

Nel mio caso, una possibile soluzione è dichiarare le funzioni RPC con la convenzione di chiamata pascal (Win32), forzando il compilatore MIDL ad analizzare gli argomenti singolarmente. Sfortunatamente il codice generato da MIDL è un codice pesante e malvagio che richiede molti aggiustamenti per essere compilato, ancora non completato)

Problemi correlati