Как компиляторы С++ объединяют одинаковые литералы строк?

Как компилятор (MS Visual С++ 2010) объединяет идентичные строковые литералы в разных исходных файлах cpp? Например, если у меня есть строковый литерал "hello world\n" в src1.cpp и src2.cpp соответственно. Скомпилированный exe файл будет иметь только 1 "hello world" строковый литерал, вероятно, в секции constant/readonly. Эта задача выполняется компоновщиком?

Я надеюсь, что я получу несколько модулей, написанных на сборке, которые будут использоваться модулями С++. И эти сборные модули содержат много длинных строковых литералов. Я знаю, что строковые литералы идентичны некоторым другим строковым литералам в источнике С++. Если я свяжу свою сборку с кодом obj с компилятором, сгенерированным кодом obj, будут ли эти строковые литералы объединены компоновщиком для удаления избыточных строк, как в случае, когда все модули находятся на С++?

Ответы

Ответ 1

(Обратите внимание, что следующее относится только к MSVC)

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

Однако это была ошибка. Оказывается, что компоновщик не имеет особого участия в слиянии строковых литералов - что происходит, когда компилятору задается параметр /GF, он помещает строковые литералы в раздел "COMDAT" объектного файла с именем объекта, которое основано на содержимое строкового литерала. Таким образом, флаг /GF необходим для этапа компиляции, а не для этапа компоновки.

Когда вы используете параметр /GF, компилятор помещает каждый строковый литерал в объектном файле в отдельный раздел как объект COMDAT. Различные объекты COMDAT с одинаковыми именами будут свернуты компоновщиком (я не совсем уверен в семантике COMDAT или в том, что может делать компоновщик, если объекты с одинаковыми именами имеют разные данные). Таким образом, файл C, который содержит

char* another_string = "this is a string";

Будет иметь что-то вроде следующего в объектном файле:

SECTION HEADER #3
  .rdata name
       0 physical address
       0 virtual address
      11 size of raw data
     147 file pointer to raw data (00000147 to 00000157)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40301040 flags
         Initialized Data
         COMDAT; sym= "'string'" ([email protected][email protected]@[email protected])
         4 byte align
         Read Only

RAW DATA #3
  00000000: 74 68 69 73 20 69 73 20 61 20 73 74 72 69 6E 67  this is a string
  00000010: 00      

с таблицей перемещений, another_string1 имя переменной another_string1 с литеральными данными.

Обратите внимание, что имя строкового литерального объекта явно основано на содержимом литеральной строки, но с некоторым искажением. Схема искажения была частично задокументирована в Википедии (см. "Строковые константы").

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

Если нет какой-либо директивы/ключевого слова сборки, специально поддерживающей этот сценарий, я думаю, вам не повезло. Конечно, может быть один, но я достаточно ржавый с ml.exe чтобы понятия не имел, и быстрый взгляд на скудные документы MSDN для ml.exe ничего не выскочил.

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

Ответ 2

Да, процесс слияния ресурсов выполняется компоновщиком.

Если ваши ресурсы в сводном коде сборки правильно помечены как ресурсы, компоновщик сможет объединить их с скомпилированным кодом C.

Ответ 3

Многое может зависеть от конкретного компилятора, компоновщика и того, как вы их управляете. Например, этот код:

// s.c
#include <stdio.h>

void f();

int main() {
    printf( "%p\n", "foo" );
    printf( "%p\n", "foo" );
    f();
}

// s2.c
#include <stdio.h>

void f() {
    printf( "%p\n", "foo" );
    printf( "%p\n", "foo" );
}

при компиляции как:

gcc s.c s2.c

дает:

00403024
00403024
0040302C
0040302C

из которого вы можете видеть, что строки только объединены в отдельные единицы перевода.

Ответ 4

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

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

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

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

Ответ 6

Язык ассемблера не предоставляет возможности работать непосредственно с анонимным строковым литералом, например, C или С++.

Таким образом, то, что вы почти наверняка хотите сделать, это определить строки в вашем ассемблере с именами. Чтобы использовать те из C или С++, вы хотите поместить объявление extern массива в заголовок, который вы можете #include в любых файлах, нуждающихся в доступе к ним (и в вашем коде на С++ вы будете использовать имена, а не сами литералы):

foo.asm

.model flat, c

.data
    string1 db "This is the first string", 10, 0
    string2 db "This is the second string\n", 10, 0

foo.h:

extern char string1[];
extern char string2[];

bar.cpp

#include "foo.h"

void baz() { std:::cout << string1; }