glibc scanf Segmentation faults when called from a function that doesn't align RSP
При компиляции кода ниже:
global main
extern printf, scanf
section .data
msg: db "Enter a number: ",10,0
format:db "%d",0
section .bss
number resb 4
section .text
main:
mov rdi, msg
mov al, 0
call printf
mov rsi, number
mov rdi, format
mov al, 0
call scanf
mov rdi,format
mov rsi,[number]
inc rsi
mov rax,0
call printf
ret
с помощью:
nasm -f elf64 example.asm -o example.o
gcc -no-pie -m64 example.o -o example
а потом беги
./example
он запускается, печатает: вводит число, но затем вылетает и печатает:ошибка сегментации (ядро сброшено)
Так что printf работает нормально, но scanf нет. Что я делаю не так с scanf так?
1 ответ
использование sub rsp, 8
/ add rsp, 8
в начале / конце вашей функции, чтобы выровнять стек до 16 байтов, прежде чем ваша функция сделает call
,
Или лучше нажмите / вставьте фиктивный регистр, например push rdx
/ pop rcx
или сохраните / восстановите сохраненный вызов регистр, такой как RBP.
При входе в функцию RSP находится на расстоянии 8 байт от 16-байтового выравнивания, потому что call
выдвинул 8-байтовый обратный адрес. См. Печать чисел с плавающей точкой из x86-64, по-видимому, требует сохранения% rbp, выравнивания основного и стекового и вызова 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 для переменных функций gcc использует test al,al / je, чтобы пролить полные 16-байтовые регистры XMM xmm0..7 с выровненными хранилищами в этом случае. __m128i
может быть аргументом для переменной функции, а не только double
и gcc не проверяет, читает ли функция когда-либо 16-байтовые аргументы FP.