Oracle RDBMS 11.2是个规模庞大的数据库系统。其主程序oracle.exe包含近124000个函数。相比之下,Windows 7 x86的内核ntoskrnl.exe只有近11000函数;Linux 3.9.8的内核(默认编译/带有默认驱动程序)包含的函数也不过31000个左右。
本章首先演示一个最简单的Oracle查询指令。我们可通过下述指令查询Oracle RDBMS数据库的版本信息:
SQL> select * from V$VERSION;
上述指令的返回结果如下:
BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production
PL/SQL Release 11.2.0.1.0 - Production
CORE 11.2.0.1.0 Production
TNS for 32-bit Windows: Version 11.2.0.1.0 - Production
NLSRTL Version 11.2.0.1.0 - Production
第一个问题就来了:字符串“V$VERSION”存储在Oracle RDBMS的什么地方?
在Win32版本的oracle.exe程序里不难发现这个字符串。但是在Linux平台的文件里,函数名称和全局变量名都会走样。因此,即使在Linux版的Oracle RDBMS里找到了正确的对象(.o)文件,挖掘相应的处理函数也会花费更多的时间。
在Linux版程序的文件里,包含字符串“V$VERSION”的文件是kqf.o。这个文件在Oracle的库文件目录lib/libserver11.a之中。
kqf.o文件在定义数据表kqfviw的时候,调用了字符串“V$VERSION”。
指令清单81.1 kqf.o
.rodata:0800C4A0 kqfviw dd 0Bh ; DATA XREF: kqfchk:loc_8003A6D
.rodata:0800C4A0 ; kqfgbn+34
.rodata:0800C4A4 dd offset _2__STRING_10102_0 ; "GV$WAITSTAT"
.rodata:0800C4A8 dd 4
.rodata:0800C4AC dd offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C4B0 dd 3
.rodata:0800C4B4 dd 0
.rodata:0800C4B8 dd 195h
.rodata:0800C4BC dd 4
.rodata:0800C4C0 dd 0
.rodata:0800C4C4 dd 0FFFFC1CBh
.rodata:0800C4C8 dd 3
.rodata:0800C4CC dd 0
.rodata:0800C4D0 dd 0Ah
.rodata:0800C4D4 dd offset _2__STRING_10104_0 ; "V$WAITSTAT"
.rodata:0800C4D8 dd 4
.rodata:0800C4DC dd offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C4E0 dd 3
.rodata:0800C4E4 dd 0
.rodata:0800C4E8 dd 4Eh
.rodata:0800C4EC dd 3
.rodata:0800C4F0 dd 0
.rodata:0800C4F4 dd 0FFFFC003h
.rodata:0800C4F8 dd 4
.rodata:0800C4FC dd 0
.rodata:0800C500 dd 5
.rodata:0800C504 dd offset _2__STRING_10105_0 ; "GV$BH"
.rodata:0800C508 dd 4
.rodata:0800C50C dd offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C510 dd 3
.rodata:0800C514 dd 0
.rodata:0800C518 dd 269h
.rodata:0800C51C dd 15h
.rodata:0800C520 dd 0
.rodata:0800C524 dd 0FFFFC1EDh
.rodata:0800C528 dd 8
.rodata:0800C52C dd 0
.rodata:0800C530 dd 4
.rodata:0800C534 dd offset _2__STRING_10106_0 ; "V$BH"
.rodata:0800C538 dd 4
.rodata:0800C53C dd offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C540 dd 3
.rodata:0800C544 dd 0
.rodata:0800C548 dd 0F5h
.rodata:0800C54C dd 14h
.rodata:0800C550 dd 0
.rodata:0800C554 dd 0FFFFC1EEh
.rodata:0800C558 dd 5
.rodata:0800C55C dd 0
在分析Oracle RDBMS的内部文件时,很多人都会奇怪“为什么函数名称和全局变量名称都那么诡异?”这大概是因为Oracle是20世纪80年代的古典作品吧。那个时代C语言编译器都遵循的ANSI标准:函数名称和变量名称不得超出6个字符(linker的局限),即“外部标识符以前6个字符为准”的规则。[1]
名字以V$-开头的数据视图,多数(很有可能是全部)都由这个文件的kqfviw表定义。这些V$视图都是内容固定视图(fixed Views)。从表面看来,这些数据具有显著的循环周期。因此,我们可以初步判断,kqfviw表的每个元素都由12个32位字段构成。借助IDA程序,我们可以轻易地再现出这种12字段的数据结构,套用到整个数据表。在Oracle RDBMS v11.2里,总共有1023个固定视图。即,这个文件可能描述了1023个预定义的视图。本章稍后讨论这个数字。
关于视图中的各字段、及各字段对应的数据,并没有多少资料可寻。虽然我们发现第一个数字就是数据库图的名称(没有最末的那个零字节)、而且这个规律适用于全部的数据元素,但是这种信息的作用不大。
我们还查到了一个叫作“V$FIXED_VIEW_DEFINITION”的固定视图[2],它能够检索所有固定视图的信息。顺便提一下,这个表有1023个元素,正好对应预定义视图的总数。
SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='V$VERSION';
VIEW_NAME
------------------------------
VIEW_DEFINITION
--------------------------------------------------------------------------------
V$VERSION
select BANNER from GV$VERSION where inst_id = USERENV('Instance')
可见,对于GV$VERSION而言,V$VERSION是thunk view(形实转换视图):
SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='GV$VERSION';
VIEW_NAME
------------------------------
VIEW_DEFINITION
--------------------------------------------------------------------------------
GV$VERSION
select inst_id, banner from x$version
另外,在Oracle数据库里,那些官方文档没有介绍的、以X$开头的数据表同样是记载系统信息的服务表。因为这些以X$开头的表由Oracle程序控制并动态更新的数据表,所以数据库用户不能修改它们。
如果我们在文件kqf.o里搜索文本“select BANNER from GV$VERSION where inst_id = USERENV('Instance')”,那么就会发现它在kqfvip表里。
指令清单81.2 kqf.o
.rodata:080185A0 kqfvip dd offset _2__STRING_11126_0 ; DATA XREF: kqfgvcn+18
.rodata:080185A0 ; kqfgvt+F
.rodata:080185A0 ; "select inst_id, decode(indx,1,'data ↙
↘ bloc" ...
.rodata:080185A4 dd offset kqfv459_c_0
.rodata:080185A8 dd 0
.rodata:080185AC dd 0
...
.rodata:08019570 dd offset _2__STRING_11378_0 ; "select BANNER from GV$VERSION ↙
↘ where in "...
.rodata:08019574 dd offset kqfv133_c_0
.rodata:08019578 dd 0
.rodata:0801957C dd 0
.rodata:08019580 dd offset _2__STRING_11379_0 ; "select inst_id,decode(bitand(↙
↘ cfflg,1),0"...
.rodata:08019584 dd offset kqfv403_c_0
.rodata:08019588 dd 0
.rodata:0801958C dd 0
.rodata:08019590 dd offset _2__STRING_11380_0 ; "select STATUS , NAME, ↙
↘ IS_RECOVERY_DEST"...
.rodata:08019594 dd offset kqfv199_c_0
这个表的每个元素由4个字段构成。而且它同样包含了1023个元素。第二个字段指向了另一个表——也就是与表名称相对应的固定视图。V$VERSION的表格只有2个元素,第一个是6(后面字符串的长度),第二个是BANNER字符串。此后是终止符——零字节和C语言字符null。
指令清单81.3 kqf.o
.rodata:080BBAC4 kqfv133_c_0 dd 6 ; DATA XREF: .rodata:08019574
.rodata:080BBAC8 dd offset _2__STRING_5017_0 ; "BANNER"
.rodata:080BBACC dd 0
.rodata:080BBAD0 dd offset _2__STRING_0_0
因此可见,综合kqfviw和kqfvip表的各项信息,我们可以获悉某个固定视图都含有哪些可被查询的字段。
基于上述分析结果,笔者编写了一个专门导出Linux Oracle数据库系统表的小程序——oracle_tables[3]。用它导出V$VERSION时,可得到如下所示的各项信息。
指令清单81.4 Result of oracle tables
kqfviw_element.viewname: [V$VERSION] ?: 0x3 0x43 0x1 0xffffc085 0x4
kqfvip_element.statement: [select BANNER from GV$VERSION where inst_id = USERENV('Instance')]
kqfvip_element.params:
[BANNER]
指令清单81.5 Result of oracle tables
kqfviw_element.viewname: [GV$VERSION] ?: 0x3 0x26 0x2 0xffffc192 0x1
kqfvip_element.statement: [select inst_id, banner from x$version]
kqfvip_element.params:
[INST_ID] [BANNER]
固定视图GV$VERSION比V$VERSION多出了一个“instance”字段,除此以外两者相同。因此,我们只要专心研究数据表X$VERSION就可举一反三地理解另一个表。与其他名字以X$-开头的数据表一样,这个表也没有资料可查。但是,我们可以直接对其进行检索:
SQL> select * from x$version;
ADDR INDX INST_ID
---------- ------------- ----------
BANNER
--------------------------------------------------------------------------------
0DBAF574 0 1
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production
...
这个表的字段名里有ADDR和INDX。
继续使用IDA分析kqf.o的时候,我们会发现在kqftab表里有一个指向X$VERSION字符串的指针。
指令清单81.6 kqf.o
.rodata:0803CAC0 dd 9 ; element number 0x1f6
.rodata:0803CAC4 dd offset _2__STRING_13113_0 ; "X$VERSION"
.rodata:0803CAC8 dd 4
.rodata:0803CACC dd offset _2__STRING_13114_0 ; "kqvt"
.rodata:0803CAD0 dd 4
.rodata:0803CAD4 dd 4
.rodata:0803CAD8 dd 0
.rodata:0803CADC dd 4
.rodata:0803CAE0 dd 0Ch
.rodata:0803CAE4 dd 0FFFFC075h
.rodata:0803CAE8 dd 3
.rodata:0803CAEC dd 0
.rodata:0803CAF0 dd 7
.rodata:0803CAF4 dd offset _2__STRING_13115_0 ; "X$KQFSZ"
.rodata:0803CAF8 dd 5
.rodata:0803CAFC dd offset _2__STRING_13116_0 ; "kqfsz"
.rodata:0803CB00 dd 1
.rodata:0803CB04 dd 38h
.rodata:0803CB08 dd 0
.rodata:0803CB0C dd 7
.rodata:0803CB10 dd 0
.rodata:0803CB14 dd 0FFFFC09Dh
.rodata:0803CB18 dd 2
.rodata:0803CB1C dd 0
上述指令中有很多处数据都引用了以X$-开头的数据表名称。很显然,这些名字都是Oracle数据库的数据表名称。鉴于公开资料没有这些信息,笔者还不能理解字符串“kqvt”的实际含义。“kq-”前缀的指令,多数是与Kernel(内核)和query(查询)有关的指令。不过,至于“v是否是version的缩写”、“t是否是type的缩写”,这些猜测都无法证明。
另外,kqf.o文件里还记录了类似的数据表名称。
指令清单81.7 kqf.o
.rodata:0808C360 kqvt_c_0 kqftap_param <4, offset _2__STRING_19_0, 917h, 0, 0, 0, 4, 0, ↙
↘ 0>
.rodata:0808C360 ; DATA XREF: .rodata:08042680
.rodata:0808C360 ; "ADDR"
.rodata:0808C384 kqftap_param <4, offset _2__STRING_20_0, 0B02h, 0, 0, 0, 4, 0, ↙
↘ 0>;"INDX"
.rodata:0808C3A8 kqftap_param <7, offset _2__STRING_21_0, 0B02h, 0, 0, 0, 4, 0, ↙
↘ 0>;"INST_ID"
.rodata:0808C3CC kqftap_param <6, offset _2__STRING_5017_0, 601h, 0, 0, 0, 50h,↙
↘ 0, 0> ; "BANNER"
.rodata:0808C3F0 kqftap_param <0, offset _2__STRING_0_0, 0, 0, 0, 0, 0, 0, 0>
这些信息可以解释X$VERSION表中的所有字段。在kqftap表中,唯一一个引用这个表的指令如下所示。
指令清单81.8 kqf.o
.rodata:08042680 kqftap_element <0, offset kqvt_c_0, offset kqvrow, 0> ; ↙
↘ element 0x1f6
值得关注的是,这个元素是表中第502个(0x1f6)元素。它就像kqftab表中指向X$VERSION字符串的指针一般。数据表kqftap和kqftab之间的关系,很可能像kqfvip和kqfviw之间的关系那样是某种互补关系。我们还在其中找到了指向kqvrow() 函数的函数指针。我们最终挖掘到了有价值的信息!
笔者把上述各表的有关信息也添加到了自制的oracle系统表查询工具——oracle_tables里[4]。用它检索X$VERSION后,可得如下所示的各项信息。
指令清单81.9 Result of oracle tables
kqftab_element.name: [X$VERSION] ?: [kqvt] 0x4 0x4 0x4 0xc 0xffffc075 0x3
kqftap_param.name=[ADDR] ?: 0x917 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INDX] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INST_ID] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[BANNER] ?: 0x601 0x0 0x0 0x0 0x50 0x0 0x0
kqftap_element.fn1=kqvrow
kqftap_element.fn2=NULL
借助笔者自创的tracer程序,我们不难发现:在查询X$VERSION表时,这个函数被连续调用了6次(由qerfxFetch() 函数)。
为了查看具体执行了哪些指令,我们以cc模式运行tracer程序:
tracer -a:oracle.exe bpf=oracle.exe!_kqvrow,trace:cc
_kqvrow_ proc near
var_7C = byte ptr -7Ch
var_18 = dword ptr -18h
var_14 = dword ptr -14h
Dest = dword ptr -10h
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
arg_8 = dword ptr 10h
arg_C = dword ptr 14h
arg_14 = dword ptr 1Ch
arg_18 = dword ptr 20h
; FUNCTION CHUNK AT .text1:056C11A0 SIZE 00000049 BYTES
push ebp
mov ebp, esp
sub esp, 7Ch
mov eax, [ebp+arg_14] ; [EBP+1Ch]=1
mov ecx, TlsIndex ; [69AEB08h]=0
mov edx, large fs:2Ch
mov edx, [edx+ecx*4] ; [EDX+ECX*4]=0xc98c938
cmp eax, 2 ; EAX=1
mov eax, [ebp+arg_8] ; [EBP+10h]=0xcdfe554
jz loc_2CE1288
mov ecx, [eax] ; [EAX]=0..5
mov [ebp+var_4], edi ; EDI=0xc98c938
loc_2CE10F6: ; CODE XREF: _kqvrow_+10A
; _kqvrow_+1A9
cmp ecx, 5 ; ECX=0..5
ja loc_56C11C7
mov edi, [ebp+arg_18] ; [EBP+20h]=0
mov [ebp+var_14], edx ; EDX=0xc98c938
mov [ebp+var_8], ebx ; EBX=0
mov ebx, eax ; EAX=0xcdfe554
mov [ebp+var_C], esi ; ESI=0xcdfe248
loc_2CE110D: ; CODE XREF: _kqvrow_+29E00E6
mov edx, ds:off_628B09C[ecx*4] ; [ECX*4+628B09Ch]= 0x2ce1116, 0x2ce11ac, 0x2ce11db↙
↘ , 0x2ce11f6, 0x2ce1236, 0x2ce127a
jmp edx ; EDX=0x2ce1116, 0x2ce11ac, 0x2ce11db, 0x2ce11f6, 0x2ce1236, ↙
↘ 0x2ce127a
loc_2CE1116: ; DATA XREF: .rdata:off_628B09C
push offset aXKqvvsnBuffer ; "x$kqvvsn buffer"
mov ecx, [ebp+arg_C] ; [EBP+14h]=0x8a172b4
xor edx, edx
mov esi, [ebp+var_14] ; [EBP-14h]=0xc98c938
push edx ; EDX=0
push edx ; EDX=0
push 50h
push ecx ; ECX=0x8a172b4
push dword ptr [esi+10494h] ;[ESI+10494h]=0xc98cd58
call _kghalf ; tracing nested maximum level (1) reached, skipping this ↙
↘ CALL
mov esi, ds:__imp__vsnnum ; [59771A8h]=0x61bc49e0
mov [ebp+Dest], eax ; EAX=0xce2ffb0
mov [ebx+8], eax ; EAX=0xce2ffb0
mov [ebx+4], eax ; EAX=0xce2ffb0
mov edi, [esi] ; [ESI]=0xb200100
mov esi, ds:__imp__vsnstr ; [597D6D4h]=0x65852148, "- Production"
push esi ; ESI=0x65852148, "- Production"
mov ebx, edi ; EDI=0xb200100
shr ebx, 18h ; EBX=0xb200100
mov ecx, edi ; EDI=0xb200100
shr ecx, 14h ; ECX=0xb200100
and ecx, 0Fh ; ECX=0xb2
mov edx, edi ; EDI=0xb200100
shr edx, 0Ch ; EDX=0xb200100
movzx edx, dl ; DL=0
mov eax, edi ; EDI=0xb200100
shr eax, 8 ; EAX=0xb200100
and eax, 0Fh ; EAX=0xb2001
and edi, 0FFh ; EDI=0xb200100
push edi ; EDI=0
mov edi, [ebp+arg_18] ; [EBP+20h]=0
push eax ; EAX=1
mov eax, ds:__imp__vsnban ; [597D6D8h]=0x65852100, "Oracle Database 11g ↙
↘ Enterprise Edition Release %d.%d.%d.%d.%d %s"
push edx ; EDX=0
push ecx ; ECX=2
push ebx ; EBX=0xb
mov ebx, [ebp+arg_8] ; [EBP+10h]=0xcdfe554
push eax ; EAX=0x65852100, "Oracle Database 11g Enterprise Edition ↙
↘ Release %d.%d.%d.%d.%d %s"
mov eax, [ebp+Dest] ; [EBP-10h]=0xce2ffb0
push eax ; EAX=0xce2ffb0
call ds:__imp__sprintf ; op1=MSVCR80.dll!sprintf tracing nested maximum level (1) ↙
↘ reached, skipping this CALL
add esp, 38h
mov dword ptr [ebx], 1
loc_2CE1192: ; CODE XREF: _kqvrow_+FB
; _kqvrow_+128 ...
test edi, edi ; EDI=0
jnz __VInfreq__kqvrow
mov esi, [ebp+var_C] ; [EBP-0Ch]=0xcdfe248
mov edi, [ebp+var_4] ; [EBP-4]=0xc98c938
mov eax, ebx ; EBX=0xcdfe554
mov ebx, [ebp+var_8] ; [EBP-8]=0
lea eax, [eax+4] ; [EAX+4]=0xce2ffb0, "NLSRTL Version 11.2.0.1.0 – Production↙
↘ ", "Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production", "PL/SQL ↙
↘ Release 11.2.0.1.0 - Production", "TNS for 32-bit Windows: Version 11.2.0.1.0 - ↙
↘ Production"
loc_2CE11A8: ; CODE XREF: _kqvrow_+29E00F6
mov esp, ebp
pop ebp
retn ; EAX=0xcdfe558
loc_2CE11AC: ; DATA XREF: .rdata:0628B0A0
mov edx, [ebx+8] ; [EBX+8]=0xce2ffb0, "Oracle Database 11g Enterprise Edition ↙
↘ Release 11.2.0.1.0- Production"
mov dword ptr [ebx], 2
mov [ebx+4], edx ; EDX=0xce2ffb0, "Oracle Database 11g Enterprise Edition ↙
↘ Release 11.2.0.1.0 - Production"
push edx ; EDX=0xce2ffb0, "Oracle Database 11g Enterprise Edition ↙
↘ Release 11.2.0.1.0 - Production"
call _kkxvsn ; tracing nested maximum level (1) reached, skipping this ↙
↘ CALL
pop ecx
mov edx, [ebx+4] ; [EBX+4]=0xce2ffb0, "PL/SQL Release 11.2.0.1.0 - Production"
movzx ecx, byte ptr [edx] ; [EDX]=0x50
test ecx, ecx ; ECX=0x50
jnz short loc_2CE1192
mov edx, [ebp+var_14]
mov esi, [ebp+var_C]
mov eax, ebx
mov ebx, [ebp+var_8]
mov ecx, [eax]
jmp loc_2CE10F6
loc_2CE11DB: ; DATA XREF: .rdata:0628B0A4
push 0
push 50h
mov edx, [ebx+8] ; [EBX+8]=0xce2ffb0, "PL/SQL Release 11.2.0.1.0 - Production"
mov [ebx+4], edx ; EDX=0xce2ffb0,"PL/SQL Release 11.2.0.1.0 - Production"
push edx ; EDX=0xce2ffb0, "PL/SQL Release 11.2.0.1.0 - Production"
call _lmxver ; tracing nested maximum level (1) reached, skipping this ↙
↘ CALL
add esp, 0Ch
mov dword ptr [ebx], 3
jmp short loc_2CE1192
loc_2CE11F6: ; DATA XREF: .rdata:0628B0A8
mov edx, [ebx+8] ; [EBX+8]=0xce2ffb0
mov [ebp+var_18], 50h
mov [ebx+4], edx ; EDX=0xce2ffb0
push 0
call _npinli ; tracing nested maximum level (1)reached, skipping this ↙
↘ CALL
pop ecx
test eax, eax ; EAX=0
jnz loc_56C11DA
mov ecx, [ebp+var_14] ; [EBP-14h]=0xc98c938
lea edx, [ebp+var_18] ; [EBP-18h]=0x50
push edx ; EDX=0xd76c93c
push dword ptr [ebx+8] ; [EBX+8]=0xce2ffb0
push dword ptr [ecx+13278h] ; [ECX+13278h]=0xacce190
call _nrtnsvrs ; tracing nested maximum level (1) reached, skipping this ↙
↘ CALL
add esp, 0Ch
loc_2CE122B: ; CODE XREF: _kqvrow_+29E0118
mov dword ptr [ebx], 4
jmp loc_2CE1192
loc_2CE1236: ; DATA XREF: .rdata:0628B0AC
lea edx, [ebp+var_7C] ; [EBP-7Ch]=1
push edx ; EDX=0xd76c8d8
push 0
mov esi, [ebx+8] ; [EBX+8]=0xce2ffb0, "TNS for 32-bit Windows: Version ↙
↘ 11.2.0.1.0 - Production"
mov [ebx+4], esi ; ESI=0xce2ffb0, "TNS for 32-bit Windows: Version 11.2.0.1.0 ↙
↘ - Production"
mov ecx, 50h
mov [ebp+var_18], ecx ; ECX=0x50
push ecx ; ECX=0x50
push esi ; ESI=0xce2ffb0, "TNS for 32-bit Windows: Version 11.2.0.1.0 ↙
↘ - Production"
call _lxvers ; tracing nested maximum level (1) reached, skipping this ↙
↘ CALL
add esp, 10h
mov edx, [ebp+var_18 ; [EBP-18h]=0x50
mov dword ptr [ebx], 5
test edx, edx ; EDX=0x50
jnz loc_2CE1192
mov edx, [ebp+var_14]
mov esi, [ebp+var_C]
mov eax, ebx
mov ebx, [ebp+var_8]
mov ecx, 5
jmp loc_2CE10F6
loc_2CE127A: ; DATA XREF: .rdata:0628B0B0
mov edx, [ebp+var_14] ; [EBP-14h]=0xc98c938
mov esi, [ebp+var_C] ; [EBP-0Ch]=0xcdfe248
mov edi, [ebp+var_4] ; [EBP-4]=0xc98c938
mov eax, ebx ; EBX=0xcdfe554
mov ebx, [ebp+var_8] ; [EBP-8]=0
loc_2CE1288: ; CODE XREF: _kqvrow_+1F
mov eax, [eax+8] ; [EAX+8]=0xce2ffb0, "NLSRTL Version 11.2.0.1.0 - Production"
test eax, eax ; EAX=0xce2ffb0, "NLSRTL Version 11.2.0.1.0 - Production"
jz short loc_2CE12A7
push offset aXKqvvsnBuffer ; "x$kqvvsn buffer"
push eax ; EAX=0xce2ffb0, "NLSRTL Version 11.2.0.1.0 - Production"
mov eax, [ebp+arg_C] ; [EBP+14h]=0x8a172b4
push eax ; EAX=0x8a172b4
push dword ptr [edx+10494h] ; [EDX+10494h]=0xc98cd58
call _kghfrf ; tracing nested maximum level (1) reached, skipping this ↙
↘ CALL
add esp, 10h
loc_2CE12A7: ; CODE XREF: _kqvrow_+1C1
xor eax, eax
mov esp, ebp
pop ebp
retn ; EAX=0
_kqvrow_ endp
不难看出,该函数从外部获取行号信息,然后按照下述顺序组装、返回字符串。
String 1 String 2 String 3 String 4 String 5 |
Using vsnstr, vsnnum, vsnban global variables. Calling sprintf(). Calling kkxvsn(). Calling lmxver(). Calling npinli(), nrtnsvrs(). Calling lxvers(). |
Oracle按照上述次序依次调用相应函数,从而获取各个模块的版本信息。
官方文件《Diagnosing and Resolving Error ORA-04031》[5]特别提到了这个数据表:
Oracle能够记录内存池内发生的、强制释放其他对象的内存占用情况。负责记录这种情况的数据表是固定表x$ksmlru。它可用来诊断内存异常消耗的具体原因。
如果内存池里发生了大量对象周期性释放的情况,那么这种问题会增加数据库的响应时间。而且当这些对象再次被加载到内存池时,这一现象还会增加库缓存(library cache)互锁的概率。
固定表 x$ksmlru 具有一个特性:只要出现了检索表的人为操作,那么这个表内的数据就会被立刻清空。此外,该数据表只会存储内存占用最大的前几项记录。“查询后立刻清空”的设定,是为了凸显那些先前并不那么耗费资源的内存分配情况。也就是说,每次检索所对应的时间段都是不同的。正因如此,数据库用户应当妥善保管该表的查询结果。
换句话说,查询这个表不是问题,问题是查询后它会被立即清空。那么,清空表的具体原因是什么?既然kqftab表和kqftap表含有X$-表的全部信息,我们可以继续使用前文介绍的oracle_tables进行分析。在oracle_tables的返回结果里,我们看到:在制备X$KSMLRU表的元素时,oracle调用了ksmlrs() 函数。
指令清单81.10 Result of oracle tables
kqftab_element.name: [X$KSMLRU] ?: [ksmlr] 0x4 0x64 0x11 0xc 0xffffc0bb 0x5
kqftap_param.name=[ADDR] ?: 0x917 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INDX] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INST_ID] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[KSMLRIDX] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[KSMLRDUR] ?: 0xb02 0x0 0x0 0x0 0x4 0x4 0x0
kqftap_param.name=[KSMLRSHRPOOL] ?: 0xb02 0x0 0x0 0x0 0x4 0x8 0x0
kqftap_param.name=[KSMLRCOM] ?: 0x501 0x0 0x0 0x0 0x14 0xc 0x0
kqftap_param.name=[KSMLRSIZ] ?: 0x2 0x0 0x0 0x0 0x4 0x20 0x0
kqftap_param.name=[KSMLRNUM] ?: 0x2 0x0 0x0 0x0 0x4 0x24 0x0
kqftap_param.name=[KSMLRHON] ?: 0x501 0x0 0x0 0x0 0x20 0x28 0x0
kqftap_param.name=[KSMLROHV] ?: 0xb02 0x0 0x0 0x0 0x4 0x48 0x0
kqftap_param.name=[KSMLRSES] ?: 0x17 0x0 0x0 0x0 0x4 0x4c 0x0
kqftap_param.name=[KSMLRADU] ?: 0x2 0x0 0x0 0x0 0x4 0x50 0x0
kqftap_param.name=[KSMLRNID] ?: 0x2 0x0 0x0 0x0 0x4 0x54 0x0
kqftap_param.name=[KSMLRNSD] ?: 0x2 0x0 0x0 0x0 0x4 0x58 0x0
kqftap_param.name=[KSMLRNCD] ?: 0x2 0x0 0x0 0x0 0x4 0x5c 0x0
kqftap_param.name=[KSMLRNED] ?: 0x2 0x0 0x0 0x0 0x4 0x60 0x0
kqftap_element.fn1=ksmlrs
kqftap_element.fn2=NULL
tracer程序可以印证这个结果:每次查询X$KSMLRU表时,Oracle都会调用这个函数。
另外,我们还看到ksmsplu_sp()函数和ksmsplu_jp()函数都引用了ksmsplu()函数。即,无论是执行ksmsplu_sp()函数、还是执行ksmsplu_jp()函数,最后都会调用ksmsplu()函数。在ksmsplu()结束之前,它调用了memset()函数。
指令清单81.11 ksm.o
…
.text:00434C50 loc_434C50: ; DATA XREF: .rdata:off_5E50EA8
.text:00434C50 mov edx, [ebp-4]
.text:00434C53 mov [eax], esi
.text:00434C55 mov esi, [edi]
.text:00434C57 mov [eax+4], esi
.text:00434C5A mov [edi], eax
.text:00434C5C add edx, 1
.text:00434C5F mov [ebp-4], edx
.text:00434C62 jnz loc_434B7D
.text:00434C68 mov ecx, [ebp+14h]
.text:00434C6B mov ebx, [ebp-10h]
.text:00434C6E mov esi, [ebp-0Ch]
.text:00434C71 mov edi, [ebp-8]
.text:00434C74 lea eax, [ecx+8Ch]
.text:00434C7A push 370h ; Size
.text:00434C7F push 0 ; Val
.text:00434C81 push eax ; Dst
.text:00434C82 call __intel_fast_memset
.text:00434C87 add esp, 0Ch
.text:00434C8A mov esp, ebp
.text:00434C8C pop ebp
.text:00434C8D retn
.text:00434C8D _ksmsplu endp
含有memset(block,0,size)的构造函数通常用于清空内存区域。如果我们阻止它调用这个memset() 函数,那么将发生什么情况?
为此,我们在程序向memset()函数传递参数的0x434C7A处设置断点、令调试程序tracer在此刻将程序计数器(PC,即EIP)调整为0x434C8A,从而使程序“跳过”清除内存的memset()函数。可以说,这种“调试”相当于令程序在0x434C7A处无条件转移到 0x434C8A。相关的tracer指令如下:
tracer -a:oracle.exe bpx=oracle.exe!0x00434C7A,set(eip,0x00434C8A)
请注意:上述地址仅对Win32版本的Oracle RDBMS 11.2有效。
经上述调试指令启动Oracle以后,无论查询X$ KSMLRU表多少次,这个表都不会被清空了。当然,不要在投入实用的业务服务器上进行这种测试。
或许这种调试的用处不大,或许这种修改有悖实用性原则。不过,当我们要查找特定的指令时,我们可以采用这样的调试步骤!
固定视图V$TIMER算得上是更新最频繁的视图之一了。
V$TIME以百分之一秒为单位、记录实际运行时间。这个值以计时原点开始测算,因此具体数值与操作系统相关。它会在4字节溢出时(大约历经497天后)循环,重新变为0。
上述内容摘自官方文档。[6]
比较有趣的是:Win32版本的Oracle程序和Linux版本的程序,返回的时间戳竟然是不同的。我们能否找到生成返回值的函数呢?
下述操作表明,时间信息最终取自X$KSUTM表:
SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='V$TIMER';
VIEW_NAME
------------------------------
VIEW_DEFINITION
--------------------------------------------------------------------------------
V$TIMER
select HSECS from GV$TIMER where inst_id = USERENV('Instance')
SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='GV$TIMER';
VIEW_NAME
------------------------------
VIEW_DEFINITION
--------------------------------------------------------------------------------
GV$TIMER
select inst_id,ksutmtim from x$ksutm
不过kqftab/kqftap表没有引用生成这项数值的函数。
指令清单81.12 Result of oracle tables
kqftab_element.name: [X$KSUTM] ?: [ksutm] 0x1 0x4 0x4 0x0 0xffffc09b 0x3
kqftap_param.name=[ADDR] ?: 0x10917 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INDX] ?: 0x20b02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INST_ID] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[KSUTMTIM] ?: 0x1302 0x0 0x0 0x0 0x4 0x0 0x1e
kqftap_element.fn1=NULL
kqftap_element.fn2=NULL
当我们搜索字符串KSUTMTIM时,我们看到了下述函数:
kqfd_DRN_ksutm_c proc near ; DATA XREF: .rodata:0805B4E8
arg_0 = dword ptr 8
arg_8 = dword ptr 10h
arg_C = dword ptr 14h
push ebp
mov ebp, esp
push [ebp+arg_C]
push offset ksugtm
push offset _2__STRING_1263_0 ; "KSUTMTIM"
push [ebp+arg_8]
push [ebp+arg_0]
call kqfd_cfui_drain
add esp, 14h
mov esp, ebp
pop ebp
retn
kqfd_DRN_ksutm_c endp
而数据表kqfd_tab_registry_0引用了kqfd_DRN_ksutm_c()函数:
dd offset _2__STRING_62_0 ; "X$KSUTM"
dd offset kqfd_OPN_ksutm_c
dd offset kqfd_tabl_fetch
dd 0
dd 0
dd offset kqfd_DRN_ksutm_c
打开Linux x86版本的这个文件,可看到如下所示的代码。
指令清单81.13 ksu.o
ksugtm proc near
var_1C = byte ptr -1Ch
arg_4 = dword ptr 0Ch
push ebp
mov ebp, esp
sub esp, 1Ch
lea eax, [ebp+var_1C]
push eax
call slgcs
pop ecx
mov edx, [ebp+arg_4]
mov [edx], eax
mov eax, 4
mov esp, ebp
pop ebp
retn
ksugtm endp
在Win32版本的程序里,相应文件的有关指令几乎相同。
这是我们寻找的函数吗?我们通过下述指令验证一下:
tracer -a:oracle.exe bpf=oracle.exe!_ksugtm,args:2,dump_args:0x4
然后在SQL*Plus里执行以下指令:
SQL> select * from V$TIMER;
HSECS
----------
27294929
SQL> select * from V$TIMER;
HSECS
----------
27295006
SQL> select * from V$TIMER;
HSECS
----------
27295167
指令清单81.14 tracer output
TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!__VInfreq__qerfxFetch↙
↘ +0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0: 38 C9 "8. "
TID=2428|(0) oracle.exe!_ksugtm () -> 0x4 (0x4)
Argument 2/2 difference
00000000: D1 7C A0 01 ".|.. "
TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!__VInfreq__qerfxFetch↙
↘ +0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0: 38 C9 "8. "
TID=2428|(0) oracle.exe!_ksugtm () -> 0x4 (0x4)
Argument 2/2 difference
00000000: 1E 7D A0 01
TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!__VInfreq__qerfxFetch↙
↘ +0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0: 38 C9 "8. "
TID=2428|(0) oracle.exe!_ksugtm () -> 0x4 (0x4)
Argument 2/2 difference
00000000: BF 7D A0 01 ".}.. "
上述数据和我们在SQL*Plus看到的数据完全一样。它是函数的第二个参数。
然后我们再来分析Linux x86程序里的slgcs() 函数:
slgcs proc near
var_4 = dword ptr -4
arg_0 = dword ptr 8
push ebp
mov ebp, esp
push esi
mov [ebp+var_4], ebx
mov eax, [ebp+arg_0]
call $+5
pop ebx
nop ; PIC mode
mov ebx, offset _GLOBAL_OFFSET_TABLE_
mov dword ptr [eax], 0
call sltrgatime64 ; PIC mode
push 0
push 0Ah
push edx
push eax
call __udivdi3 ; PIC mode
mov ebx, [ebp+var_4]
add esp, 10h
mov esp, ebp
pop ebp
retn
slgcs endp
这个函数调用了sltrgatime64(),然后把返回值除以10。[7]
在Win32版本的程序里,这个函数则是:
_slgcs proc near ; CODE XREF: _dbgefgHtElResetCount+15
; _dbgerRunActions+1528
db 66h
nop
push ebp
mov ebp, esp
mov eax, [ebp+8]
mov dword ptr [eax], 0
call ds:__imp__GetTickCount@0 ; GetTickCount()
mov edx, eax
mov eax, 0CCCCCCCDh
mul edx
shr edx, 3
mov eax, edx
mov esp, ebp
pop ebp
retn
_slgcs endp
Win32的结果就是GetTickCount() 函数返回值的十分之一。[8]
这就是Oracle在Win32下和Linux x86下返回不同结果的根本原因——它调用了完全不同的操作系统函数。
“call kqfd_cfui_drain”里有个“drain”。这个关键字有“表中的某个列取自特定函数的返回值”的含义。
前面介绍过的oracle_tables工具能够处理kqfd_tab_registry_0。因此,我们可以用它分析“列”的值与特定函数之间的关联关系:
[X$KSUTM] [kqfd_OPN_ksutm_c] [kqfd_tabl_fetch] [NULL] [NULL] [kqfd_DRN_ksutm_c]
[X$KSUSGIF] [kqfd_OPN_ksusg_c] [kqfd_tabl_fetch] [NULL] [NULL] [kqfd_DRN_ksusg_c]
上述信息中的OPN代表“Open”和“DRN”。DRN当然还是“drain”的意思。
[1] 1988年的ANSI标准请可参见笔者的摘录:http://yurichev.com/ref/ Draft%20ANSI%20C%20Standard%20(ANSI%20X3J11-88-090)%20(May%2013, %201988).txt。作为对比,微软的标识符标准可参阅https://msdn.microsoft.com/en-us/library/e7f8y25b.aspx。
[2] 笔者通过挖掘kqfviw和kqfvip表里的数据,最终发现了这个视图的信息。
[3] http://yurichev.com/oracle_tables.html。
[4] http://yurichev.com/oracle_tables.html。
[5] http://www.oralab.net/METANOTES/DIAGNOSING%20AND%20RESOLVING%20ORA-04031%20ERROR.htm。
[6] http://docs.oracle.com/cd/B28359_01/server.111/b28320/dynviews_3104.htm。
[7] 有关除法运算的有关细节,请参见本书第41章。
[8] 有关GetTickCount() 函数,请参见MSDN:https://msdn.microsoft.com/en-us/library/ windows/desktop/ms724408(v=vs.85).aspx。