Используйте sub rsp, 8
/ add rsp, 8
в начале / конце вашей функции , чтобы заново выровнять стек до 16 байтов, прежде чем ваша функция выполнит call
.
Или лучше выдвинуть / выдвинуть фиктивный регистр, например, push rdx
/ pop rcx
, или сохраните / восстановите регистр, сохраняющий вызов, такой как RBP.
При входе в функцию RSP находится на расстоянии 8 байтов от 16-байтового выравнивания, поскольку call
выдвинул 8-байтовый адрес возврата. См. Для печати чисел с плавающей запятой из x86-64 требуется% rbp для сохранения ,
main и stack выравнивание и Вызов printf в x86_64 с использованием ассемблера GNU . Это требование ABI, которое вы привыкли обходить нарушением, когда не было аргументов FP для printf. Но не больше.
Код gcc для glibc scanf теперь зависит от выравнивания стека 16 байтов, даже если AL == 0
.
Кажется, что оно имеет векторизованное копирование 16 байтов где-то в __GI__IO_vfscanf
, которое регулярно scanf
вызывает после выброса его регистровых аргументов в стек 1 . (Многие похожие способы вызова scanf совместно используют одну большую реализацию в качестве серверной части для различных точек входа libc, таких как scanf
, fscanf
и т. Д.)
Я скачал двоичный пакет libc6 для Ubuntu 18.04: https://packages.ubuntu.com/bionic/amd64/libc6/download и извлек файлы (с 7z x blah.deb
и tar xf data.tar
, потому что 7z знает, как извлечь много форматов файлов).
Я могу воспроизвести вашу ошибку с помощью LD_LIBRARY_PATH=/tmp/bionic-libc/lib/x86_64-linux-gnu ./bad-printf
, а также получается с системой glibc 2.27-3 на моем рабочем столе Arch Linux.
С GDB я запустил его в вашей программе и сделал set env LD_LIBRARY_PATH /tmp/bionic-libc/lib/x86_64-linux-gnu
, затем run
. С layout reg
окно разборки выглядит так в том месте, где оно получило SIGSEGV:
│0x7ffff786b49a <_IO_vfscanf+602> cmp r12b,0x25 │
│0x7ffff786b49e <_IO_vfscanf+606> jne 0x7ffff786b3ff <_IO_vfscanf+447> │
│0x7ffff786b4a4 <_IO_vfscanf+612> mov rax,QWORD PTR [rbp-0x460] │
│0x7ffff786b4ab <_IO_vfscanf+619> add rax,QWORD PTR [rbp-0x458] │
│0x7ffff786b4b2 <_IO_vfscanf+626> movq xmm0,QWORD PTR [rbp-0x460] │
│0x7ffff786b4ba <_IO_vfscanf+634> mov DWORD PTR [rbp-0x678],0x0 │
│0x7ffff786b4c4 <_IO_vfscanf+644> mov QWORD PTR [rbp-0x608],rax │
│0x7ffff786b4cb <_IO_vfscanf+651> movzx eax,BYTE PTR [rbx+0x1] │
│0x7ffff786b4cf <_IO_vfscanf+655> movhps xmm0,QWORD PTR [rbp-0x608] │
>│0x7ffff786b4d6 <_IO_vfscanf+662> movaps XMMWORD PTR [rbp-0x470],xmm0 │
Таким образом, он скопировал два 8-байтовых объекта в стек с movq
+ movhps
для загрузки и movaps
для хранения. Но со смещением стека, movaps [rbp-0x470],xmm0
сбоев.
Я не взял отладочную сборку, чтобы выяснить, какая именно часть исходного кода C превратилась в эту, но функция написана на C и скомпилирована GCC с включенной оптимизацией. GCC всегда разрешалось делать это, но лишь недавно он стал достаточно умным, чтобы таким образом использовать преимущества SSE2.
Сноска 1: для printf / scanf с AL != 0
всегда требовалось выравнивание по 16 байт, потому что код gcc для переменных функций использует test al, al / je, чтобы пролить полные 16-байтовые регистры XMM xmm0..7 с выровненным магазины в этом случае. __m128i
может быть аргументом переменной функции, а не только double
, и gcc не проверяет, действительно ли функция когда-либо читает какие-либо 16-байтовые аргументы FP.