linux - libstdc - glibc scanf從不對齊RSP的函數調用時的分段錯誤



x86 calling convention (1)

編譯以下代碼時:

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上做錯了?


在函數開始/結束時使用 sub rsp, 8 / add rsp, 8 ,在 函數 call 之前將堆棧重新對齊到16個字節。

或者更好地推送/彈出一個虛擬寄存器,例如 push rdx / pop rcx ,或者保存/恢復一個像RBP這樣的保持呼叫的寄存器。

在函數輸入時,RSP與16字節對齊相距8個字節,因為 call 推送了一個8字節的返回地址。 請參閱 從x86-64打印浮點數似乎需要保存%rbp 主要和堆棧對齊 ,以及 使用GNU彙編程序在x86_64中調用printf 。 這是一個ABI要求,你曾經能夠在沒有任何用於printf的FP args時違反規定。 但不是了。

即使 AL == 0 gcc的glibc scanf代碼也依賴於16字節的堆棧對齊

它似乎在 __GI__IO_vfscanf 某處自動向量化複製16個字節,在將其寄存器args溢出到堆棧 1 之後,常規 scanf 調用。 (調用scanf的許多類似方法共享一個大的實現作為各種libc入口點的後端,如 scanffscanf 等)

我下載了Ubuntu 18.04的libc6二進制包: https://packages.ubuntu.com/bionic/amd64/libc6/download7z x blah.deb 並解 tar xf data.tar 文件(使用 7z x blah.debtar xf data.tar ,因為7z知道如何提取了很多文件格式)。

我可以使用 LD_LIBRARY_PATH=/tmp/bionic-libc/lib/x86_64-linux-gnu ./bad-printf 您的錯誤,而且我的Arch Linux桌面上的系統glibc 2.27-3也是如此。

使用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:帶有 AL != 0 printf / scanf始終需要16字節對齊,因為gcc的可變函數代碼使用test al,al / je將完整的16字節XMM寄存器xmm0..7溢出到對齊的存儲中那種情況。 __m128i 可以是可變函數的參數,而不僅僅是 double ,並且gcc不會檢查函數是否實際讀取任何16字節的FP args。





calling-convention