Как компьютеры запоминают, где хранят вещи?

32

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

MCMastery
источник
17
Это не так; «компьютер» совершенно не замечает. Мы должны жестко закодировать все адреса. (Что немного упрощает, но не слишком сильно.)
Рафаэль
1
@ Рафаэль: Давайте обобщим это на «мы должны жестко закодировать базовые адреса».
Френель
Каждый раз, когда вы объявляете переменную, программа, ответственная за запуск вашего кода, включает имя переменной с ее адресом в хеш-таблицу (или пространство имен). Я бы посоветовал прочитать книгу «Структура и реализация компьютерных программ (SICP)», чтобы хорошо ознакомиться с такими маленькими деталями.
Абхират Махипал
Ваша исходная программа использует переменную. Компилятор или интерпретатор решает, как его реализовать: он генерирует инструкции для выполнения компьютером и должен убедиться, что эти операции извлечения извлекают значения из мест, в которых их сохранили предыдущие инструкции.
PJTraill
1
@AbhirathMahipal: переменная не должна иметь адрес во время компиляции или даже во время выполнения; «Пространство имен» - это концепция языка, а таблица (хэшированная или иная) - деталь реализации; имя должно сохраняться в программе при ее запуске.
PJTraill

Ответы:

31

Я бы посоветовал вам заглянуть в удивительный мир компиляции! Ответ в том, что это немного сложный процесс.

Чтобы попытаться дать вам интуицию, помните, что имена переменных существуют исключительно ради программиста. В конце концов компьютер превратит все в адреса.

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

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

Каждой переменной присваивается «смещение стека», в котором она хранится в стеке. Затем, когда программе требуется доступ к переменной x, компилятор заменяется xна STACK_POINTER + x_offset, чтобы получить фактическое физическое место, которое она хранит в памяти.

Обратите внимание, что именно поэтому вы получаете указатель назад, когда используете mallocили newв C или C ++. Вы не можете определить, где именно в памяти находится выделенное кучей значение, поэтому вам нужно сохранить указатель на него. Этот указатель будет в стеке, но он будет указывать на кучу.

Детали обновления стеков для вызовов и возвратов функций сложны, поэтому я бы порекомендовал «Книгу дракона» или «Книгу тигра», если вам интересно.

jmite
источник
24

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

Программа говорит это. В компьютерах изначально нет понятия «переменные» - это язык высокого уровня!

Вот программа на C:

int main(void)
{
    int a = 1;
    return a + 3;
}

и вот код сборки, в который он компилируется: (комментарии начинаются с ;)

main:
    ; {
    pushq   %rbp
    movq    %rsp, %rbp

    ; int a = 1
    movl    $1, -4(%rbp)

    ; return a + 3
    movl    -4(%rbp), %eax
    addl    $3, %eax

    ; }
    popq    %rbp
    ret

Для "int a = 1;" CPU видит инструкцию «сохранить значение 1 по адресу (значение регистра rbp, минус 4)». Он знает, где хранить значение 1, потому что программа сообщает это.

Аналогично, следующая инструкция говорит: «загрузить значение по адресу (значение регистра rbp, минус 4) в регистр eax». Компьютер не должен знать о таких вещах, как переменные.

user253751
источник
2
Чтобы связать это с ответом jmite, %rspуказатель стека процессора. %rbpрегистр, который ссылается на бит стека, используемый текущей функцией. Использование двух регистров упрощает отладку.
MSalters
2

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

Адрес, записанный в таблице символов, может быть смещением от регистра (такого как указатель стека), но это деталь реализации.

Скотт Купер
источник
0

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

Большинство систем используют своего рода механизм каталогов / индексов / реестра, чтобы позволить компьютеру находить данные и получать к ним доступ. Этот индекс / каталог будет содержать один или несколько ключей и адрес, в котором фактически находятся данные (будь то жесткий диск, ОЗУ, база данных и т. Д.).

Пример компьютерной программы

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

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

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

Но, учитывая универсальность программирования, на самом деле нет единственного ответа, поскольку программист может в любой момент выбрать прямую запись по любому адресу в пределах выделенного ему пространства (при условии, что он использует язык программирования, который позволяет это). Затем он может сохранить его местоположение в массиве или даже просто жестко закодировать его в программу (т.е. переменная «альфа» всегда хранится в начале стека или всегда сохраняется в первых 32 битах выделенной памяти).

Резюме

Таким образом, в принципе, должен быть какой-то механизм за сценой, который сообщает компьютеру, где хранятся данные. Одним из наиболее популярных способов является своего рода индекс / каталог, который содержит ключ (и) и адрес памяти. Это реализуется всевозможными способами и обычно инкапсулируется пользователем (а иногда даже инкапсулируется программистом).

Справка: Как компьютеры запоминают, где хранят вещи?

Скотт М. Штольц
источник
0

Это знает из-за шаблонов и форматов.

Программа / функция / компьютер фактически не знает, где что-либо находится. Он просто ожидает, что что-то будет в определенном месте. Давайте использовать пример.

class simpleClass{
    public:
        int varA=58;
        int varB=73;
        simpleClass* nextObject=NULL;
};

Наш новый класс 'simpleClass' содержит 3 важные переменные - два целых числа, которые могут содержать некоторые данные, когда они нам нужны, и указатель на другой объект 'simpleClass'. Давайте предположим, что мы находимся на 32-битной машине ради простоты. 'gcc' или другой компилятор 'C' создаст шаблон для работы с нами для выделения некоторых данных.

Простые типы

Во-первых, когда кто-то использует ключевое слово для простого типа, такого как «int», компилятор делает примечание в разделе «.data» или «.bss» исполняемого файла, так что когда он выполняется операционной системой, данные доступно для программы. Ключевое слово int выделяет 4 байта (32 бита), а длинное int выделяет 8 байтов (64 бита).

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

...
clear register EAX
clear register EBX
load the immediate (next) value into EAX
5
copy the value in register EAX to register EBX
...

Это закончится значением «5» в EAX и EBX.

Во время выполнения программы выполняется каждая инструкция, кроме «5», так как немедленная загрузка ссылается на нее и заставляет процессор пропускать ее.

Недостатком этого метода является то, что он действительно практичен только для констант, так как было бы нецелесообразно хранить массивы / буферы / строки в середине вашего кода. Поэтому, как правило, большинство переменных хранятся в заголовках программ.

Если нужно получить доступ к одной из этих динамических переменных, то можно обработать непосредственное значение, как если бы это был указатель:

...
clear register EAX
clear register EBX
load the immediate value into EAX
0x0AF2CE66 (Let's say this is the address of a cell containing '5')
load the value pointed to by EAX into EBX
...

Это закончится значением «0x0AF2CE66» в регистре EAX и значением «5» в регистре EBX. Можно также добавлять значения в регистры вместе, поэтому мы сможем найти элементы массива или строки, используя этот метод.

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

Сложные типы

Если мы сделаем два объекта этого класса:

simpleClass newObjA;
simpleClass newObjB;

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

newObjA.nextObject=&newObjB;

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

newObjA:    58
            73
            &newObjB
            ...
newObjB:    58
            73
            NULL

Здесь следует отметить один очень важный факт: у 'newObjA' и 'newObjB' нет имен при компиляции. Это просто места, где мы ожидаем, что некоторые данные будут в. Таким образом, если мы добавим 2 ячейки в & newObjA, мы найдем ячейку, которая действует как 'nextObject'. Следовательно, если мы знаем адрес 'newObjA' и где ячейка 'nextObject' относительно него, то мы можем узнать адрес 'newObjB':

...
load the immediate value into EAX
&newObjA
add the immediate value to EAX
2
load the value in EAX into EBX

Это закончится словами «2 + & newObjA» в «EAX» и «& newObjB» в «EBX».

Шаблоны / Форматы

Когда компилятор компилирует определение класса, он на самом деле компилирует способ создания формата, способ записи в формат и способ чтения из формата.

Приведенный выше пример является шаблоном для односвязного списка с двумя переменными int. Эти типы конструкций очень важны для динамического распределения памяти, наряду с двоичными и n-арными деревьями. Практическое применение n-арных деревьев - это файловые системы, состоящие из каталогов, указывающих на файлы, каталоги или другие экземпляры, распознаваемые драйверами / операционной системой.

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

Мистер Минти Фреш
источник
Используемые здесь слова «шаблон» и «формат» не встречаются ни в одном из учебников по компилятору или компилятору, которые я когда-либо видел, и, похоже, нет никаких оснований использовать оба слова для одной и той же несуществующей вещи. Переменные имеют адреса и / или смещения, это все, что вам нужно знать.
user207421
Я использую слова, так как они являются абстракциями для размещения данных, так же как числа, файлы, массивы и переменные являются абстракциями.
Мистер Минти Фреш