Как заменить alloca в реализации execvp ()? - PullRequest
1 голос
/ 23 мая 2011

Взгляните на реализацию NetBSD execvp здесь:

http://cvsweb.netbsd.se/cgi-bin/bsdweb.cgi/src/lib/libc/gen/execvp.c?rev=1.30.16.2;content-type=text%2Fplain

Обратите внимание на комментарий в строке 130, в особом случае для обработки ENOEXEC:

/*
 * we can't use malloc here because, if we are doing
 * vfork+exec, it leaks memory in the parent.
 */
if ((memp = alloca((cnt + 2) * sizeof(*memp))) == NULL)
     goto done;
memp[0] = _PATH_BSHELL;
memp[1] = bp;
(void)memcpy(&memp[2], &argv[1], cnt * sizeof(*memp));
(void)execve(_PATH_BSHELL, __UNCONST(memp), environ);
goto done;

Я пытаюсь перенести эту реализацию execvp на автономный C ++.alloca нестандартно, поэтому я хочу избежать этого.(На самом деле мне нужна функция execvpe из FreeBSD, но это демонстрирует проблему более четко.)

Мне кажется, я понимаю, почему это привело бы к утечке памяти, если использовался простой malloc - при вызове execvp может выполнять код в родительском элементе, внутренний вызов execve никогда не возвращается, поэтому функция не может освободить указатель memp, и нет способа вернуть указатель вызывающей стороне.Тем не менее, я не могу придумать способ заменить alloca - кажется, необходимо волшебство, чтобы избежать этой утечки памяти.Я слышал, что C99 предоставляет массивы переменной длины, которые я не могу использовать, к сожалению, поскольку конечной целью является C ++.

Возможно ли заменить это использование alloca?Если он обязан оставаться в C ++ / POSIX, есть ли неизбежная утечка памяти при использовании этого алгоритма?

Ответы [ 2 ]

0 голосов
/ 24 мая 2011

Вы можете заменить звонок на alloca звонком на malloc, совершенным до звонка на vfork. После возврата vfork в вызывающую память память можно удалить. (Это безопасно, потому что vfork не вернется, пока не будет вызван exec и не будет запущена новая программа.) Затем вызывающая сторона может освободить память, выделенную ей с помощью malloc.

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

Другое возможное решение - переключиться на fork вместо vfork. Это потребует немного дополнительного кода в вызывающей стороне, потому что fork возвращается до завершения вызова exec, поэтому вызывающей стороне придется его ждать. Но однажды forked новый процесс может безопасно использовать malloc. Я понимаю, что vfork - это, по сути, fork для бедняков, потому что fork был дорог в те дни, когда ядра имели страницы копирования при записи. Современные ядра реализуют fork очень эффективно, и нет необходимости прибегать к несколько опасным vfork.

0 голосов
/ 24 мая 2011

Редактировать: Как Майкл указал в комментариях, то, что написано ниже, действительно не будет работать в реальном мире из-за относительной к стеку адресации оптимизирующим компилятором. Поэтому для производственного уровня alloca нужна помощь компилятора, чтобы фактически "работать". Но, надеюсь, приведенный ниже код может дать некоторые идеи о том, что происходит внутри, и о том, как могла бы работать такая функция, как alloca, если бы не было оптимизаций адресации относительно стека, о которых можно было бы беспокоиться.

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

Предполагая, что вы используете какой-то вариант Linux на платформе x86_64 с 64-битным ABI Unix, поместите следующее в файл с именем "my_alloca.s":

.section .text
.global my_alloca

my_alloca:
    movq (%rsp), %r11       # save the return address in temp register
    subq %rdi, %rsp         # allocate space on stack from first argument
    movq $0x10, %rax
    negq %rax
    andq %rax, %rsp         # align the stack to 16-byte boundary
    movq %rsp, %rax         # save address in return register
    pushq %r11              # push return address on stack
    ret                     # return back to caller

Затем внутри вашего модуля кода C / C ++ (т. Е. Ваших файлов ".cpp") вы можете использовать его следующим образом:

extern my_alloca(unsigned int size);

void function()
{
    void* stack_allocation = my_alloca(BUFFERSIZE);
    //...do something with the allocated space

    return; //WARNING: stack_allocation will be invalid after return
}

Вы можете скомпилировать my_alloca.s, используя gcc -c my_alloca.s. Это даст вам файл с именем «my_alloca.o», который вы затем сможете использовать для связи с другими объектными файлами, используя gcc -o или ld.

.

Основная «ошибка», о которой я мог подумать в этой реализации, заключается в том, что вы можете потерпеть крах или получить неопределенное поведение, если компилятор не сработал, выделив место в стеке с помощью записи активации и базового указателя стека ( т. е. указатель RBP в x86_64), а точнее выделенная память для каждого вызова функции. Затем, так как компилятор не будет знать о памяти, которую мы распределили в стеке, когда он очищает стек при возврате вызывающей стороны и пытается вернуться назад, используя то, что, по его мнению, является возвращаемым адресом вызывающей стороны. в стеке в начале вызова функции он перейдет к указателю инструкции, указывающему на no-wheres-ville, и вы, скорее всего, столкнетесь с ошибкой шины или каким-либо типом ошибки доступа, так как попытаетесь выполнять код в ячейке памяти, к которой вы не имеете права.

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

В целом, подобная функция будет зависеть от платформы.

...