大致讲一下VS中32位的x86汇编基本写法,我觉得这是跟C代码先讲int main()一样重要的事,但似乎并没有人提及汇编的基本格式,汇编教材执行代码纯命令行全责。
简单的汇编代码如下,功能从王爽老师《汇编语言》第四版第二单元的代码中截取。
.386
.model flat, stdcall
.code
main proc
mov ax, 4e20h
add ax, 1416h
ret
main endp
end main
参考文心一言的回答,我逐行解释代码含义。其他汇编代码的格式或许可以参考。
.386指示汇编器(Assembler)目标处理器是Intel 80386或更高版本的处理器,可以使用386及后续处理器特有的指令和功能;允许使用386及更高版本处理器支持的指令集,包括32位操作、浮点运算指令等;保护模式可以被使用,这允许更复杂的内存管理和访问控制。在VS中,.386并非固定格式,默认可以不写。
.model flat, stdcall指示汇编器如何处理内存模型和函数调用约定的代码。
flat 模型指的是一种内存模型,在这种模型中,所有的段(代码段、数据段等)都被视为在一个单一的、平坦的地址空间中。这意味着程序员不需要担心段之间的边界或偏移,而是可以直接使用32位(或64位,取决于处理器架构)地址来访问内存中的任何位置。在32位和64位编程中,平坦模型是默认的,也是最常见的内存模型。它简化了内存访问,并允许更直观和灵活地使用内存。不过在VS中,flat是必须的,不指定flat的代码无法成功生成exe。
stdcall 是一种函数调用约定,它定义了函数如何接收参数、谁负责清理堆栈以及函数名如何被修饰(或“装饰”)以便在链接时区分不同的函数。稍微深入一点说,函数调用约定决定了参数接收方式,例如32位代码通常直接使用栈传递全部参数,64位则通常使用4个寄存器传递参数,若寄存器不够才使用栈。清理堆栈的主体从调用函数或被调用函数中选择,从执行流程说,函数调用约定决定堆栈清理前还是后将执行权移交回调用函数。stdcall在VS中无须指定。
随后可以声明C函数,在后续代码中使用。这听起来或许有些奇怪,由于Windows系统用C/C++实现大部分功能,包括硬件调用的API相关接口,因此在Windows中,调用C接口实现功能是较为实用方便的做法。
.code标识代码段起始,该名称固定,我尝试改为.text,VS当即报错,根据文心一言所说,不同平台、汇编器对代码段名称有不同规定。
以下代码是汇编中“函数”的标准格式。proc意为函数,结束时需要以endp进行标识,函数中以ret或其他代码交还执行权。实际上main proc …… main endp可以直接使用main:代替,main:只是纯粹的标识符,具体效果视代码而定。我认为有必要对main函数表示最基本的尊重,好歹强调下它“主函数”的牌面。另外,proc不可嵌套proc,但:作为纯标识符,可随意嵌套。
main proc
……
ret
main endp
end main表示汇编代码书写完毕,以main为入口函数。end即为“结束”,后接标识符意为以标识符为入口函数。如果没有指定入口,文件默认入口main,不过这个默认我没有尝试过,为了程序安全我都固定写end main。
《汇编语言》要求观察寄存器的变化情况,调试情况如下。
接下来用ida freeware分析生成的exe。一个函数main(),代码3行,ret被解析为了retn。根据文心一言,NASM中retn与ret可等价。
查看段信息,只有.text .rdata两个段,比C代码生成的exe的段数少。
peview则认为有3个段,还包括.rsrc。个人更支持peview的分段方式,因为这样文件头 0x400+.text 0x200+.rdata 0x200+.rsrc 0x200与文件0xA00的大小正好吻合,大概是rsrc段没有需要载入的内容,所以ida忽视了它。
进入rdata段,查看相关内容,发现是version信息(大概),甚至可以看到代码的生成路径,有种底裤被ida扒掉的感觉……