Справка с указателями C

git -P diff

Или --no-pager.

BTW: сохранить цвет с кошкой

git diff --color=always | cat
6
задан nubela 3 September 2009 в 09:01
поделиться

6 ответов

Технически этот код выходит далеко за рамки того, что определяет стандарт C, поэтому он может делать что угодно . Он делает огромное количество предположений, на которые не имеет права, и эти предположения определенно не всегда верны. Тем не менее, я могу предложить весьма вероятное объяснение того, почему вы видите результат, который вы делаете:

Вы правы до того момента, когда вы скопировали код функции g () в память, занятая переменной локального массива a .

Чтобы понять следующую строку, вам нужно знать немного о том, как функции обычно вызываются в обычных архитектурах на основе стека. Когда функция вызывается, параметры помещаются в стек, затем адрес возврата помещается в стек, и выполнение переходит к начальной точке функции. Внутри функции указатель предыдущего кадра помещается в стек, после чего освобождается место для локальных переменных. Стеки имеют тенденцию увеличиваться вниз в памяти (от высоких адресов к низким адресам), хотя это не так на всех распространенных архитектурах.

Таким образом, когда основной вызов функции f () , стек изначально выглядит так (указатель кадра и указатель стека - это два регистра ЦП, содержащих адреса ячеек в стеке):

                    | ...                     | (higher addresses)
                    | char **argv (parameter) |
                    |-------------------------|
                    | int argc (parameter)    |
                    |-------------------------|
FRAME POINTER ->    | saved frame pointer     |
                    |-------------------------|
                    | int a                   |
                    |-------------------------|
                    | int x (parameter)       | &x
                    |-------------------------|
STACK POINTER ->    | return address          | &x - 1
                    |-------------------------|
                    | ...                     | (lower addresses)

Пролог функции затем сохраняет указатель кадра вызывающей функции и перемещает указатель стека, чтобы освободить место для локальных переменных в f () . Итак, когда код C в f () начинает выполняться, стек теперь выглядит примерно так:

                    | ...                     | (higher addresses)
                    | char **argv (parameter) |
                    |-------------------------|
                    | int argc (parameter)    |
                    |-------------------------|
                    | saved frame pointer     |
                    |-------------------------|
                    | int a                   |
                    |-------------------------|
                    | int x (parameter)       | &x
                    |-------------------------|
                    | return address          | &x - 1
                    |-------------------------|
FRAME POINTER ->    | saved frame pointer     | 
                    |-------------------------|
                    | a[99]                   | &a[99]
                    | a[98]                   | &a[98]
                    | ...                     | ...
STACK POINTER ->    | a[0]                    | &a[0]
                    | ...                     | (lower addresses)

Что такое указатель кадра? Он используется для ссылки на локальные переменные и параметры в функции. Компилятор знает, что при выполнении f () адрес локальной переменной a равен всегда FRAME_POINTER - 100 * sizeof (int) , и адрес параметра x равен FRAME_POINTER + sizeof (FRAME_POINTER) + sizeof (RETURN_ADDRESS) . Все локальные переменные и параметры могут быть доступны как фиксированное смещение от указателя кадра, независимо от того, как указатель стека перемещается по мере выделения и освобождения пространства стека.

В любом случае, вернемся к коду. Когда эта строка выполняется:

x = *(&x-1) ;

Она копирует значение, которое хранится в памяти на 1 целое число меньше, чем x , в x . Если вы посмотрите мой ASCII-арт, вы увидите, что это обратный адрес. Итак, что на самом деле выполняет это:

x = RETURN_ADDRESS;

Следующая строка:

*(&x-1) = (int)(&a) ;

Затем устанавливает адрес возврата на адрес массива a . Это действительно говорит:

RETURN_ADDRESS = &a;

Приведение требуется, потому что вы обрабатываете адрес возврата как int , а не как указатель (так что на самом деле этот код будет работать только только архитектуры, где int имеет тот же размер, что и указатель - это НЕ будет работать, например, в 64-битных системах POSIX!).

Код C в функции f () сейчас завершается, и эпилог функции освобождает локальные переменные (путем перемещения указателя стека назад) и восстанавливает указатель кадра вызывающего объекта. На данный момент стек выглядит так:

                    | ...                     | (higher addresses)
                    | char **argv (parameter) |
                    |-------------------------|
                    | int argc (parameter)    |
                    |-------------------------|
FRAME POINTER ->    | saved frame pointer     |
                    |-------------------------|
                    | int a                   |
                    |-------------------------|
                    | int x (parameter)       | &x
                    |-------------------------|
STACK POINTER ->    | return address          | &x - 1
                    |-------------------------|
                    | saved frame pointer     | 
                    |-------------------------|
                    | a[99]                   | &a[99]
                    | a[98]                   | &a[98]
                    | ...                     | ...
                    | a[0]                    | &a[0]
                    | ...                     | (lower addresses)

Теперь функция возвращается, перейдя к значению RETURN_ADDRESS, но мы установили для него значение и , поэтому вместо возврата к тому месту, откуда он был вызван, он переходит к значению начала массива a - теперь он выполняет код из стека. Здесь вы скопировали код из функции g () , так что код (по-видимому) успешно работает. Обратите внимание: поскольку указатель стека был перемещен назад над массивом здесь, любой асинхронный код, который выполняется с тем же стеком (например, сигнал UNIX, поступающий в неподходящий момент), перезапишет код!

Итак, вот что такое стек. теперь выглядит как начало g () перед прологом функции:

                    | ...                     | (higher addresses)
                    | char **argv (parameter) |
                    |-------------------------|
                    | int argc (parameter)    |
                    |-------------------------|
FRAME POINTER ->    | saved frame pointer     |
                    |-------------------------|
                    | int a                   |
                    |-------------------------|
STACK POINTER ->    | int x (parameter)       | 
                    |-------------------------|
                    | return address          | 
                    |-------------------------|
                    | saved frame pointer     | 
                    |-------------------------|
                    | a[99]                   | 
                    | a[98]                   | 
                    | ...                     | 
                    | a[0]                    | 
                    | ...                     | (lower addresses)

Пролог для g () затем устанавливает кадр стека как обычно, выполняет его и раскручивает это, Это вызовет некоторые странные эффекты - если вы вызовете другую функцию из main на этом этапе, содержимое a будет изменено!

Надеюсь, вы (и другие) узнали кое-что ценное из этого обсуждения, но важно помнить, что это похоже на технику программирования «Пятиконечная ладонь взрывающегося сердца» - НИКОГДА не используйте ее в реальной системе. Новая подархитектура, компилятор или даже просто другие флаги компилятора могут и будут изменять среду выполнения в достаточной степени, чтобы заставить этот вид слишком умного наполовину кода полностью потерпеть неудачу всеми видами восхитительных и забавных способов.

Надеюсь, вы (и другие) узнали что-то ценное из этого обсуждения, но важно помнить, что это похоже на технику программирования «Пятиконечная ладонь взрывающегося сердца» - НИКОГДА не используйте ее в реальной системе . Новая подархитектура, компилятор или даже просто другие флаги компилятора могут и будут изменять среду выполнения в достаточной степени, чтобы заставить этот вид слишком умного наполовину кода полностью потерпеть неудачу всеми видами восхитительных и забавных способов.

Надеюсь, вы (и другие) узнали что-то ценное из этого обсуждения, но важно помнить, что это похоже на технику программирования «Пятиконечная ладонь взрывающегося сердца» - НИКОГДА не используйте ее в реальной системе . Новая подархитектура, компилятор или даже просто другие флаги компилятора могут и будут изменять среду выполнения в достаточной степени, чтобы заставить этот вид слишком умного наполовину кода полностью потерпеть неудачу всеми видами восхитительных и забавных способов.

20
ответ дан 8 December 2019 в 03:27
поделиться

Хорошо, это всего лишь возможное объяснение того, что происходит, поскольку, как уже упоминалось, когда вы перезаписываете " важные адреса памяти: все может случиться.

С учетом сказанного, похоже, что происходит что-то вроде этого:

  • Вы вызываете f () из main. Адрес возврата (вызов printf) помещается в стек, за которым следует значение a.
  • Вы копируете код из g () в a [].
  • (Вы меняете x, но это не так '' t делать что-либо).
  • Вы перезаписываете адрес возврата в стеке адресом [] (содержащим копию кода g ()).
  • f () возвращается к коду в [] , выполняя код g ().

Опять же, это все предположения - все зависит от компилятора, его параметров и платформы, на которой вы его используете.

3
ответ дан 8 December 2019 в 03:27
поделиться

Хорошо, я разобрался, как он возвращается в основной (см. Комментарий к ответу Тала).

Вам нужно знать, как работает стек, в частности, на процессоре Intel .

В основном стек выглядит так:

stacktop:   1234 - the a variable, locals are normally on the stack)

в начале f (x) он выглядит так

stacktop:   1234 - main's a
-1          1234 - the argument to f(), pushed onto the stack
-2          ret_addr - points to the printf in main, where f() will go when it's finished
-3          a[99]
-4          a[98]
            ...
-101        a[1]
-102        a[0]

Стеки растут сверху вниз.

Код "(& x-1)" указывает на stacktop-2 в этом случае, поскольку & x является адресом параметра, переданного в f (), который равен stacktop-1.

После копирования функции g () в массив a [] вы затем устанавливаете переданное значение of x равным ret_addr, поэтому стек будет:

stacktop:   1234 - main's a
-1          ret_addr - the modified value of x
-2          ret_addr - points to the printf in main, where f() will go when it's finished
-3          a[99]
            ...

Затем вы устанавливаете (& x-1) в a []:

stacktop:   1234 - main's a
-1          ret_addr - the modified value of x
-2          &a[0] - points to the copy of g
-3          a[99]
            ...

Затем функция завершается. Это перемещает указатель стека на stacktop-2, освобождая выделенные локальные переменные (в данном случае a []), а затем переходит к тому, что находится в стеке, в данном случае & a [0] (stacktop-2), и уменьшает размер стека.

Это указывает на копию g (). g () выполняется, а затем завершается, переходит к адресу на вершине стека (stacktop-1, в данном случае теперь это указатель на printf в main) и снова уменьшает стек.

У этого много проблемы.

  1. Если функция g больше 100 байтов, вы получите переполнение буфера.
  2. Если функция g содержит абсолютные адреса для кодирования в g, скажем, условный переход> 128 байт, то копия будет пытаться перейти к исходному g.
  3. Если есть прерывание в конце f () между освобождением локальных переменных и переходом к адресу возврата, копия g () может быть повреждена.
  4. В оптимизированной сборке параметр, переданный в f (), скорее всего, будет передан в регистре, а не в стеке, что нарушает порядок адресов возврата, т.е.
3
ответ дан 8 December 2019 в 03:27
поделиться

Вызов функции с аргументами "по значению" не делает аргументы изменяемыми функцией. Простые целые числа передаются по значению, когда вы вызываете f (a); из main (), что не позволяет функции f изменять значение a , он получает только значение. Если вы хотите изменить исходную переменную, вам нужно вызвать ее по ссылке, например f (& a); , после изменения функции, чтобы она принимала указатель, конечно.

Это немного ... бессмысленно спорить о том, чего ожидать, когда вы делаете неопределенные вещи, например, перезаписываете память. Также попытка скопировать код функции с ее адреса не очень безопасна.

2
ответ дан 8 December 2019 в 03:27
поделиться

Вам следует взглянуть на эту классическую статью , в которой объясняется механизм переполнения стека.

Эта программа предполагает, что порядок параметров в стеке следующий:

[адрес возврата] [x]

Итак, & x-1 - это адрес адреса возврата

1
ответ дан 8 December 2019 в 03:27
поделиться

В a у вас есть вся функция 'g'. При изменении 'x' x указывает на исходную точку возврата другое изменение X и перезапись его указателем на «a» должно изменить возвращаемое значение основной функции, но на самом деле я не уверен насчет вывода, это будет зависеть от используемых оптимизаций.

0
ответ дан 8 December 2019 в 03:27
поделиться
Другие вопросы по тегам:

Похожие вопросы: