linux - glibc scanf Ошибка сегментации при вызове из функции, которая не выравнивает RSP



assembly nasm (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 .

Похоже, что где-то в __GI__IO_vfscanf происходит автоматическое векторизованное копирование 16 байт, которое регулярно вызывает 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 ошибки 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.

При компиляции кода ниже:

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 так?





calling-convention