LINUX.ORG.RU

История изменений

Исправление kostyarin_, (текущая версия) :

Вообще, если брать общий случай, то программа под Linux выглядит примерно таким вот образом.

[куча]
   |
   V


   ^
   |
[стек]

Типа куча растёт вниз, от начального адреса (который вовсе не нулевой, но пока этим не заморачивайся). А стек растёт снизу вверх. И в куче само тело программы, которое исполняется. А между ними неразмеченная память, которую нельзя использовать.

При запуске программы Linux кладёт в стек переменные окружения и аргументы. Потом передаёт управление точке входа программы. Для программ на Си в точке входа выполняется кое-какая работа, чтобы функция main выглядела именно так как выглядит. И чтобы её результат был кодом завершения.

Программа состоит из функций. Для обычного случая, допусти, если нет оптимизаций и все функции публичные (т.е. не static и не inline и прочее) вызов функции в Linux следует договорённости fast-call. Т.е. частично используются регистры - -частично стек. Для простого случая, когда функция не имеет аргументов и ничего не возвращает – вызов функции проходит так:

  1. выровнять стек по 16-ти-байтной границе
  2. вытолкнуть в стек адрес следующей инструкции
  3. передать управление вызываемой функции

вызываемая функция при этом ещё и сохраняет базу в стеке, и возвращает её потом обратно

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) происходит следующее:

  1. стек выравнивается, но он и так выровнен, так что пропускаем
  2. в стек помещается текущий адрес следующей за вызовом инструкции (следующая инструкция return (2))
  3. передаётся управление функции do_something
  4. do_domething помещает базу в стек
  5. 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. Т.е. частично используются регистры - -частично стек. Для простого случая, когда функция не имеет аргументов и ничего не возвращает – вызов функции проходит так:

  1. выровнять стек по 16-ти-байтной границе
  2. вытолкнуть в стек текущий адрес
  3. передать управление вызываемой функции

вызываемая функция при этом ещё и сохраняет базу в стеке, и возвращает её потом обратно

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) происходит следующее:

  1. стек выравнивается, но он и так выровнен, так что пропускаем
  2. в стек помещается текущий адрес следующей за вызовом инструкции (следующая инструкция return (2))
  3. передаётся управление функции do_something
  4. do_domething помещает базу в стек
  5. 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) происходит следующее:

  1. стек выравнивается, но он и так выровнен, так что пропускаем
  2. в стек помещается текущий адрес следующей за вызовом инструкции (следующая инструкция return (2))
  3. передаётся управление функции do_something
  4. do_domething помещает базу в стек
  5. 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.

Вот и всё. Это на самом деле просто, если сильно не углубляться в ненужные дебри.