История изменений
Исправление
kostyarin_,
(текущая версия)
:
Вообще, если брать общий случай, то программа под Linux выглядит примерно таким вот образом.
[куча]
|
V
^
|
[стек]
Типа куча растёт вниз, от начального адреса (который вовсе не нулевой, но пока этим не заморачивайся). А стек растёт снизу вверх. И в куче само тело программы, которое исполняется. А между ними неразмеченная память, которую нельзя использовать.
При запуске программы Linux кладёт в стек переменные окружения и аргументы. Потом передаёт управление точке входа программы. Для программ на Си в точке входа выполняется кое-какая работа, чтобы функция main
выглядела именно так как выглядит. И чтобы её результат был кодом завершения.
Программа состоит из функций. Для обычного случая, допусти, если нет оптимизаций и все функции публичные (т.е. не static и не inline и прочее) вызов функции в Linux следует договорённости fast-call. Т.е. частично используются регистры - -частично стек. Для простого случая, когда функция не имеет аргументов и ничего не возвращает – вызов функции проходит так:
- выровнять стек по 16-ти-байтной границе
- вытолкнуть в стек адрес следующей инструкции
- передать управление вызываемой функции
вызываемая функция при этом ещё и сохраняет базу в стеке, и возвращает её потом обратно
push %rbp
movq %rsp, %rbp
; код самой функции
pop %rbp
movq %rbp, %rsp
ret
Если gcc передать -fomit-frame-pointer, то базу сохранять она не будет. Но это пока опустим. Будет плясать от обычного случая. Т.е. везде при вызове функции используется стек. Как и в самой функции используется стек для локальных переменных. Например
int
do_domething () {
uint64_t value;
uint64_t another;
// тело функции
return 0;
}
У этой функции есть две локальные переменные. Когда функция вызвана, то их место – это место на вершине стека. Т.е., допустим есть вызов этой функции из main
int
main () {
do_something (); // <---- (1)
return EXIT_FAILURE; // <--- (2)
}
В момент вызова (1) происходит следующее:
- стек выравнивается, но он и так выровнен, так что пропускаем
- в стек помещается текущий адрес следующей за вызовом инструкции (следующая инструкция
return
(2)) - передаётся управление функции do_something
- do_domething помещает базу в стек
- do_domething использует стек для своих двух локальных переменных
(стек растёт снизу вверх)
[ место для uint64_t another; ] } subq $16, %rsp
[ место для uint64_t value; ] }
[база вытолкнутая сюда в do_domthing] push %rbp
[адрес возврата (2) ] call do_something
[стек до вызова do_domething ]
Таким образом, если do_something вызовет ещё какую-то функцию, то стек будет наращиваться сверху этого, и переменные сохраняться.
При возврате из do_domething стек разматывается обратно. Вот и всё. При этом то что было «записано» в стек – там и остаётся. Например, есть две функции, которые вызываются одна за другой
// gcc -Wall -Werror -Wno-uninitialized -O0 read_stack_left.c && ./a.out
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
void write ();
void read ();
int
main() {
write ();
read ();
return EXIT_SUCCESS;
}
void
write () {
uint64_t value = 50; // <--- будет лежать на стеке в своём месте
printf ("%ld was written\n", value);
}
void
read () {
uint64_t value; // <--- возьмёт то что там лежало
printf ("%ld was read\n", value);
}
Результатом которых будет
50 was written
50 was read
Т.е. первая функция пишет в переменную (место на стеке) 50, потом она возвращается и стек разматывается обратно. Во время вызова второй функции на месте уже ёё переменной уже записано 50.
Вот и всё. Это на самом деле просто, если сильно не углубляться в ненужные дебри.
Исправление
kostyarin_,
:
Вообще, если брать общий случай, то программа под Linux выглядит примерно таким вот образом.
[куча]
|
V
^
|
[стек]
Типа куча растёт вниз, от начального адреса (который вовсе не нулевой, но пока этим не заморачивайся). А стек растёт снизу вверх. И в куче само тело программы, которое исполняется. А между ними неразмеченная память, которую нельзя использовать.
При запуске программы Linux кладёт в стек переменные окружения и аргументы. Потом передаёт управление точке входа программы. Для программ на Си в точке входа выполняется кое-какая работа, чтобы функция main
выглядела именно так как выглядит. И чтобы её результат был кодом завершения.
Программа состоит из функций. Для обычного случая, допусти, если нет оптимизаций и все функции публичные (т.е. не static и не inline и прочее) вызов функции в Linux следует договорённости fast-call. Т.е. частично используются регистры - -частично стек. Для простого случая, когда функция не имеет аргументов и ничего не возвращает – вызов функции проходит так:
- выровнять стек по 16-ти-байтной границе
- вытолкнуть в стек текущий адрес
- передать управление вызываемой функции
вызываемая функция при этом ещё и сохраняет базу в стеке, и возвращает её потом обратно
push %rbp
movq %rsp, %rbp
; код самой функции
pop %rbp
movq %rbp, %rsp
ret
Если gcc передать -fomit-frame-pointer, то базу сохранять она не будет. Но это пока опустим. Будет плясать от обычного случая. Т.е. везде при вызове функции используется стек. Как и в самой функции используется стек для локальных переменных. Например
int
do_domething () {
uint64_t value;
uint64_t another;
// тело функции
return 0;
}
У этой функции есть две локальные переменные. Когда функция вызвана, то их место – это место на вершине стека. Т.е., допустим есть вызов этой функции из main
int
main () {
do_something (); // <---- (1)
return EXIT_FAILURE; // <--- (2)
}
В момент вызова (1) происходит следующее:
- стек выравнивается, но он и так выровнен, так что пропускаем
- в стек помещается текущий адрес следующей за вызовом инструкции (следующая инструкция
return
(2)) - передаётся управление функции do_something
- do_domething помещает базу в стек
- do_domething использует стек для своих двух локальных переменных
(стек растёт снизу вверх)
[ место для uint64_t another; ] } subq $16, %rsp
[ место для uint64_t value; ] }
[база вытолкнутая сюда в do_domthing] push %rbp
[адрес возврата (2) ] call do_something
[стек до вызова do_domething ]
Таким образом, если do_something вызовет ещё какую-то функцию, то стек будет наращиваться сверху этого, и переменные сохраняться.
При возврате из do_domething стек разматывается обратно. Вот и всё. При этом то что было «записано» в стек – там и остаётся. Например, есть две функции, которые вызываются одна за другой
// gcc -Wall -Werror -Wno-uninitialized -O0 read_stack_left.c && ./a.out
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
void write ();
void read ();
int
main() {
write ();
read ();
return EXIT_SUCCESS;
}
void
write () {
uint64_t value = 50; // <--- будет лежать на стеке в своём месте
printf ("%ld was written\n", value);
}
void
read () {
uint64_t value; // <--- возьмёт то что там лежало
printf ("%ld was read\n", value);
}
Результатом которых будет
50 was written
50 was read
Т.е. первая функция пишет в переменную (место на стеке) 50, потом она возвращается и стек разматывается обратно. Во время вызова второй функции на месте уже ёё переменной уже записано 50.
Вот и всё. Это на самом деле просто, если сильно не углубляться в ненужные дебри.
Исходная версия
kostyarin_,
:
Вообще, если брать общий случай, то программа под Linux выглядит примерно таким вот образом.
[куча]
|
V
^
|
[стек]
Типа куча растёт вниз, от начального адреса (который вовсе не нулевой, но пока этим не заморачивайся). А стек растёт снизу вверх. И в куче само тело программы, которое исполняется. А между ними неразмеченная память, которую нельзя использовать.
При запуске программы Linux кладёт в стек переменные окружения и аргументы. Потом передаёт управление точке входа программы. Для программ на Си в точке входа выполняется кое-какая работа, чтобы функция main
выглядела именно так как выглядит. И чтобы её результат был кодом завершения.
Программа состоит из функций. Для обычного случая, допусти, если нет оптимизаций и все функции публичные (т.е. не static и не inline и прочее) вызов функции в Linux следует договорённости fast-call. Т.е. частично используются регистры - -частично стек. Для простого случая, когда функция не имеет аргументов и ничего не возвращает – вызов функции проходит так:
[выровнять стек по 16-ти-байтной границе]
[вытолкнуть в стек текущий адрес ]
[передать управление вызываемой функции ]
вызываемая функция при этом ещё и сохраняет базу в стеке, и возвращает её потом обратно
push %rbp
movq %rsp, %rbp
; код самой функции
pop %rbp
movq %rbp, %rsp
ret
Если gcc передать -fomit-frame-pointer, то базу сохранять она не будет. Но это пока опустим. Будет плясать от обычного случая. Т.е. везде при вызове функции используется стек. Как и в самой функции используется стек для локальных переменных. Например
int
do_domething () {
uint64_t value;
uint64_t another;
// тело функции
return 0;
}
У этой функции есть две локальные переменные. Когда функция вызвана, то их место – это место на вершине стека. Т.е., допустим есть вызов этой функции из main
int
main () {
do_something (); // <---- (1)
return EXIT_FAILURE; // <--- (2)
}
В момент вызова (1) происходит следующее:
- стек выравнивается, но он и так выровнен, так что пропускаем
- в стек помещается текущий адрес следующей за вызовом инструкции (следующая инструкция
return
(2)) - передаётся управление функции do_something
- do_domething помещает базу в стек
- do_domething использует стек для своих двух локальных переменных
(стек растёт снизу вверх)
[ место для uint64_t another; ] } subq $16, %rsp
[ место для uint64_t value; ] }
[база вытолкнутая сюда в do_domthing] push %rbp
[адрес возврата (2) ] call do_something
[стек до вызова do_domething ]
Таким образом, если do_something вызовет ещё какую-то функцию, то стек будет наращиваться сверху этого, и переменные сохраняться.
При возврате из do_domething стек разматывается обратно. Вот и всё. При этом то что было «записано» в стек – там и остаётся. Например, есть две функции, которые вызываются одна за другой
// gcc -Wall -Werror -Wno-uninitialized -O0 read_stack_left.c && ./a.out
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
void write ();
void read ();
int
main() {
write ();
read ();
return EXIT_SUCCESS;
}
void
write () {
uint64_t value = 50; // <--- будет лежать на стеке в своём месте
printf ("%ld was written\n", value);
}
void
read () {
uint64_t value; // <--- возьмёт то что там лежало
printf ("%ld was read\n", value);
}
Результатом которых будет
50 was written
50 was read
Т.е. первая функция пишет в переменную (место на стеке) 50, потом она возвращается и стек разматывается обратно. Во время вызова второй функции на месте уже ёё переменной уже записано 50.
Вот и всё. Это на самом деле просто, если сильно не углубляться в ненужные дебри.