Step to UEFI (211)调用 ConOut 的条件(下)

前面的研究中提到了调用 ConOut 必须满足的2个条件,这次就研究一下其中的另外条件:

“代码起始处的 push rdi , 不一定是RDI,任何8Bytes的寄存器都可以,但是如果没有这语句在调用ConOut的时候这个函数内部会发生错误”

针对这个问题,使用单步跟踪的方式,尝试定位出现问题的位置:

1.CoreStartImage() 函数中的下列代码, 准备跳入 Hello.EFI 代码执行

Image->Status = Image->EntryPoint (ImageHandle, Image->Info.SystemTable);

0000000075829870 48 8B 53 38          mov         rdx,qword ptr [rbx+38h]  
0000000075829874 48 8B CE             mov         rcx,rsi  
0000000075829877 40 88 7B 18          mov         byte ptr [rbx+18h],dil  
000000007582987B FF 53 20             call        qword ptr [rbx+20h]  
000000007582987E 48 89 83 A8 00 00 00 mov         qword ptr [rbx+0A8h],rax  

2.下面是 Hello.EFI 的代码,这次使用的代码我们已经加入了对于 rsp 的调整

Hello.asm

    ; reserve space for 2 arguments
    sub rsp, 2 * 8

    ; rdx points to the EFI_SYSTEM_TABLE structure
    ; which is the 2nd argument passed to us by the UEFI firmware
    ; adding 64 causes rcx to point to EFI_SYSTEM_TABLE.ConOut
    mov rcx, [rdx + 64]

    ; load the address of our string into rdx
    lea rdx, [rel strHello]

    ; EFI_SYSTEM_TABLE.ConOut points to EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
    ; call OutputString on the value in rdx
call [rcx + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.OutputString]

3. 上面的 call 会转入下面的函数中继续

ConsoleLoggerOutputString()

  } else {
    return (ConsoleLoggerOutputStringSplit(WString, ConsoleInfo));
000000007404CE18 E8 A3 FB FF FF       call        ConsoleLoggerOutputStringSplit (07404C9C0h)  
  }

4.跳转到下面的函数继续

ConsoleLoggerOutputStringSplit()

  //
  // Forward the request to the original ConOut
  //
  Status = ConsoleInfo->OldConOut->OutputString (ConsoleInfo->OldConOut, (CHAR16*)String);
000000007404C9CA 48 8B 42 58          mov         rax,qword ptr [rdx+58h]  
000000007404C9CE 48 8B DA             mov         rbx,rdx  
000000007404C9D1 48 8B F9             mov         rdi,rcx  
000000007404C9D4 48 8B D1             mov         rdx,rcx  
000000007404C9D7 48 8B C8             mov         rcx,rax  
000000007404C9DA FF 50 08             call        qword ptr [rax+8]  

5.上述代码会继续调用下面这个函数

ConSplitterTextOutOutputString()

Status = Private->TextOutList[Index].TextOut->OutputString (
0000000075514940 48 8B 83 E0 00 00 00 mov         rax,qword ptr [rbx+0E0h]  
0000000075514947 48 8B D7             mov         rdx,rdi  
000000007551494A 4E 8B 44 30 10       mov         r8,qword ptr [rax+r14+10h]  
000000007551494F 49 8B C8             mov         rcx,r8  
0000000075514952 41 FF 50 08          call        qword ptr [r8+8]  
                                                    Private->TextOutList[Index].TextOut,
                                                    WString
                                                    );

6.上述代码会继续调用下面这个函数

GraphicsConsoleConOutOutputString ()

  //
  // Forward the request to the original ConOut
  //
  Status = ConsoleInfo->OldConOut->OutputString (ConsoleInfo->OldConOut, (CHAR16*)String);
0000000073F4C9CA 48 8B 42 58          mov         rax,qword ptr [rdx+58h]  
0000000073F4C9CE 48 8B DA             mov         rbx,rdx  
0000000073F4C9D1 48 8B F9             mov         rdi,rcx  
0000000073F4C9D4 48 8B D1             mov         rdx,rcx  
0000000073F4C9D7 48 8B C8             mov         rcx,rax  
0000000073F4C9DA FF 50 08             call        qword ptr [rax+8]  

7.继续执行

ConSplitterTextOutOutputString

    Status = Private->TextOutList[Index].TextOut->OutputString (
0000000075394940 48 8B 83 E0 00 00 00 mov         rax,qword ptr [rbx+0E0h]  
0000000075394947 48 8B D7             mov         rdx,rdi  
000000007539494A 4E 8B 44 30 10       mov         r8,qword ptr [rax+r14+10h]  
000000007539494F 49 8B C8             mov         rcx,r8  
0000000075394952 41 FF 50 08          call        qword ptr [r8+8]  
                                                    Private->TextOutList[Index].TextOut,
                                                    WString
                                                    );

8. 继续执行

WinNtGopBlt ( )

      //LABZ Mart
      CopyMem (Blt, VScreen, sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL) * Width);
000000007542217A 48 85 ED             test        rbp,rbp  
000000007542217D 74 1E                je          WinNtGopBlt+175h (07542219Dh)  
      Blt = (EFI_GRAPHICS_OUTPUT_BLT_PIXEL *) ((UINT8 *) BltBuffer + (DstY * Delta) + DestinationX * sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL));
000000007542217F 48 8B 96 28 02 00 00 mov         rdx,qword ptr [rsi+228h]  
0000000075422186 4A 8D 0C BB          lea         rcx,[rbx+r15*4]  
000000007542218A 48 03 8C 24 98 00 00 00 add         rcx,qword ptr [BltBuffer]  
      VScreen = &Private->VirtualScreen[(VerticalResolution - SrcY - 1) * HorizontalResolution + SourceX];
0000000075422192 49 03 D6             add         rdx,r14  
0000000075422195 4C 8B C5             mov         r8,rbp  
      VScreen = &Private->VirtualScreen[(VerticalResolution - SrcY - 1) * HorizontalResolution + SourceX];
0000000075422198 E8 5F 22 00 00       call        CopyMem (0754243FCh)  

9. 出乎意料的是问题发生在 CopyMem 的函数中

CopyMem()

  return InternalMemCopyMem (DestinationBuffer, SourceBuffer, Length);
0000000075424471 4D 8B C6             mov         r8,r14  
0000000075424474 48 8B D6             mov         rdx,rsi  
0000000075424477 48 8B CB             mov         rcx,rbx  
000000007542447A E8 81 CB FF FF       call        InternalMemCopyMem (075421000h)  

10.这个函数体结构如下,出现问题的是 movdqa xmmword ptr [rsp+18h],xmm0

InternalMemCopyMem()

InternalMemCopyMem:
00000000749B1000 56                   push        rsi  
00000000749B1001 57                   push        rdi  
00000000749B1002 48 89 D6             mov         rsi,rdx  
00000000749B1005 48 89 CF             mov         rdi,rcx  
00000000749B1008 4E 8D 4C 06 FF       lea         r9,[rsi+r8-1]  
00000000749B100D 48 39 FE             cmp         rsi,rdi  
00000000749B1010 48 89 F8             mov         rax,rdi  
00000000749B1013 73 05                jae         InternalMemCopyMem+1Ah (0749B101Ah)  
00000000749B1015 49 39 F9             cmp         r9,rdi  
00000000749B1018 73 48                jae         InternalMemCopyMem+62h (0749B1062h)  
InternalMemCopyMem.0:
00000000749B101A 48 31 C9             xor         rcx,rcx  
00000000749B101D 48 29 F9             sub         rcx,rdi  
00000000749B1020 48 83 E1 0F          and         rcx,0Fh  
00000000749B1024 74 0C                je          InternalMemCopyMem+32h (0749B1032h)  
00000000749B1026 4C 39 C1             cmp         rcx,r8  
00000000749B1029 49 0F 47 C8          cmova       rcx,r8  
00000000749B102D 49 29 C8             sub         r8,rcx  
00000000749B1030 F3 A4                rep movs    byte ptr [rdi],byte ptr [rsi]  
InternalMemCopyMem.1:
00000000749B1032 4C 89 C1             mov         rcx,r8  
00000000749B1035 49 83 E0 0F          and         r8,0Fh  
00000000749B1039 48 C1 E9 04          shr         rcx,4  
00000000749B103D 74 2C                je          InternalMemCopyMem+6Bh (0749B106Bh)  
00000000749B103F 66 0F 7F 44 24 18    movdqa      xmmword ptr [rsp+18h],xmm0  
InternalMemCopyMem.2:
00000000749B1045 F3 0F 6F 06          movdqu      xmm0,xmmword ptr [rsi]  
00000000749B1049 66 0F E7 07          movntdq     xmmword ptr [rdi],xmm0  
00000000749B104D 48 83 C6 10          add         rsi,10h  
00000000749B1051 48 83 C7 10          add         rdi,10h  
00000000749B1055 E2 EE                loop        InternalMemCopyMem+45h (0749B1045h)  
00000000749B1057 0F AE F0             mfence  
00000000749B105A 66 0F 6F 44 24 18    movdqa      xmm0,xmmword ptr [rsp+18h]  
00000000749B1060 EB 09                jmp         InternalMemCopyMem+6Bh (0749B106Bh)  

经过上面的分析,出现问题是是 movdqa      xmmword ptr [rsp+18h],xmm0  或者 movdqu      xmm0,xmmword ptr [rsi]   语句,对于这个指令,资料上【参考1】有如下解释:

MOVDQA - 移动对齐的双四字

将双四字从源操作数(第二个操作数)移到目标操作数(第一个操作数)。此指令可以用于在 XMM 寄存器与 128 位内存位置之间移入/移出双四字,或是在两个 XMM 寄存器之间移动。源操作数或目标操作数是内存操作数时,操作数必须对齐 16 字节边界,否则将生成一般保护性异常 (#GP)。

实际跟踪发现出现问题的时候要么是 rsp 没有16位对齐,要么是 rsi 没有对齐。因此,再回头来看这个问题应该是堆栈没有对齐导致的问题。

“3.栈按照16字节对齐

现在我们应该明白了.在调用一个函数的时候. 使用 *sub rsp,xxx**进行抬栈,函数内部则进行参数赋值.其实也是相当于push了参数.只不过它不像x86一样.在里面进行平栈了.而是外面进行平栈了.那么有个疑问.比如说我们就4个参数. 通过上面来说.我们应该申请 sub rsp,0x20个字节才对.在CALL的时候x86 x64都是一样的会将返回地址入栈. 那为什么要rsp,0x28.这样的话会多申请一个参数的值哪.

原因是这样的.栈要按照16字节对齐进行申请.

那么还有人会说.按照16字节对齐,那么我们的参数已经是16字节对齐了.比如为我们4个寄存器申请预留空间. rsp,0x20. (4 * 8 = 32 = 16j进制的 0x20) 那为什么还是会申请 rsp,0x28个字节,并且不对齐.其实是这样的.当我们在 Call函数的时候.返回地址会入栈.如果按照我们之前申请的rsp,0x20个字节的话.那么当

返回地址入栈之后,现在总共抬栈大小是 0x28个字节.并不是16进制对齐. 但是当我们一开始就申请0x28个字节.当返回地址入栈.那么就是0x28+8 = 0x30个字节. 0x30个字节不是正好跟16字节对齐吗.所以我们的疑问也就没有了.

所以申请了0x28个字节,其实多出了的8字节是要跟返回地址一样.进行栈对齐使用.

那么申请的这个8字节空间,是没有用的.只是为了对齐使用.“【参考2】

为了验证上面的说法对此,跟踪前面提到的每一次调用进行检查:

函数 对齐
Hello.asm RSP = 00000184E41CF5C8
ConsoleLoggerOutputString() RSP = 00000184E41CF578
ConsoleLoggerOutputStringSplit() RSP = 00000184E41CF548
ConSplitterTextOutOutputString() RSP = 00000184E41CF4F8
GraphicsConsoleConOutOutputString () RSP = 00000184E41CF438
FlushCursor() RSP = 00000184E41CF158
GraphicsConsoleConOutOutputString () RSP = 00000184E41CF0C8
CopyMem() RSP = 00000184E41CF098
InternalMemCopyMem() RSP = 00000184E41CF080RDX = 00000184E84EE86C

上面提到堆栈并不是调用到每个函数之后的,而是经过每个函数调整的堆栈,因为调用参数不同,因此弹出的参数数量会有差别。例如下面这个函数,进入之后会通过三条语句对堆栈进行调整,我们检查的位置就在 sub rsp,20h之后。

EFI_STATUS
ConsoleLoggerOutputStringSplit(
  IN CONST CHAR16   *String,
  IN CONSOLE_LOGGER_PRIVATE_DATA *ConsoleInfo
  )
{
0000000073F2C9C0 48 89 5C 24 08       mov         qword ptr [rsp+8],rbx  
0000000073F2C9C5 57                   push        rdi  
0000000073F2C9C6 48 83 EC 20          sub         rsp,20h  
  EFI_STATUS    Status;

  //
  // Forward the request to the original ConOut
  //
  Status = ConsoleInfo->OldConOut->OutputString (ConsoleInfo->OldConOut, (CHAR16*)String);

所以可以使用之前提到的方法多PUSH 一个 RXX寄存器,或者直接 SUB rsp,x8h 同样可以解决。

参考:

  1. https://blog.csdn.net/u011019337/article/details/9260257
  2. https://www.cnblogs.com/iBinary/p/10959444.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注