2016-12-18 12 views
4

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

int* q = new int(13); 

int main() { 
    return 0; 
} 

Очевидно, что переменная q является глобальным и инициализируется. Начиная с this answer, мы ожидаем, что переменная, подлежащая хранению в , будет храниться в файле программных файлов, но это указатель, поэтому это значение (которое является адресом в кучном сегменте) определяется во время выполнения. Итак, каково значение, хранящееся в сегменте данных в файле программы?

Моя попытка:
В моем мышлении, компилятор выделяет некоторое пространство для переменных q (обычно 8 байт для 64 битного адреса) в данных сегмента, без значимого значения. Затем помещает некоторый код инициализации в текст сегмент перед main код функции для инициализации переменной q во время выполнения. Что-то вроде этого в сборе:

 .... 
    mov edi, 4 
    call operator new(unsigned long) 
    mov DWORD PTR [rax], 13 // rax: 64 bit address (pointer value) 

    // offset : q variable offset in data segment, calculated by compiler 
    mov QWORD PTR [ds+offset], rax // store address in data segment 
    .... 
main: 
    .... 

Любая идея?

+1

При связывании с 'gcc' начальной точкой по умолчанию является' _start'. В этом случае эта инициализация будет иметь код, а затем он будет называться 'main'. Поэтому люди, выполняющие программирование в сборке без клиффа, но связывая их с 'gcc', должны поставить в начале своего кода метку' _start: ', в то время как люди, ссылающиеся на клип по умолчанию, начинаются с' main: '(в их источнике начинается двоичный код в '_start:' из lib). :) – Ped7g

+1

Чтобы уточнить это, '_start' не является функцией, поэтому вы не можете писать' _start' в C или C++. В сборке вам не нужно писать функции, вы можете написать произвольный код, чтобы вы могли самостоятельно писать '_start'. –

+0

Понял, спасибо Ped7g & Dietrich Epp. –

ответ

3

Да, это по существу, как это работает.

Обратите внимание, что в ELF .data, .bss и .text фактически являются сечениями, а не сегментами. Вы можете посмотреть на сборку самостоятельно, запустив компилятор:

c++ -S -O2 test.cpp 

Вы обычно видите main функцию, и некоторый код инициализации вне этой функции. Точка входа в программу (часть вашей среды C++) вызовет код инициализации, а затем вызовет main. Код инициализации также отвечает за запуск таких объектов, как конструкторы.

+0

Спасибо, можете ли вы вкратце упомянуть разницу между сегментом и секцией? –

+2

Разделы используются в link-time для создания двоичного файла из ваших объектных файлов. Сегменты используются во время выполнения для загрузки вашего двоичного кода в память. Сегменты не имеют имен. –

+0

@ DietrichEpp: Довольно стандартно говорить о текстовом сегменте (где компоновщик помещает '.text',' .rodata' и другие другие вещи) или сегмент данных ('.data') или BSS. Я предполагаю, что это не является официальной частью ELF, хотя, поскольку вывод 'readelf -a' показывает только сегменты с нумерацией в отображении отрезка к сегменту. –

2

int *q будет идти в .bss, а не раздел .data, так как он только инициализируется во время выполнения с помощью непостоянного инициализатора (так это законно только в C++, а не в C). Для него нет необходимости иметь 8 байтов в сегменте данных исполняемого файла.

Компилятор организует запуск функции инициализатора путем помещения ее адреса в массив инициализаторов, вызывающий код запуска CRT (C Run-Time) перед вызовом main.

В проводнике компилятора Godbolt вы можете увидеть asm функции init без всякого шума директив. Обратите внимание, что режим адресации - это просто RIP-относительный доступ к q. В этот момент компоновщик заполняет правое смещение от RIP, так как это постоянная времени соединения, даже если секции .text и находятся в отдельных сегментах.

Godbolt's compiler-noise filtering не подходит для нас. Некоторые из директив актуальны, но многие из них не являются. Ниже представлена ​​комбинация, выбранная вручную от gcc6.2 -O3 asm output with Godbolt's "filter directives" option unchecked, всего за int* q = new int(13);. (Нет необходимости компилировать main одновременно, мы не связываем исполняемый файл).

# gcc6.2 -O3 output 
_GLOBAL__sub_I_q:  # presumably stands for subroutine 
    sub  rsp, 8   # align the stack for calling another function 
    mov  edi, 4   # 4 bytes 
    call operator new(unsigned long) # this is the demangled name, like from objdump -dC 
    mov  DWORD PTR [rax], 13 
    mov  QWORD PTR q[rip], rax  # clang uses the equivalent `[rip + q]` 
    add  rsp, 8 
    ret 

    .globl q 
    .bss 
q: 
    .zero 8  # reserve 8 bytes in the BSS 

Нет ссылки на базу данных ELF (или любого другого).

Также определенно не переопределяется регистр сегментов. Сегменты ELF не имеют ничего общего с сегментами x86. (И регистр сегмента по умолчанию для этого - DS в любом случае, поэтому компилятору не нужно выделять [ds:rip+q] или что-то в этом роде. Некоторые дизассемблеры могут быть явными и показывать DS, несмотря на то, что в инструкции не было префикса переопределения сегмента).


Это, как компилятор организует для того, чтобы быть вызван до main():

# the "aw" sets options/flags for this section to tell the linker about it. 
    .section  .init_array,"aw" 
    .align 8 
    .quad _GLOBAL__sub_I_q  # this assembles to the absolute address of the function. 

код запуска CRT имеет цикл, который знает размер секции .init_array и использует память косвенной call инструкции на каждой функциональной клавише r в свою очередь.

Раздел .init_array помечен как записываемый, поэтому он переходит в сегмент данных. Я не уверен, что это пишет. Может быть, CRT-код отмечает это как уже сделанное путем обнуления указателей после их вызова?


Там есть аналогичный механизм в Linux для запуска инициализаторов в динамических библиотеках, которые делаются интерпретатором ELF при этом динамическом связывании. Вот почему вы можете вызвать printf() или другие функции glibc stdio от _start в динамически связанном двоичном файле, созданном из рукописного asm, и почему это не удается в статически связанном двоичном файле, если вы не вызываете правильные функции init. (См. this Q&A для получения дополнительной информации о создании статических или динамических двоичных файлов, которые определяют их собственные _start или только main(), с или без libc).

+0

Я скомпилировал код в своем вопросе с переменной & без указателя, и результатом было изменение размера раздела данных. Проверял его с помощью команды 'size'. Поэтому он должен храниться в разделе данных. –

+0

@MehranTorki: С каким компилятором? 64 или 32-битный код? Какая ОС (я предполагал Linux)? Поскольку 'q' является глобальным, возможно, какой-то дополнительный размер' .data' используется для GOT или что-то еще? На выходе asm, который я показал, сам указатель определенно хранится в '.bss', как и ожидалось. Для любого компилятора не имеет смысла помещать его в '.data', если он не может оценить' new' во время компиляции и избежать инициализатора во время выполнения.Но это означало бы, что в исполняемый файл будет встроен указатель 'delete'-able, поэтому компилятор должен будет зависеть от внутренней работы libstdC++. –

+1

@MehranTorki: Я просто попробовал это сам. Пустой .cpp компилируется в .o с помощью теста/данных/bss 0/0/0. gcc5.2 -O3 компилирует 'int * q = new int (13);' to .o, который 'size' говорит, имеет 80 байт в текстовом сегменте, 8 в сегменте данных и 8 в bss. 8 байтов в bss для 'q', как я могу видеть с помощью' objdump -D' (который показывает символ как часть раздела '.bss'). 'readelf -a' подтверждает это:' q' находится в разделе номер 3, который является '.bss'. Я не понял, что * * входит в '.data', но это определенно не пространство для хранения' q'. –