
参照上图做了一个投石机


前面介绍了 IDT ,这里继续研究Shell 下是如何处理 Exception的。
同样,前面提到过,Shell下当一个中断发生之后会从 IDT 中查找Vector入口,然后跳转进去执行。
1.代码在\UefiCpuPkg\Library\CpuExceptionHandlerLib\X64\ExceptionHandlerAsm.nasm 可以看作用是在尽量短的代码中跳转到对应的处理代码
AsmIdtVectorBegin:
%rep 32
db 0x6a ; push #VectorNum
db ($ - AsmIdtVectorBegin) / ((AsmIdtVectorEnd - AsmIdtVectorBegin) / 32) ; VectorNum
push rax
mov rax, strict qword 0 ; mov rax, ASM_PFX(CommonInterruptEntry)
jmp rax
%endrep
AsmIdtVectorEnd:
2.同一个文件中CommonInterruptEntry 函数,在堆栈中有当前的 Vector Number
;---------------------------------------;
; CommonInterruptEntry ;
;---------------------------------------;
; The follow algorithm is used for the common interrupt routine.
; Entry from each interrupt with a push eax and eax=interrupt number
; Stack frame would be as follows as specified in IA32 manuals:
;
; +---------------------+ <-- 16-byte aligned ensured by processor
; + Old SS +
; +---------------------+
; + Old RSP +
; +---------------------+
; + RFlags +
; +---------------------+
; + CS +
; +---------------------+
; + RIP +
; +---------------------+
; + Error Code +
; +---------------------+
; + Vector Number +
; +---------------------+
; + RBP +
; +---------------------+ <-- RBP, 16-byte aligned
; The follow algorithm is used for the common interrupt routine.
global ASM_PFX(CommonInterruptEntry)
ASM_PFX(CommonInterruptEntry):
cli
pop rax
;
; All interrupt handlers are invoked through interrupt gates, so
; IF flag automatically cleared at the entry point
;
xchg rcx, [rsp] ; Save rcx into stack and save vector number into rcx
and rcx, 0xFF
cmp ecx, 32 ; Intel reserved vector for exceptions?
jae NoErrorCode
bt [ASM_PFX(mErrorCodeFlag)], ecx
jc HasErrorCode
……省略…….
;
; Per X64 calling convention, allocate maximum parameter stack space
; and make sure RSP is 16-byte aligned
;
sub rsp, 4 * 8 + 8
call ASM_PFX(CommonExceptionHandler)
add rsp, 4 * 8 + 8
……省略…….
3.对于 Shell 下,会跳转到\UefiCpuPkg\Library\CpuExceptionHandlerLib\DxeException.c 这个文件中。 所有的 Shell 下中断都会经过这里。
/**
Common exception handler.
@param ExceptionType Exception type.
@param SystemContext Pointer to EFI_SYSTEM_CONTEXT.
**/
VOID
EFIAPI
CommonExceptionHandler (
IN EFI_EXCEPTION_TYPE ExceptionType,
IN EFI_SYSTEM_CONTEXT SystemContext
)
{
CommonExceptionHandlerWorker (ExceptionType, SystemContext, &mExceptionHandlerData);
}
4.真正工作的是 \UefiCpuPkg\Library\CpuExceptionHandlerLib\PeiDxeSmmCpuException.c
/**
Internal worker function for common exception handler.
@param ExceptionType Exception type.
@param SystemContext Pointer to EFI_SYSTEM_CONTEXT.
@param ExceptionHandlerData Pointer to exception handler data.
**/
VOID
CommonExceptionHandlerWorker (
IN EFI_EXCEPTION_TYPE ExceptionType,
IN EFI_SYSTEM_CONTEXT SystemContext,
IN EXCEPTION_HANDLER_DATA *ExceptionHandlerData
)
{
EXCEPTION_HANDLER_CONTEXT *ExceptionHandlerContext;
RESERVED_VECTORS_DATA *ReservedVectors;
EFI_CPU_INTERRUPT_HANDLER *ExternalInterruptHandler;
ExceptionHandlerContext = (EXCEPTION_HANDLER_CONTEXT *) (UINTN) (SystemContext.SystemContextIa32);
ReservedVectors = ExceptionHandlerData->ReservedVectors;
ExternalInterruptHandler = ExceptionHandlerData->ExternalInterruptHandler;
……省略……
这里,可以使用 DEBUG宏输出调试信息,可以用串口输出看到完整的信息。
上面的流程使用 DCI 确认过,有兴趣的朋友可以同样尝试使用 DCI来观察。需要注意的是,有时候调用无法使用单步跟踪指令完成,推荐使用在要跳转到的位置下断点的方式来进行追踪。
此外,IBV 的代码可能会对文件进行 Override,就是同一个代码中出现两个同样名称的文件,两者大部分代码相同,但是会存在一些细节差别,这在追踪过程中需要特别注意。
前面介绍了静态条件下的分析,下面研究一下EFI 文件加载在内存中的情况。
用一个图来说明情况:
左侧是文件,右侧是加载到内存后的情况,可以看到对于头是照搬到内存中。对于Section 的话,看起来就比较麻烦,一般情况下内存的对齐要求会比PE文件要求的大。比如:PE 中按照 16Bytes对齐,在内存中可能要求按照 64Bytes对齐,相比是因为 PE文件希望紧凑一些,内存的数据希望读取更快所以要做成这样的。
用一个GenCRC32.exe为例:
加载到内存中的是按照 0x1000对齐,数据在文件中存放是按照 0x200对齐。
继续查看 .text Section 可以看到 RVA=0x1000,意思是:当这个PE被加载到内存后,会放在 BaseAddress+0x1000的内存地址;Pointer to Raw Data 给出 0x400意思是这个段在文件中的位置是从 0x400开始的。
下面我们查看之前的 es.efi ,可以看到文件对齐和内存对齐是相同的。
再查看 Section,可以看到 RVA 和 Pointer to Raw Data 是相同的:
有兴趣的可以多查看几个EFI和 Section ,和上面是相同的。为此,我们再做一个实验,在\MdePkg\Library\UefiApplicationEntryPoint\ApplicationEntryPoint.c 加入一个中断:
EFI_STATUS
EFIAPI
_ModuleEntryPoint (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
if (_gUefiDriverRevision != 0) {
//
// Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
//
if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
return EFI_INCOMPATIBLE_VERSION;
}
}
CpuBreakpoint(); //LABZ_Debug
//
// Call constructor for all libraries.
//
ProcessLibraryConstructorList (ImageHandle, SystemTable);
重新编译一次es.efi(注意不要重新编译 NT32Pkg)。执行之后报错可以用 VS2015断下来:
跳出int中断就来到我们EFI 代码的领空:
对照ApplicationEntryPoint.cod 查看停在CpuBreakpoint() 之后的语句了:
_ModuleEntryPoint PROC ; COMDAT
; 45 : {
$LN25:
00000 48 89 5c 24 08 mov QWORD PTR [rsp+8], rbx
00005 48 89 74 24 10 mov QWORD PTR [rsp+16], rsi
0000a 57 push rdi
0000b 48 83 ec 20 sub rsp, 32 ; 00000020H
0000f 48 8b fa mov rdi, rdx
00012 48 8b f1 mov rsi, rcx
; 46 : EFI_STATUS Status;
; 47 :
; 48 : if (_gUefiDriverRevision != 0) {
; 49 : //
; 50 : // Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
; 51 : //
; 52 : if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
; 53 : return EFI_INCOMPATIBLE_VERSION;
; 54 : }
; 55 : }
; 56 : CpuBreakpoint(); //LABZ_Debug
00015 e8 00 00 00 00 call CpuBreakpoint
; 57 : //
; 58 : // Call constructor for all libraries.
; 59 : //
; 60 : ProcessLibraryConstructorList (ImageHandle, SystemTable);
0001a 48 8b d7 mov rdx, rdi
0001d 48 8b ce mov rcx, rsi
00020 e8 00 00 00 00 call ProcessLibraryConstructorList
当前的位置是 0x 26BABD252DA,那么这个 .text 段是在26BABD252C0 开始的,我们还可以推理出Image 加载位置(BaseAddress)0x26BABD252C0 – 0x2c0=0x26BABD25000:
.text 段在 0x26BABD25000 + 0x2C0
.rdata段在0x26BABD25000 + 0x1700
.data段在0x26BABD25000 + 0x1F20
无名段在0x26BABD25000 + 0x1F60
.xdata段在0x26BABD25000 + 0x2020
.reloc段在0x26BABD25000 + 0x20C0
我们只验证一下 .reloc 段:
可见他们是完全相同的,就是说 EFI 文件加载到内存之后依然是相同的对齐。从设计上说,这样可以使得文件更加紧凑,能够化简加载动作,同时方便调试。
参考:
2019年10月19-20日,本站会参加位于上海市杨浦区五角场市级副中心国和路 346 号江湾体育场举办的创客嘉年华活动,展位编号 M10。主要展示 Arduino 设计相关内容。
展览中出售的模块如下:
欢迎新老朋友前来捧场。
很多年前我去 DELL 面试,里面的BIOS工程师问如何实现一个delay。我讲了一些使用硬件Timer的方法来实现精确的delay,他都一直摇头。最后我实在忍不住问他正确答案是什么。他的回答是 90h 也就是 NOP指令。当然,NOP是最简单的方法,但是这种方法密切和CPU速度相关的,在不同的CPU上实现的效果不同。
最近查看UEFI 发现其中实现了一个delay使用的是 PAUSE 指令看起来很有意思,于是做了一番研究。具体代码在 \MdePkg\Library\BaseLib\Ia32\CpuPause.c 和\MdePkg\Library\BaseLib\X64\CpuPause.nasm 中。殊途同归,最终都是通过pause 指令来实现。
PAUSE Spin Loop Hint
Opcode Mnemonic Description
F3 90 PAUSE Gives hint to processor that improves performance of spin-wait loops.
Description
Improves the performance of spin-wait loops. When executing a “spin-wait loop,” a Pentium 4 or Intel Xeon processor suffers a severe performance penalty when exiting the loop because it detects a possible memory order violation. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation in most situations, which greatly improves processor performance. For this reason, it is recommended that a PAUSE instruction be placed in all spin-wait loops.
An additional function of the PAUSE instruction is to reduce the power consumed by a Pentium 4 processor while executing a spin loop. The Pentium 4 processor can execute a spinwait loop extremely quickly, causing the processor to consume a lot of power while it waits for the resource it is spinning on to become available. Inserting a pause instruction in a spinwait loop greatly reduces the processor’s power consumption.
This instruction was introduced in the Pentium 4 processors, but is backward compatible with all IA-32 processors. In earlier IA-32 processors, the PAUSE instruction operates like a NOP instruction. The Pentium 4 and Intel Xeon processors implement the PAUSE instruction as a pre-defined delay. The delay is finite and can be zero for some processors. This instruction does not change the architectural state of the processor (that is, it performs essentially a delaying noop operation). 【参考1】
从上面的资料来看 Pause 的机器码和 NOP 的非常像。因此,这样可以实现兼容之前的 CPU。
上面一段话翻译如下:
“PAUSE指令提升了自旋等待循环(spin-wait loop)的性能。当执行一个循环等待时,Intel P4或Intel Xeon处理器会因为检测到一个可能的内存顺序违规(memory order violation)而在退出循环时使性能大幅下降。PAUSE指令给处理器提了个醒:这段代码序列是个循环等待。处理器利用这个提示可以避免在大多数情况下的内存顺序违规,这将大幅提升性能。因为这个原因,所以推荐在循环等待中使用PAUSE指令。
PAUSE的另一个功能就是降低Intel P4在执行循环等待时的耗电量。Intel P4处理器在循环等待时会执行得非常快,这将导致处理器消耗大量的电力,而在循环中插入一个PAUSE指令会大幅降低处理器的电力消耗。”【参考2】
参考:
首先测试1M 速率, 测试D15(SCK). 因为先是发送0x90,然后再发送一个 0x00(也是读取),所以会送2次 SCK.
查看 MOSI 上面的信号
再测试8M,这是 32U4可以达到的最高速度(16Mhz的一半)
信号幅度是5V,因此,如果和 3.3V设备通讯,必须进行电平转换.
=============================================================
2023年2月1日
使用 USBTinyISP 烧写 Booloader的方法:
1:MISO 接 D14
2.VCC 接 VIN
3.SCK接D15
4.MOSI接D16
5.RESET接RST
6.GND接GND
之后,直接使用 Arduino IDE 自带的Bootloader功能烧录即可。
前面的文章“EFI 文件研究(1)”提到了入口地方有一个奇怪的现象,直接从ProcessLibraryConstructorList 函数跳到了ProcessModuleEntryPointList。百思不得其解之后咨询天杀,他提到有编译器有一种优化方式,针对“连续两个函数的调用 ,在优化后可能会将第一个函数的尾部返回优化成对第二个函数的跳转,然后由第二个函数来进行返回”。经过这样的提醒后,我在代码中查找,找到了非常类似的代码。
在\MdePkg\Library\UefiApplicationEntryPoint\ApplicationEntryPoint.c 定义了Application 的入口:
/**
Entry point to UEFI Application.
This function is the entry point for a UEFI Application. This function must call
ProcessLibraryConstructorList(), ProcessModuleEntryPointList(), and ProcessLibraryDestructorList().
The return value from ProcessModuleEntryPointList() is returned.
If _gUefiDriverRevision is not zero and SystemTable->Hdr.Revision is less than _gUefiDriverRevison,
then return EFI_INCOMPATIBLE_VERSION.
@param ImageHandle The image handle of the UEFI Application.
@param SystemTable A pointer to the EFI System Table.
@retval EFI_SUCCESS The UEFI Application exited normally.
@retval EFI_INCOMPATIBLE_VERSION _gUefiDriverRevision is greater than SystemTable->Hdr.Revision.
@retval Other Return value from ProcessModuleEntryPointList().
**/
EFI_STATUS
EFIAPI
_ModuleEntryPoint (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
if (_gUefiDriverRevision != 0) {
//
// Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
//
if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
return EFI_INCOMPATIBLE_VERSION;
}
}
//
// Call constructor for all libraries.
//
ProcessLibraryConstructorList (ImageHandle, SystemTable);
//
// Call the module's entry point
//
Status = ProcessModuleEntryPointList (ImageHandle, SystemTable);
//
// Process destructor for all libraries.
//
ProcessLibraryDestructorList (ImageHandle, SystemTable);
//
// Return the return status code from the driver entry point
//
return Status;
}
因此,前面_gUefiDriverRevision == 0 在编译期内部代码直接会被优化掉,剩下的就是连续两次调用ProcessLibraryConstructorList 和ProcessModuleEntryPointList 函数。
为了证明这一点,我在 Inf 文件中加入关闭优化的指令 /Od:
[BuildOptions]
MSFT:*_*_X64_CC_FLAGS = /FAsc /Od
加入之后再次编译,
在 \Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\ApplicationEntryPoint.cod 中可以看到完整的图景:
_ModuleEntryPoint PROC ; COMDAT
; 45 : {
$LN25:
00000 48 89 5c 24 08 mov QWORD PTR [rsp+8], rbx
00005 48 89 74 24 10 mov QWORD PTR [rsp+16], rsi
0000a 57 push rdi
0000b 48 83 ec 20 sub rsp, 32 ; 00000020H
0000f 48 8b fa mov rdi, rdx
00012 48 8b f1 mov rsi, rcx
; 46 : EFI_STATUS Status;
; 47 :
; 48 : if (_gUefiDriverRevision != 0) {
; 49 : //
; 50 : // Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
; 51 : //
; 52 : if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
; 53 : return EFI_INCOMPATIBLE_VERSION;
; 54 : }
; 55 : }
; 56 :
; 57 : //
; 58 : // Call constructor for all libraries.
; 59 : //
; 60 : ProcessLibraryConstructorList (ImageHandle, SystemTable);
00015 e8 00 00 00 00 call ProcessLibraryConstructorList
; 61 :
; 62 : //
; 63 : // Call the module's entry point
; 64 : //
; 65 : Status = ProcessModuleEntryPointList (ImageHandle, SystemTable);
0001a 48 8b d7 mov rdx, rdi
0001d 48 8b ce mov rcx, rsi
00020 e8 00 00 00 00 call ProcessModuleEntryPointList
; 66 :
; 67 : //
; 68 : // Process destructor for all libraries.
; 69 : //
; 70 : ProcessLibraryDestructorList (ImageHandle, SystemTable);
00025 48 8b d7 mov rdx, rdi
00028 48 8b ce mov rcx, rsi
0002b 48 8b d8 mov rbx, rax
0002e e8 00 00 00 00 call ProcessLibraryDestructorList
; 71 :
; 72 : //
; 73 : // Return the return status code from the driver entry point
; 74 : //
; 75 : return Status;
; 76 : }
00033 48 8b 74 24 38 mov rsi, QWORD PTR [rsp+56]
00038 48 8b c3 mov rax, rbx
0003b 48 8b 5c 24 30 mov rbx, QWORD PTR [rsp+48]
00040 48 83 c4 20 add rsp, 32 ; 00000020H
00044 5f pop rdi
00045 c3 ret 0
_ModuleEntryPoint ENDP
这里可以清楚的看到分别调用了2个函数,并且和我们上面找到的位置代码是一致的。
从上面的试验可以得知:
EFI 文件使用的是 PE 格式(PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件【参考1】),所以很多关于PE文件的知识在 EFI 文件上仍然是通用的。
我们编写一个代码来进行研究。功能非常简单,如果运行时加入 a 参数,那么打印一串字符,否则无任何动作。具体代码如下:
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/ShellCEntryLib.h>
/***
Print a welcoming message.
Establishes the main structure of the application.
@retval 0 The application exited normally.
@retval Other An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
IN UINTN Argc,
IN CHAR16 **Argv
)
{
if ((Argc>1)&&(Argv[1][0]=='a')) {
Print(L"Hello there fellow Programmer.\n");
}
return(0);
}
此外在对应的 inf 文件加入下面一段:
[BuildOptions]
MSFT:*_*_X64_CC_FLAGS = /FAsc
编译之后,可以在\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\DEBUG目录下看到 es.efi 和 es.map 文件。打开 es.map 文件,有如下 Section:
Start Length Name Class
0001:00000000 000011aeH .text$mn CODE
0002:00000000 00000654H .rdata DATA
0002:00000654 00000114H .rdata$zzzdbg DATA
0003:00000000 00000020H .data DATA
0003:00000020 00000020H .bss DATA
0004:00000000 00000084H .pdata DATA
0005:00000000 0000007cH .xdata DATA
这些 Section 的作用是:
.rdata – 保存常量数据的节。这个可以对应C语言中的常数和常量字符串,同上面一样的原因,他们的初值被保存到了PE文件的次Section中,而不是在运行时被赋值。
.data – 保存数据的节,这个对应C语言中以初始化的全局变量数据。想想为什么你在源码里初始化一个全局变量后运行时这个变量的值正是你想要的那个?int a = 12;并不意味着CRT为你执行了一个赋值语句,而是a在PE文件中保存的位置已经被硬编码了一个12的值。这样loader加载程序时,你给的初值被从PE文件读取到了内存中变量a的位置,这样才使你的变量a有了初值。
.bss – (Block Start with Symbol) 这个section对应C程序中的全局未初始化变量。啥?你说C中未初始化的全局变量实际上全被初始化成了0?这是因为实际上操作系统是这样干的——你的全局未初始化变量由于没有初值,所以不需要将值像上面两个一样保存到PE文件中(所以.bss节除了描述信息之外不占据磁盘空间),但是.bss会描述一段内存区域,loader在加载.bss section时直接开辟这么一块包括所有未初始化数据的内存区域,然后直接将这区域清零。这就是C中全局未初始化数据之所以为零的原因了。
.pdata和 .xdata都存放的是异常处理相关的内容。
.text 是最重要的执行代码段 。如果我们想直接修改 EFI 文件中的代码,需要直接在 .text section 中查找修改。【参考2】【参考3】
使用 NikPEViwer(这是一个 Windows PE 文件查看工具) 打开 es.efi。在左侧的 .text 双击可以直接定位到 TEXT Section。
同样的,我们可以使用这个工具找到这个 EFI文件的EntryPoint为 0x2C0,具体是在 NT Headers-> Optional header –> AddressOfEntryPoint【参考5】
在编译之后的结果中查找,第一个条语句在
\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\AutoGen.cod 文件中(从实习结果来看,这样生成的COD文件中有很多注释是不对的,所以具体要根据C语句和汇编语言对照来进行分析)
ProcessLibraryConstructorList PROC ; COMDAT
; File c:\buildbs\201903\mdepkg\library\uefibootservicestablelib\uefibootservicestablelib.c
; 62 : gBS = SystemTable->BootServices;
00000 48 8b 42 60 mov rax, QWORD PTR [rdx+96]
00004 48 89 05 00 00
00 00 mov QWORD PTR gBS, rax
其中COD 文件列出的 0004 开始的 48 89 05 00 00 00 00 末尾的4字节00 是Link时才会确定的取值,因此和实际 48 89 05 19 00 00 00 是有差别的。
此外这个函数的末尾有一个 ret,但是最后生成的代码中是 E9 03 00 00 00 这个字样。
0001d 48 89 15 00 00
00 00 mov QWORD PTR gST, rdx
; 208 : }
00024 c3 ret 0
ProcessLibraryConstructorList ENDP
_TEXT ENDS
反编译结果如下【参考4】:
就是说在编译过程中 ret 被替换成跳转指令,跳转的位置是下一个函数。
ProcessModuleEntryPointList PROC ; COMDAT
; 232 : {
$LN27:
00000 4c 8b dc mov r11, rsp
00003 49 89 5b 08 mov QWORD PTR [r11+8], rbx
00007 49 89 73 20 mov QWORD PTR [r11+32], rsi
0000b 57 push rdi
我们 C 代码中的判断条件if ((Argc>1)&&(Argv[1][0]==’a’)) { 也在这个 COD文件中,可以看到 000a2 处有一个 cmp ,之后000a6 会根据结果进行跳转:
; 33 : if ((Argc>1)&&(Argv[1][0]=='a')) {
0008e 48 8b 44 24 50 mov rax, QWORD PTR EfiShellInterface$2[rsp]
00093 48 83 78 18 01 cmp QWORD PTR [rax+24], 1
00098 76 1a jbe SHORT $LN18@ProcessMod
0009a 48 8b 40 10 mov rax, QWORD PTR [rax+16]
$LN25@ProcessMod:
0009e 48 8b 48 08 mov rcx, QWORD PTR [rax+8]
000a2 66 83 39 61 cmp WORD PTR [rcx], 97 ; 00000061H
000a6 75 0c jne SHORT $LN18@ProcessMod
对于我们来说,只要修改 jne 为 je (机器码74)即可完成目标。前面图片展示过,ProcessModuleEntryPointList 函数从0x2ec开始,因此,0006a 在文件中的偏移是 0x2ec+0xa6=0x392.直接查看这里是 75 修改为 74 (JE),保存为 esM.efi
修改之后可以直接在 Nt32 环境中测试:
可以看到,修改之后输入 a 参数不会输出,反而输入其他参数会输出字符。
本文提到的源代码和 EFI 文件下载(注意:EFI文件并非试验中使用的,具体偏移可能有差别,只供参考)
通过上述试验我们可以了解下面的知识:
参考:
pNTHeader->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;
上述资料来自 https://docs.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)?redirectedfrom=MSDN
有一千个读者便有一千个哈姆雷特。又好比鲁迅先生评价《红楼梦》 “经学家看见《易》,道学家看见淫,才子看见缠绵,革命家看见排满,流言家看见宫闱秘事。” 再比如经常被人批判的《古惑仔》系列电影,觉得它教坏小朋友,我觉得并不是。在我看起来应该属于青春励志题材。展现了“以陈浩南为首的香港下层青年人不甘平庸,通过打拼努力向上”的故事。当然,还可以说他是批判现实主义题材的作品,也包含了做人的道理。比如,影片中靓坤转眼间就被几个小时前羞辱的小警察射杀。
再比如3Q大战之时,记者采访了红衣教主,没成想有牛人直接从采访的视频声音中分析出来了他的电话【参考1】。谁能想到流氓软件之父竟然栽倒这样的事情中。
这次我们使用 Teensy 和数字麦克风来实现这个功能。
先介绍一下原理。电话实现拨号有两种方式:脉冲拨号和音频拨号。
脉冲拨号是一种时域处理方法,它用脉冲的个数来表示号码数字。脉冲拨号方式对脉冲的宽度、大小、间距、形状都有着严格的要求,如果由于线路的干扰或其他原因而使得这些参数发生了变化,则可能引起号码接收的错误。另一方面,由于每个脉冲都占有一定的时间(一般每个脉冲占用的时间为100ms),而使得这种拨号方式比较慢。当拨号时,用户通常会听到一串拨号音,老式的转盘电话就使用脉冲拨号。比如,拨号“0”时,电路“断”、“续”10次,代表数字“0”。可以看到,如果号码较长拨号耗时也会很长。因此,这种拨号方式逐渐为音频拨号所取代。
我们常用的音频拨号是双音多频 DTMF(Dual Tone Multi Frequency),双音多频,由高频群和低频群组成,低频群包含3个频率,高频群包含4个频率。一个高频信号和一个低频信号叠加组成一个组合信号,代表一个数字。
1209 | 1336 | 1447 | |
697 | 1 | 2 | 3 |
770 | 4 | 5 | 6 |
852 | 7 | 8 | 9 |
941 | * | 0 | # |
比如,用频率为770Hz 的正弦波加到1366Hz 的正弦波合成一个声音表示数字“5”
这个合成过程用Matlab 模拟如下:
首先是 770Hz 的正弦波:
这是 1366Hz 的正弦波
二者相加的结果是
对我们来说,目标是将上面这个合成后的结果分解得到具体是哪两个信号合成的。自然而然想到使用傅立叶变换来处理:
放大,可以看到是 770 和 1366 合成的。
这次选择 Teensy+SPH0645LM4H实现声音的采集和分析,最终结果显示在一个 1602 LCD上。
接线方式:
Teensy 3.2 SPH0645LM4H
GND SEL
D23 LRCL
D13 DOUT
D9 BCLK
GND GND
3.3V 3V
Teensy 3.2 LCD1602
GND GND
Vin VCC
Pin19 SCL
Pin18 SDA
代码上分析获得频率并没有通过傅立叶变换,而是直接使用 Teensy 的音频库中的AudioAnalyzeToneDetect 函数。这个函数使用的是Goertzel算法,该算法的主要思想是检查音频数据中是否包含某一个给定的频率【参考2】。因此,对于我们来说就是不断测试当前的信号中是否有697到1477这些频率,如果存在的话就转化为对应的数字。
完整代码如下:
// Dial Tone (DTMF) decoding
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x3f,20,4);
// Create the Audio components. These should be created in the
// order data flows, inputs/sources -> processing -> outputs
//
AudioInputI2S audioIn;
AudioAnalyzeToneDetect row1; // 7 tone detectors are needed
AudioAnalyzeToneDetect row2; // to receive DTMF dial tones
AudioAnalyzeToneDetect row3;
AudioAnalyzeToneDetect row4;
AudioAnalyzeToneDetect column1;
AudioAnalyzeToneDetect column2;
AudioAnalyzeToneDetect column3;
// Create Audio connections between the components
//
AudioConnection patchCord01(audioIn, 0, row1, 0);
AudioConnection patchCord02(audioIn, 0, row2, 0);
AudioConnection patchCord03(audioIn, 0, row3, 0);
AudioConnection patchCord04(audioIn, 0, row4, 0);
AudioConnection patchCord05(audioIn, 0, column1, 0);
AudioConnection patchCord06(audioIn, 0, column2, 0);
AudioConnection patchCord07(audioIn, 0, column3, 0);
int charpos=0;
void setup() {
// Audio connections require memory to work. For more
// detailed information, see the MemoryAndCpuUsage example
AudioMemory(12);
lcd.init();
lcd.backlight();
while (!Serial);
delay(100);
Serial.println("Start decoding");
// Configure the tone detectors with the frequency and number
// of cycles to match. These numbers were picked for match
// times of approx 30 ms. Longer times are more precise.
row1.frequency(697, 21);
row2.frequency(770, 23);
row3.frequency(852, 25);
row4.frequency(941, 28);
column1.frequency(1209, 36);
column2.frequency(1336, 40);
column3.frequency(1477, 44);
}
const float row_threshold = 0.009;
const float column_threshold = 0.009;
char lastdigit=0;
void loop() {
float r1, r2, r3, r4, c1, c2, c3;
char digit=0;
// read all seven tone detectors
r1 = row1.read();
r2 = row2.read();
r3 = row3.read();
r4 = row4.read();
c1 = column1.read();
c2 = column2.read();
c3 = column3.read();
/*
// print the raw data, for troubleshooting
Serial.print("tones: ");
Serial.print(r1);
Serial.print(", ");
Serial.print(r2);
Serial.print(", ");
Serial.print(r3);
Serial.print(", ");
Serial.print(r4);
Serial.print(", ");
Serial.print(c1);
Serial.print(", ");
Serial.print(c2);
Serial.print(", ");
Serial.println(c3);
*/
// check all 12 combinations for key press
if (r1 >= row_threshold) {
if (c1 > column_threshold) {
digit = '1';
} else if (c2 > column_threshold) {
digit = '2';
} else if (c3 > column_threshold) {
digit = '3';
}
} else if (r2 >= row_threshold) {
if (c1 > column_threshold) {
digit = '4';
} else if (c2 > column_threshold) {
digit = '5';
} else if (c3 > column_threshold) {
digit = '6';
}
} else if (r3 >= row_threshold) {
if (c1 > column_threshold) {
digit = '7';
} else if (c2 > column_threshold) {
digit = '8';
} else if (c3 > column_threshold) {
digit = '9';
}
} else if (r4 >= row_threshold) {
if (c1 > column_threshold) {
digit = '*';
} else if (c2 > column_threshold) {
digit = '0';
} else if (c3 > column_threshold) {
digit = '#';
}
}
// print the key, if any found
if ((digit > 0)&&(lastdigit!=digit)) {
//if (digit > 0) {
Serial.print(" --> Key: ");
Serial.println(digit);
lcd.setCursor(charpos % 16, (charpos / 16)%2);
lcd.print(digit);
charpos++;
}
lastdigit=digit;
// uncomment these lines to see how much CPU time
// the tone detectors and audio library are using
//Serial.print("CPU=");
//Serial.print(AudioProcessorUsage());
//Serial.print("%, max=");
//Serial.print(AudioProcessorUsageMax());
//Serial.print("% ");
}
工作的照片:
工作的视频:
https://zhuanlan.zhihu.com/p/70966370
很明显,你的按键信息包含在了发出的声音中,这是一个安全隐患。现实生活中,自动柜员机(ATM)也有一个键盘。仔细观察能发现每个按键会发出相同的声音。但是在十几年前,那个键盘的数字对应着的是不同的声音………..
参考:
1. https://www.cnblogs.com/emouse/archive/2012/09/01/2666308.html?utm_source=debugrun&utm_medium=referral 转:技术宅逆天了!如何从按键音中听出周鸿祎的手机号码
2. https://blog.csdn.net/silent123go/article/details/54022037
前面做过了 GDT 的解析,这次研究一下 IDT。
X86 上有2种中断模式:中断(interrupts)和异常(exceptions )
Interrupt 是异步,通常由 I/O 设备来生成,比如:设置一个定时器在某个时间之后发生;
Exception 是同步的,当处理器执行某个指令之后产生的。对于 Exception 还分为 faults, traps 和 abort. X86处理器对于上面两种处理方式相同,当某一个中断发生之后,CPU会通过 IDT来找到对应的处理函数。处理完成之后将控制权返回产生的位置。
X86处理器有32个预定义中断异常,余下224个可以由用户自己定义。每个IDT有一个定义的数值: vector。当然,我们更常见的是IRQXX 的说法,在X86中还有一套让 IRQ 和 Vector对应起来的机制,未来会继续研究。
和 GDT 非常类似,可以通过 IDTR寄存器获得IDT 长度和它在内存中的位置。
其中的描述符定义在\MdePkg\Include\Library\BaseLib.h 文件中如下:
///
/// Byte packed structure for an x64 Interrupt Gate Descriptor.
///
typedef union {
struct {
UINT32 OffsetLow:16; ///< Offset bits 15..0.
UINT32 Selector:16; ///< Selector.
UINT32 Reserved_0:8; ///< Reserved.
UINT32 GateType:8; ///< Gate Type. See #defines above.
UINT32 OffsetHigh:16; ///< Offset bits 31..16.
UINT32 OffsetUpper:32; ///< Offset bits 63..32.
UINT32 Reserved_1:32; ///< Reserved.
} Bits;
struct {
UINT64 Uint64;
UINT64 Uint64_1;
} Uint128;
} IA32_IDT_GATE_DESCRIPTOR;
GateType可能是下面三种类型之一:
具体类型定义如下:
#define IA32_IDT_GATE_TYPE_TASK 0x85
#define IA32_IDT_GATE_TYPE_INTERRUPT_16 0x86
#define IA32_IDT_GATE_TYPE_TRAP_16 0x87
#define IA32_IDT_GATE_TYPE_INTERRUPT_32 0x8E
#define IA32_IDT_GATE_TYPE_TRAP_32 0x8F
接下来编程解析 Shell 下 IDT完整代码如下:
/** @file
Application for Cryptographic Primitives Validation.
Copyright (c) 2009 - 2016, Intel Corporation. All rights reserved.<BR>
This program and the accompanying materials
are licensed and made available under the terms and conditions of the BSD License
which accompanies this distribution. The full text of the license may be found at
http://opensource.org/licenses/bsd-license.php
THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
**/
#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/DebugLib.h>
#include <Protocol/PciIo.h>
#include <Library/ShellLib.h>
EFI_STATUS
EFIAPI
GDTMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
IA32_DESCRIPTOR Idtr;
IA32_IDT_GATE_DESCRIPTOR *IdtTable;
UINT16 Index;
UINT16 IdtEntryCount;
ShellSetPageBreakMode(TRUE);
AsmReadIdtr (&Idtr);
IdtEntryCount = (UINT16) ((Idtr.Limit + 1) / sizeof (IA32_IDT_GATE_DESCRIPTOR));
IdtTable = (IA32_IDT_GATE_DESCRIPTOR *) Idtr.Base;
Print(L"IDTR=0x%lX\n",IdtTable);
for (Index = 0; Index < IdtEntryCount; Index++) {
Print(L"No.[%d] ",Index);
Print(L"Selector[%d] ",IdtTable->Bits.Selector);
Print(L"Type [0x%X] ",IdtTable->Bits.GateType);
Print(L"Offset [0x%lX]\n",
(IdtTable->Bits.OffsetUpper<<32)|
(IdtTable->Bits.OffsetHigh <<16)|
(IdtTable->Bits.OffsetLow)
);
IdtTable++;
}
return EFI_SUCCESS;
}
在实体机上运行结果如下:
感觉最早实模式下的中断向量表非常类似,只是中断向量表一定是在内存从0开始的位置,而这个可以放置在内存的任何位置。
前面提到处理器有18个预定义的异常:
可以看到上面预定义了21个异常,此外还有11个被 Reserved 起来。
接下来我们用DCI来trace #DE(Divide Error)的处理。
首先编写一个产生 #DE 的代码。这个是除数为0错误。主要代码如下:
INTN
EFIAPI
ShellAppMain (
IN UINTN Argc,
IN CHAR16 **Argv
)
{
volatile x=0;
//CpuDeadLoop();
for (x = 0; x == 0;);
x=x/x;
return(0);
}
对应的汇编语言如下:
ShellAppMain PROC ; COMDAT
; 21 : {
$LN6:
00000 48 89 54 24 10 mov QWORD PTR [rsp+16], rdx
00005 48 89 4c 24 08 mov QWORD PTR [rsp+8], rcx
0000a 48 83 ec 18 sub rsp, 24
; 22 : volatile x=0;
0000e c7 04 24 00 00
00 00 mov DWORD PTR x$[rsp], 0
; 23 :
; 24 : //CpuDeadLoop();
; 25 : for (x = 0; x == 0;);
00015 c7 04 24 00 00
00 00 mov DWORD PTR x$[rsp], 0
$LN2@ShellAppMa:
0001c 8b 04 24 mov eax, DWORD PTR x$[rsp]
0001f 85 c0 test eax, eax
00021 75 02 jne SHORT $LN3@ShellAppMa
00023 eb f7 jmp SHORT $LN2@ShellAppMa
$LN3@ShellAppMa:
; 26 :
; 27 : x=x/x;
00025 8b 04 24 mov eax, DWORD PTR x$[rsp]
00028 8b 0c 24 mov ecx, DWORD PTR x$[rsp]
0002b 99 cdq
0002c f7 f9 idiv ecx
0002e 89 04 24 mov DWORD PTR x$[rsp], eax
; 28 :
; 29 : return(0);
00031 33 c0 xor eax, eax
; 30 : }
00033 48 83 c4 18 add rsp, 24
00037 c3 ret 0
ShellAppMain ENDP
为了便于观察这里面我们使用 for (x = 0; x == 0;); 等效于 CpuDeadLoop(); 可以让我们在运行的时候停下来。具体的调试操作如下:
1.先用 IDT 确定 Vector 0 的入口是 0x42254018 (这里和上一次查看的位置不同,这是一个安全特性,同样的BIOS每次运行各种 Table 的位置有所不同);
2.运行DETest.efi。运行之后会 Hang 在 Shell下。在调试机上运行 itp.halt() 停止;
3.使用 itp.threads[0].asm(“$”,20)查看停的位置
可以看到这里就是前面的 “eb f7 jmp SHORT $LN2@ShellAppMa”,是我们让CPU Dead loop 的代码。
4.使用 itp.threads[0].mem 修改内存代码跳过去。再次查看,可以看到 jmp 已经被修改为 nop
5.使用 itp.threads[0].brnew(“0x42254018”,”exe global”) 加入一个断点,当运行到 vector 0 给出的内存位置时自动停止。这时,使用 itp.go() 即触发了了停在了入口。
6.使用命令查看当前位置的汇编代码
具体代码在 \UefiCpuPkg\Library\CpuExceptionHandlerLib\X64\ExceptionHandlerAsm.nasm 中:
AsmIdtVectorBegin:
%rep 32
db 0x6a ; push #VectorNum
db ($ - AsmIdtVectorBegin) / ((AsmIdtVectorEnd - AsmIdtVectorBegin) / 32) ; VectorNum
push rax
mov rax, strict qword 0 ; mov rax, CommonInterruptEntry 【参考2】
jmp rax
%endrep
AsmIdtVectorEnd:
7.下面可以使用 itp.trheads[0].step 单步执行,最终代码跳入下面的代码段中
对应的代码还是上面还在提到的 ExceptionHandlerAsm.nasm 文件中:
global ASM_PFX(CommonInterruptEntry)
ASM_PFX(CommonInterruptEntry):
cli
pop rax
;
; All interrupt handlers are invoked through interrupt gates, so
; IF flag automatically cleared at the entry point
;
xchg rcx, [rsp] ; Save rcx into stack and save vector number into rcx
and rcx, 0xFF
cmp ecx, 32 ; Intel reserved vector for exceptions?
jae NoErrorCode
bt [ASM_PFX(mErrorCodeFlag)], ecx
jc HasErrorCode
这里使用的是在vector 0 入口地址下断点拦截的方式,除此之外,我还尝试了使用 step指令一直跟踪 到 idiv 指令的方法,会导致死机,具体原因不清楚。我记得有看过介绍性的文章,说 step 是通过不断在下一条指令之后插入 int 1来实现的,而这里我们同时在调试int0,有可能是因为这样的原因会导致冲突所以无法用 step 跟踪查看到。
参考: