前面的研究中提到了调用 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 同样可以解决。
参考: