На самом деле это действительно интересно и помогает, когда вы полностью понимаете, как на самом деле все работает глубоко внутри компьютера. Чтобы действительно понимать язык C, вы также должны иметь глубокие знания о сборке, производимой компилятором.

На первый взгляд, давайте возьмем программу на c и попробуем поставить цель для достижения чего-то, используя знание языка ассемблера и распределения памяти. Все файлы, использованные в этом руководстве, можно найти здесь.

#include <stdio.h>
int main(void) 
{  
   int n;
   int a[5];
   int *p;
   a[2] = 1024;
   p = &n;
   /*
    * write your line of code here...
    * Remember:   * - you are not allowed to use a
    * - you are not allowed to modify p
    * - only one statement
    * - you are not allowed to code anything else
    */   
    /* ...so that this prints 98\n */  
    printf("a[2] = %d\n", a[2]);  
    return (0);
}
OUTPUT:
a[2] = 1024

Таким образом, нам разрешено поместить только одну строку в этот код без использования переменной a или изменения значения указателя p . Наша первая первоначальная проверка должна состоять в том, чтобы проверить размер этих объявленных переменных. Так что давайте просто проверим их в нашей программе на c, добавив эти строки в основную функцию:

printf("size of n = %d\n", sizeof(n));
printf("size of a = %d\n", sizeof(a));
printf("size of p = %d\n", sizeof(p));
printf("total = %d\n", sizeof(p)+sizeof(n)+sizeof(a));
OUTPUT:
size of n = 4
size of a = 20
size of p = 8
total = 32

Таким образом, общий размер наших переменных равен 32, а массив - 4 * 5 = 20. Теперь давайте скомпилируем наш исходный код с помощью objdump и посмотрим на основной раздел кода сборки.

$ gcc file.c
$ objdump -d a.out 
000000000040052d <main>:
  40052d:       55                      push   %rbp
  40052e:       48 89 e5                mov    %rsp,%rbp
  400531:       48 83 ec 30             sub    $0x30,%rsp
  400535:       c7 45 e8 00 04 00 00    movl   $0x400,-0x18(%rbp)
  40053c:       48 8d 45 d4             lea    -0x2c(%rbp),%rax
  400540:       48 89 45 d8             mov    %rax,-0x28(%rbp)
  400544:       8b 45 e8                mov    -0x18(%rbp),%eax
  400547:       89 c6                   mov    %eax,%esi
  400549:       bf e4 05 40 00          mov    $0x4005e4,%edi
  40054e:       b8 00 00 00 00          mov    $0x0,%eax
  400553:       e8 b8 fe ff ff          callq  400410 <printf@plt>
  400558:       b8 00 00 00 00          mov    $0x0,%eax
  40055d:       c9                      leaveq
  40055e:       c3                      retq
  40055f:       90                      nop
NOTE: ***Only main section is shown here****

Параметр -d по умолчанию выводит инструкции в синтаксисе AT&T. Формат этого синтаксиса: мнемонический источник, место назначения. Мнемоника - это машинная инструкция, источник и назначение операндов могут содержать регистры (с префиксом %), непосредственные значения (являются константами и начинаются с префикса $ ), адреса памяти и т. д.

Стек - это область памяти для хранения элементов данных вместе с указателем на «верх» стека.

Как вы можете видеть в нашем примере выше,% rbp и% rsp относятся к категории регистров специального назначения. % rbp - это базовый указатель, который указывает на основание текущего кадра стека, а% rsp - это указатель стека, который указывает на верхнюю часть текущего кадра стека. Подробнее о стеке вызовов.

Мы можем сохранять значения в стеке, нажимая их, и что операция push уменьшает значение в регистре указателя стека, rsp. Другими словами, выделение переменных в стеке вызовов включает вычитание значения из указателя стека. Точно так же освобождение переменных из стека вызовов включает добавление значения в указатель стека.

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

Поскольку% rbp указывает на базу текущего кадра стека, а% rsp указывает на вершину текущего кадра стека, чтобы действовать как база, вторая инструкция в коде сборки копирует значение% rsp в% rbp. Затем в следующей инструкции sub $ 0x30,% rsp наша программа резервирует 0x30 ячеек памяти, вычитая текущее значение на 0x30 и сохраняя его в rsp.

В следующей инструкции мы видим movl $ 0x400, -0x18 (% rbp), что в основном означает, что со значением смещения -0x18 по отношению к rbp переместите значение 0x400 (десятичное: 1024) в это адрес памяти. Что, когда мы смотрим на наш код c, напоминает a [2] = 1024;

Поскольку мы знаем, что у нас есть [5], то есть 5 элементов в нашем массиве по 4 байта каждый, мы можем знать, как он представлен в памяти.

Следующая инструкция - lea -0x2c (% rbp),% rax (lea обозначает эффективный адрес загрузки), что в основном означает, что взять адрес% rbp, сместить его на -0x2c и взять этот адрес памяти (не значение, сохраненное по этому адресу) и сохраните его в% rax. Видя оператор c p = & n, это означает, что он берет адрес n и сохраняет в% rax, а затем в следующем операторе mov% rax, -0x28 (% rbp) значение% rax сохраняется в -0x28 (% rbp), который является нашим указателем p.

Поскольку p имеет адрес n. Мы можем перейти от p к значению a [2] и изменить его. Давайте его вычислим. (0x2C-0x18) / 4 = 5, поскольку указатель будет перемещаться по 4 ячейкам памяти одновременно. Таким образом, мы можем использовать * (p + 5) = 98 в нашем коде c, чтобы изменить значение!

#include <stdio.h>
int main(void) 
{  
   int n;
   int a[5];
   int *p;
   a[2] = 1024;
   p = &n;
   /*
    * write your line of code here...
    * Remember:   * - you are not allowed to use a
    * - you are not allowed to modify p
    * - only one statement
    * - you are not allowed to code anything else
    */ 
    *(p+5)=98;
    /* ...so that this prints 98\n */  
    printf("a[2] = %d\n", a[2]);  
    return (0);
}
OUTPUT:
a[2] = 98