nasm - BasicWindow_64
文章目录
- nasm - BasicWindow_64
- 概述
- 笔记
- my_build.bat
- nasm_main.asm
- END
nasm - BasicWindow_64
概述
学习网上找到的demo.
x64和x86的汇编源码还差挺多的。
x64的汇编代码不好写,细节整不对,程序就不运行。
如果要查为啥不运行,也要看和正向生成的C工程的反汇编代码有哪些区别,才看的出来。
LEA, MOV很容易弄混。
MOV时,是否该加 地址数据类型的修饰(e.g. qword, dword), 这些也是细节。
感觉真是有需求用反汇编实现关键函数时,也是要以正向的代码生成的反汇编代码的基础上小步快跑的改,才不至于改了一坨后跑不起来。
笔记
my_build.bat
@echo off
cls
rem ----------------------------------------
rem my_build.bat
rem env
rem NASM version 2.16.03 compiled on Apr 17 2024
rem GoLink.Exe Version 1.0.4.6 Copyright Jeremy Gordon 2002-2025
set path=C:\Program Files\NASM;C:\soft\Golink;%path%
rem . bat默认是不支持中文的 .
rem echo full path name - %~f0
rem echo full path - %~dp0
rem echo file name - %~nx0
rem echo work path - %cd%
rem all param - %*
rem . rem注释的第1个字符和最后1个字符,不能是中文,否则有概率会当作命令来执行 .
rem . 调试.bat的方法 .
rem . 如果.bat写的不对,又不容易看出来,只能在每行后面加pause, 然后执行.bat, 然后按一下空格执行一行 .
set prj_src_name_1=nasm_main.asm
set prj_obj_name_1=nasm_main.obj
set prj_exe_name=BasicWindow_64.exe
rem win32 or win64
set prj_build_type=win64
rem /console or /SUBSYSTEM:WINDOWS
set prj_sub_sys=/SUBSYSTEM:WINDOWS
echo [%~nx0 %*]
if "%1" == "build" (
call :fn_build
) else if "%1" == "clear" (
call :fn_clear
) else (
call :fn_usage
)
goto end
rem ----------------------------------------
rem function
rem ----------------------------------------
:fn_usage
echo usage my_build.bat [option]
echo build - build asm to EXE
echo clear - clear trush on the project
exit /b
rem ----------------------------------------
:fn_build
echo build ...
rem find file on work path
call :fn_del_file "%prj_obj_name_1%"
nasm -f %prj_build_type% %prj_src_name_1% -o %prj_obj_name_1%
rem 用IDA打开.obj 已经可以看到实现逻辑了
call :fn_del_file "%prj_exe_name%"
rem 如果不指定要连接的dll, 会报错
golink /entry:fn_Start '%prj_sub_sys%' kernel32.dll user32.dll %prj_obj_name_1% /fo %prj_exe_name%
call :fn_exec "%prj_exe_name%"
exit /b
rem ----------------------------------------
:fn_clear
echo clear ...
call :fn_del_file "%prj_obj_name_1%"
call :fn_del_file "%prj_exe_name%"
exit /b
rem ----------------------------------------
:fn_del_file
if exist "%~1" (
echo del "%~1"
del "%~1"
)
exit /b
:fn_exec
if exist "%~1" (
echo exec "%~1"
%~1
)
exit /b
rem ----------------------------------------
:end
echo END
rem pause
call cmd
nasm_main.asm
; @file nasm_main.asm
; @brief 用NASM实现一个64bits的基本窗口
; nasm - BasicWindow_64
; ----------------------------------------
; 宏定义
; ----------------------------------------
; Basic Window, 64 bit. V1.01
COLOR_WINDOW EQU 5 ; Constants
CS_BYTEALIGNWINDOW EQU 2000h
CS_HREDRAW EQU 2
CS_VREDRAW EQU 1
CW_USEDEFAULT EQU 80000000h
IDC_ARROW EQU 7F00h
IDI_APPLICATION EQU 7F00h
IMAGE_CURSOR EQU 2
IMAGE_ICON EQU 1
LR_SHARED EQU 8000h
NULL EQU 0
SW_SHOWNORMAL EQU 1
WM_DESTROY EQU 2
WS_EX_COMPOSITED EQU 2000000h
WS_OVERLAPPEDWINDOW EQU 0CF0000h
WindowWidth EQU 640
WindowHeight EQU 480
; ----------------------------------------
; 导入函数声明
; ----------------------------------------
extern CreateWindowExA ; Import external symbols
extern DefWindowProcA ; Windows API functions, decorated
extern DispatchMessageA
extern ExitProcess
extern GetMessageA
extern GetModuleHandleA
extern IsDialogMessageA
extern LoadImageA
extern PostQuitMessage
extern RegisterClassExA
extern ShowWindow
extern TranslateMessage
extern UpdateWindow
; ----------------------------------------
; 区段定义
; ----------------------------------------
; 函数的入口名称是啥都可以, 只要link的时候指定主函数是啥
global fn_Start ; Export symbols. The entry point
section .data ; Initialized data segment
WindowName db "Basic Window 64", 0
ClassName db "Window", 0
section .bss ; Uninitialized data segment
hInstance resq 1
section .text ; Code segment
; ----------------------------------------
; 函数实现
; 如果对x64汇编不熟,可以先写一个x86版的NASM工程,正常用了,再改一个x64工程出来,比较简单,不用想事
; ----------------------------------------
fn_Start:
sub RSP, 8 ; // 16对齐
SUB RSP, 32
XOR RCX, RCX
call GetModuleHandleA
mov [hInstance], RAX
ADD RSP, 32
call fn_WinMain
; .LB_Exit:
XOR RCX, RCX
call ExitProcess
ret
; ----------------------------------------
fn_WinMain:
; 如果函数内有局部变量,需要自己开栈空间
; EBP - X 是局部变量
; EBP + X 是入参
push RBP ; Set up a stack frame
mov RBP, RSP
sub RSP, 136 + 8 ; 136 bytes for local variables. 136 is not
; a multiple of 16 (for Windows API functions),
; the + 8 takes care of this.
%define wc RBP - 136 ; WNDCLASSEX structure, 80 bytes
%define wc.cbSize RBP - 136 ; 4 bytes. Start on an 8 byte boundary
%define wc.style RBP - 132 ; 4 bytes
%define wc.lpfnWndProc RBP - 128 ; 8 bytes
%define wc.cbClsExtra RBP - 120 ; 4 bytes
%define wc.cbWndExtra RBP - 116 ; 4 bytes
%define wc.hInstance RBP - 112 ; 8 bytes
%define wc.hIcon RBP - 104 ; 8 bytes
%define wc.hCursor RBP - 96 ; 8 bytes
%define wc.hbrBackground RBP - 88 ; 8 bytes
%define wc.lpszMenuName RBP - 80 ; 8 bytes
%define wc.lpszClassName RBP - 72 ; 8 bytes
%define wc.hIconSm RBP - 64 ; 8 bytes. End on an 8 byte boundary
%define msg RBP - 56 ; MSG structure, 48 bytes
%define msg.hwnd RBP - 56 ; 8 bytes. Start on an 8 byte boundary
%define msg.message RBP - 48 ; 4 bytes
%define msg.Padding1 RBP - 44 ; 4 bytes. Natural alignment padding
%define msg.wParam RBP - 40 ; 8 bytes
%define msg.lParam RBP - 32 ; 8 bytes
%define msg.time RBP - 24 ; 4 bytes
%define msg.py.x RBP - 20 ; 4 bytes
%define msg.pt.y RBP - 16 ; 4 bytes
%define msg.Padding2 RBP - 12 ; 4 bytes. Structure length padding
%define hWnd RBP - 8 ; 8 bytes
; 在函数内 %define x, 当使用x时,只在函数内生效
mov dword [wc.cbSize], 80
mov dword [wc.style], CS_HREDRAW | CS_VREDRAW | CS_BYTEALIGNWINDOW
LEA RAX, [REL fn_WndProc]
mov qword [wc.lpfnWndProc], RAX
mov qword [wc.cbClsExtra], NULL
mov qword [wc.cbWndExtra], NULL
mov RAX, qword [REL hInstance]
mov qword [wc.hInstance], RAX
sub RSP, 32 + 16 ; // x64调用函数时,必须留shadow space, 这是x64函数调用约定,阴影区size = 32 = 4 x 8 = (size RCX + RDX + R8 + R9)
mov qword [RSP + 32 + 1 * 8], LR_SHARED ; // param6
mov qword [RSP + 32 + 0 * 8], NULL ; // param5
XOR R9, R9 ; // param4
mov R8, IMAGE_ICON ; // param3
mov RDX, IDI_APPLICATION ; // param2
XOR RCX, RCX ; // param1
call LoadImageA
mov [wc.hIcon], RAX
add RSP, 32 + 16; 调用WinApi时,是被调用者平衡堆栈(阴影区 + 入参)
sub RSP, 32 + 16
mov qword [RSP + 32 + 1 * 8], LR_SHARED
mov qword [RSP + 32 + 0 * 8], NULL
XOR R9, R9
MOV R8, IMAGE_CURSOR
MOV RDX, IDC_ARROW
XOR RCX, RCX
call LoadImageA
mov qword [wc.hCursor], RAX
add RSP, 32 + 16
mov qword [wc.hbrBackground], COLOR_WINDOW + 1
mov qword [wc.lpszMenuName], NULL
LEA RAX, [REL ClassName]
mov qword [wc.lpszClassName], RAX
; 小图标内装的也是大图标...
sub RSP, 32 + 16
mov qword [RSP + 5 * 8], LR_SHARED
mov qword [RSP + 4 * 8], NULL
mov R9, NULL
mov R8, IMAGE_ICON
mov RDX, IDI_APPLICATION
XOR ECX, ECX
call LoadImageA ; Small program icon
mov qword [wc.hIconSm], RAX
add RSP, 32 + 16
sub RSP, 32
lea RCX, [wc]
call RegisterClassExA
add RSP, 32
; 假设操作数为x
; [x] 代表x的地址
; x 代表x是一个立即数
; [REL x] 是相对RIP的地址,支持重定位, [x]是绝对地址,不支持重定位
; 所以要优先考虑使用 [REL x] 这样的用法
; mov [hWnd], RAX 和 mov qword [hWnd] 的区别
; mov [hWnd], RAX 依赖于编译器推断, 在复杂寻址或者代码优化后可能不靠谱
; 既然已经是汇编了, 还是最好是显势指定地址类型, e.g. mov qword [hWnd]
; push qword [hInstance] 强制将hInstance的地址作为qword类型,不安全, 如果hInstance的类型size < sizeof(qword), 风险就来了
; push [hInstance] 会自动扩展, 安全,优先使用这种来压栈
; 但是 NASM中,push一个地址中的内容时,必须指定数据类型的size
; 所以,如果一个数据地址的类型不是qwrod, 必须先载入寄存器(进行数据扩展,高端字节扩展为0),再将寄存器入栈
; 如果 hWnd 是定义在栈上变量的别名,MOV RCX, [REL hWnd] 就是错误的,因为REL 是相对于RIP的相对便宜。
; 应该用 MOV RCX, [hWnd]
; LEA 地址的操作, MOV是数据的操作
; e.g. lea RCX, [msg] 等价语句为 mov RCX, msg
; 不过最好操作地址还是用 lea, 因为可以用REL修饰改为产生相对地址。
; x64程序,参数的压入,最好使用手工调整RSP, 避免栈中数据不对齐和其他风险.
; 入参在影子区后面(影子区结尾 = RSP + 32)
; parma1 = RSP + 32 + 0 *8
; parma2 = RSP + 32 + 1 *8
; parma3 = RSP + 32 + 2 *8
; 准备调用 CreateWindowExA,入参12个, 4个在寄存器(RCX,RDX,R8,R9),其余8个需要入栈
sub RSP, 32 + (8 * 8)
mov qword [RSP + 32 + (7 * 8)], NULL
mov RAX, qword [REL hInstance]
mov qword [RSP + 32 + (6 * 8)], RAX
mov qword [RSP + 32 + (5 * 8)], NULL
mov qword [RSP + 32 + (4 * 8)], NULL
mov qword [RSP + 32 + (3 * 8)], WindowHeight
mov qword [RSP + 32 + (2 * 8)], WindowWidth
mov dword [RSP + 32 + (1 * 8)], CW_USEDEFAULT
mov dword [RSP + 32 + (0 * 8)], CW_USEDEFAULT
mov R9, WS_OVERLAPPEDWINDOW
LEA R8, [REL WindowName]
LEA RDX, [REL ClassName]
mov RCX, WS_EX_COMPOSITED
call CreateWindowExA
mov [hWnd], RAX
add RSP, 32 + (8 * 8) ; // 平衡栈指针, 阴影区 + 入参的8个参数(不算RCX, RDX, R8, R9)
SUB RSP, 32
MOV RDX, SW_SHOWNORMAL
MOV RCX, [hWnd]
call ShowWindow
ADD RSP, 32
SUB RSP, 32
mov RCX, [hWnd]
call UpdateWindow
ADD RSP, 32
.LB_MessageLoop:
SUB RSP, 32
XOR R9, R9
XOR R8, R8
XOR RDX, RDX
LEA RCX, [msg]
call GetMessageA
ADD RSP, 32
cmp EAX, 0
je .LB_Fn_End
SUB RSP, 32
LEA RDX, [msg]
MOV RCX, [hWnd]
call IsDialogMessageA
ADD RSP, 32
cmp EAX, 0
jne .LB_MessageLoop ; Skip TranslateMessage and DispatchMessage
SUB RSP, 32
LEA RCX, [msg]
call TranslateMessage
ADD RSP, 32
SUB RSP, 32
lea RCX, [msg]
call DispatchMessageA
ADD RSP, 32
jmp .LB_MessageLoop
; .LB_X 这种在函数内的标签的,作用域只在函数内
; 多个函数内,可以有多个同名的标签
; 所以函数内的标签一定要以.开头
.LB_Fn_End:
mov RSP, RBP ; Remove the stack frame
pop RBP
xor RAX, RAX
ret
; ----------------------------------------
fn_WndProc:
push RBP ; Set up a Stack frame
mov RBP, RSP
; 用RBP来标记入参比较好
; 入参开始栈地址为(RBP + 16)的原因, 因为进了函数栈顶是返回地址(+8), 然后又保存了RBP(+8)
; 此时RBP + 16 才是入参开始地址
%define hWnd RBP + 16 + (0 * 8) ; Location of the 4 passed parameters from
%define uMsg RBP + 16 + (1 * 8) ; the calling function
%define wParam RBP + 16 + (2 * 8) ; We can now access these parameters by name
%define lParam RBP + 16 + (3 * 8)
; 将前4个参数从寄存器写入阴影区, 然后就可以自由使用4个寄存器了,也可以从阴影区读取前4个入参了
MOV qword [hWnd], RCX
MOV qword [uMsg], RDX
MOV qword [wParam], R8
MOV qword [lParam], R9
cmp qword [uMsg], WM_DESTROY ; [EBP + 12]
je .LB_WMDESTROY
; .LB_DefaultMessage:
SUB RSP, 32
MOV R9, qword [lParam]
MOV R8, qword [wParam]
MOV RDX, qword [uMsg]
MOV RCX, qword [hWnd]
call DefWindowProcA
ADD RSP, 32
jmp .LB_fn_end
.LB_WMDESTROY:
SUB RSP, 32
XOR RCX, RCX
call PostQuitMessage
ADD RSP, 32
xor EAX, EAX
.LB_fn_end:
mov RSP, RBP
pop RBP
ret
; ----------------------------------------
; 如果先写好一个x86版本的实现,再从x86版改出一个x64版,挺麻烦的,容易搞错(x86和x64的汇编代码,其实差的还是挺多的,改还不如重写)
; 还是用VS2019写一个工程,编译成release + x64(关掉SDL,不优化),然后用IDA看,将需要的部分摘出来整理比较方便。
; NASM的代码实现和VS2019反汇编的代码基本一致,搬过来改,要方便的多。