#linux #dynamic #linker #qemu #ptrace
#linux #динамический #компоновщик #qemu #ptrace
Вопрос:
Что я такого сделал?
Я побежал qemu-x86_64 -singlestep -d nochain,cpu ./dummy
сбрасывать все регистры фиктивной программы после каждой инструкции и использовал grep для сохранения всех значений RIP в текстовый файл (qemu_rip_dump.txt ). Затем я выделил фиктивную программу с помощью ptrace и сбросил значения RIP после каждой инструкции в другой текстовый файл (ptrace_rip_dump.txt ). Затем я сравнил оба файла .txt с diff
.
Какого результата я ожидал?
Я ожидал, что оба запуска фиктивной программы будут выполнять одни и те же инструкции, таким образом, оба файла дампа будут одинаковыми (одинаковые значения rip и одинаковое количество значений rip).
Какой результат я получил на самом деле?
Ptrace сбросил около 33.500 значений RIP, а Qemu сбросил 29.800 значений RIP. Значения RIP обоих текстовых файлов начинают отличаться от 240. instruction, большинство значений rip идентичны, но ptrace выполняет около 5500 инструкций, которые qemu не выполняет, а qemu выполняет около 1800 инструкций, которые ptrace не выполняет, что приводит к разнице примерно в 3700 инструкций. Похоже, что оба запуска выполняются по-разному во всей программе, например, есть блок из 3500 инструкций из 26.500-30.000 инструкций (очистка?), Которые выполняет собственный запуск, но не qemu.
В чем мой вопрос
Почему значения RIP не одинаковы на протяжении всего выполнения программы и, самое главное: что мне нужно сделать, чтобы оба запуска были одинаковыми?
Дополнительная информация
- фиктивная программа была основной функцией, которая возвращает 0, но эта проблема существует в каждом исполняемом файле, который я отслеживал
- я пробовал форсировать qemu с помощью
ld-linux-x86-64.so.2
компоновщика с-L /lib64/
помощью — это не имело никакого эффекта - если я запускаю qemu несколько раз, дампы будут одинаковыми (равное количество и значение RIP), то же самое относится и к ptrace
Комментарии:
1. Что происходит, когда вы запускаете одну и ту же программу в двух разных системах?
2. @stark запуск кода в другой системе немного изменяет количество выполняемых инструкций, но разница между ptrace и qemu остается примерно такой же
3. Вам нужно будет проанализировать фактический запуск выполнения (если они расходятся на insn 240 или около того, это будет не очень сложно), чтобы определить, почему. Возможные причины включают в себя то, что среда, которую QEMU предоставляет программе, не будет в точности идентична родной версии — например, набор вещей, которые он помещает во вспомогательный вектор, немного отличается, поэтому, если динамический компоновщик выполняет итерацию через auxv, он будет повторять цикл разное количество раз.
4. Кстати, если вы действительно не заботитесь о динамическом компоновщике, вы, вероятно, могли бы просто отбросить все значения RIP перед первым insn в main() — я подозреваю, что это с большей вероятностью даст идентичные результаты в обоих случаях, хотя, безусловно, есть гостевые программы, которые также будут показывать разницу после main().
5. Итак, первая часть этого действительно заключается в том, что динамический компоновщик просматривает вспомогательный вектор. Некоторые из других выглядят так, как будто они находятся там, где гостевой код проверяет, какие функции поддерживает процессор — на вашем хост-процессоре есть поддержка SSE2, поэтому гостевой libc выбирает оптимизированные версии функций, таких как strlen и memcpy, которые его используют, но QEMU не поддерживает эмуляцию SSE2, поэтому гостевой libc использует разные версии.
Ответ №1:
С программой «ничего не делает», подобной той, которую вы тестируете, большая часть выполнения будет выполняться в гостевом динамическом компоновщике и libc. Они выполняют много работы за кулисами, прежде чем ваша программа получит управление, и часть этой работы варьируется между «собственным» запуском и запуском «QEMU». Есть два основных источника расхождений, судя по некоторым дополнительным деталям, которые вы приводите в комментариях:
- Среда, которую QEMU предоставляет гостевому двоичному файлу, не на 100% идентична той, которую предоставляет реальное ядро хоста; она предназначена только для того, чтобы быть «достаточно близкой, чтобы правильные гостевые двоичные файлы вели себя разумным образом». Например, гостю передается структура данных, называемая «вспомогательный вектор ELF»; она содержит информацию, в том числе «какие функции процессора поддерживаются», «какой идентификатор пользователя вы выполняете как» и так далее. Динамический компоновщик выполняет итерации по этой структуре данных при запуске, поэтому незначительные безобидные различия в том, какие записи находятся в векторе в каком порядке, приведут к незначительным различиям в путях выполнения в гостевом коде.
- Эмулируемый QEMU процессор не обеспечивает в точности те же функции, что и ваш центральный процессор. Например, нет поддержки эмуляции AVX или SSE2. Гостевой libc настроит свое поведение таким образом, чтобы использовать возможности процессора, когда они доступны, поэтому он выбирает различные оптимизированные версии функций, таких как memcpy() или strlen() под капотом. Поскольку динамический компоновщик в конечном итоге вызовет эти функции, это также приводит к расхождениям в выполнении.
Возможно, вы сможете обойти некоторые из этих проблем, ограничив область отслеживания инструкций, на которую вы смотрите, только начиная с начала функции «main», чтобы избежать отслеживания всех запусков динамического компоновщика. Однако я не могу придумать способ обойти различия в том, какие функции процессора доступны на хосте по сравнению с QEMU.
Комментарии:
1. большое вам спасибо за ваше объяснение! Есть ли у вас предложения по подписям, которые я мог бы поискать, чтобы узнать больше о том, как libc ведет себя по-другому?