简体   繁体   English

在Delphi中检测VMT或堆损坏的正确工具是什么?

[英]What is the right tool to detect VMT or heap corruption in Delphi?

I'm a member in a team that use Delphi 2007 for a larger application and we suspect heap corruption because sometimes there are strange bugs that have no other explanation. 我是一个使用Delphi 2007用于更大应用程序的团队的成员,我们怀疑堆损坏,因为有时会有奇怪的错误,没有其他解释。 I believe that the Rangechecking option for the compiler is only for arrays. 我相信编译器的Rangechecking选项仅适用于数组。 I want a tool that give an exception or log when there is a write on a memory address that is not allocated by the application. 我想要一个工具,当存储器地址没有被应用程序分配时,它会发出异常或日志。

Regards 问候

EDIT : The error is of type: 编辑 :错误类型:

Error: Access violation at address 00404E78 in module 'BoatLogisticsAMCAttracsServer.exe'. 错误:模块“BoatLogisticsAMCAttracsServer.exe”中地址00404E78处的访问冲突。 Read of address FFFFFFDD 读取地址FFFFFFDD

EDIT2 : Thanks for all suggestions. EDIT2 :感谢所有建议。 Unfortunately I think that the solution is deeper than that. 不幸的是,我认为解决方案比这更深。 We use a patched version of Bold for Delphi as we own the source. 我们使用补丁版本的Bold for Delphi,因为我们拥有源代码。 Probably there are some errors introduced in the Bold framwork. 可能在Bold框架中引入了一些错误。 Yes we have a log with callstacks that are handled by JCL and also trace messages. 是的,我们有一个带有由JCL处理的callstack的日志以及跟踪消息。 So a callstack with the exception can lock like this: 所以带异常的callstack可以像这样锁定:

20091210 16:02:29 (2356) [EXCEPTION] Raised EBold: Failed to derive ServerSession.mayDropSession: Boolean
OCL expression: not active and not idle and timeout and (ApplicationKernel.allinstances->first.CurrentSession <> self)
Error: Access violation at address 00404E78 in module 'BoatLogisticsAMCAttracsServer.exe'. Read of address FFFFFFDD. At Location BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)

Inner Exception Raised EBold: Failed to derive ServerSession.mayDropSession: Boolean
OCL expression: not active and not idle and timeout and (ApplicationKernel.allinstances->first.CurrentSession <> self)
Error: Access violation at address 00404E78 in module 'BoatLogisticsAMCAttracsServer.exe'. Read of address FFFFFFDD. At Location BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)
Inner Exception Call Stack:
 [00] System.TObject.InheritsFrom (sys\system.pas:9237)

Call Stack:
 [00] BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)
 [01] BoldSystem.TBoldMember.DeriveMember (BoldSystem.pas:3846)
 [02] BoldSystem.TBoldMemberDeriver.DoDeriveAndSubscribe (BoldSystem.pas:7491)
 [03] BoldDeriver.TBoldAbstractDeriver.DeriveAndSubscribe (BoldDeriver.pas:180)
 [04] BoldDeriver.TBoldAbstractDeriver.SetDeriverState (BoldDeriver.pas:262)
 [05] BoldDeriver.TBoldAbstractDeriver.Derive (BoldDeriver.pas:117)
 [06] BoldDeriver.TBoldAbstractDeriver.EnsureCurrent (BoldDeriver.pas:196)
 [07] BoldSystem.TBoldMember.EnsureContentsCurrent (BoldSystem.pas:4245)
 [08] BoldSystem.TBoldAttribute.EnsureNotNull (BoldSystem.pas:4813)
 [09] BoldAttributes.TBABoolean.GetAsBoolean (BoldAttributes.pas:3069)
 [10] BusinessClasses.TLogonSession._GetMayDropSession (code\BusinessClasses.pas:31854)
 [11] DMAttracsTimers.TAttracsTimerDataModule.RemoveDanglingLogonSessions (code\DMAttracsTimers.pas:237)
 [12] DMAttracsTimers.TAttracsTimerDataModule.UpdateServerTimeOnTimerTrig (code\DMAttracsTimers.pas:482)
 [13] DMAttracsTimers.TAttracsTimerDataModule.TimerKernelWork (code\DMAttracsTimers.pas:551)
 [14] DMAttracsTimers.TAttracsTimerDataModule.AttracsTimerTimer (code\DMAttracsTimers.pas:600)
 [15] ExtCtrls.TTimer.Timer (ExtCtrls.pas:2281)
 [16] Classes.StdWndProc (common\Classes.pas:11583)

The inner exception part is the callstack at the moment an exception is reraised. 内部异常部分是异常重新启动时的callstack。

EDIT3: The theory right now is that the Virtual Memory Table (VMT) is somehow broken. EDIT3:现在的理论是虚拟内存表(VMT)在某种程度上被打破了。 When this happen there is no indication of it. 当发生这种情况时,没有迹象表明它。 Only when a method is called an exception is raised ( ALWAYS on address FFFFFFDD, -35 decimal) but then it is too late. 只有当一个方法被调用时才会引发异常( 总是在地址FFFFFFDD,-35十进制),但是为时已晚。 You don't know the real cause for the error. 您不知道错误的真正原因。 Any hint of how to catch a bug like this is really appreciated!!! 任何关于如何捕捉这样的bug的提示都非常感谢!!! We have tried with SafeMM, but the problem is that the memory consumption is too high even when the 3 GB flag is used. 我们尝试过使用SafeMM,但问题是即使使用3 GB标志,内存消耗也太高。 So now I try to give a bounty to the SO community :) 所以现在我尝试给SO社区一个赏金:)

EDIT4: One hint is that according the log there is often (or even always) another exception before this. 编辑4:一个提示是,根据日志,在此之前经常(或甚至总是)有另一个例外。 It can be for example optimistic locking in the database. 它可以是例如数据库中的乐观锁定。 We have tried to raise exceptions by force but in test environment it just works fine. 我们试图通过武力提出例外,但在测试环境中它只是工作正常。

EDIT5: Story continues... I did a search on the logs for the last 30 days now. 编辑5:故事继续......我现在搜索了过去30天的日志。 The result: 结果:

  • "Read of address FFFFFFDB" 0 “读取地址FFFFFFDB”0
  • "Read of address FFFFFFDC" 24 “读取地址FFFFFFDC”24
  • "Read of address FFFFFFDD" 270 “读取地址FFFFFFDD”270
  • "Read of address FFFFFFDE" 22 “读取地址FFFFFFDE”22
  • "Read of address FFFFFFDF" 7 “读取地址FFFFFFDF”7
  • "Read of address FFFFFFE0" 20 “读取地址FFFFFFE0”20
  • "Read of address FFFFFFE1" 0 “读取地址FFFFFFE1”0

So the current theory is that an enum (there is a lots in Bold) overwrite a pointer. 因此,目前的理论是枚举(Bold中有很多)覆盖指针。 I got 5 hits with different address above. 我有5个点击,上面有不同的地址。 It could mean that the enum holds 5 values where the second one is most used. 这可能意味着enum保存5个值,其中第二个值最常用。 If there is an exception a rollback should occur for the database and Boldobjects should be destroyed. 如果存在异常,则应对数据库进行回滚,并且应销毁Boldobjects。 Maybe there is a chance that not everything is destroyed and a enum still can write to an address location. 也许有可能不是所有东西都被破坏而且枚举仍然可以写入地址位置。 If this is true maybe it is possible to search the code by a regexpr for an enum with 5 values ? 如果这是真的,也许可以通过regexpr搜索具有5个值的枚举的代码?

EDIT6: To summarize, no there is no solution to the problem yet. 编辑6:总而言之,目前还没有解决问题的方法。 I realize that I may mislead you a bit with the callstack. 我意识到我可能会用callstack误导你一点。 Yes there are a timer in that but there are other callstacks without a timer. 是的,有一个计时器,但有其他没有计时器的callstack。 Sorry for that. 对不起。 But there are 2 common factors. 但有两个共同因素。

  • An exception with Read of address FFFFFFxx. 读取地址FFFFFFxx的例外情况。
  • Top of callstack is System.TObject.InheritsFrom (sys\\system.pas:9237) callstack的顶部是System.TObject.InheritsFrom(sys \\ system.pas:9237)

This convince me that VilleK best describe the problem. 这让我相信VilleK能够最好地描述这个问题。 I'm also convinced that the problem is somewhere in the Bold framework. 我也确信问题出现在Bold框架中。 But the BIG question is, how can problems like this be solved ? 但是问题是,这样的问题如何解决? It is not enough to have an Assert like VilleK suggest as the damage has already happened and the callstack is gone at that moment. VilleK这样的断言是不够的,因为损坏已经发生并且那时候的信号堆消失了。 So to describe my view of what may cause the error: 因此,要描述我可能导致错误的原因:

  1. Somewhere a pointer is assigned a bad value 1, but it can be also 0, 2, 3 etc. 某处指针被分配了错误值1,但它也可以是0,2,3等。
  2. An object is assigned to that pointer. 将一个对象分配给该指针。
  3. There is method call in the objects baseclass. 对象基类中有方法调用。 This cause method TObject.InheritsForm to be called and an exception appear on address FFFFFFDD. 这会导致调用TObject.InheritsForm方法,并在地址FFFFFFDD上显示异常。

Those 3 events can be together in the code but they may also be used much later. 这三个事件可以在代码中一起使用,但也可以在以后使用它们。 I think this is true for the last method call. 我认为对于最后一次方法调用都是如此。

EDIT7: We work closely with the the author of Bold Jan Norden and he recently found a bug in the OCL-evaluator in Bold framework. 编辑7:我们与Bold Jan Norden的作者密切合作,他最近在Bold框架中发现了OCL评估员的一个错误。 When this was fixed these kinds of exceptions decreased a lot but they still occasionally come. 当这个被修复时,这些异常减少了很多,但它们偶尔也会出现。 But it is a big relief that this is almost solved. 但这几乎解决了,这是一个很大的缓解。

You write that you want there to be an exception if 你写的是你希望那里有例外

there is a write on a memory address that is not allocated by the application 存储器地址上有一个未由应用程序分配的写入

but that happens anyway, both the hardware and the OS make sure of that. 但无论如何,无论如何, 硬件操作系统都会确保这一点。

If you mean you want to check for invalid memory writes in your application's allocated address range, then there is only so much you can do. 如果您的意思是要在应用程序的分配地址范围内检查无效的内存写入,那么您只能做很多事情。 You should use FastMM4 , and use it with its most verbose and paranoid settings in debug mode of your application. 您应该使用FastMM4 ,并在应用程序的调试模式下使用它的最详细和偏执设置。 This will catch a lot of invalid writes, accesses to already released memory and such, but it can't catch everything. 这将捕获大量无效写入,访问已释放的内存等,但它无法捕获所有内容。 Consider a dangling pointer that points to another writeable memory location (like the middle of a large string or array of float values) - writing to it will succeed, and it will trash other data, but there's no way for the memory manager to catch such access. 考虑一个指向另一个可写内存位置的悬空指针(如大字符串或浮点值数组的中间位置) - 写入它会成功,它会丢弃其他数据,但内存管理器无法捕获这样的内容访问。

I don't have a solution but there are some clues about that particular error message. 我没有解决方案,但有一些关于该特定错误消息的线索。

System.TObject.InheritsFrom subtracts the constant vmtParent from the Self-pointer (the class) to get pointer to the adress of the parent class. System.TObject.InheritsFrom从Self-pointer(类)中减去常量vmtParent,以获取指向父类地址的指针。

In Delphi 2007 vmtParent is defined: 在Delphi 2007中定义了vmtParent:

vmtParent = -36; vmtParent = -36;

So the error $FFFFFFDD (-35) sounds like the class pointer is 1 in this case. 所以错误$ FFFFFFDD(-35)在这种情况下听起来类指针是1。

Here is a test case to reproduce it: 这是一个重现它的测试用例:

procedure TForm1.FormCreate(Sender: TObject);
var
  I : integer;
  O : tobject;
begin
  I := 1;
  O := @I;
  O.InheritsFrom(TObject);
end;

I've tried it in Delphi 2010 and get 'Read of address FFFFFFD1' because the vmtParent is different between Delphi versions. 我已经在Delphi 2010中尝试过并获得“读取地址FFFFFFD1”,因为vmtParent在Delphi版本之间是不同的。

The problem is that this happens deep inside the Bold framework so you may have trouble guarding against it in your application code. 问题是,这种情况发生在Bold框架内部,因此您可能无法在应用程序代码中防范它。

You can try this on your objects that are used in the DMAttracsTimers-code (which I assume is your application code): 您可以在DMAttracsTimers代码中使用的对象上尝试此操作(我假设您的应用程序代码):

Assert(Integer(Obj.ClassType)<>1,'Corrupt vmt');

It sounds like you have memory corruption of object instance data. 听起来你有对象实例数据的内存损坏。

The VMT itself isn't getting corrupted, FWIW: the VMT is (normally) stored in the executable and the pages that map to it are read-only. VMT本身没有被破坏,FWIW:VMT(通常)存储在可执行文件中,映射到它的页面是只读的。 Rather, as VilleK says, it looks like the first field of the instance data in your case got overwritten with a 32-bit integer with value 1. This is easy enough to verify: check the instance data of the object whose method call failed, and verify that the first dword is 00000001. 相反,正如VilleK所说,看起来你的情况下实例数据的第一个字段被一个值为1的32位整数覆盖。这很容易验证:检查方法调用失败的对象的实例数据,并验证第一个双字是00000001。

If it is indeed the VMT pointer in the instance data that is being corrupted, here's how I'd find the code that corrupts it: 如果它确实是实例数据中的VMT指针被破坏,那么我就是如何找到破坏它的代码:

  1. Make sure there is an automated way to reproduce the issue that doesn't require user input. 确保有一种自动方式来重现不需要用户输入的问题。 The issue may be only reproducible on a single machine without reboots between reproductions owing to how Windows may choose to lay out memory. 由于Windows可能选择布局内存,因此问题可能只能在一台机器上重现,而不会在复制之间重新启动。

  2. Reproduce the issue and note the address of the instance data whose memory is corrupted. 重现问题并记下内存已损坏的实例数据的地址。

  3. Rerun and check the second reproduction: make sure that the address of the instance data that was corrupted in the second run is the same as the address from the first run. 重新运行并检查第二次复制:确保第二次运行中已损坏的实例数据的地址与第一次运行时的地址相同。

  4. Now, step into a third run, put a 4-byte data breakpoint on the section of memory indicated by the previous two runs. 现在,进入第三次运行,在前两次运行指示的内存部分放置一个4字节的数据断点。 The point is to break on every modification to this memory. 重点是打破对此内存的每次修改。 At least one break should be the TObject.InitInstance call which fills in the VMT pointer; 至少有一个中断应该是填充VMT指针的TObject.InitInstance调用; there may be others related to instance construction, such as in the memory allocator; 可能还有其他与实例构造相关的内容,例如内存分配器; and in the worst case, the relevant instance data may have been recycled memory from previous instances. 在最坏的情况下,相关的实例数据可能是以前实例中的回收内存。 To cut down on the amount of stepping needed, make the data breakpoint log the call stack, but not actually break. 要减少所需的步进量,请使数据断点记录调用堆栈,但不要实际中断。 By checking the call stacks after the virtual call fails, you should be able to find the bad write. 通过在虚拟调用失败后检查调用堆栈,您应该能够找到错误的写入。

mghie is right of course. mghie当然是对的。 (fastmm4 calls the flag fulldebugmode or something like that). (fastmm4调用标志fulldebugmode或类似的东西)。

Note that that works usually with barriers just before and after an heap allocation that are regularly checked (on every heapmgr access?). 请注意,通常在定期检查的堆分配之前和之后(在每个heapmgr访问时都有障碍)处理障碍。

This has two consequences: 这有两个后果:

  • the place where fastmm detects the error might deviate from the spot where it happens fastmm检测到错误的位置可能会偏离发生错误的位置
  • a total random write (not overflow of existing allocation) might not be detected. 可能无法检测到总随机写入(不是现有分配的溢出)。

So here are some other things to think about: 所以这里还有一些需要考虑的事情:

  • enable runtime checking 启用运行时检查
  • review all your compiler's warnings. 检查所有编译器的警告。
  • Try to compile with a different delphi version or FPC. 尝试使用不同的delphi版本或FPC进行编译。 Other compilers/rtls/heapmanagers have different layouts, and that could lead to the error being caught easier. 其他编译器/ rtls / heapmanagers具有不同的布局,这可能导致更容易捕获错误。

If that all yields nothing, try to simplify the application till it goes away. 如果这一切都没有产生任何结果,请尝试简化应用程序直到它消失。 Then investigate the most recent commented/ifdefed parts. 然后调查最近的评论/ ifdefed部分。

The first thing I would do is add MadExcept to your application and get a stack traceback that prints out the exact calling tree, which will give you some idea what is going on here. 我要做的第一件事就是将MadExcept添加到你的应用程序中,并获得一个打印出确切调用树的堆栈追溯,这将让你知道这里发生了什么。 Instead of a random exception and a binary/hex memory address, you need to see a calling tree, with the values of all parameters and local variables from the stack. 您需要查看调用树,而不是随机异常和二进制/十六进制内存地址,其中包含堆栈中所有参数和局部变量的值。

If I suspect memory corruption in a structure that is key to my application, I will often write extra code to make tracking this bug possible. 如果我怀疑在我的应用程序关键的结构中存在内存损坏,我会经常编写额外的代码来跟踪这个bug。

For example, in memory structures (class or record types) can be arranged to have a Magic1:Word at the beginning and a Magic2:Word at the end of each record in memory. 例如,在内存结构(类或记录类型)中可以安排在开头有一个Magic1:Word,在内存中每个记录的末尾有一个Magic2:Word。 An integrity check function can check the integrity of those structures by looking to see for each record Magic1 and Magic2 have not been changed from what they were set to in the constructor. 完整性检查功能可以通过查看每条记录查看Magic1和Magic2是否从构造函数中设置的更改来检查这些结构的完整性。 The Destructor would change Magic1 and Magic2 to other values such as $FFFF. 析构函数会将Magic1和Magic2更改为其他值,例如$ FFFF。

I also would consider adding trace-logging to my application. 我还会考虑将trace-logging添加到我的应用程序中。 Trace logging in delphi applications often starts with me declaring a TraceForm form, with a TMemo on there, and the TraceForm.Trace(msg:String) function starts out as "Memo1.Lines.Add(msg)". 在delphi应用程序中跟踪日志记录通常从我声明一个TraceForm表单开始,其中有一个TMemo,而TraceForm.Trace(msg:String)函数则以“Memo1.Lines.Add(msg)”开头。 As my application matures, the trace logging facilities are the way I watch running applications for overall patterns in their behaviour, and misbehaviour. 随着我的应用程序的成熟,跟踪日志记录工具是我观察运行应用程序的行为和行为的整体模式的方式。 Then, when a "random" crash or memory corruption with "no explanation" happens, I have a trace log to go back through and see what has lead to this particular case. 然后,当发生“随机”崩溃或内存损坏并且“无解释”时,我会有一个跟踪日志返回并查看导致此特定情况的原因。

Sometimes it is not memory corruption but simple basic errors (I forgot to check if X is assigned, then I go dereference it: X.DoSomething(...) that assumes X is assigned, but it isn't. 有时它不是内存损坏而是简单的基本错误(我忘了检查是否分配了X,然后我去取消引用它:X.DoSomething(...)假定X被分配,但事实并非如此。

I Noticed that a timer is in the stack trace. 我注意到计时器在堆栈跟踪中。
I have seen a lot of strange errors where the cause was the timer event is fired after the form i free'ed. 我看到了很多奇怪的错误,其中的原因是我自由的表单后触发了计时器事件。
The reason is that a timer event cound be put on the message que, and noge get processed brfor the destruction of other components. 原因是可以在消息队列上放置一个计时器事件,并且对其他组件的销毁进行处理。
One way around that problem is disabling the timer as the first entry in the destroy of the form. 解决该问题的一种方法是禁用计时器作为表单销毁中的第一个条目。 After disabling the time call Application.processMessages, so any timer events is processed before destroying the components. 禁用时间调用Application.processMessages后,在销毁组件之前处理任何计时器事件。
Another way is checking if the form is destroying in the timerevent. 另一种方法是检查表格是否在时间的推移中破坏。 (csDestroying in componentstate). (cs在组件状态中销毁)。

Can you post the sourcecode of this procedure? 你能发布这个程序的源代码吗?

BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016) BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression(BoldSystem.pas:4016)

So we can see what's happening on line 4016. 所以我们可以看到4016行发生了什么。

And also the CPU view of this function? 还有这个功能的CPU视图?
(just set a breakpoint on line 4016 of this procedure and run. And copy+paste the CPU view contents if you hit the breakpoint). (只需在此过程的第4016行设置断点并运行。如果点击断点,则复制+粘贴CPU视图内容)。
So we can see which CPU instruction is at address 00404E78. 因此,我们可以看到哪个CPU指令位于地址00404E78。

Could there be a problem with re-entrant code? 重入代码会有问题吗?

Try putting some guard code around the TTimer event handler code: 尝试在TTimer事件处理程序代码周围添加一些保护代码:

procedure TAttracsTimerDataModule.AttracsTimerTimer(ASender: TObject);
begin
  if FInTimer then
  begin
    // Let us know there is a problem or log it to a file, or something. 
    // Even throw an exception
    OutputDebugString('Timer called re-entrantly!'); 
    Exit; //======> 
  end;

  FInTimer := True;
  try

    // method contents

  finally
    FInTimer := False;
  end;
end;

N@ N - [

I think there is another possibility: the timer is fired to check if there are "Dangling Logon Sessions". 我认为还有另一种可能性:计时器被激活以检查是否存在“悬空登录会话”。 Then, a call is done on a TLogonSession object to check if it may be dropped (_GetMayDropSession), right? 然后,在TLogonSession对象上进行调用以检查它是否可以被删除(_GetMayDropSession),对吧? But what if the object is destroyed already? 但是如果对象已被破坏怎么办? Maybe due to thread safety issues or just a .Free call and not a FreeAndNil call (so a variable is still <> nil) etc etc. In the mean time, other objects are created so the memory gets reused. 可能是由于线程安全问题或只是一个.Free调用而不是FreeAndNil调用(所以变量仍然是<> nil)等等。同时,创建其他对象以便重用内存。 If you try to acces the variable some time later, you can/will get random errors... 如果您稍后尝试访问变量,则可能/将会出现随机错误...

An example: 一个例子:

procedure TForm11.Button1Click(Sender: TObject);
var
  c: TComponent;
  i: Integer;
  p: pointer;
begin
  //create
  c := TComponent.Create(nil);
  //get size and memory
  i := c.InstanceSize;
  p := Pointer(c);
  //destroy component
  c.Free;
  //this call will succeed, object is gone, but memory still "valid"
  c.InheritsFrom(TObject);
  //overwrite memory
  FillChar(p, i, 1);
  //CRASH!
  c.InheritsFrom(TObject);
end;

Access violation at address 004619D9 in module 'Project10.exe'. 模块“Project10.exe”中地址004619D9的访问冲突。 Read of address 01010101. 阅读地址01010101。

Isn't the problem that "_GetMayDropSession" is referencing a freed session variable? 是不是“_GetMayDropSession”引用了一个释放的会话变量?

I have seen this kind of errors before, in TMS where objects were freed and referenced in an onchange etc (only in some situations it gave errors, very difficult/impossible to reproduce, is fixed now by TMS :-) ). 我之前已经看到过这种错误,在TMS中,对象被释放并在onchange等中被引用(仅在某些情况下它会产生错误,很难/不可能重现,现在由TMS修复:-))。 Also with RemObjects sessions I got something similar (due to bad programming bug by myself). 另外,使用RemObjects会话,我得到了类似的东西(由于我自己的编程错误)。

I would try to add a dummy variable to the session class and check for it's value: 我会尝试在会话类中添加一个虚拟变量并检查它的值:

  • public variable iMagicNumber: integer; public变量iMagicNumber:integer;
  • constructor create: iMagicNumber := 1234567; 构造函数创建:iMagicNumber:= 1234567;
  • destructor destroy: iMagicNumber := -1; 析构函数destroy:iMagicNumber:= -1;
  • "other procedures": assert(iMagicNumber = 1234567) “其他程序”:断言(iMagicNumber = 1234567)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM