2016-06-09 20 views
3

Ich habe ein sehr einfaches C-Programm, das ich mit GDB bin mit mehr über den Stapel lernen:Wie groß ist der Abstand zwischen argv und argc auf meinem Stack?

#include<stdlib.h> 
#include<stdio.h> 

int main(int argc, char* argv[]){ 
    printf("argc is %d", argc); 
    int i = 0; 
    for(i; i<argc; i++){ 
    printf("argv at %d is %s", i, argv[i]); 
    } 
    return; 
} 

ich dieses Programm kompilierte gcc foo.c -g verwenden und es dann mit gdb ./a.out gdb laufen. Innerhalb GDB, habe ich einen Haltepunkt an Haupt b main verwenden und zeigen dann den Stapelzeiger und Basiszeiger:

Reading symbols from ./a.out...done. 
(gdb) b main 
Breakpoint 1 at 0x40053c: file foo.c, line 5. 
(gdb) r 
Starting program: /tmp/a.out 

Breakpoint 1, main (argc=1, argv=0x7fffffffdf48) at foo.c:5 
5  printf("argc is %d", argc); 
(gdb) p $sp 
$1 = (void *) 0x7fffffffde40 

(gdb) p $rbp 
$2 = (void *) 0x7fffffffde60 

(gdb) x/8x $sp 
0x7fffffffde40: 0xffffdf48 0x00007fff 0x00400440 0x00000001 
0x7fffffffde50: 0xffffdf40 0x00007fff 0x00000000 0x00000000 

(gdb) p &argv 
$3 = (char ***) 0x7fffffffde40 
(gdb) p &argc 
$4 = (int *) 0x7fffffffde4c 

So kann ich hier sehen, dass argv verweist auf die gleiche Adresse wie sp $, die oben auf der Stapel, 0x7fffffffde40. Und ich sehe auch, dass die Adresse von argc kurz darauf unter 0x7fffffffde4c ist.

Allerdings bin ich nicht sicher, was die Daten an 0x7fffffffde48 durch 0x7fffffffde4b halten. Ist es etwas Wichtiges oder nur Müll? Warum liegt argv nicht direkt neben argc auf dem Stack?

Danke!

+0

Related ? http://StackOverflow.com/Questions/1408900/Question-about-Pointer-Ausrichtung – rrauenza

+0

Könnte verwandt sein: http://StackOverflow.com/A/37713560/257645 (erklärt, was _Start tut, die den Stapel mit ArgC/setzt argv) – kfsone

Antwort

3

In dem x86-64 System V ABI, Funktion args werden in Registern übergeben. (Links zu anderen ABI-Dokumenten und Erklärungen zu einer ABI-Dokumentation finden Sie unter tag wiki.)

Sie haben nur Adressen überhaupt, weil gcc -O0 sie an den Stapel verschüttet. Dies macht das Debuggen von C/C++ einfacher/konsistenter: alles hat eine Adresse, und der dort gespeicherte Wert ist nach jeder C-Anweisung immer aktuell. Es macht jedoch schrecklich ineffizienten ASM-Code. gcc -Og ist nicht so strikt bei der Speicherung in den Speicher, so dass Sie manchmal "Wert optimiert aus", aber es ist immer noch "für das Debuggen optimiert". Das andere Ziel von gcc -O0 ist, schnell zu kompilieren, nicht, um guten Code zu machen. Wundern Sie sich also nicht, dass Sie nicht optimale Entscheidungen treffen, wenn Sie Locals auf dem Stack platzieren. z.B. es könnte nur 16 Bytes reserviert haben und argv bei [rbp-16] (8 Byte ausgerichtet), argc bei [rbp-8] (4 Byte ausgerichtet) setzen und das 4B temporär bei [rbp-4] halten, wie die tatsächliche Wahl von gcc5.3.

Der einzige "Grund" für die Lücke zwischen ihren tatsächlichen Speicherorten ist die innere Funktionsweise der Algorithmen von gcc zur Erstellung von Locals, bevor weitere Optimierungen durchgeführt werden.


Um zu sehen, was wirklich passiert, wenn Sie eine Funktion kompilieren, von -O3 -march=native -fverbose-asm oder etwas an asm-Ausgang (-S) buchen. (Tun Sie dies mit Funktionen, die Eingaben verwenden und einen Wert zurückgeben, statt Kompilierung-Konstante Eingänge, so dass sie nicht optimieren weg.)

Dies ist der Beginn der main(), wie gcc 5.3 on the Godbolt Compiler Explorer (with -O0 -fverbose-asm) zusammengestellt:

main: 
    push rbp  # 
    mov  rbp, rsp #, 
    sub  rsp, 32 #, 
    mov  DWORD PTR [rbp-20], edi # argc, argc 
    mov  QWORD PTR [rbp-32], rsi # argv, argv 
    mov  eax, DWORD PTR [rbp-20] # tmp92, argc  # see how dumb gcc -O0 is: it reloads from memory instead of using the value in edi 
    ... 

Auf Funktionseingabe, edi hält ArgC, und rsi hält Argv. main() 's Aufrufer (der libc C-Laufzeit-Startup-Code) legen Sie sie dort. mov QWORD PTR [rbp-32], rsi ist die Anweisung, die argv am Ende des reservierten Speicherplatzes speichert (mit sub rsp, 32). [rbp-32] ist zufällig die gleiche Adresse wie [rsp], aber da gcc die Mühe machte, einen Stapelrahmen (ist nur der Standard bei -O1 oder höher) zu machen, adressiert es Einheimische mit Offsets von rbp.


In der 32-Bit-SysV ABI, würden diese Argumente in Erinnerung sein bereits auf dem Stapel auf Funktion Eintrag, denn das ABI leider keine Register für arg-passing zu verwenden ist. Die zusätzlichen Befehle und die Latenzzeit, die durch die zusätzlichen Weiterleitungs-Rundlaufvorgänge auferlegt werden, die von der Alt-ABI benötigt werden, sind einer der Gründe, warum 32 Bit langsamer als 64 Bit sind, sogar abgesehen von den Überläufen/Neuladungen, die durch weniger Register verursacht werden. Einige 32-Bit-Windows-ABIs verwenden 2 Regs zum Arg-Passing, z. die __vectorcall ABI. Das ist gut, denn viele Windows-Programme werden immer noch als 32bit verbreitet. (64-Bit-Linux-Systeme in der Regel müssen keine 32-Bit-Code ausführen.)


BTW, die ABI Standarddokumente wie argc/argv/envp auf den Stapel gelegt werden für Ihren neuer execve(2) ed Prozess, und dass Bei den meisten anderen Registern als %rsp muss davon ausgegangen werden, dass sie Müll enthalten. d. h. die Prozess Startup-Umgebung für _start, die sich deutlich unterscheidet, was der C-Laufzeitcode vor dem Aufruf main() erstellt. z.B. Beim Eintrag auf _start ist der Anfang des Stapels keine Absenderadresse, Sie können also nicht ret. (Sie haben einen exit(2) Systemaufruf zu machen, das ist, was schließlich passiert ist, nachdem Sie von main() zurückzukehren.)

Siehe Tag Wiki für viele andere Links zu docs/tutorials/Anfänger-Fragen


+0

Kurz gesagt, in x64 werden die Argumente immer vom Register übergeben ... '-O0' wird sie in den Stack des' callee' verschütten, und ich nehme an, dass es eine Verschüttungskonvention gibt, wenn die Anzahl der Variablen vom 'caller' 'die Anzahl der Register überschreiten. Und das Verschütten innerhalb des "Angerufenen" ist nicht notwendigerweise optimal oder streng definiert. Ist das nahe? – rrauenza

+0

@rrauenza: Ja, 6 Regs werden für die Übergabe von Integer-/Pointer-Args verwendet und darüber hinaus drückt der Aufrufer sie auf den Stack wie im 32-Bit-ABI. Es liegt ganz am Anrufer, was er mit ihnen machen soll. (Dies wird auch nicht als Spill bezeichnet, da die Werte * * im Speicher sein müssen. Wie ich es verstehe, gilt der Begriff "Überlauf" nur dann, wenn Sie etwas in einem Register gespeichert haben könnten, um ein erneutes Laden zu vermeiden Spätere Verwendung, dh das Speichern eines Ergebnisses in einem Array ist ebenfalls kein Überlauf, das ist ein erforderlicher Speicher.) –

+0

@rrauenza Ein Compiler speichert sie alle nur im Speicher von '-O0', um beim Debuggen zu helfen. Ein erneutes Laden aus dem Speicher, anstatt den Wert zu verwenden, der bereits in einem Register ist, wird * nur * durchgeführt, um das Debuggen zu unterstützen (z.B. 'set argc = 10' in gdb). d. h. GCC überspritzt alles, selbst wenn es nicht notwendig ist. Vergleichen Sie den Code mit "-O1" oder "-O3", und beachten Sie, wie er Werte um einen Funktionsaufruf herum lebendig hält, indem er sie in ein Anruf-beibehaltenes Register schreibt (das am Anfang/Ende der Funktion gespeichert/wiederhergestellt wird) , anstatt innerhalb der Schleife zu verschütten/neu zu laden. Also ja, immer nicht optimal und nicht standardisiert –

2

Es geht um Adressenausrichtung. Prozessoren bevorzugen bestimmte Typen, die an bestimmten Grenzen ausgerichtet sind. Manchmal ist es ein Hardwareleistungsproblem, manchmal wird es gar nicht funktionieren und eine Art Interrupt auslösen. (Manchmal werden Software-Traps hinzugefügt, um diese Ausnahmen zu behandeln/auszublenden, können aber die Leistung beeinträchtigen.)

Sie werden etwas ähnliches in Strukturen sehen. Eine Struktur wird nicht dicht gepackt, wenn Sie sie nicht manuell packen.

int main() { 
    struct { 
     char c; 
     void *p; 
     int i; 
    } a; 
    printf("%d\n", sizeof(char)); 
    printf("%d\n", sizeof(void *)); 
    printf("%d\n", sizeof(int)); 
    printf("%d\n", sizeof(a)); 
} 

Sie werden sehen, dass 1+8+424 nicht gleich. Das ist, weil die int und void * ausgerichtet sind:

$ ./a.out 
1 
8 
4 
24 

x86 und x86_64 sind hier aufgeführt: https://en.wikipedia.org/wiki/Data_structure_alignment#Typical_alignment_of_C_structs_on_x86

Hier ist auch eine schöne Tabelle von Intel: https://software.intel.com/en-us/articles/coding-for-performance-data-alignment-and-structures

+----------+---------+--------+ 
| DATATYPE | 32-BIT | 64-BIT | 
+----------+---------+--------+ 
|   |   |  | 
| char  | 1  | 1  | 
|   |   |  | 
| short | 2  | 2  | 
|   |   |  | 
| int  | 4  | 4  | 
|   |   |  | 
| long  | 8  | 8  | 
|   |   |  | 
| float | 4  | 4  | 
|   |   |  | 
| double | 8  | 8  | 
|   |   |  | 
| long  | long | 8  | 
|   |   |  | 
| long  | double | 4  | 
|   |   |  | 
| Any  | pointer | 4  | 
+----------+---------+--------+ 
+0

Dies scheint nicht wirklich die Frage zu beantworten. Es sieht so aus, als ob gcc 'argv' in' [rbp-16] ', zusammenhängend mit' argc' in '[rbp-20]' setzen könnte, aber das tut es nicht. –

+0

@PeterCordes Glaubst du, dass das für das Setup von main() einzigartig ist? – rrauenza

+0

Nein. Sehen Sie meine Bearbeitung zu meiner Antwort, wo ich versucht habe, diesen Teil besser zu adressieren. Es ist nur ein Artefakt der Algorithmen von gcc. –