第81章 Oracle RDBMS

81.1 V$VERSION表

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按照上述次序依次调用相应函数,从而获取各个模块的版本信息。

81.2 X$KSMLRU表

官方文件《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表多少次,这个表都不会被清空了。当然,不要在投入实用的业务服务器上进行这种测试。

或许这种调试的用处不大,或许这种修改有悖实用性原则。不过,当我们要查找特定的指令时,我们可以采用这样的调试步骤!

81.3 V$TIMER表

固定视图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。