Где хранится значение переменной при выполнении программного кода на жестком диске
учитывая, что память разделена на четыре сегмента: данные, куча, стек и код, где хранятся глобальные переменные, статические переменные, постоянные типы данных, локальные переменные (определенные и объявленные в функциях), переменные (в основной функции), указатели и динамически выделенное пространство (с использованием malloc и calloc)?
Я думаю, что они будут распределены следующим образом:
- глобальные переменные -------> данные
- статические переменные ------- >данные
- постоянные типы данных - - - - - > код
- локальные переменные (объявленные и определенные в функциях) --------> стек
- переменные, объявленные и определенные в основной функции - - - - - > heap
- указатели (например, char *arr , int *arr ) -------> кучи
- динамически выделенное пространство (с помощью malloc и calloc) --------> стек
Я имею в виду эти переменные только из C перспектива.
пожалуйста, поправьте меня, если я ошибаюсь, поскольку я новичок в C.
вы получили некоторые из них правильно, но тот, кто написал вопросы, обманул вас по крайней мере на один вопрос:
- глобальные переменные -------> данные (правильные)
- статические переменные -------> данные (правильные)
- постоянные типы данных - - - - - > код и / или данные. Рассмотрим строковые литералы для ситуации, когда сама константа будет храниться в сегменте данных, а ссылки на нее будут встроены в код
- локальные переменные(объявленные и определено в функциях) --------> стек (правильно)
- переменные, объявленные и определенные в -0-->
кучутакже стек (учитель пытался вас обмануть) - указателей(пример: char *arr , int *arr ) ------->
кучуданные или стек, в зависимости от контекста. C позволяет объявить глобальный или static указатель, в этом случае сам указатель окажется в сегменте данных. - динамически выделенную память(используя malloc , calloc , realloc ) -------->
стеккучу
стоит отметить, что "стек" официально называется "автоматический класс памяти".
для тех будущих посетителей, которые могут быть заинтересованы в знании об этих сегментах памяти, я пишу важные моменты о 5 сегментах памяти в C:
некоторые головы:
- всякий раз, когда программа C выполняется, некоторая память выделяется в ОЗУ для выполнения программы. Эта память используется для хранения наиболее часто выполняемого кода (двоичных данных), программные переменные и т. д. Ниже сегменты памяти говорит о же:
- обычно существует три типа переменных:
- локальные переменные (также называемые автоматическими переменными в C)
- глобальные переменные
- статические переменные
- вы можете иметь глобальные статические или локальные статические переменные, но вышеуказанные три являются родительскими типами.
5 сегментов памяти в C:
- код сегмент, также называемый сегментом текста, - это область памяти, содержащая часто выполняемый код.
- сегмент кода часто доступен только для чтения, чтобы избежать риска переопределения программными ошибками, такими как переполнение буфера и т. д.
- сегмент кода не содержит переменных программы, таких как локальная переменная (также называется автоматическими переменными в C), глобальные переменные и т. д.
- на основе реализации C сегмент кода также может содержит строковые литералы только для чтения. Например, когда вы делаете printf("Hello, world") затем строка "Hello, world" создается в сегменте кода/текста. Вы можете проверить это, используя в ОС Linux.
сегмент данных разделен на две части ниже и обычно лежит ниже области кучи или в некоторых реализациях над стеком, но сегмент данных никогда не лежит между кучей и стеком область.
2. Неинициализированный сегмент данных
- этот сегмент также известен как bss.
- это часть памяти, которая содержит:
- неинициализированные глобальные переменные(включая указатель переменных)
- неинициализированные константы глобальные переменные.
- неинициализированный локальный статический переменные.
- любая глобальная или статическая локальная переменная, которая не инициализирована будет храниться в неинициализированный сегмент данных
- например: глобальная переменная int globalVar; или статическая локальная переменная static int localStatic; будет храниться в сегмент неинициализированных данных.
- если вы объявляете глобальную переменную и инициализировать ее как 0 или NULL тогда все равно он перейдет к неинициализированному сегменту данных или bss.
1 ответ 1
На самом деле, у переменной может не быть адреса вовсе.
Если переменная используется лишь на небольшом участке, компилятор может эту переменную затолкать в регистр процессора и держать её там в течение всего времени её жизни, не выделяя ей место в оперативной памяти вовсе.
Если в программе используется лишь изначальное значение неменяющейся переменной, компилятор волен вставить вместо чтения этой переменной само значение. В качестве непосредственного (immediate) значения, прямо в генерируемый программный код.
Если переменная вообще не оказывает никакого влияния на ход программы и не торчит наружу, компилятор может выкинуть с корнем её, все операции над ней и все её значения.
Переменная это уровень языка. Она абстрактна. У вас есть имя, тип и область видимости, определённые языком. Всё. А при сборке компилятор уже сам разберётся, как реализовать это, чтобы программа вела себя соответственно своему исходному коду. Чем меньше требований вы к переменной предъявляете, тем больше у компилятора свободы к самовыражению своей оптимизаторской сущности.
Однако, компиляторы обычно реализуют возможность сборки с отладочной информацией, нужной для осмотра состояния программы в любой момент с помощью отладчика. Там название переменной и её местонахождение указано. Но это удобство компилятора для разработки, а не особенность языка и, что интересно, включение этого удобства может изменить реализацию переменной (см. опцию /ZI в MSVC).
Класс ведь ссылочный тип, а переменные в классе могут быть типами по значению. Например:
Где хранится maxSpeed — в стеке или куче?
3. Инициализированный сегмент данных
- этот сегмент магазинах:
- инициализированные глобальные переменные(включая указатель переменных)
- значение константы глобальные переменные.
- инициализированные локальные статические переменные.
- например: глобальная переменная int globalVar = 1; или статический локальная переменная static int localStatic = 1; будет храниться в инициализировать сегмент данных.
- этот сегмент может быть далее классифицируется в инициализированной области только для чтения и инициализированной области чтения и записи. инициализированные постоянные глобальные переменные будут входить в инициализированную область только для чтения, а переменные, значения которых могут быть изменены во время выполнения, будут входить в инициализированную область чтения и записи.
- размер этого сегмента определяется размер значения в исходном коде программы и не меняются во время выполнения.
- сегмент стека используется для хранения переменных, которые создаются внутри функции (функция может быть основной функцией или пользовательской функцией), переменная как
- локальные переменные функции (включая указатель переменные)
- аргументы, переданные функции
- обратный адрес
- переменные, хранящиеся в стеке, будут удалены, как только закончится выполнение функции.
- этот сегмент предназначен для поддержки динамического выделения памяти. Если программист хочет выделить некоторые память динамически, то в C это делается с помощью malloc , calloc или realloc методы.
- например, когда int* prt = malloc(sizeof(int) * 2) тогда восемь байтов будут выделены в куче, и адрес памяти этого местоположения будет возвращен и сохранен в ptr переменной. The ptr переменная будет находиться либо в стеке, либо в сегменте данных в зависимости от способа ее объявления/использования.
исправлены неправильные предложения
локальные постоянные переменные - - - - - > stack
инициализированная глобальная постоянная переменная - - - - - > сегмент данных
неинициализированная глобальная постоянная переменная - - - - - > bss
переменные, объявленные и определенные в основной функции - - - - - > stack
указатели (например: char * arr, int * arr) -------> размер этой переменной указателя будет находиться в стеке.
считайте, что вы выделение памяти из n байт (используя malloc или calloc ) динамически, а затем делает переменную указателя, чтобы указать ее. Теперь что n байты памяти находятся в куче, а переменная указателя requries 4 байта (если 64-битная машина 8 байт), которая будет в стеке для хранения начального указателя n байт блока памяти.
Примечание: переменные указателя могут указывать на память любого сегмента.
динамически выделенное пространство(с помощью malloc, calloc) --------> кучи
популярная архитектура рабочего стола делит виртуальную память процесса на несколько сегментов:
фрагмент текста, содержит исполняемый код. Указатель инструкции принимает значения в этом диапазоне.
сегмент данных: содержит глобальные переменные (т. е. объекты со статической связью). Подразделяется на данные только для чтения (например, строковые константы) и неинициализированные данные ("BSS").
сегмент стека содержит динамический память для программы, т. е. свободное хранилище ("куча") и локальные кадры стека для всех потоков. Традиционно с стека и кучи росли в сегменте стека с противоположных концов, но я считаю, что практика была оставлена, потому что это слишком опасно.
программа C обычно помещает объекты со статической длительностью хранения в сегмент данных, динамически выделенные объекты в свободном хранилище и автоматические объекты в вызове стек нити, в которой он живет.
на других платформах, таких как старый реальный режим x86 или на встроенных устройствах, вещи, очевидно, могут радикально отличаться.
Я имею в виду эти переменные только с точки зрения Си.
С точки зрения язык C, все, что имеет значение, это экстент, область, связь и доступ; точно, как элементы сопоставляются с различными сегментами памяти до индивидуальной реализации, и это будет отличаться. Языковой стандарт не говорит о сегментах памяти на всех. Большинство современных архитектур действуют в основном одинаково; переменные области блока и аргументы функции будут выделены из стека, переменные области файла и статические переменные будут выделены из сегмента данных или кода,динамическая память будет выделена из кучи, некоторые постоянные данные будут сохранены в сегментах только для чтения и т. д.
указатели (например: char * arr, int * arr) ------- > куча
Неа, они могут быть в стеке или в сегменте данных. Они могут указать куда угодно.
Управление памятью – одна из главных задач ОС. Она критична как для программирования, так и для системного администрирования. Я постараюсь объяснить, как ОС работает с памятью. Концепции будут общего характера, а примеры я возьму из Linux и Windows на 32-bit x86. Сначала я опишу, как программы располагаются в памяти.
Каждый процесс в многозадачной ОС работает в своей «песочнице» в памяти. Это виртуальное адресное пространство, которое в 32-битном режиме представляет собою 4Гб блок адресов. Эти виртуальные адреса ставятся в соответствие (mapping) физической памяти таблицами страниц, которые поддерживает ядро ОС. У каждого процесса есть свой набор таблиц. Но если мы начинаем использовать виртуальную адресацию, приходится использовать её для всех программ, работающих на компьютере – включая и само ядро. Поэтому часть пространства виртуальных адресов необходимо резервировать под ядро.
Это не значит, что ядро использует так много физической памяти – просто у него в распоряжении находится часть адресного пространства, которое можно поставить в соответствие необходимому количеству физической памяти. Пространство памяти для ядра отмечено в таблицах страниц как эксклюзивно используемое привилегированным кодом, поэтому если какая-то программа пытается получить в него доступ, случается page fault. В Linux пространство памяти для ядра присутствует постоянно, и ставит в соответствие одну и ту же часть физической памяти у всех процессов. Код ядра и данные всегда имеют адреса, и готовы обрабатывать прерывания и системные вызовы в любой момент. Для пользовательских программ, напротив, соответствие виртуальных адресов реальной памяти меняется, когда происходит переключение процессов:
Голубым отмечены виртуальные адреса, соответствующие физической памяти. Белым – пространство, которому не назначены адреса. В нашем примере Firefox использует гораздо больше места в виртуальной памяти из-за своей легендарной прожорливости. Полоски в адресном пространстве соответствуют сегментам памяти таким, как куча, стек и проч. Эти сегменты – всего лишь интервалы адресов памяти, и не имеют ничего общего с сегментами от Intel. Вот стандартная схема сегментов у процесса под Linux:
Когда программирование было белым и пушистым, начальные виртуальные адреса сегментов были одинаковыми для всех процессов. Это позволяло легко удалённо эксплуатировать уязвимости в безопасности. Зловредной программе часто необходимо обращаться к памяти по абсолютным адресам – адресу стека, адресу библиотечной функции, и т.п. Удаленные атаки приходилось делать вслепую, рассчитывая на то, что все адресные пространства остаются на постоянных адресах. В связи с этим получила популярность система выбора случайных адресов. Linux делает случайными стек, сегмент отображения в память и кучу, добавляя смещения к их начальным адресам. К сожалению, в 32-битном адресном пространстве особо не развернёшься, и для назначения случайных адресов остаётся мало места, что делает эту систему не слишком эффективной.
Самый верхний сегмент в адресном пространстве процесса – это стек, в большинстве языков хранящий локальные переменные и аргументы функций. Вызов метода или функции добавляет новый кадр стека (stack frame) к существующему стеку. После возврата из функции кадр уничтожается. Эта простая схема приводит к тому, что для отслеживания содержимого стека не требуется никакой сложной структуры – достаточно всего лишь указателя на начало стека. Добавление и удаление данных становится простым и однозначным процессом. Постоянное повторное использование районов памяти для стека приводит к кэшированию этих частей в CPU, что добавляет скорости. Каждый поток выполнения (thread) в процессе получает свой собственный стек.
Можно прийти к такой ситуации, в которой память, отведённая под стек, заканчивается. Это приводит к ошибке page fault, которая в Linux обрабатывается функцией expand_stack(), которая, в свою очередь, вызывает acct_stack_growth(), чтобы проверить, можно ли ещё нарастить стек. Если его размер не превышает RLIMIT_STACK (обычно это 8 Мб), то стек увеличивается и программа продолжает исполнение, как ни в чём не бывало. Но если максимальный размер стека достигнут, мы получаем переполнение стека (stack overflow) и программе приходит ошибка Segmentation Fault (ошибка сегментации). При этом стек умеет только увеличиваться – подобно государственному бюджету, он не уменьшается обратно.
Динамический рост стека – единственная ситуация, в которой может осуществляться доступ к свободной памяти, которая показана белым на схеме. Все другие попытки доступа к этой памяти вызывают ошибку page fault, приводящую к Segmentation Fault. А некоторые занятые области памяти служат только для чтения, поэтому попытки записи в эти области также приводят к Segmentation Fault.
После стека идёт сегмент отображения в память. Тут ядро размещает содержимое файлов напрямую в памяти. Любое приложение может запросить сделать это через системный вызов mmap() в Linux или CreateFileMapping() / MapViewOfFile() в Windows. Это удобный и быстрый способ организации операций ввода и вывода в файлы, поэтому он используется для подгрузки динамических библиотек. Также возможно создать анонимное место в памяти, не связанное с файлами, которое будет использоваться для данных программы. Если вы сделаете в Linux запрос на большой объём памяти через malloc(), библиотека C создаст такую анонимное отображение вместо использования памяти из кучи. Под «большим» подразумевается объём больший, чем MMAP_THRESHOLD (128 kB по умолчанию, он настраивается через mallopt().)
Если в куче оказывается недостаточно места для выполнения запроса, эту проблему может обработать сама программа без вмешательства ядра. В ином случае куча увеличивается системным вызовом brk(). Управление кучей – дело сложное, оно требует хитроумных алгоритмов, которые стремятся работать быстро и эффективно, чтобы угодить хаотичному методу размещению данных, которым пользуется программа. Время на обработку запроса к куче может варьироваться в широких пределах. В системах реального времени есть специальные инструменты для работы с ней. Кучи тоже бывают фрагментированными:
И вот мы добрались до самой нижней части схемы – BSS, данные и текст программы. BSS и данные хранят статичные (глобальные) переменные в С. Разница в том, что BSS хранит содержимое непроинициализированных статичных переменных, чьи значения не были заданы программистом. Кроме этого, область BSS анонимна, она не соответствует никакому файлу. Если вы пишете static int cntActiveUsers , то содержимое cntActiveUsers живёт в BSS.
Сегмент данных, наоборот, содержит те переменные, которые были проинициализированы в коде. Эта часть памяти соответствует бинарному образу программы, содержащему начальные статические значения, заданные в коде. Если вы пишете static int cntWorkerBees = 10 , то содержимое cntWorkerBees живёт в сегменте данных, и начинает свою жизнь как 10. Но, хотя сегмент данных соответствует файлу программы, это приватное отображение в память (private memory mapping) – а это значит, что обновления памяти не отражаются в соответствующем файле. Иначе изменения значения переменных отражались бы в файле, хранящемся на диске.
Пример данных на диаграмме будет немного сложнее, поскольку он использует указатель. В этом случае содержимое указателя, 4-байтный адрес памяти, живёт в сегменте данных. А строка, на которую он показывает, живёт в сегменте текста, который предназначен только для чтения. Там хранится весь код и разные другие детали, включая строковые литералы. Также он хранит ваш бинарник в памяти. Попытки записи в этот сегмент оканчиваются ошибкой Segmentation Fault. Это предотвращает ошибки, связанные с указателями (хотя не так эффективно, как если бы вы вообще не использовали язык С). На диаграмме показаны эти сегменты и примеры переменных:
Изучить области памяти Linux-процесса можно, прочитав файл /proc/pid_of_process/maps. Учтите, что один сегмент может содержать много областей. К примеру, у каждого файла, сдублированного в память, есть своя область в сегменте mmap, а у динамических библиотек – дополнительные области, напоминающие BSS и данные. Кстати, иногда, когда люди говорят «сегмент данных», они имеют в виду данные + bss + кучу.
Бинарные образы можно изучать при помощи команд nm и objdump – вы увидите символы, их адреса, сегменты, и т.п. Схема виртуальных адресов, описанная в этой статье – это т.н. «гибкая» схема, которая по умолчанию используется уже несколько лет. Она подразумевает, что переменной RLIMIT_STACK присвоено какое-то значение. В противном случае Linux использует «классическую» схему:
За последнюю неделю дважды объяснял людям как организована работа с памятью в х86, с целью чтобы не объяснять в третий раз написал эту статью.
И так, чтобы понять организацию памяти от вас потребуется знания некоторых базовых понятий, таких как регистры, стек и тд. Я по ходу попробую объяснить и это на пальцах, но очень кратко потому что это не тема для этой статьи. Итак начнем.
Как известно программист, когда пишет программы работает не с физическим адресом, а только с логическим. И то если он программирует на ассемблере. В том же Си ячейки памяти от программиста уже скрыты указателями, для его же удобства, но если грубо говорить указатель это другое представление логического адреса памяти, а в Java и указателей нет, совсем плохой язык. Однако грамотному программисту не помешают знания о том как организована память хотя бы на общем уровне. Меня вообще очень огорчают программисты, которые не знают как работает машина, обычно это программисты Java и прочие php-парни, с квалификацией ниже плинтуса.
Так ладно, хватит о печальном, переходим к делу.
Рассмотрим адресное пространство программного режима 32 битного процессора (для 64 бит все по аналогии)
Адресное пространство этого режима будет состоять из 2^32 ячеек памяти пронумерованных от 0 и до 2^32-1.
Программист работает с этой памятью, если ему нужно определить переменную, он просто говорит ячейка памяти с адресом таким-то будет содержать такой-то тип данных, при этом сам програмист может и не знать какой номер у этой ячейки он просто напишет что-то вроде:
int data = 10;
компьютер поймет это так: нужно взять какую-то ячейку с номером стопицот и поместить в нее цело число 10. При том про адрес ячейки 18894 вы и не узнаете, он от вас будет скрыт.
Все бы хорошо, но возникает вопрос, а как компьютер ищет эту ячейку памяти, ведь память у нас может быть разная:
3 уровень кэша
2 уровень кэша
1 уровень кэша
основная память
жесткий диск
Это все разные памяти, но компьютер легко находит в какой из них лежит наша переменная int data.
Этот вопрос решается операционной системой совместно с процессором.
Вся дальнейшая статья будет посвящена разбору этого метода.
Архитектура х86 поддерживает стек.
Стек это непрерывная область оперативной памяти организованная по принципу стопки тарелок, вы не можете брать тарелки из середины стопки, можете только брать верхнюю и класть тарелку вы тоже можете только на верх стопки.
В процессоре для работы со стеком организованны специальные машинные коды, ассемблерные мнемоники которых выглядят так:
push operand
помещает операнд в стек
pop operand
изымает из вершины стека значение и помещает его в свой операнд
Стек в памяти растет сверху вниз, это значит что при добавлении значения в него адрес вершины стека уменьшается, а когда вы извлекаете из него, то адрес вершины стека увеличивается.
Теперь кратко рассмотрим что такое регистры.
Это ячейки памяти в самом процессоре. Это самый быстрый и самый дорогой тип памяти, когда процессор совершает какие-то операции со значением или с памятью, он берет эти значения непосредственно из регистров.
В процессоре есть несколько наборов логик, каждая из которых имеет свои машинные коды и свои наборы регистров.
Basic program registers (Основные программные регистры) Эти регистры используются всеми программами с их помощью выполняется обработка целочисленных данных.
Floating Point Unit registers (FPU) Эти регистры работают с данными представленными в формате с плавающей точкой.
Еще есть MMX и XMM registers эти регистры используются тогда, когда вам надо выполнить одну инструкцию над большим количеством операндов.
Рассмотрим подробнее основные программные регистры. К ним относятся восемь 32 битных регистров общего назначения: EAX, EBX, ECX, EDX, EBP, ESI, EDI, ESP
Для того чтобы поместить в регистр данные, или для того чтобы изъять из регистра в ячейку памяти данные используется команда mov:
mov eax, 10
загружает число 10 в регистр eax.
mov data, ebx
копирует число, содержащееся в регистре ebx в ячейку памяти data.
Регистр ESP содержит адрес вершины стека.
Кроме регистров общего назначения, к основным программным регистрам относят шесть 16битных сегментных регистров: CS, DS, SS, ES, FS, GS, EFLAGS, EIP
EFLAGS показывает биты, так называемые флаги, которые отражают состояние процессора или характеризуют ход выполнения предыдущих команд.
В регистре EIP содержится адрес следующей команды, которая будет выполнятся процессором.
Я не буду расписывать регистры FPU, так как они нам не понадобятся. Итак наше небольшое отступление про регистры и стек закончилось переходим обратно к организации памяти.
Как вы помните целью статьи является рассказ про преобразование логической памяти в физическую, на самом деле есть еще промежуточный этап и полная цепочка выглядит так:
Логический адрес --> Линейный (виртуальный)--> Физический
Все линейное адресное пространство разбито на сегменты. Адресное пространство каждого процесса имеет по крайней мере три сегмента:
Сегмент кода. (содержит команды из нашей программы, которые будут исполнятся.)
Сегмент данных. (Содержит данные, то бишь переменные)
Сегмент стека, про который я писал выше.
Линейный адрес вычисляется по формуле:
линейный адрес=Базовый адрес сегмента(на картинке это начало сегмента) + смещение
Сегмент кода
Базовый адрес сегмента кода берется из регистра CS. Значение смещения для сегмента кода берется из регистра EIP, в котором хранится адрес инструкции, после исполнения которой, значение EIP увеличивается на размер этой команды. Если команда занимает 4 байта, то значение EIP увеличивается на 4 байта и будет указывать уже на следующую инструкцию. Все это делается автоматически без участия программиста.
Сегментов кода может быть несколько в нашей памяти. В нашем случае он один.
Сегмент данных
Данные загружаются в регистры DS, ES, FS, GS
Это значит что сегментов данных может быть до 4х. На нашей картинке он один.
Смещение внутри сегмента данных задается как операнд команды. По дефолту используется сегмент на который указывает регистр DS. Для того чтобы войти в другой сегмент надо это непосредственно указать в команде префикса замены сегмента.
Сегмент стека
Используемый сегмент стека задается значением регистра SS.
Смещение внутри этого сегмента представлено регистром ESP, который указывает на вершину стека, как вы помните.
Сегменты в памяти могут друг друга перекрывать, мало того базовый адрес всех сегментов может совпадать например в нуле. Такой вырожденный случай называется линейным представлением памяти. В современных системах, память как правило так организована.
Теперь рассмотрим определение базовых адресов сегмента, я писал что они содержаться в регистрах SS, DS, CS, но это не совсем так, в них содержится некий 16 битный селектор, который указывает на некий дескриптор сегментов, в котором уже хранится необходимый адрес.
Так выглядит селектор, в тринадцати его битах содержится индекс дескриптора в таблице дескрипторов. Не хитро посчитать будет что 2^13 = 8192 это максимальное количество дескрипторов в таблице.
Вообще дескрипторных таблиц бывает два вида GDT и LDT Первая называется глобальная таблица дескрипторов, она в системе всегда только одна, ее начальный адрес, точнее адрес ее нулевого дескриптора хранится в 48 битном системном регистре GDTR. И с момента старта системы не меняется и в свопе не принимает участия.
А вот значения дескрипторов могут меняться. Если в селекторе бит TI равен нулю, тогда процессор просто идет в GDT ищет по индексу нужный дескриптор с помощью которого осуществляет доступ к этому сегменту.
Пока все просто было, но если TI равен 1 тогда это означает что использоваться будет LDT. Таблиц этих много, но использоваться в данный момент будет та селектор которой загружен в системный регистр LDTR, который в отличии от GDTR может меняться.
Индекс селектора указывает на дескриптор, который указывает уже не на базовый адрес сегмента, а на память в котором хранится локальная таблица дескрипторов, точнее ее нулевой элемент. Ну а дальше все так же как и с GDT. Таким образом во время работы локальные таблицы могут создаваться и уничтожаться по мере необходимости. LDT не могут содержать дескрипторы на другие LDT.
Итак мы знаем как процессор добирается до дескриптора, а что содержится в этом дескрипторе посмотрим на картинке:
Дескрипторы состоит из 8 байт.
Биты с 15-39 и 56-63 содержат линейный базовый адрес описываемым данным дескриптором сегмента. Напомню нашу формулу для нахождения линейного адреса:
линейный адрес = базовый адрес + смещение
[база; база+предел)
В зависимости от 55 G-бита(гранулярити), предел может измеряться в байтах при нулевом значении бита и тогда максимальный предел составит 1 мб, или в значении 1, предел измеряется страницами, каждая из которых равна 4кб. и максимальный размер такого сегмента будет 4Гб.
Для сегмента стека предел будет в интервале:
(база+предел; вершина]
Кстати интересно почему база и предел так рвано располагаются в дескрипторе. Дело в том что процессоры х86 развивались эволюционно и во времена 286х дескрипторы были по 8 бит всего, при этом старшие 2 байта были зарезервированы, ну а в последующих моделях процессоров с увеличением разрядности дескрипторы тоже выросли, но для сохранения обратной совместимости пришлось оставить структуру как есть.
Значение адреса «вершина» зависит от 54го D бита, если он равен 0, тогда вершина равна 0xFFF(64кб-1), если D бит равен 1, тогда вершина равна 0xFFFFFFFF (4Гб-1)
С 41-43 бит кодируется тип сегмента.
000 — сегмент данных, только считывание
001 — сегмент данных, считывание и запись
010 — сегмент стека, только считывание
011 — сегмент стека, считывание и запись
100 — сегмент кода, только выполнение
101- сегмент кода, считывание и выполнение
110 — подчиненный сегмент кода, только выполнение
111 — подчиненный сегмент кода, только выполнение и считывание
44 S бит если равен 1 тогда дескриптор описывает реальный сегмент оперативной памяти, иначе значение S бита равно 0.
Самым важным битом является 47-й P бит присутствия. Если бит равен 1 значит, что сегмент или локальная таблица дескрипторов загружена в оперативку, если этот бит равен 0, тогда это означает что данного сегмента в оперативке нет, он находится на жестком диске, случается прерывание, особый случай работы процессора запускается обработчик особого случая, который загружает нужный сегмент с жесткого диска в память, если P бит равен 0, тогда все поля дескриптора теряют смысл, и становятся свободными для сохранения в них служебной информации. После завершения работы обработчика, P бит устанавливается в значение 1, и производится повторное обращение к дескриптору, сегмент которого находится уже в памяти.
На этом заканчивается преобразование логического адреса в линейный, и я думаю на этом стоит прерваться. В следующий раз я расскажу вторую часть преобразования из линейного в физический.
А так же думаю стоит немного поговорить о передачи аргументов функции, и о размещении переменных в памяти, чтобы была какая-то связь с реальностью, потому размещение переменных в памяти это уже непосредственно, то с чем вам приходится сталкиваться в работе, а не просто какие-то теоретические измышления для системного программиста. Но без понимания, как устроена память невозможно понять как эти самые переменные хранятся в памяти.
В общем надеюсь было интересно и до новых встреч.
Помогите разобраться поглубже чем по сути является именованная ячейка памяти (переменная). Не могу правильно сформулировать свой вопрос, но т.е понимаю, что при хранения данных в блоке памяти выделяется место, но не понимаю как хранится именованная ее часть. К примеру:
По сути начало блока памяти для числа 5 является первый его адрес, к примеру 0х0000132 но чем является переменная а ? Его неким псевдонимом? Или блок памяти имеет кроме хранения значения еще и метаинформацию для хранения того же а ?
Не хочу показаться тупым, но не может быть что а является ссылочным типом на int , который смотрит в этом случае на стек первого адреса значения 5 ?
Да. При компиляции с++ получается набор команд процессора. А процессор оперирует адресами, но не именами.
3 ответа 3
Сам по себе вопрос вызван двумя проблемами
- достаточно распространенным, но в общем случае неверным утверждением "переменные value-type хранятся в стеке"
- небольшой путаницей в терминологии - поля класса это не "переменные" - не локальные переменные.
- Ссылочные. Значение переменной или поля класса этого типа хранит в себе адрес памяти, по которому лежит экземпляр типа. Сам по себе экземпляр при этом обычно лежит в куче. Пример - строки.
- Value-types (типы-значения). Значение переменной или поля класса хранит значение напрямую. Пример - int.
Поля типов хранятся как часть самого экземпляра типа. Они физически лежат в том куске памяти, который выделен под конкретных экземпляр типа. Т.е. если экземпляр сам по себе лежит в стеке (например, это экземпляр структуры) - то поле хранится в стеке. Если экземпляр лежит в куче - то и поле лежит в куче.
- В методе объявлен int a - он лежит прямо в стеке
- В методе объявлен string s - в переменной в стеке лежит адрес строки в куче.
- В методе объявлен Car c - в переменной в стеке лежит адрес объекта Car. Сам объект лежит в куче. Частью объекта в куче является поле maxSpeed, в нем лежит значение скорости.
- В методе объявлен int [] arr - в переменной в стеке лежит адрес массива в куче. Прямо в объекте массива в куче лежат значения отдельных ячеек.
- Объявлена структура Point с полями X / Y . В методе объявлена переменная Point p - в переменной в стеке лежит сама структура, со всеми полями.
по последнему пункту вопрос насчет структуры: Объявлена структура Point с полями X/Y, если в методе изменить значение полей, почему они не изменяться по факту, а только в самом методе?
@user210691 потому что структура - это value type, в метод передается копия структуры, а не ссылка на оригинал. точно так же не меняются снаружи, например, переменные типа int, переданные в метод.
можете подсказать еще один момент: а если в структуре указан ссылочный тип - напр. экземпляр класса, то он где в куче получается? (создается ссылка в стеке на объект в куче, если я верно понял?) Спасибо!
Типы-значения называются "типы-значения", потому что копируются по значению. Ссылочные типы называются "ссылочными типами", потому что копируются по ссылке. Это не совсем верно, что "типы-значения всегда находятся на стеке". Если бы это было правдой они бы назывались "Типы стека"(stack types) и "Типы кучи"(heap types).
Правда в том, что все зависит от деталей реализации. Различные реализации фреймворков могут выбирать использовать им стек или кучу, так как они хотят. Ниже пример как это реализовано у Микрософта:
значение переменной ссылочного типа указывает на значение в куче. Ссылки в основном 32-х или 64-х битное целое.
значение переменной значимого типа - это само значение.
значения локальных переменных хранятся на стеке, пока эти переменные не в блоке итератора, или не замкнуты в анонимном методе или лямбда-выражении. В этих случаях значения локальных переменных хранятся в куче. Конечно, пока эти переменные не выкинуты оптимизатором, в этом случае они нигде не хранятся. Или, возможно они могут быть перенесены в регистры и в этом случае они не будут ни на стеке, ни в куче.
значения экземпляров ссылочных типов и статические переменные хранятся в куче.
maxSpeed - это поле, следовательно, хранится в куче.
Единственная вещь идущая в стек - это локальные переменные (и временные переменные сгенерированные компилятором), которые не замкнуты в лямбда функциях или анонимных методах, и не находятся в блоке итератора. И, конечно, jitter может не использовать стек, а сложить все в регистры, если достаточно свободных регистров.
Но, на самом деле, я должен спросить: почему вы беспокоитесь о том, что пойдет в стек, а что в кучу? В стек идет только то, что мы можем быстро положить в него, все остальное идет в кучу.
Читайте также: