kwanz 发表于 2012-4-24 21:46:27

[GTAF进口]SA内存控制

本帖最后由 kwanz 于 2012-4-25 21:07 编辑

原帖地址 http://www.gtaforums.com/index.php?showtopic=262280
SA内存控制
原帖作者/ Seemann @GTAF 翻译/ kwanz @虚拟世界
未经许可 谢绝转载
译注 本帖的一楼包含了Seemann大人及其他论坛高手在SB开发早期对内存操作技术的讨论,有一些例子和结果已经过时了。有个别不准确的说法已在后续回复中补充修正,译文中不再指出。所有的示例代码仅供研究学习原理使用。新版的SB+CLEO已经实现了opcode级别的WriteMemory和ReadMemory操作。具体请参考
EN - CLEO Script Tutorial
CH - CLEO编程教程(未完成)的地板(4#)
---------------------------------
我决定开一帖来集中讲解用SCM操作SA内存的所有例子。
如果你看不懂,也许不是你的错。我稍后会编辑帖子解释更清楚一点。
所有的代码是用SB2.99编写的。要成功运行,你需要下载最新版本的SB
http://www.gtaforums.com/index.php?showtopic=211077
译者注:或者下载【下载】SB汉化版+最小启动包 有汉化版SB
下载地址:http://game.55660.net/thread-138-1-1.html

所有代码在SA1.0美版总测试通过,不同版本的内存地址可能不同。如果什么东西用不了,请确认你的程序版本正确。


现在,我们有三种方法操作游戏内存。
1. 这个帖子讨论了操作内存的最初的方法。
Stat相关opcode提供了统计数据池附近有限的地址的内存访问。它允许一些简单的操作,比如更改玩家的金钱。
优点
- 只需要用到opcode
缺点
- 可访问地址非常有限,很多有用的地址不能访问

2. 方法二:用Xieon的补丁修改gta-sa.exe的3个opcode,提供了极大量的操作内存的方式。点此下载
优点
- 所有地址都可访问
- 可以保护带有VirtualProtect的内存区域,实现重写
缺点
- 需要破解exe;可能不兼容其他版本(不过 我还没有见过这个补丁不能正常工作的例子)
3. 方法三:用SA的数组访问0到FFFFFFFF之间的任何地址,此法首发在这一帖。
优点
- 可以访问所有地址
- 容易使用
- 无特殊要求,纯scm实现
缺点
- 某些地址仍然不可写(会发生非法访问错误)


对法三,可以调用三个函数访问特定地址,简单地讲,包括
1 MemoryWrite: 写入特定长度的新值
参数 0@=地址 2@=新值 3@=长度(1,2,3,4)
2 MemoeryWrite_DWORD:写入新的双字节数值
参数 0@=地址 1@=新值
3 MemeoryRead: 从内存读取双字节数值
参数 0@=地址 1@=返回值

注意仍有部分地址是不可读/写的!!!
要破解这些地址,尝试用Xieon's的内存补丁: ..\tools\Sa Memory Path\

//--写特定数值

:MemoryWrite   
0085: 5@ = 0@
0@ /= 4
0@ *= 4         // 地址
0062: 5@ -= 0@    // 偏移 (0, 1, 2, 3)
:_GetInitValue      // 如果你已经把偏移赋给5@, 就可以跳到这里
gosub @MemoryRead // 取初值
3@ *= 8 // bytes -> bits
5@ *= 8
dec(3@)
for 6@ = 0 to 3@
if
    08B6: test 2@ bit 6@
then
    08BF: set 1@ bit 5@    // 1
else
    08C5: clear 1@ bit 5@// 0
end
inc(5@) // next memory bit
end   
008A: &0(0@,1i) = 1@// 写入新值
return


//--写32位数值-----------

:MemoryWrite32bit
0@ -= 0xA49960
0@ /= 4
008A: &0(0@,1i) = 1@   
return

//--读32位数值-----------

:MemoryRead
0@ -= 0xA49960
0@ /= 4
008B: 1@ = &0(0@,1i)
return


我写了几个用这些函数的例子。多数的例子都发在GTAF上,有的发在别的地方
---------------------------------
例1
用CLEO做超长火车,15+节车厢!
原文链接(俄文)
效果截图
---------------------------------
:LONGTRAINS
thread 'TRAINS'
for 0@ = -382229 to -382216
    wait 0
    &0(0@,1i) = #STREAKC      
end
                                             
// 修改type0

// 加载模型
#FREIGHT.Load
#FREIFLAT.Load
#STREAKC.Load

while true
    if and
      Model.Available(#FREIGHT)
      Model.Available(#FREIFLAT)
      Model.Available(#STREAKC)   
    then
      Break
    end
    wait 0
end
   
// 生成带有新车厢的火车
06D8: 1@ = create_train_at 2278.1771 -1144.8823 27.5108 type 0 direction 1

#FREIGHT.Destroy
#FREIFLAT.Destroy
#STREAKC.Destroy

end_thread
---------------------------------
例2
运行时作弊码的新实现方法
原文链接
---------------------------------
:NEWCHEATS
// 例1:测试1个键(空格)
thread 'CHEATS'
0@ = -229908
while true
008B: 1@ = &0(0@,1i)      // 获得最后按键
0085: 2@ = 1@         
div(1@, 0x 1 00)          // 1个字符: 0x100, 2个字符: 0x10000: 3个字符: 0x1000000
mul(1@, 0x 1 00)          // 同上
0062: 2@ -= 1@            // 获得指定数目的按键记录 (1)

if
2@ == 0x20            // 检测是否空格
then
   03E5: text_box 'CHEAT1'// 显示作弊启用字样
   Break
end
wait 1000
end

// 例2:测试2个键的序列 (NO)
0@ = -229908
while true
008B: 1@ = &0(0@,1i)       // 获得最后按键
0085: 2@ = 1@         
div(1@, 0x 1 00 00)      // 1个字符: 0x100, 2个字符: 0x10000: 3个字符: 0x1000000
mul(1@, 0x 1 00 00)      // 同上
0062: 2@ -= 1@             // 获得指定数目的按键记录 (2)
if
2@ == 0x4e4f             // NO
then   
03E5: text_box 'CHEAT1'// 显示作弊启用字样
Break
end
wait 1000
end

// 例3:测试3个键的序列('WOW')
0@ = -229908
while true
008B: 1@ = &0(0@,1i)       // 获得最后按键
0085: 2@ = 1@         
div(1@, 0x 1 00 00 00)   // 1个字符: 0x100, 2个字符: 0x10000: 3个字符: 0x1000000
mul(1@, 0x 1 00 00 00)   // 同上
0062: 2@ -= 1@             // 获得指定数目的按键记录 (3)
if
2@ == 0x574f57         // WOW
then
    03E5: text_box 'CHEAT1'// 显示作弊启用字样
Break
end
wait 1000
end

// 例4:测试4个键的序列 ('HACK')
0@ = -229908
while true
if
&0(0@,1i) == 0x4841434B// HACK
then
03E5: text_box 'CHEAT1'// 显示作弊启用字样
Break
end
wait 1000
end

// 例5:测试5个键的序列 ('SANNY')
// 首先在偏移+4位的地址上检测第5键是否为s,然后再测试anny
//地址      按键
// -229908: X X X X    |-229908: A N N Y
// -229907: X X X O <- |-229907: _ _ _ S                  
while true
0@ = -229907 // 偏移+32位; 也就是最后按键记录地址偏移+4字节/字符处
008B: 1@ = &0(0@,1i)
0085: 2@ = 1@         
div(1@, 0x100)
mul(1@, 0x100)
0062: 2@ -= 1@               // 第5个字符是第二个按键块的末字母
if
2@ == 0x53               // 检测第5键是否为s
then                        
   0@ = -229908
   008B: 3@ = &0(0@,1i)
   if 3@ == 0x414E4E59       // 检测后续按键是否为anny
   then
   03E5: text_box 'CHEAT1' // 作弊启用
   Break
   end   
end
wait 1000
end
   
// 例6:测试16个键的序列: '1234567812345678'
0@ = -57477
while true
if
&0(0@,1v) == "8765432187654321" // "1234567812345678"
then
03E5: text_box 'CHEAT1'    // 作弊启用
Break
end
wait 1000
end
end_thread
---------------------------------
例3
修改任意线程的局部变量数值
原文链接(俄文)
---------------------------------
// ---------------------------------------------
//此线程搜索名为'TEST'的线程
//并修改它的局部变量10@
// ---------------------------------------------
:CHANGELOCALVAR
thread 'CLV'
{
0@=thread address
1@=temp
}
0@ = 0xA8B42C
// 查找线程循环
while true
gosub @MemoryRead
// 1@ = 第一个活动线程
if 1@ == 0
then
    Break // 没有找到活动线程,搜索失败
end
0085: 0@ = 1@    // 保存指针
// 获得线程名称的指针
div(1@, 8)
dec(1@, 1 348 395)
{
   重要提示:
    线程名称是以小写形式保存的,
    所以我们要用小写进行比较.
    注意SB选项'Case Converting'(大小写转换)必须设为'As is'(保持原值)!
}
if &0(1@,1s) == 'test'
// 测试线程名称是否"test"
then   
    // 找到了,我们可以为所欲为
    // 0@ 包含线程地址

    // 获得10@的地址
    inc(0@, 0x3c)   // 线程局部存储池
    inc(0@, 40)       // 局部变量号 * 4; 比如说要修改9@,就设为36
   
    // 写入双字节数值
    1@ = 3333 // 新值
    gosub @MemoryWrite32bit

    Break // 结束循环
else
    // 否,线程名称不符
    // 检查下一个
    wait 0
end
// 跳转到while循环的开始处
end
// 变量值已修改,结束线程
end_thread

// ---------------------------------------------
//这个线程在被激活1秒后
//显示10@的数值
// ---------------------------------------------
:TEST1
thread 'TEST'
10@ = 10000
wait 3000
054C: use_GXT_table 'POOL'
01E3: text_1number_styled 'NUM' 10@ 5000 ms 1// ~1~
end_thread

以下是一个改进版的CLV线程
:CLV
03A4: name_thread 'CLV'
0006: 0@ = 67251

:CLV_LOOP
008B: 0@ = &0(0@,1i)
00D6: if
8039:   not0@ == 0
004D: jump_if_false @CLV_END
0001: wait 0 ms
0085: 1@ = 0@
0016: 1@ /= 8
000E: 1@ -= 1348395
0016: 0@ /= 4
000E: 0@ -= 2696792
00D6: if
05AD:   &0(1@,1s) == 'test'
004D: jump_if_false @CLV_LOOP

// 25 为局部变量号+15
// 比如,要修改9@就设为24
000A: 0@ += 25

// 3333为新值
0004: &0(0@,1i) = 3333

:CLV_END
004E: end_thread
---------------------------------
例4
去除CJ第一次抢车时"To stop Carl..."的提示
原文链接
---------------------------------
:MSGREMOVE
thread 'NOMSG'
0@ = 0xC0BC15 //地址
2@ = 1      //数值
3@ = 1      //长度 (字节)
gosub @MemoryWrite
end_thread


你可以点这个下载带有全部示例的main.scm
http://www.mysharefile.com/v/7409206/memhandling.rar.html
已包含自述文件和源代码

更新历史
2006.12.06
- 添加了Xieon的补丁的链接
- 添加了改进版的CLV线程(例3)

---------------------------------
---------------------------------
main.scm已破解!
我终于找到不用任何补丁从main.scm运行汇编代码的方法了。
从此以后编程无极限。
这种方法类似CyQ的注入汇编代码修改opcode句柄的方法
我找到SA中有两个opcode可以实现这个效果
它们是0572和0A3D
方法是这样的
1. SA中有92个作弊码,有些是没有用的,有些大家都知道,很常用
2. Opcode里也有很常见的,比如0572给所有出租车加氮气,0A3D让女支女给你钱(无所谓,我都用0572)
但不是所有人都知道这些opcode的原理和作弊十分类似。它们会开启92个标志位之一,游戏会认为你作弊(但不会影响作弊计数,只是和调用作弊效果一样)
3. 0A3D与作弊标志90相关,0572-标志位91
4. 在标志位上,所有的作弊调用函数地址都以简单的双字节形式存储,可以用数组法在运行时改变
5. 也就是说,这个opcode做到的是用一个新数值修改main.scm的91号作弊标志(所有的地址我们都已熟知)。然后我们启用作弊,游戏就会跳转到我们设定的地址,换句话说,它跳进了main.scm,也就达到了汇编修改运行代码的效果。
在SB中使用hex...end结构,我们可以执行任意代码。
我个人asm用的不好,所以这里给的例子没什么意思(给玩家+1000刀),但这只是个开始。
随便一个会用asm的人可以写出带VirtualProtect的函数,然后我们甚至不需要Xieon的补丁就可以操作被保护的地址(比如,重力相关的地址)
// ----------------------------------
//      Asm代码注入器
// ----------------------------------
// 设置新的句柄地址
0@ = -429863
&0(0@,1i) = 0xA49960
&0(0@,1i) += @__AsmInjection
// 运行Asm注入
0572: 1
// 还原句柄
&0(0@,1i) = 0
return

:__AsmInjection
hex
// 这里写上asm代码


// 我的例子是加钱
B8 E8030000   // mov eax, 1000
01 05 50CEB700// add , eax
C3            // return

end
这段程序对0xB7CE50(金钱)加1000。有点没意思,不过很好用!

注意:程序在1.0美版中测试通过。如果你用的是v1.01,请自行测试。

额,下面是另一个例子
http://sannybuilder.com/files/cheater.rar
这个程序允许你按下动作键启用所有作弊
---------------------------------
---------------------------------
我已经实现了从main.scm运行VirtualProtect函数的方法,现在已经没有必要使用Xieon的补丁了。我们用scm可以达到相同的效果。在1.0美版,欧版,德版中可用
下面是向带VP保护的地址写入定长数值(1,2,4字节)的完整代码
:MemoryProofWrite
3@ == 1
jf @_____novp
4@ = -429863
&0(4@,1i) =0xA49960
&0(4@,1i) += @_____vpsv
0052: gap 0 virtual_protect_at_address 0@ size 1@ newprotect 4 gap 0 0
0572: 1
gosub @MemoryWrite
0052: gap 0 virtual_protect_at_address 0@ size 1@ newprotect -1 gap 0 0
0572: 1
&0(4@,1i) = 0
end_thread

:_____novp
gosub @MemoryWrite
end_thread

:MemoryWrite
3@ = -429864
&0(3@,1i) =0xA49960
&0(3@,1i) += @_____mwss
0052: gap 0 target_address 0@ size 1@ value 2@ gap 0 0
0A3D: 1
&0(3@,1i) = 0
return

:_____vpsv
hex
68 F4 3C A4 00 83 3D 84 3C A4 00 FF
75 08 FF 35 F4 3C A4 00 EB 06 FF 35
84 3C A4 00 FF 35 80 3C A4 00 FF 35
7C 3C A4 00 FF 15 2C 80 85 00 C3
end

:_____mwss
hex
8B 15 7C 3C A4 00 8B 05 84 3C A4 00
83 3D 80 3C A4 00 01 75 03 88 02 C3
83 3D 80 3C A4 00 02 75 04 66 89 02
C3 89 02 C3
end
这个线程只执行一次,然后停止。跟你写其他线程一样,把它摆到你的程序里。
当你用004F创建线程的时候,要传4个参数
create_thread @MemoryProofWrite address (DWORD) size (Byte: 1,2,4) value (DWORD) VirtualProtect (BOOL)
若你要写入无VP保护的地址,只需要使用
create_thread @MemoryProofWrite address XXXX size XXXX value XXXX VirtualProtect 0
或者略去VirtualProtect参数

Hello, World!
例1 修改重力(用补丁的例子)带VP
create_thread @MemoryProofWrite address XXXX size XXXX value XXXX VirtualProtect 0
编译版scm和源代码

例2 去除首次抢车消息提示 不带VP
create_thread @MemoryProofWrite address 0xC0BC15 size 1 value 1
---------------------------------
---------------------------------
引用 (DexX @ Mar 3 2007, 13:16)
你是说我们可以往游戏内存的任意位置写入任意数据么?

内存地址任意,但数值长度必须为1,2,4字节
可以包含类似这样的asm代码
cmp size, 1
ja @2
mov byte ptr , value
ret

:2
cmp size, 2
ja @4
mov word ptr , value
ret

:4
mov dword ptr , value
ret
我可以实现写入任何长度的BlockWrite函数,需要不?
---------------------------------
---------------------------------
引用 (PLPynton @ Mar 3 2007, 19:12)
在反编带有十六进制数据的脚本时会不会出现丢失数据的情况?

从v.298, IIRC以后就不会了
SB控制台可以接受如何输入命令,你可以打开IGNORE_UKNNOWN选项来反编任何受保护或损坏的scm。请参考帮助文件和控制台输出。
---------------------------------
---------------------------------
引用
SB返回
"Thread not found. Base: 194426. Probably Script.img is broken"

我成功反编过带十六进制的文件啊
顺便说,没有一种确保正确反编所有文件的方法。反编的时候标签(_____vpsv, _____mwss)也会消失。所以对其他人最有效的方法就是祈祷作者在他的mod里包含了源文件吧。

引用
我在用
create_thread @MemoryProofWrite address 0x00863984 size 4 value 0.002 VirtualProtect 1
的时候出状况了

神马状况?无法通过编译还是游戏出错?这个我搞过,用的好好的啊(顺便说一句,gravity=2.0的效果太喜感了)

(译注:修改重力只接受整型数值)
---------------------------------
---------------------------------
引用 (PLPynton @ Mar 3 2007, 21:45)
其实我们反编以后差不多就得到源文件了吧?

反编译器不需要代码中的特殊标记(nop等等)来检测是否存在内嵌十六进制代码。编译器只是把源代码和插入代码的偏移并在一起,并在结尾记录长度。这样也节省了空间。
所以,反编译器只是检测一下长度,然后直接插入纯文本,没有字节码了(本来就是源码。。)。很简单的。
不过想像一下,比如说,我有两个自定义线程,我准备把其中一个的源码插入main.scm。但是它使用了另一个线程的自定义变量和标签。结果是:得到的源码使用原有的名字,但反编以后县城名都变掉了,结果就会和源代码不兼容。
我觉得想要避免这个挺困难的,大家觉得呢
我觉得目前只有在用hex...end结构的时候需要用到这样的代码。我可以让编译器存储所有十六进制嵌入的偏移和长度,然后反编译器解析它们,从而解决兼容问题。

引用 (PLPynton @ Mar 3 2007, 20:32)
开头的if语句漏掉了

单行if无须用到00D6。我在节省main.scm的空间
你那gravity.rar好用吧。。。
---------------------------------
---------------------------------
引用 (SteaVor @ Mar 4 2007, 00:41)
现在从内存读取数值的最佳方法是什么?

我会为读取数据写类似的代码,不过我还是不确定传递目标指针的最佳方式
理论上结果大概是这样的:
create_thread @MemoryProofRead address XXX size XXX variable_offset 20
代码将数值存入变量$5
不过我还是可能用别的方法

引用 (PLPynton @ Mar 4 2007, 01:00)
我觉得我们对内嵌代码的讨论好像没有什么结果,无视我。。

不不不,这主意很好。我会首先对十六进制代码实现它,然后再看看它对纯文本源码的效果如何。
多谢各位的反馈。
---------------------------------
---------------------------------
好了,结果是这样
{ -----------------------------------------------------------------
内存访问线程

要向内存地址写入定长数值,使用
create_thread @MemoryProofWrite address X:DWord size X:BYTE value X:DWORD VirtualProtect X:BOOL

要从内存地址读取定长数值,使用
create_thread @MemoryProofRead address X:DWord size X:BYTE VirtualProtect X:BOOL
The read value will be stored to the variable $MEMORY_PROOF_READ

可用的size值为 1-字节 2-字 4-双字
VirtualProtect参数为可选
----------------------------------------------------------------- }

:MemoryProofWrite
4@ = @MemoryWrite
3@ == 1
jf @_____novp

:_____mpvp
5@ = -429863
&0(5@,1i) =0xA49960
&0(5@,1i) += @_____vpsv
0052: gap 0 virtual_protect_at_address 0@ size 1@ newprotect 4 gap 0 0
0572: run_VPSV 1
gosub 4@
0052: gap 0 virtual_protect_at_address 0@ size 1@ newprotect -1 gap 0 0
0572: run_VPSV 1
&0(5@,1i) = 0
end_thread

:MemoryProofRead
4@ = @MemoryRead
2@ == 0
jf @_____mpvp

:_____novp
gosub 4@
end_thread

:MemoryWrite
4@ = -429864
&0(4@,1i) =0xA49960
&0(4@,1i) += @_____mwss
0052: gap 0 target_address 0@ size 1@ value 2@ gap 0 0
0A3D: run_MWSS 1
&0(4@,1i) = 0
return

:MemoryRead
4@ = -429864
&0(4@,1i) =0xA49960
&0(4@,1i) += @_____mrmm
02EC: run_MRMM 1 read_address 0@ size 1@
hex
3D0A008D0100000000 {store_to} 02 $MEMORY_PROOF_READ
end
&0(4@,1i) = 0
return

:_____vpsv
hex
68 F4 3C A4 00 83 3D 84 3C A4 00 FF
75 08 FF 35 F4 3C A4 00 EB 06 FF 35
84 3C A4 00 FF 35 80 3C A4 00 FF 35
7C 3C A4 00 FF 15 2C 80 85 00 C3
end

:_____mwss
hex
8B 15 7C 3C A4 00 8B 05 84 3C A4 00
8B 0D 80 3C A4 00 83 F9 01 75 03 88
02 C3 83 F9 02 75 04 66 89 02 C3 89
02 C3
end

:_____mrmm
hex
31 C0 BA 78 3C A4 00 89 02 8B 0D 80
3C A4 00 8B 05 7C 3C A4 00 83 F9 01
75 05 8A 00 88 02 C3 83 F9 02 75 07
66 8B 00 66 89 02 C3 8B 00 89 02 C3            
end

{ ---------内存访问线程结束----------------------------- }

线程写得有点复杂了,实际情况下你可能需要更简短的,随意修改。
示例:减少重力5秒,然后恢复
create_thread @MemoryProofReadaddress 0x863984 size 4 VirtualProtect 1
wait 0
create_thread @MemoryProofWrite address 0x863984 size 4 value 0.002 VirtualProtect 1
wait 5000
create_thread @MemoryProofWrite address 0x863984 size 4 value $MEMORY_PROOF_READ VirtualProtect 1
---------------------------------
---------------------------------
以下是多版本的内存访问线程
你可以传递两个地址,一个用于v1,一个用于v1.01。程序检测运行游戏的版本,并且能正确处理
参数:
create_thread @MemoryProofMultiWrite/Read address for v1.0 (X:DWord) address for v1.01 (X:DWord) size (X:BYTE) value (X:DWORD) VirtualProtect (X:BOOL)
{ -----------------------------------------------------------------
内存访问线程
多版本 :: 支持 GTA SA v1 and v1.01

要向内存地址写入定长数值,使用
create_thread @MemoryProofMultiWrite v1地址 X:DWord v1.01地址 X:DWord size X:BYTE value X:DWORD VirtualProtect X:BOOL

示例:
create_thread @MemoryProofMultiWrite address_V1 0xC0BC15 address_V2 0xC0E295 size 1 value 1

要从内存地址读取定长数值,使用
create_thread @MemoryProofMultiRead v1地址 X:DWord v1.01地址 X:DWord size X:BYTE VirtualProtect X:BOOL

结果存入 $MEMORY_PROOF_READ

示例:
create_thread @MemoryProofMultiRead address_V1 0x863984 address_V2 0 size 4 VirtualProtect 1

可用的size值为 1-字节 2-字 4-双字
VirtualProtect参数为可选
----------------------------------------------------------------- }

:MemoryProofMultiWrite
gosub @_____gsvo01
7@ = @MemoryWrite
4@ == 1
jf @_____novp

:_____mpvp
0085: 8@ = 10@(9@,2i)
008A: &0(8@,1i) = 14@(9@,2i)
005E: &0(8@,1i) += 16@(9@,2i)
0052: gap 0 virtual_protect_at_address 0@(9@,2i) size 2@ newprotect 4 gap 0 0
0572: run_VPSV 1
gosub 7@
0052: gap 0 virtual_protect_at_address 0@(9@,2i) size 2@ newprotect -1 gap 0 0
0572: run_VPSV 1
&0(8@,1i) = 0
end_thread

:MemoryProofMultiRead
gosub @_____gsvo01
7@ = @MemoryRead
3@ == 0
jf @_____mpvp

:_____novp
gosub 7@
end_thread

:MemoryWrite
0085: 7@ = 12@(9@,2i)
008A: &0(7@,1i) = 14@(9@,2i)
005E: &0(7@,1i) += 18@(9@,2i)
0052: gap 0 target_address 0@(9@,2i) size 2@ value 3@ gap 0 0
0A3D: run_MWSS 1
&0(7@,1i) = 0
return

:MemoryRead
0085: 7@ = 12@(9@,2i)
008A: &0(7@,1i) = 14@(9@,2i)
005E: &0(7@,1i) += 20@(9@,2i)
02EC: run_MRMM 1 read_address 0@(9@,2i) size 2@
hex
3D0A008D0100000000 {store_to} 02 $MEMORY_PROOF_READ
end
&0(7@,1i) = 0
return

:_____gsvo01
8@ = -429566
&0(8@,1i) == 4611680
jf @_____gsvo012 // 1.0
//    9@= 0
   10@ = -429863
   12@ = -429864
   14@ = 0xA49960
   16@ = @_____vpsv
   18@ = @_____mwss
   20@ = @_____mrmm
return
:_____gsvo012    // 1.01
   9@= 1
   11@ = -431117
   13@ = -431118
   15@ = 0xA4BFE0
   17@ = @_____vpsv2
   19@ = @_____mwss2
   21@ = @_____mrmm2   
return

:_____vpsv2
hex
68 74 63 A4 00 83 3D 04 63 A4 00 FF
75 08 FF 35 74 63 A4 00 EB 06 FF 35
04 63 A4 00 FF 35 FC 62 A4 00 FF 35
F8 62 A4 00 FF 15 2C 90 85 00 C3
end

:_____vpsv
hex
68 F4 3C A4 00 83 3D 84 3C A4 00 FF
75 08 FF 35 F4 3C A4 00 EB 06 FF 35
84 3C A4 00 FF 35 80 3C A4 00 FF 35
7C 3C A4 00 FF 15 2C 80 85 00 C3
end

:_____mwss2
hex
8B 15 FC 62 A4 00 8B 05 04 63 A4 00
8B 0D 63 00 A4 00 EB 12
end

:_____mwss
hex
8B 15 7C 3C A4 00 8B 05 84 3C A4 00
8B 0D 80 3C A4 00 83 F9 01 75 03 88
02 C3 83 F9 02 75 04 66 89 02 C3 89
02 C3
end

:_____mrmm2
hex
31 C0 BA F8 62 A4 00 89 02 8B 0D 00
63 A4 00 8B 05 FC 62 A4 00 EB 15
end

:_____mrmm
hex
31 C0 BA 78 3C A4 00 89 02 8B 0D 80
3C A4 00 8B 05 7C 3C A4 00 83 F9 01
75 05 8A 00 88 02 C3 83 F9 02 75 07
66 8B 00 66 89 02 C3 8B 00 89 02 C3            
end

kwanz 发表于 2012-4-24 21:50:30

本帖最后由 kwanz 于 2012-4-25 21:05 编辑

引用 (ceedj @ Mar 5 2007, 05:25)
那段SB的asm代码是操作内存的标准写法吗?

是的,但仅用十六进制代码不足以操作内存。整个线程是这样处理的:一部分向内存加载参数,另一部分运行asm代码。你要复制其中一个版本的线程(仅v1或多版本,随你便)到main.scm的某处,然后调用004F来使用它。
另外,我知道用程序检测键盘上的任意键按下的方法,任意是指除了ESC和PrintScreen以外的全部按键。其他的按键都很容易处理,

从而打破了opcode00E1只能检测大约20个预定义按键的限制。
有兴趣么?
-------------------------------
-------------------------------
Y_Less的回复
SA作弊码处理器。我写的程序的确是一个作弊码处理器,是为了简单易用设计的。它用了一个稍作修改的内存操作程序,允许直接调用或create_thread调用,但它完全向下兼容。比如说,你像正常调线程一样调create_thread,并且首先把3@和4@初始化为1(4@是内部返回标记,替代end_thread用,3@是为了表示MemoryProofRead进程里少一个参数而设的),这是没问题的。
我不知道Seemann获取按键的方法,不过我只是从最新按键池0x969110中读取数据,这表示我们不需要时时保存最新的按键,但是这种应用还是有一定限制的。
要做一个新的作弊码,只需要
create_thread @CHEAT_PROCESSOR @CHEAT_CODE_LABEL
然后在:CHEAT_CODE_LABEL后定义你的作弊码
:CHEAT_CODE_LABEL
0661: set_cheat_text "ACTIVATE"
03E5: show_text_box 'HELP101'
return
0661设定了启用作弊需要输入的字符串,处理器会自动读取和识别。剩下的部分就是作弊启用的动作,最后需要不带参的return,请不要使用end_thread什么的。
另外注意在作弊程序中绝对禁用7@ 8@和9@,否则必挂...
完整的作弊处理器(包含了修改过的单版本内存访问程序)
{ -----------------------------------------------------------------
作弊码处理器线程

要创建新线程只需要调用这个,并传入作弊程序的标签作为参数
然后设置作弊程序的第一行命令为

0661: set_cheat_text "<作弊码>"

(类似name_thread,但用于注册作弊码)

字符串最长为255个字符,但必须全为大写字母
空格,数字等字符是不允许的

另外,作弊程序必须以return结束
绝对不能用end_thread,除非你想立即当机
----------------------------------------------------------------- }

:CHEAT_PROCESSOR
0085: 7@ = 0@
0@ += 0xA49963 // SCM偏移 + 3
1@ = 1 // 大小
2@ = 0 // virtual protect
3@ = 1 // 用return, 千万别用end
gosub @MemoryProofRead
008B: 8@ = $MEMORY_PROOF_READ
7@ += 4
005A: 7@ += 8@
9@ = 0

{
   5@现在存储字符串的长度
   我真希望scm编程能用&&和>>
   然后直接转换为asm。。。

   用到的变量
   0@-6@ - 保留作程序内部处理用
   7@ - 作弊程序段的标签
   8@ - 字符串长度
   9@ - 字符表的原先双字节首地址
   
   其他说明:
   9867536 - 字符表的十进制偏移量
}

:CP_MAIN_LOOP
wait 1000
0@ = 0x969110
1@ = 4
2@ = 0
3@ = 1
gosub @MemoryProofRead
803C: $MEMORY_PROOF_READ != 9@
jf @CP_MAIN_LOOP
008B: 9@ = $MEMORY_PROOF_READ
1@ = 1
11@ = 0

:CP_STRCMP
802D: 11@ < 8@ // (!>=)
jf @CP_SUCCESS
0@ = 0x969110
005A: 0@ += 11@
gosub @MemoryProofRead
008B: 10@ = $MEMORY_PROOF_READ
0085: 0@ = 7@
0@ += 0xA4995F // SCM offset - 1
0062: 0@ -= 11@
gosub @MemoryProofRead
003C: $MEMORY_PROOF_READ == 10@
jf @CP_FAILURE
11@ += 1
jump @CP_STRCMP

:CP_SUCCESS
gosub 7@
// 显示作弊启用
03E5: text box "CHEAT1"
0@ = 0x96918C
1@ = 4
2@ = 1
3@ = 0
4@ = 1
// 记录已作弊
gosub @MemoryProofWrite
0@ = 0xBAA472
gosub @MemoryProofWrite
// 增加作弊计数
0@ = 0xB79044
2@ = 0
3@ = 1
gosub @MemoryProofRead
008B: 2@ = $MEMORY_PROOF_READ
2@ += 1
3@ = 0
gosub @MemoryProofWrite

:CP_FAILURE
jump @CP_MAIN_LOOP

{ -----------------------------------------------------------------
Memory Access Thread
内存访问线程请参考1楼的单版本内存访问线程

to write a value with the specified length to the memory addressuse
create_thread @MemoryProofWrite address X:DWord size X:BYTE value X:DWORD VirtualProtect X:BOOL

to read a value with the specified length by the memory addressuse
create_thread @MemoryProofRead address X:DWord size X:BYTE VirtualProtect X:BOOL
The read value will be stored to the variable $MEMORY_PROOF_READ

The possible Size values: 1 (byte), 2 (word), 4 (dword)
The VirtualProtect parameter is optional
----------------------------------------------------------------- }

:MemoryProofWrite
5@ = @MemoryWrite
3@ == 1
jf @_____novp

:_____mpvp
6@ = -429863
&0(6@,1i) =0xA49960
&0(6@,1i) += @_____vpsv
0052: gap 0 virtual_protect_at_address 0@ size 1@ newprotect 4 gap 0 0
0572: run_VPSV 1
gosub 5@
0052: gap 0 virtual_protect_at_address 0@ size 1@ newprotect -1 gap 0 0
0572: run_VPSV 1
&0(6@,1i) = 0
4@ == 0
jf @MemoryReturn
end_thread

:MemoryProofRead
5@ = @MemoryRead
0085: 4@ = 3@
2@ == 0
jf @_____mpvp

:_____novp
gosub 5@
4@ == 0
jf @MemoryReturn
end_thread

:MemoryWrite
5@ = -429864
&0(5@,1i) =0xA49960
&0(5@,1i) += @_____mwss
0052: gap 0 target_address 0@ size 1@ value 2@ gap 0 0
0A3D: run_MWSS 1
&0(5@,1i) = 0
return

:MemoryRead
5@ = -429864
&0(5@,1i) =0xA49960
&0(5@,1i) += @_____mrmm
02EC: run_MRMM 1 read_address 0@ size 1@
hex
3D0A008D0100000000 {store_to} 02 $MEMORY_PROOF_READ
end
&0(5@,1i) = 0
return

:MemoryReturn
0051: return

:_____vpsv
hex
68 F4 3C A4 00 83 3D 84 3C A4 00 FF
75 08 FF 35 F4 3C A4 00 EB 06 FF 35
84 3C A4 00 FF 35 80 3C A4 00 FF 35
7C 3C A4 00 FF 15 2C 80 85 00 C3
end

:_____mwss
hex
8B 15 7C 3C A4 00 8B 05 84 3C A4 00
8B 0D 80 3C A4 00 83 F9 01 75 03 88
02 C3 83 F9 02 75 04 66 89 02 C3 89
02 C3
end

:_____mrmm
hex
31 C0 BA 78 3C A4 00 89 02 8B 0D 80
3C A4 00 8B 05 7C 3C A4 00 83 F9 01
75 05 8A 00 88 02 C3 83 F9 02 75 07
66 8B 00 66 89 02 C3 8B 00 89 02 C3            
end

{ ---------end of memory access thread----------------------------- }
还有一些没什么意思的“作弊”的例子
create_thread @CHEAT_PROCESSOR @HEALTH_CHEAT
create_thread @CHEAT_PROCESSOR @CAR_CHEAT
:HEALTH_CHEAT
0661: set_cheat_text "HELLO"
0256:   player $PLAYER_CHAR defined
jf @HEALTH_CHEAT_END
03E5: show_text_box 'HELP101'// 弹出提示:关于声望的帮助信息
// Respect can be earned be passing certain missions, killing rival gangs members, gaining territory and tagging.
08AF: set_actor $PLAYER_ACTOR max_health_to 255
0223: set_actor $PLAYER_ACTOR health_to 255
00A0: store_actor $PLAYER_ACTOR position_to 10@ 11@ 12@
12@ += 100.0
00A1: put_actor $PLAYER_ACTOR at 10@ 11@ 12@

:HEALTH_CHEAT_END
return

// 刷车
:CAR_CHEAT
0661: sat_cheat_text "BURN"
0256:   player $PLAYER_CHAR defined
jf @CAR_CHEAT_END
0247: load_model #INFERNUS
038B: load_requested_models

:CAR_CHEAT_LOAD
wait 0
0248:   model #INFERNUS available
jf @CAR_CHEAT_LOAD
00A0: store_actor $PLAYER_ACTOR position_to 10@ 11@ 12@
10@ += 5.0
00A5: 0@ = create_car #INFERNUS 10@ 11@ 12@
0249: release_model #INFERNUS

:CAR_CHEAT_END
return;

再次提醒:在代码里绝对不可以使用7@ 8@和9@!!

附,Seemann,读过你的代码之后我发现那个缺少的if是一个很给力的技巧,赞。
修改内容:向success标签中添加了一些内容,它们现在就像真的作弊一样,会弹出提示,并且在存盘的时候吐槽。。
修改2:你有没有想过修改程序,用写好的asm程序替换没有使用的opcode,那就不用使用现有的那几个了。如果这样改好的话,我们只需要传入另外一个opcode的指针指向所需的数据,操作内存可以简化为:
0D1E: write_memory 0x01234567 size 1 data 0@ protect 1
01DF: 0@ = read_memory $SOME_LABEL size 4 protect 0
(0D1E是我举的一个未知opcode的例子,不解释)
伪造一个合法的opcode调用格式应该不难,我一开始就想这么做的。另外,根据exe的实际情况作调整也不难,因为用到各种来回调用的地址都是我们熟知的。
-------------------------------
-------------------------------
引用 (ceedj @ Mar 5 2007, 21:11)
这帖和之前Y_Less讨论的编写自己的作弊有什么关系么?

没有什么关系,其实我在第一帖已经发了我写作弊的方法了。我只是举例说明了用内存地址检测按键的方法。我稍后会发这个。
自定义按键

引用 (Y_Less @ Mar 6 2007, 02:27)
你也可以考虑从文本条目和伪造GXT入手,不过我某天研究SA的伪造GXT时没有什么头绪,VC里很容易做到,SA没那么简单

额,那个我做过。我写过一个子过程,用于运行时把GXT条目修改为自己的文本。
我稍后会发那个。

引用 (Y_Less @ Mar 6 2007, 06:26)
你有没有想过修改程序,用写好的asm程序替换没有使用的opcode,那就不用使用现有的那几个了。

如果你的意思是通过任务脚本做到直接调用(就像本帖中讨论的),应该说,的确有办法来伪造opcode句柄,并且在范围内增加大量的伪opcode来完成我们想做的事情(0D1E不错,可惜它就不可以)。其他的opcode只能通过修改exe来改变,不能运行时修改。
不过,我们这样又能得到什么呢?这使我们迫使游戏在每次加载时都使用伪造的opcode加载句柄。那用Xieon的补丁不也能达到添加opcode的目的吗。
顺便说,我没有修改MemoryProof中的任何opcode,0052和02EC都是NOP,我用它们只是用来向内存加载参数(十六进制代码段读取数组OpcodeParameters,地址已知)
另外在用到MemoryProof时,若大小等于4,大小参数不是必须的。也就是说默认为双字。如果你要进行无VP访问,你同样没法指定大小
create_thread @MemoryProofRead address 100
它从地址100读取4个字节。
别忘了在004F后添加一条wait命令,使新线程立即创建。
-------------------------------
-------------------------------
你不能向访问线程传入字符串参数
create_thread @CLV thread_name 'vchng'
这句是不行滴。你可以传递字符串的索引,或者字符串储存位置的标签
:mystring
hex
"vchng" 00
end
-------------------------------
-------------------------------
这是获取最近的随机人物/车辆/物品的程序
原作者PLPynton
但那程序有bug,在我这从未好用过。我修正并改进了它
此程序接受4个参数进行搜索:搜索点的xyz坐标和搜索半径
如果搜索点附近有人/车/物品什么的,程序把它的句柄存储在9@
否则它返回-1。程序忽略玩家和他的车辆
这段程序是用传统的逐行句式写的,所以MB也可用(如果有人还用的话,呵呵)
只有标签和&需要转换(@->JJ &->$)
{
   物品截取程序
      
   by PLPynton and Seemann   

   参数:
   0@ - 坐标X
   1@ - 坐标Y
   2@ - 坐标Z
   3@ - 搜索半径

   占用变量4@..20@
   
   9@ = 结果 (若搜索失败返回-1)
         
}
:AIC_GETACTOR
0006: 16@ = 305868
0006: 17@ = @AIC_TESTACTORHANDLE
0006: 20@ = 1988
0002: jump @AIC_STARTSEARCH

:AIC_GETVEHICLE
0006: 16@ = 305869
0006: 17@ = @AIC_TESTVEHICLEHANDLE
0006: 18@ = -1
0006: 20@ = 2584
0256:   player $PLAYER_CHAR defined
004D: jump_if_false @AIC_STARTSEARCH
00DF:   actor $PLAYER_ACTOR driving
004D: jump_if_false @AIC_STARTSEARCH
03C0: 18@ = actor $PLAYER_ACTOR car
0002: jump @AIC_STARTSEARCH

:AIC_GETOBJECT
0006: 16@ = 305871
0006: 17@ = @AIC_TESTOBJECTHANDLE
0006: 20@ = 412

:AIC_STARTSEARCH
0085: 4@ = 16@ // (int)
008B: 4@ = &0(4@,1i) // (int)
0085: 19@ = 4@ // (int)
000E: 19@ -= 10787160
0016: 19@ /= 4
008B: 19@ = &0(19@,1i) // (int)
000E: 4@ -= 10787168
0016: 4@ /= 4
008B: 4@ = &0(4@,1i) // (int)
0006: 9@ = -1
0085: 11@ = 4@ // (int)
0006: 10@ = 0

:AIC_SEARCHLOOPMAIN
000A: 4@ += 20
0050: gosub @AIC_READ4B
0085: 4@ = 5@ // (int)
8039:   not4@ == 0
004D: jump_if_false @AIC_SEARCHLOOPNEXT
000A: 4@ += 48
0050: gosub @AIC_READ4B
0085: 12@ = 5@ // (int)
000A: 4@ += 4
0050: gosub @AIC_READ4B
0085: 13@ = 5@ // (int)
000A: 4@ += 4
0050: gosub @AIC_READ4B
0085: 14@ = 5@ // (int)
050A: 15@ = distance_between_XYZ 0@ 1@ 2@ and_XYZ 12@ 13@ 14@
0025:   3@ > 15@ // (float)
004D: jump_if_false @AIC_SEARCHLOOPNEXT
0085: 9@ = 10@ // (int)
0085: 4@ = 16@ // (int)
008B: 4@ = &0(4@,1i) // (int)
000A: 4@ += 4
0050: gosub @AIC_READ4B
0085: 4@ = 5@ // (int)
005A: 4@ += 9@ // (int)
0050: gosub @AIC_READ1B
0012: 9@ *= 256
005A: 9@ += 5@ // (int)
0002: jump 17@

:AIC_TESTVEHICLEHANDLE
056E:   is 9@ valid_vehicle_handle
004D: jump_if_false @AIC_NOHANDLE
00D6: if or
003B:   18@ == 9@ // (int)
0119:   car 9@ wrecked
004D: jump_if_false @AIC_NEWSEARCHRADIUS
0002: jump @AIC_NOHANDLE

:AIC_TESTACTORHANDLE
056D:   is 9@ valid_actor_handle
004D: jump_if_false @AIC_NOHANDLE
00D6: if or
003C:   $PLAYER_ACTOR == 9@ // (int)
0118:   actor 9@ dead
004D: jump_if_false @AIC_NEWSEARCHRADIUS
0002: jump @AIC_NOHANDLE

:AIC_TESTOBJECTHANDLE
//0001: wait 0
83CA:   not object 9@ exists
004D: jump_if_false @AIC_NEWSEARCHRADIUS

:AIC_NOHANDLE
0006: 9@ = -1
0002: jump @AIC_SEARCHLOOPNEXT

:AIC_NEWSEARCHRADIUS
{
一旦搜索成功就
调0051: return退出循环
不管是否最近
}

// 在这里写上额外的筛选条件

{
21@ 包含了排除对象
比如说前一次搜索的结果
}
//803B:   21@ <> 9@
//004D: jump_if_false @AIC_NOHANDLE

   
0085: 3@ = 15@

:AIC_SEARCHLOOPNEXT
005A: 11@ += 20@ // (int)
0085: 4@ = 11@ // (int)
000A: 10@ += 1
002D:   10@ >= 19@ // (int)
004D: jump_if_false @AIC_SEARCHLOOPMAIN
0051: return

// ---------------------------------
:AIC_READ1B
0085: 6@ = 4@ // (int)
0085: 8@ = 4@ // (int)
0016: 4@ /= 4
0012: 4@ *= 4
0062: 8@ -= 4@ // (int)
0012: 8@ *= 8
000E: 4@ -= 10787168
0016: 4@ /= 4
008B: 7@ = &0(4@,1i) // (int)
0085: 4@ = 6@ // (int)
0006: 5@ = 0
0006: 6@ = 0

:AIC_READ1B_LOOP
08B9:   test 7@ bit 8@
004D: jump_if_false @AIC_READ1B_NEXTBIT
08BF: set 5@ bit 6@

:AIC_READ1B_NEXTBIT
000A: 6@ += 1
000A: 8@ += 1
0039:   6@ == 8
004D: jump_if_false @AIC_READ1B_LOOP
0051: return

:AIC_READ4B
0085: 6@ = 4@ // (int)
000E: 4@ -= 10787168
0016: 4@ /= 4
008B: 5@ = &0(4@,1i) // (int)
0085: 4@ = 6@ // (int)
0051: return
------------------------
示例
while true
wait 100

00A0: store_actor $PLAYER_ACTOR position_to 0@ 1@ 2@
3@ = 15.0

gosub @AIC_GETVEHICLE
if
   9@ <> -1
then
    020B: explode_car 9@ // versionA
    // 0085: 21@ = 9@ // 在下次搜索中排除此对象,参考代码的筛选部分
end
end
-------------------------------
-------------------------------
-------------------------------------------------
实现作弊码的最简单方法
-------------------------------------------------
编写作弊(自定义按键)似乎是SA编程中的常见问题。人们都在问如何实现。尽管已有检测按键的方法(第一帖就有),整套方法还是很复杂的,需要内存结构知识。
先前我写过一段简化编写作弊的程序。你只需要把作弊码存入一个变量,调scm函数并检查结果(true/false)就行了。
首先,阅读这一帖理解以下如何向scm函数或线程传递字符串。
下面的程序检测自定义作弊码
:TestCheat
if
   0AA9:    is_game_version_original
then
   10@ = 0x969110 // keypresses buffer 1.0
   11@ = 0xA48960 // mission locals 1.0
else
   10@ = 0x96B790 // keypresses buffer 1.01
   11@ = 0xA4AFE0 // mission locals 1.01
end
// get 0@'s offset
0A9F: 4@ = current_thread_pointer
0A8E: 5@ = 4@ + 0xDC // mission Flag
0A8D: 5@ = read_memory 5@ size 1 virtual_protect 0
if
   5@ == 1
then
   0085: 4@ = 11@
else
   4@ += 0x3C
end
// get cheat string length
6@ = 0
while true
   0A8D: 5@ = read_memory 4@ size 1 virtual_protect 0
   if and
       5@ > 0
       6@ < 16
   then
       inc(4@)
       inc(6@)      
   else
       Break
   end
end
0085: 8@ = 10@
while 6@ > 0
   dec(4@)      
   dec(6@)
   0A8D: 5@ = read_memory 4@ size 1 virtual_protect 0 // last cheat char
   // lowercase to uppercase (a->A)
   if
       5@ > 90
   then
       5@ -= 32
   end
   0A8D: 7@ = read_memory 8@ size 1 virtual_protect 0 // last pressed key
   inc(8@)   
   if
       803B:   5@ <> 7@ // (int)
   then
       059A: return_false
       0AB2: ret 0
   end
end
0A8C: write_memory 10@ size 1 value 0 virtual_protect 0
0485: return_true
0AB2: ret 0
把这段代码原样复制到你的程序里,无论是任务还是脚本都无所谓
你也可以把代码存入外部文件,然后用$INCLUDE(或$I)指令把它包含到你的程序里。比如说,下载TestCheat.inc,把它复制到你编写的程序目录下,然后在程序开头写上
$I TestCheat.inc}
编译的时候,编译器会自动包含外部文件。
那么怎么用这段程序来实现自己的作弊码呢?超简单的。
先把作弊码(比如NOCOPS)存入变量
0@s = 'nocops'
然后调用scm函数TestCheat,把作弊码传入 原理参考上面发的链接
0AB1: call_scm_func @TestCheat 2 0@ 1@
如果函数返回true,表示玩家在游戏中输入了作弊码,所以,你需要写成循环
CLEO的例子
{$CLEO}
0000:
while true
   wait 250 ms
   0@s = 'nocops'
   if
       0AB1: call_scm_func @TestCheat 2 0@ 1@
   then
       0110: clear_player $PLAYER_CHAR wanted_level
   end
end

{$I TestCheat.inc}
当你输入nocops的时候,函数返回true,并且调用clear_wanted_level

如果你的作弊码长于7个字符,请使用长字符串
{$CLEO}
0000:
while true
   wait 250 ms
   22@v = "leavemealone"
   if
       0AB1: call_scm_func @TestCheat 4 22@ 23@ 24@ 25@
   then
       0110: clear_player $PLAYER_CHAR wanted_level
   end
end   

{$I TestCheat.inc}


1. 作弊码大小写无关。nocops,NoCoPs,nOCOPs效果都是一样的
2. 作弊码长度可以为1至16个字符。

Ycccccc 发表于 2012-4-24 21:52:41

现在可以抢了咩?~

为主的祥助 发表于 2012-4-24 22:15:33

哈 这个教程以前看过

Sid 发表于 2012-4-25 08:50:52

各種不懂@@
慢慢看吧…囧

霸气侧漏 发表于 2012-4-30 14:56:43

这、、让我情何以堪

BAMRZ 发表于 2012-5-11 20:25:53

到底怎么看明白那些代码的?

疯人愿~ 发表于 2012-5-15 17:21:55

囧死,完全看不明白
页: [1]
查看完整版本: [GTAF进口]SA内存控制