希望bm吧 关注:636贴子:1,116
  • 5回复贴,共1

讨论自动存档引发的函数分析及其调用,以及几个汇编语句的使用

只看楼主收藏回复


这是服务器端给游戏人物存档的函数(入口 | call),代码不多,差不多2眼能扫完吧


IP属地:广东1楼2022-11-12 19:54回复
    要调用一个函数,得先分析它,弄明白它的全部参数
    一般我们直接跳到(看)函数返回处(函数尾), 看它 ret 后面是否有数值
    像这个函数是 ret 04 可以认定它有一个参数,是从堆栈传进来 ( push XX)
    这个数值04 代表 一个参数的存储长度
    推而广之
    ret 08 = 2 个参数 即 调用该函数时,必须有 2个 push XX
    ret 0C = 3 个参数 即 调用该函数时,必须有 3个 push XX
    ...
    如此,我们确定调用它的 push 个数(实参)(压堆栈)
    接着,我们回到函数头,查看寄存器的使用,以确定有没有通过寄存器的带进来数据的参数(隐形参数?)
    即,是否有寄存器带进来的数据,有给其它寄存器传数据(赋值)吗?
    像这个函数:
    push ebp
    ...
    mov esi,ecx
    这一段就是这函数的"开头"部分.
    这里要理解 push 的另一种作用,是存储寄存器的数据,并非是调用函数前的传参. 存储起来,是为了运行代码后,还原寄存器的数据,这也是调用函数时有使用寄存器,但调用结束后,部分寄存器数据不会变动的原因.
    所以, push ebp 这条语句的结论是 ebp 没有赋值操作, ebp 暂时不能算是参数; 后面 push eax, push esi 的作用类似.
    mov ebp,esp 这个是固定使用形式, esp 永是堆栈顶部的地址,肯定不是参数;这里是记录栈顶,方面后续寄存器还原与使用.
    mov eax,fs:[00000000] 是读取新数据到eax里, 没考虑eax原来的数据, eax也不是参数
    xor eax,ebp 这个也是固定的,由前面mov ebp,esp, ebp 已经是进入函数后的栈顶地址,它当然不是参数
    mov esi,ecx ecx给esi赋值,而前面没有1条是给ecx赋值的语句,那么ecx的数据从哪里来? 当然是调用函数前带进来的,所以ecx是参数.


    IP属地:广东2楼2022-11-12 20:37
    回复
      2025-06-12 09:12:53
      广告
      好了,存档函数分析结束.
      至于参数的数据及代表的意义,我们要通过下断追踪代码的执行过程,才知道2个参数都是(某个)人物的地址.
      那么实现存档的代码就是:
      mov ecx, 某个人物地址
      push ecx
      call GameSvr.exe+99C10
      保证参数正确(ecx为有效人物地址)时,我们就一定能顺利调用它,实现给某角色存档了
      这是一个简单且常规的函数分析及其调用.下面讲讲不常规、复杂、要使用多参数的函数的调用吧。
      回过头,再讲 ret 04 这语句对堆栈(栈顶地址 esp)的影响, 数值 04 改变 esp 的数值,它将增加 4个长度(理解为 esp = esp+4吧)
      这里可以理解为有平衡堆栈的作用,因为 push 语句正好会让 esp减4
      以实例来说明,大家也可以在游戏中下断追踪存档的上一层代码来验证:
      // 假设 此时 esp = 20
      mov ecx, 某个人物地址 //esp = 20
      push ecx // esp = 20
      call GameSvr.exe+99C10 // esp = 16 注意,执行完 push ecx, esp才改变
      // 调用函数结束 esp = 20 还原了, 这里就是函数尾 ret 04 直接作用的结果,esp +4, 16 +4 = 20
      那么推而广之
      ret 08 堆栈平衡 8 个长度(2个参数) +8
      ret 0C 堆栈平衡 C 个长度(3个参数) +C (12)
      ...
      调用函数后,一定要记得平衡堆栈,不然要出问题的。 上面的调用是自动平衡,有要“手动”平衡的吗?当然有
      可以这样调用存档函数:
      mov ecx, 某个人物地址 //esp = 20
      push ecx //esp = 20
      push ecx //esp = 16
      call GameSvr.exe+99C10 //esp = 12
      // esp = 16 与最早的 20 不等
      add esp,04 // esp = 20 这就是“手动”平衡,如此脱裤子放放屁,才能避免出问题
      所以,你在游戏代码里,偶尔看到 调用某函数后,有 add esp,XX 都是在平衡堆栈。
      为什么游戏原代码也有“脱裤子放屁”的操作呢? 关键在于所调用的函数有 push 参数,但它返回时没有平衡,即直接ret ; 等于是 ret 00 堆栈无平衡, +0 , 所以调用它结束后,根据前面有n个push ,要用 add esp n*4 来平衡了。
      这就是调用一个函数的2大秘诀:
      1, 保障所有参数的正确性
      2, 调用结束后,平衡好堆栈


      IP属地:广东3楼2022-11-12 21:24
      回复
        再讲一讲函数里,如何使用(读取)参数的数据吧。
        用寄存器直传的参数,有给别的寄存器赋值的,比如上面分析的 mov esi,ecx;还有的是存储于地址里(堆栈也是地址,可以理解为一长串连续排列的地址),如 mov [ebp-4],ecx 等等,这个容易理解。
        那么如何读取堆栈里的参数?下面的代码都可能是,又要具体分析,这也是难搞,难理解的地方。
        mov eax,[ebp+8]
        mov ecx,[ebp+10]
        mov edx,[esp+1C]
        如果调用函数时,有2个实参(2个push),进入函数后,有这些代码的 push ebp ; mov ebp,esp
        那么 mov eax,[ebp+8] 是读取第1个参数; mov eax,[ebp+C] 是读取第2个参数;
        没有 push ebp, mov ebp,esp 的呢?如果这之前,没有操作堆栈的语句(push XX,; add esp,XX ;sub esp,XX;等等改变esp的数值的语句),那么 mov eax,[esp+4] 是读取第1个参数; mov eax,[esp+8] 是读取第2个参数
        建议仍然是下断追踪游戏代码的执行过程来理解吧,注意这里的数值是十六进制了,不然与代码的数值难对应。
        还要注意,参数的顺序与阅读的秩序相反,即第1个push是倒数第1个参数,倒数第1个push是第1个参数。
        push ecx //esp = 20
        push eax //esp = 1C 即参数2存储在 20 这个地址中(位置)
        call XXX //esp = 18 即参数1存储在 1C 这个地址中(位置)
        // 下面是 函数 XXX 代码
        push ebp // esp = 18
        mov ebp,esp // esp = 14 ebp = 14
        ...
        mov eax,[ebp+8] // ebp = 14 14+8 = 1C , 即存储第1个参数的地址
        mov eax,[ebp+C] // ebp = 14 14+C = 20 , 即存储第2个参数的地址
        另一种情况:
        push ecx //esp = 20
        push eax //esp = 1C 即参数2存储在 20 这个地址中(位置)
        call ZZZ //esp = 18 即参数1存储在 1C 这个地址中(位置)
        // 下面是 函数 ZZZ 代码
        ... // esp = 18 无任何改变
        mov eax,[esp+4] // esp = 18 18+4 = 1C , 即存储第1个参数的地址
        mov eax,[esp+8] // esp = 18 18+8 = 20 , 即存储第2个参数的地址
        头大了吧。那么忘掉这楼层前面的内容吧
        记住,函数开头有 pus ebp ; mov ebp,esp; 那么[ebp+8]就是读取第1个参数吧,
        简记 +8 为参数头 ,然后 +C +10 。。。。
        复杂函数,我们直接下断以追踪结果来判断吧,不要记那么多。


        IP属地:广东4楼2022-11-12 22:11
        回复
          最后总结汇编语句的使用:
          push 存储寄存器的数据或给函数传参数
          ret 函数返回,带数字时,有平衡堆栈功能,并可确定函数参数的个数
          mov 数据传输,从一个寄存器传到寄存器里,mov esi,ecx
          或从地址传到寄存器(读取)mov eax,[ebp+4], 方括号在后面,必定是读取
          或从寄存器传到地址里(保存)mov [ebp-4],ecx 方括号在前面,必定是保存
          add esp,04 esp +4 还原堆栈 add 加法
          sub esp,100 esp -100 拉长堆栈,以存储临时数据 sub 减法
          。。。
          这帖子到此结束


          IP属地:广东5楼2022-11-12 22:20
          回复
            太高深了,虽然看不懂,还是为大佬点赞~


            IP属地:湖南6楼2024-03-18 10:58
            回复