longs75 发表于 2021-10-14 09:08

32位、64位汇编语言混合编程终极大法

本帖最后由 longs75 于 2021-10-14 15:32 编辑

论坛中有两个贴子专门讲述了32位、64位汇编语言混合编程的实现方法。
首先感谢:“SysWOW64的奇技淫巧”的作者
https://www.52pojie.cn/thread-1232264-1-1.html
给出了从32位模式进入64位模式、以及从64位模式返回32位模式的实现方法。

其次感谢:“32位内联64位代码”的作者
https://www.52pojie.cn/thread-1271576-1-1.html
尝试在32位汇编程序中规范化使用64位汇编指令的设想。

但是,如何让编程者摆脱在32位、64位两种模式中进行切换的细节,在程序的任何地方可以随意切换两种编程模式,类似下面这样:
……
(32位代码)
Begin64位指令
(64位代码)
End64位指令
(32位代码)
……
这样就使得32位、64位汇编语言混合编程这种高级技术白菜化了。

我这篇文章就是要解决这个问题。

在上面两个贴子的实现方法中,存在一个问题,就是在通过 jmp far切换指令模式时,都使用了绝对地址:
“SysWOW64的奇技淫巧”中使用了“jmp far 0x33:(绝对地址)”直接远跳转方式进入64位模式,使用
push offset sys32_start         ;绝对地址入栈
mov dword ptr,023h
jmp fword ptr
间接远跳转方式从64位模式返回32位模式。

“32位内联64位代码”贴子中,进入和退出64位模式都使用的是间接远跳转方式,同样都使用了绝对地址。

如果代码使用了绝对地址,代码就不可重用,因为每次使用时绝对地址不同,代码也会不一样。所以要对进入和退出64位模式这两个代码段进行改造,不使用绝对地址,解决代码重用问题。

改造如下:
一、进入64位模式代码:
00401000 | 60                         | pushad                                  | 保护寄存器
00401001 | 6A 33                      | push 33                                       | 立即数0x00000033入栈
00401003 | E8 00000000                | call a32to64_2.401008                         | call $0,32位EIP入栈
00401008 | 830424 07                  | add dword ptr ss:,7                      | 计算64位代码入口地址
0040100C | FF2C24                     | jmp far fword ptr ss:                  |间接远跳转
0040100F | 90                         | nop                                           |这里开始是64位模式了

第二行至第五行是关键代码。
第三行称为“E8 00 00 00 00”大法,作用是把下一条指令的地址(EIP的值)入栈,并转入下一条指令继续运行。这个地址是在程序运行时动态获得的,是个相对地址,
第四行直接修改这个值为64位代码入口
第五行远跳转到64位代码入口,也就是第六行代码处。
上面这四行指令,使用了相对寻址,不依赖具体的内存地址,所以指令代码是固定的,放在任意内存地址都可以正确执行,不需要修改代码。

同样方式对退出64位代码进行改造:
二、退出64位模式代码:
000000013FAC117C | E8 00000000            | call empty64.13FAC1181                  | call $0,64位RIP入栈
000000013FAC1181 | 830424 0E                | add dword ptr ss:,E                |修改为返回32位模式的地址
000000013FAC1185 | 66:C74424 04 2300      | mov word ptr ss:,23            |只需要低16位置0x0023,高16位不用
000000013FAC118C | FF2C24                   | jmp far fword ptr ss:            |间接远跳转
FAC118F | 83C4 10                  | add esp,10                              |,平衡堆栈,这里开始已经是32位模式了
FAC1190 | 61                         | popad                                 | 恢复寄存器

上面两个代码段需要说明一点:在32位模式下,“E8 00 00 00 00”将32位的EIP入栈,在64位模式下,“E8 00 00 00 00”将64位的RIP入栈,因此堆栈中的地址长度是不同的。
Jmp far 远跳转需要一个fword (三字)数据,即一个32位的地址和一个16位cs段寄存器的值。

下面来看一个具体应用的例子:
.386
.model flat, stdcall
option casemap :none

include user32.inc
include kernel32.inc

includelib user32.lib
includelib kernel32.lib

Begin64 TEXTEQU <DD 0E8336A60h, 00000000h, 07240483h, 90242CFFh>                  ;宏定义,进入64位模式的代码
End64 TEXTEQU <DD 000000E8h, 24048300h, 44C7660Eh, 00230424h, 83242CFFh, 906110C4h> ;宏定义,结束64位模式的代码
.data
      strTitle      db '问题',0
      strQustion db '我在干什么?',0
      db 100 dup(0)
      
.CODE
START:
      
      lea esi,strTitle
      lea edi,strQustion
      ;
      Begin64   ;进入64位指令模式
      ;
   DD 51415041h, 53415241h, 55415441h, 0B8415641h, 0B8B0F0B4h, 06894467h, 0D2CEB949h, 0A7D1DAD4h
   DD 0BA49B0CFh, 0BBCE3233h, 3436EBD3h, 0BBCEBB49h, 0EEC1B8D6h, 0BC49A3C4h, 0D0C7BDCAh, 0ACA3BBBBh
   DD 0D2CEBD49h, 0A6B9C9B3h, 0BE49CBC1h, 0A1A3A1A3h, 0A1A3A1A3h, 0F894C67h, 57894C67h, 894C6708h
   DD 4C67105Fh, 67186789h, 206F894Ch, 77894C67h, 415E4128h, 415C415Dh, 415A415Bh, 90584159h
      ;
      
      ;    地址                操作码                        汇编语言                           注释
      ;000000013FAC10F2 | 41:50                  | push r8                                 |
      ;000000013FAC10F4 | 41:51                  | push r9                                 |所有用到的寄存器必须保护
      ;000000013FAC10F6 | 41:52                  | push r10                              |
      ;000000013FAC10F8 | 41:53                  | push r11                              |否则调用API函数就会异常
      ;000000013FAC10FA | 41:54                  | push r12                              |
      ;000000013FAC10FC | 41:55                  | push r13                              |因为Windows不知道你会弄这一手
      ;000000013FAC10FE | 41:56                  | push r14                              |
      ;000000013FAC1100 | 41:B8 B4F0B0B8         | mov r8d,B8B0F0B4                        |几乎每个新增的64位寄存器都用到了
      ;000000013FAC1106 | 6744:8906                | mov dword ptr ds:,r8d            |
      ;000000013FAC110A | 49:B9 CED2D4DAD1A7CFB0   | mov r9,B0CFA7D1DAD4D2CE               |就是为了试验一下是不是好用
      ;000000013FAC1114 | 49:BA 3332CEBBD3EB3634   | mov r10,3436EBD3BBCE3233                |
      ;000000013FAC111E | 49:BB CEBBD6B8C1EEC4A3   | mov r11,A3C4EEC1B8D6BBCE                |为什么寄存器要赋这些数?
      ;000000013FAC1128 | 49:BC CABDC7D0BBBBA3AC   | mov r12,ACA3BBBBD0C7BDCA                |
      ;000000013FAC1132 | 49:BD CED2B3C9B9A6C1CB   | mov r13,CBC1A6B9C9B3D2CE                |运行一下你就知道了
      ;000000013FAC113C | 49:BE A3A1A3A1A3A1A3A1   | mov r14,A1A3A1A3A1A3A1A3                |
      ;000000013FAC1146 | 674C:890F                | mov qword ptr ds:,r9               |
      ;000000013FAC114A | 674C:8957 08             | mov qword ptr ds:,r10            |
      ;000000013FAC114F | 674C:895F 10             | mov qword ptr ds:,r11         |
      ;000000013FAC1154 | 674C:8967 18             | mov qword ptr ds:,r12         |
      ;000000013FAC1159 | 674C:896F 20             | mov qword ptr ds:,r13         |
      ;000000013FAC115E | 674C:8977 28             | mov qword ptr ds:,r14         |
      ;000000013FAC1163 | 41:5E                  | pop r14                                 |
      ;000000013FAC1165 | 41:5D                  | pop r13                                 |
      ;000000013FAC1167 | 41:5C                  | pop r12                                 |
      ;000000013FAC1169 | 41:5B                  | pop r11                                 |
      ;000000013FAC116B | 41:5A                  | pop r10                                 |
      ;000000013FAC116D | 41:59                  | pop r9                                  |
      ;000000013FAC116F | 41:58                  | pop r8                                  |
      ;000000013FAC1171 | 90                     | nop                                     |
      
      End64   ;结束64位模式,返回32位模式
      ;
      invoke MessageBox,0,edi,esi,0
      invoke ExitProcess,0
      
end START

总结一下:
在程序开头定义两个宏,分别是Begin64和End64,然后在程序中任意地方都可以使用这两个宏进行64位编程,非常方便。
模板如下:
Begin64 TEXTEQU <DD 0E8336A60h, 00000000h, 07240483h, 90242CFFh>                  ;宏定义,进入64位模式的代码
End64 TEXTEQU <DD 000000E8h, 24048300h, 44C7660Eh, 00230424h, 83242CFFh, 906110C4h> ;宏定义,结束64位模式的代码

Begin64
;
;(此处为64位代码)
;
End64


==========================
补充说明:
有同学问:既然是从32位模式进入64位模式,我能不能在32位Windows中使用,比如在XP中使用?
答:不能。32位Windows系统,CPU的运行模式是保护模式,只支持32位指令集;64位Windows系统,CPU的运行模式是IA-32e模式,IA-32e模式有两个子模式:64位模式和兼容模式,我们这个切换方法,实际上只是在IA-32e模式的64位模式和兼容模式这两个子模式中进行切换,跟保护模式完全没有关系,所以不能在32位Windows系统中使用。

汇编指令学习建议:
以前学习16位汇编语言时,有个Emu8086软件非常好用,可以一边运行指令代码,一边观察寄存器、标志位的值,对理解汇编指令的含义很有帮助。但这个软件只支持16位汇编指令,有没有一个类似的软件,帮助我们学习32位指令和64位指令呢?
答案是:有!用跟踪调试软件x64dbg完全可以实现Emu8086的学习功能。

x64dbg有两个主模块,分别是:x64dbg和x32dbg,它判断EXE文件是64位时,自动调用x64dbg,在64位模式下调试程序,判断EXE文件是32位时,自动调用x32dbg,在兼容模式下调试程序。根据这个特点,我们编写一个空文件(以汇编语言格式为例):
.data
db 4096 dup(0)   ;定义4kb数据段
.code
START:
db 4095 dup(90h);定义4kb代码段,用NOP指令填充,最后ret操作系统。
ret
end START

将这个空文件分别编译成64位模式和32位模式,用x64dbg加载,就可以练习64位和32位指令了。








longs75 发表于 2021-10-15 09:13

帖子反复修改了好几处小地方,每次都审核,我都不好意思一直麻烦版主了,还有一点儿补充说明放这里:

进入和离开64位模式时,代码使用了PUSHAD和POPAD,这是为了确保程序稳定性,但也会造成一个小问题,就是64位代码不能用寄存器返回任何结果给32位代码,只能通过内存地址返回结果,因为从任何寄存器返回的结果都被POPAD掉了。实际应用中如果需要用寄存器返回结果,可以把进入64位模式代码中的PUSHAD和离开64位模式代码中的POPAD用NOP取代,这三个指令都是单字节,所以修改也非常容易。修改如下:

Begin64 TEXTEQU <DD 0E8336A90h, 00000000h, 07240483h, 90242CFFh>                  ;宏定义,进入64位模式的代码
End64 TEXTEQU <DD 000000E8h, 24048300h, 44C7660Eh, 00230424h, 83242CFFh, 909010C4h> ;宏定义,结束64位模式的代码

;进入64位模式前不再保护寄存器,64位代码可以用寄存器返回结果给32位代码

Begin64
;
;(此处为64位代码)
;
End64

longs75 发表于 2021-10-16 21:13

何故 发表于 2021-10-14 11:02
看不懂咋办

没什么难度,就是理解jmp far指令的用法,还有 E8 00 00 00 00的技巧。:Dweeqw

yuleniwo 发表于 2021-10-29 09:14

居然还有这种操作,学习了。

tlf 发表于 2021-10-29 14:10

hiodis 发表于 2022-1-3 06:20

通俗易懂,点赞
页: [1]
查看完整版本: 32位、64位汇编语言混合编程终极大法