Общий вопрос по управлению памятью для переменных в сборке / C - PullRequest
0 голосов
/ 19 февраля 2019

Прежде всего: я знаю, что на многих веб-страницах (включая обсуждение по стеку стека) обсуждаются различия между .bss и .data для объявления данных, но у меня есть конкретный вопрос, и я не нашелОтвет на этих страницах, к сожалению, поэтому я спрашиваю здесь :-).

Я большой новичок в сборке, поэтому я прошу прощения, если вопрос глуп: -).

Я изучаю сборку на 64-битной ОС Linux x86 (но я думаю, что мойвопрос более общий и, вероятно, не специфичный для архитектуры / архитектуры).

Я нахожу определение разделов .bss и .data немного странным.Я всегда могу объявить переменную в .bss и затем переместить значение этой переменной в моем коде (раздел .text), верно?Так зачем мне объявлять переменную в разделе .data, если я знаю, что переменные, объявленные в этом разделе, увеличат размер моего исполняемого файла?

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

Я полагаю, что мой подход к управлению памятью наивен и не верен, но я не понимаю, почему.

Спасибо за помощь :-)!

Ответы [ 3 ]

0 голосов
/ 19 февраля 2019

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

Рассмотрим ОС, в которой количество страниц, инициализированных для некоторой константы, может быть инициализировано для различных констант.Эти страницы могут быть подготовлены ОС в фоновом потоке.Когда программа запрашивает страницу, которая инициализируется определенной константой, ОС может просто отобразить одну из страниц, которые она уже инициализировала, в свою таблицу страниц, тем самым избегая необходимости выполнять цикл во время выполнения.Фактически ОС Windows всегда инициализирует все восстановленные страницы постоянным значением «все биты - ноль».Это одновременно и функция безопасности, и функция повышения производительности.

Статические переменные обычно не инициализируются во время компиляции или обнуляются.Некоторые языки, такие как C и C ++, требуют, чтобы среда выполнения инициализировала неинициализированные статические переменные в ноль.Каков наиболее эффективный способ инициализации страниц до нуля?Среда выполнения C может, например, выдавать последовательность инструкций или цикл в точке входа объектного файла для инициализации всех неинициализированных статических переменных указанными константами времени компиляции.Но тогда каждый объектный файл потребовал бы этих инструкций.Для пространства более эффективно делегировать ОС для выполнения этой инициализации по требованию (в Linux) или с упреждением (в Windows).

Исполняемый формат ELF определяет раздел bss какчасть объектного файла, которая содержит неинициализированные переменные.Следовательно, раздел bss должен указывать только общий размер всех переменных, в отличие от раздела данных, в котором также необходимо указывать значения каждой переменной. не требует, чтобы ОС инициализировала (или нет) секцию bss на ноль или любое другое значение, но обычно это действительно так.Кроме того, хотя C / C ++ требует, чтобы среда выполнения инициализировала все статические переменные, которые не были явно инициализированы равными нулю / нулю, языковой стандарт не определяет конкретную битовую комбинацию для нуля / нуля.Только когда реализация языка и совпадение реализации bss могут быть выделены неинициализированной статической переменной в разделе bss.

Когда Linux загружает двоичный файл ELF, он отображает раздел bss на выделенную нулевую страницу, помеченную как copy-on-запись (см .: Как работает копирование при записи ).Таким образом, нет никаких накладных расходов для инициализации этой страницы до нуля.В некоторых случаях bss может занимать часть страницы (см., Например, Значение раздела .data ассемблера Gnu повреждено после syscall ).В этом случае эта дробь явно инициализируется равным нулю со всеми битами, используя цикл movb/incq/decl/jnz.

Гипотетическая ОС может, например, инициализировать каждый байт секции bss на 0000_0001b.Также в гипотетической реализации C битовая комбинация указателей NULL может быть (несколько байтов) 0000_0010b.В этом случае инициализированные по умолчанию переменные и массивы статических указателей могут быть размещены в секции bss без какого-либо цикла инициализации внутри программы C.Но любым другим значениям, таким как целочисленные массивы, потребуется цикл инициализации, если только они не будут явно инициализированы в источнике C значением, соответствующим битовому шаблону.

(C допускает реализацию, не определенную реализацией- нулевое представление объекта для NULL указателей, но целые числа более ограничены. Правила C требуют, чтобы статические переменные класса хранения были неявно инициализированы 0, если не явно инициализированы . И unsigned char требуется длябыть основанием 2 без дополнения. 0 в качестве инициализатора для указателя в источнике сопоставляется с битовой комбинацией NULL, в отличие от использования memcpy из unsigned char нулей в представлении объекта.)

0 голосов
/ 19 февраля 2019

Я сделаю это в качестве примера, и ARM, несмотря на тег x86, проще для чтения и т. Д. Функционально то же самое.

bootstrap

.globl _start
_start:
    ldr r0,=__bss_start__
    ldr r1,=__bss_end__
    mov r2,#0
bss_fill:
    cmp r0,r1
    beq bss_fill_done
    strb r2,[r0],#1
    b bss_fill
bss_fill_done:
    /* data copy would go here */
    bl main
    b .

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

код C

unsigned int ba;
unsigned int bb;
unsigned int da=5;
unsigned int db=0x12345678;
int main ( void )
{
    ba=5;
    bb=0x88776655;
    return(0);
}

может также использовать сборку, но .bss, .data и т.д. не имеют такого большого смысла в asm, какони делают это в скомпилированном коде.

MEMORY
{
    rom : ORIGIN = 0x08000000, LENGTH = 0x1000
    ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > rom
    .rodata : { *(.rodata*) } > ram
    __bss_start__ = .;
    .bss : { *(.bss*) } > ram
    __bss_end__ = .;
    __data_start__ = .;
    .data : { *(.data*) } > ram
    __data_end__ = .;
}

используется скрипт компоновщика.

результат:

Disassembly of section .text:

08000000 <_start>:
 8000000:   e59f001c    ldr r0, [pc, #28]   ; 8000024 <bss_fill_done+0x8>
 8000004:   e59f101c    ldr r1, [pc, #28]   ; 8000028 <bss_fill_done+0xc>
 8000008:   e3a02000    mov r2, #0

0800000c <bss_fill>:
 800000c:   e1500001    cmp r0, r1
 8000010:   0a000001    beq 800001c <bss_fill_done>
 8000014:   e4c02001    strb    r2, [r0], #1
 8000018:   eafffffb    b   800000c <bss_fill>

0800001c <bss_fill_done>:
 800001c:   eb000002    bl  800002c <main>
 8000020:   eafffffe    b   8000020 <bss_fill_done+0x4>
 8000024:   08000058    stmdaeq r0, {r3, r4, r6}
 8000028:   20000008    andcs   r0, r0, r8

0800002c <main>:
 800002c:   e3a00005    mov r0, #5
 8000030:   e59f1014    ldr r1, [pc, #20]   ; 800004c <main+0x20>
 8000034:   e59f3014    ldr r3, [pc, #20]   ; 8000050 <main+0x24>
 8000038:   e59f2014    ldr r2, [pc, #20]   ; 8000054 <main+0x28>
 800003c:   e5810000    str r0, [r1]
 8000040:   e5832000    str r2, [r3]
 8000044:   e3a00000    mov r0, #0
 8000048:   e12fff1e    bx  lr
 800004c:   20000004    andcs   r0, r0, r4
 8000050:   20000000    andcs   r0, r0, r0
 8000054:   88776655    ldmdahi r7!, {r0, r2, r4, r6, r9, r10, sp, lr}^

Disassembly of section .bss:

20000000 <bb>:
20000000:   00000000    andeq   r0, r0, r0

20000004 <ba>:
20000004:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

20000008 <db>:
20000008:   12345678    eorsne  r5, r4, #120, 12    ; 0x7800000

2000000c <da>:
2000000c:   00000005    andeq   r0, r0, r5

ясно, в конце вы видите хранилище для четырех переменных, и они.bss и .data, как и ожидалось.

, но есть разница, которую люди пытаются объяснить.

Должен быть код для обнуления .bss, и это пустая трата циклов, да, и некоторые компиляторы начинают предупреждать об использовании неинициализированных переменных, и это хорошо, но в любом случае .bss имеет некоторый код длянуль..data также может иметь некоторый код для копирования. Я не завершил этот пример, чтобы показать, как это работает, вы сообщаете сценарию компоновщика, что .data находится в оперативной памяти, но помещаете копию в rom и имеют как адреса, так и размеры / окончания начала данных.и начинается загрузка оперативной памяти, и вы копируете из rom в ram.

Таким образом, разница в стоимости .data против .bss заключается в том, что .data у вас есть выделенная память и через загрузчик операционной системы, или через ваш собственный загрузочный ремень.эти данные, возможно, потребуется скопировать в дополнительное время, а может и нет.

20000008 <db>:
20000008:   12345678

для .bss

20000000 <bb>:
20000000:   00000000    andeq   r0, r0, r0

снова загрузчик ОС и / или как вы собираете (в этом случае установка.data после .bss и наличие хотя бы одного элемента .data, если вы должны использовать objcopy -O двоичный файл, то вы получите нулевые данные в .bin и не нужно заполнять эти данные .bss, зависит от загрузчика и места назначения).

Таким образом, хранилище равно, но дополнительная стоимость для .bss составляет

 800002c:   e3a00005    mov r0, #5
 8000030:   e59f1014    ldr r1, [pc, #20]   ; 800004c <main+0x20>

 800003c:   e5810000    str r0, [r1]

 800004c:   20000004

и

 8000034:   e59f3014    ldr r3, [pc, #20]   ; 8000050 <main+0x24>
 8000038:   e59f2014    ldr r2, [pc, #20]   ; 8000054 <main+0x28>

 8000040:   e5832000    str r2, [r3]

 8000050:   20000000
 8000054:   88776655

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

Другой ответ здесь попытался доказать, что выне имеют статической стоимости, потому что они являются непосредственными, но в наборах команд переменной длины есть те непосредственные значения, которые есть и читаются из памяти точно так же, как и фиксированная длина, это не отдельный цикл памяти, это часть предварительной выборки, но это все еще статическое хранилище,Разница в том, что у вас есть хотя бы один цикл памяти для хранения значения в памяти (.bss и .data подразумевают глобальный характер, поэтому требуется сохранение в памяти).Поскольку они связаны, адрес к переменным должен быть установлен компоновщиком, в данном случае с набором команд риска фиксированной длины, который является пулом поблизости, для cisc, подобного x86, который будет встроен в mov, немедленный для регистрации,в любом случае статическое хранилище для адреса и статическое хранилище для значения, x86 против arm x86 будет использовать меньше байтов инструкций для выполнения задачи из двух инструкций, три команды из трех отдельных циклов памяти.Функционально то же самое.

Теперь, где это может вас спасти, нарушив ожидания, но находясь под полным контролем (голый металл).

.globl _start
_start:
    ldr sp,=0x20002000
    bl main
    b .



unsigned int ba;
unsigned int bb;
int main ( void )
{
    ba=5;
    bb=0x88776655;
    return(0);
}
MEMORY
{
    rom : ORIGIN = 0x08000000, LENGTH = 0x1000
    ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > rom
    .rodata : { *(.rodata*) } > ram
    .bss : { *(.bss*) } > ram
}


Disassembly of section .text:

08000000 <_start>:
 8000000:   e59fd004    ldr sp, [pc, #4]    ; 800000c <_start+0xc>
 8000004:   eb000001    bl  8000010 <main>
 8000008:   eafffffe    b   8000008 <_start+0x8>
 800000c:   20002000    andcs   r2, r0, r0

08000010 <main>:
 8000010:   e3a00005    mov r0, #5
 8000014:   e59f1014    ldr r1, [pc, #20]   ; 8000030 <main+0x20>
 8000018:   e59f3014    ldr r3, [pc, #20]   ; 8000034 <main+0x24>
 800001c:   e59f2014    ldr r2, [pc, #20]   ; 8000038 <main+0x28>
 8000020:   e5810000    str r0, [r1]
 8000024:   e5832000    str r2, [r3]
 8000028:   e3a00000    mov r0, #0
 800002c:   e12fff1e    bx  lr
 8000030:   20000004    andcs   r0, r0, r4
 8000034:   20000000    andcs   r0, r0, r0
 8000038:   88776655    ldmdahi r7!, {r0, r2, r4, r6, r9, r10, sp, lr}^

Disassembly of section .bss:

20000000 <bb>:
20000000:   00000000    andeq   r0, r0, r0

20000004 <ba>:
20000004:   00000000    andeq   r0, r0, r0

(я думаю, что я удалил стек init в предыдущемпример)

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

Еще один ярлык, скажем, raspberry piголый металл:

.globl _start
_start:
    ldr sp,=0x8000
    bl main
    b .

unsigned int ba;
unsigned int bb;
unsigned int da=5;
int main ( void )
{
    return(0);
}

MEMORY
{
    ram : ORIGIN = 0x00008000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > ram
    .rodata : { *(.rodata*) } > ram
    .bss : { *(.bss*) } > ram
    .data : { *(.data*) } > ram
}



Disassembly of section .text:

00008000 <_start>:
    8000:   e3a0d902    mov sp, #32768  ; 0x8000
    8004:   eb000000    bl  800c <main>
    8008:   eafffffe    b   8008 <_start+0x8>

0000800c <main>:
    800c:   e3a00000    mov r0, #0
    8010:   e12fff1e    bx  lr

Disassembly of section .bss:

00008014 <bb>:
    8014:   00000000    andeq   r0, r0, r0

00008018 <ba>:
    8018:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

0000801c <da>:
    801c:   00000005    andeq   r0, r0, r5

hexdump -C so.bin
00000000  02 d9 a0 e3 00 00 00 eb  fe ff ff ea 00 00 a0 e3  |................|
00000010  1e ff 2f e1 00 00 00 00  00 00 00 00 05 00 00 00  |../.............|
00000020

существование элемента .data и .data, определяемых после .bss в сценарии компоновщика и двоичного файла, копируется графическим процессором в ram для нас как целого .text, .bss, .data и т. Д.из .bss была бесплатной, нам не нужно было добавлять дополнительный код для .bss, и если у нас есть больше .data и мы используем его, мы также получили бесплатную инициацию / копию .data.

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

Чтобы сделать это правильно, .bss стоит вам инструкций и времени выполнения этих инструкций, включая отдельный цикл (циклы) памяти.Но там должен быть скрипт компоновщика и загрузочный код, независимо от того, что для .bss.Аналогично для .data, но если только на основе rom / flash нет вероятности, что источник и назначение для .data совпадают с копией, произошедшей в загрузчике (операционная система копирует двоичный файл из rom / flash / disk в память) и не нуждается в дополнительномкопировать, если вы не форсируете его в скрипте компоновщика.

Хорошо, основываясь на комментариях к другим вопросам, «правильно», скажем, исходя из предположений, элементы .data должны отображаться так, как определено в скомпилированном коде, что вы найдетедля .bss исторически было специфично для цепочки инструментов, что в спецификации сказано, что мне придется искать, и какую версию для какой цепочки инструментов вы могли бы в конечном итоге использовать, поскольку, несмотря на распространенное мнение, не все цепочки инструментов, которые используются сегодня, находятся в постоянном обслуживании, чтобы соответствоватьстандарт, который на месте в эту секунду.Некоторые люди могут позволить себе роскошь ограничивать свои проекты теми, у которых есть современные инструменты, многие этого не делают.

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

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

unsigned int more_fun ( unsigned int, unsigned int );
void fun ( unsigned int x )
{
    static int ba;
    static int da=0x12345678;
    ba+=x;
    da=more_fun(ba,da);
}
int main ( void )
{
    return(0);
}

0000800c <fun>:
    800c:   e59f2028    ldr r2, [pc, #40]   ; 803c <fun+0x30>
    8010:   e5923000    ldr r3, [r2]
    8014:   e92d4010    push    {r4, lr}
    8018:   e59f4020    ldr r4, [pc, #32]   ; 8040 <fun+0x34>
    801c:   e0803003    add r3, r0, r3
    8020:   e5941000    ldr r1, [r4]
    8024:   e1a00003    mov r0, r3
    8028:   e5823000    str r3, [r2]
    802c:   ebfffff6    bl  800c <fun>
    8030:   e5840000    str r0, [r4]
    8034:   e8bd4010    pop {r4, lr}
    8038:   e12fff1e    bx  lr
    803c:   0000804c    andeq   r8, r0, r12, asr #32
    8040:   00008050    andeq   r8, r0, r0, asr r0

00008044 <main>:
    8044:   e3a00000    mov r0, #0
    8048:   e12fff1e    bx  lr

Disassembly of section .bss:

0000804c <ba.3666>:
    804c:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

00008050 <da.3667>:
    8050:   12345678    eorsne  r5, r4, #120, 12    ; 0x7800000

, будучи локальными статическими или локальными глобальными переменными, они все еще попадают в .dataили .bss.

0 голосов
/ 19 февраля 2019

.bss - это место, где вы помещаете инициализированные нулем статические данные, например, C int x; (в глобальном масштабе).Это то же самое, что и int x = 0; для статических / глобальных (класс статического хранения) 1 .

.data - это место, куда вы помещаете не инициализированные нулями статические данные, как int x = 2; Если вы поместите это в BSS, вам понадобится статический «конструктор» времени выполнения для инициализации местоположения BSS.Как то, что компилятор C ++ сделал бы для static const int prog_starttime = __rdtsc();.(Несмотря на то, что он const, инициализатор не является константой времени компиляции, поэтому он не может войти в .rodata)


.bss с инициализатором времени выполнения, который будет иметь смысл для больших массивов, которые в основном ноль или заполнены тем же значением (memset / rep stosd), но на практике запись char buf[1024000] = {1}; поместит 1 МБ почти всех нулей в .data с текущими компиляторами.

В противном случае не более эффективно .Команда mov dword [myvar], imm32 имеет длину не менее 8 байтов и стоит в два раза больше байтов в вашем исполняемом файле, чем если бы она была статически инициализирована в .data.Кроме того, инициализатор должен быть выполнен.


В отличие от этого, section .rodata (или .rdata в Windows) - это место, где компиляторы помещают строковые литералы, константы FP и static const int x = 123;


Сноска 1. Внутри функции int x; будет в стеке, если компилятор не оптимизирует его вне или в регистры, при компиляции для обычной машины регистров со стеком, подобным x86.


Я мог бы задать этот вопрос и в контексте программирования на C

В C оптимизирующий компилятор будет обрабатывать int x; x=5; почти так же, как int x=5; внутрифункция.Статическое хранилище не используется.Глядя на фактический вывод компилятора, часто бывает поучительно: см. Как убрать "шум" из вывода сборки GCC / clang? .

Вне функция, в глобальном масштабе,Вы не можете писать такие вещи, как x=5;.Вы можете сделать это в верхней части main, а затем обмануть компилятор, чтобы сделать худший код.

Внутри функции с static int x = 5; инициализация происходит один раз.(Во время компиляции).Если вы сделали static int x; x=5;, статическое хранилище будет переинициализироваться каждый раз при входе в функцию, и вы, возможно, с тем же успехом не использовали static, если у вас нет других причин для необходимости использования статического хранилища.(например, возврат указателя на x, который все еще действителен после возврата функции.)

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