Что я сделал?
Я запустил qemu-x86_64 -singlestep -d nochain,cpu ./dummy
, чтобы сбросить все регистры фиктивной программы после каждой инструкции, и использовал grep для сохранения всех значений RIP в текстовый файл (qemu_rip_dump.txt). Затем я выполнил фиктивную программу с помощью ptrace и сбрасывал значения RIP после каждой инструкции в другой текстовый файл (ptrace_rip_dump.txt). Затем я сравнил оба файла .txt с diff
.
Какого результата я ожидал?
Я ожидал, что оба запуска фиктивной программы будут выполнять одни и те же инструкции, поэтому оба файла дампа будут одинаковыми (одинаковые значения рипа и одинаковое количество значений рипа).
Какой результат я на самом деле получил?
Ptrace выдал около 33 500 значений RIP, а Qemu — 29 800 значений RIP. Значения RIP обоих текстовых файлов начинают отличаться от инструкции 240., большинство значений rip идентичны, но ptrace выполняет около 5500 инструкций, qemu не выполняет, а qemu выполняет около 1800 инструкций, ptrace не выполняет, что приводит к разнице примерно в 3700 инструкций. Оба прогона, кажется, выполняют разные вещи во всей программе, например, есть блок из 3500 инструкций из инструкции 26.500-30.000 (очистка?), который выполняется в собственном прогоне, но не qemu.
Какой у меня вопрос
Почему значения RIP не одинаковы на протяжении всего выполнения программы и, самое главное: что мне нужно сделать, чтобы оба запуска были одинаковыми?
Дополнительная информация
ld-linux-x86-64.so.2
с -L /lib64/
- это не дало никакого эффекта@stark, запускающий код в другой системе, немного меняет количество выполняемых инструкций, но разница между ptrace и qemu остается примерно такой же.
Вам нужно будет проанализировать фактический запуск выполнения (если они расходятся на insn 240 или около того, это не будет очень сложно), чтобы определить, почему. Возможные причины включают в себя то, что среда QEMU, предоставляемая программе, не будет полностью идентична исходной версии — например, набор вещей, которые она помещает во вспомогательный вектор, немного отличается, поэтому, если динамический компоновщик выполняет итерацию через auxv, он пройтись по петле разное количество раз.
Между прочим, если вы действительно не заботитесь о динамическом компоновщике, вы, вероятно, могли бы просто отбросить все значения RIP перед первым insn в main() - я подозреваю, что это с большей вероятностью даст идентичные результаты в обоих случаях, хотя, безусловно, есть гостевые программы это показало бы разницу и после main().
@PeterMaydell Я использовал ведение журнала qemu in_asm, чтобы выяснить, где возникают различия. я узнал, что первая разница происходит в _dl_aux_init
. Другие различия происходят в __tunables_init
get_common_indices.constprop.0
__libc_start_main
strchr_ifunc
tcache_init.part.0
_dl_non_dynamic_init
__strlen_sse2
__mempcpy_sse2_unaligned
и __strrchr_sse2
Хорошо, так что первая часть этого действительно там, где динамический компоновщик просматривает вспомогательный вектор. Некоторые из других выглядят так, как будто гостевой код смотрит, какие функции поддерживает ЦП — на вашем хост-ЦП есть поддержка SSE2, поэтому гостевая libc выбирает оптимизированные версии функций, таких как strlen и memcpy, которые ее используют, но QEMU не не поддерживает эмуляцию SSE2, поэтому гостевая библиотека использует разные версии.
С программой «ничего не делающей», подобной той, которую вы тестируете, большая часть выполнения будет выполняться в гостевом динамическом компоновщике и libc. Они делают много работы за кулисами, прежде чем ваша программа получит управление, и часть этой работы варьируется между «родным» запуском и запуском «QEMU». Есть два основных источника расхождений, судя по некоторым дополнительным деталям, которые вы даете в комментариях:
Среда, которую QEMU предоставляет гостевому двоичному файлу, не на 100% идентична той, которую предоставляет реальное ядро хоста; он предназначен только для того, чтобы быть «достаточно близким, чтобы правильные гостевые двоичные файлы вели себя разумным образом». Например, гостю передается структура данных, которая называется «вспомогательный вектор ELF»; он содержит информацию, в том числе «какие функции ЦП поддерживаются», «под каким идентификатором пользователя вы работаете» и т. д. Динамический компоновщик выполняет итерацию по этой структуре данных при запуске, поэтому незначительные безобидные различия в том, какие записи находятся в векторе и в каком порядке, вызовут несколько разные пути выполнения в гостевом коде.
Процессор, который эмулирует QEMU, не предоставляет точно те же функции, что и ваш хост-процессор. Например, нет поддержки эмуляции AVX или SSE2. Гостевая библиотека настраивает свое поведение таким образом, чтобы использовать возможности ЦП, когда они доступны, поэтому она выбирает различные оптимизированные версии функций, таких как memcpy() или strlen() под капотом. Поскольку динамический компоновщик в конечном итоге вызовет эти функции, это также приведет к расхождениям в выполнении.
Вы можете обойти некоторые из них, ограничив область отслеживания инструкций, на которую вы смотрите, только начиная с начала «основной» функции, чтобы избежать отслеживания всего запуска динамического компоновщика. Однако я не могу придумать способ обойти различия в том, какие функции ЦП доступны на хосте и QEMU.
большое спасибо за ваше объяснение! Есть ли у вас предложения по подписям, которые я мог бы искать, чтобы узнать больше о том, что libc ведет себя по-другому?
Что происходит, когда вы запускаете одну и ту же программу в двух разных системах?