目前windows平台上还在使用汇编语言开发的场景已经非常罕见,所以这里仅仅给出汇编语言的一些基础性描述,以及如何使用汇编语言进行开发。
汇编语言基础-寄存器、地址、指令
在汇编语言的中,由于是直接操作硬件,所以对应的就是寄存器、内存地址这两个重要的概念,理解了它们之后,再看许多事情就非常简单了。
寄存器适用于将指令和地址送入计算器,并读取计算器计算结果的,这个概念在后来被引申到设备驱动开发中的一段内存地址上,作为向CPU传送指令和地址的重要工具,我们以下面的图来快速理解一下:
在整个系统中,常用的寄存器如下:
4个数据寄存器(EAX、EBX、ECX和EDX)
2个变址和指针寄存器(ESI和EDI)
2个指针寄存器(ESP和EBP)
6个段寄存器(ES、CS、SS、DS、FS和GS)
1个指令指针寄存器(EIP)
1个标志寄存器(EFlags)
4个32位的控制寄存器(CR0,CR1,CR2和CR3)
8个调试寄存器调试寄存器(DR0~DR7)
4个系统地址寄存器(GDTR/IDTR/LDTR/TR)
2个测试寄存器(TR6和TR7)
如果是64位的cpu,那么寄存器的数量就更多了,不过理解那些寄存器其实对于开发没什么用处。
至于内存地址,那么更加简单,内存地址指的就是硬件地址,虽然有很多种寻址方式,但是总结下来就是这么几类:
直接从固定地址/变量寻址: 一般在操作系统初始化的时候,经常这么做,此时无论是立即数(0x0000)或者某个地址,都可以视为直接从固定位置获取地址;
寄存器寻址: 将地址保存到寄存器中,然后到寄存器中获取地址,这一点在函数调用中非常常见,函数调用前会将返回地址压入堆栈,然后当函数执行完成后,从栈中取出地址寻址即可返回原先的代码流程。
指令则是具体操作的具象化,这一点非常简单直接,毕竟汇编就是给人看的。一般分为寄存器指令、算术指令、控制指令等等。每种架构的CPU对应的指令集是不一样的,这个要查指令手册才知道,例如对于一些特殊的寄存器,是需要特殊的指令来存取的。
汇编比C语言快
大部分学习过C语言的都曾经在教材上看到过一句话,就是 C语言仅仅比汇编慢10% ;同时在一些文献上也能看到,现代编译器,已经能将指令优化到和汇编语言一样的速度了。不过事实上的真相并不是这两者说的那样绝对和片面。
首先汇编语言已经是机器指令了,在同样的一个a+b=c的计算中,二者不应该有区别,也不会有区别,但是问题在于,在C层面和汇编层面看到的并不是一回事:
这个反汇编非常容易理解: 2C、30、34分别是c、b、a的地址,xor eax, eax是将寄存器清零,add则是加法运算,mov是赋值。
上面这一段汇编语言中,我们可以看出,如果将三次xor eax, eax合并为1次,那么指令数会减少两条,毕竟对于开发者来说,我们清楚的知道在赋值过程中,eax寄存器的值不会发生变化。
从这一点我们可以窥见到上面两个问题的第一部分答案:
汇编开发工程师会更加接近底层,故可以将一些不必要的冗余指令优化掉,这部分可以带来速度的提升;但是同时C编译器的开发人员非常肯定,这些指令的优化和提升,不会超过10%,毕竟对于x86这样的CISC指令集来说,xor eax, eax这样的语句执行速度肯定非常快,所以这个层面上的优化存在但是不多。
这一点还指出了另外几个更加严重的问题(汇编的缺陷):
1. 我们可能需要十分钟就能教会一个人完成四则运算,但是一个人要使用汇编语言完成四则运算,至少需要先看明白寄存器、地址、指令这些部分,能在两个小时之内教完汇编的4则运算已经很不错。二者的学习难度差不多是10倍,并且这个难度随着内容的增加额外上升;
2. 当我们将CPU升级/更换的时候,上面的汇编语言很可能无法编译和运行,这意味着硬件的改动会带开发的巨大改动;
正确的使用汇编
所以汇编语言一般在系统底层开发中使用,原因有下面几个:
- 在系统的初始化阶段,由于此时需要初始化CPU来初始化系统,所以汇编语言肯定时需要使用的;
- 在一些特殊的场景下,一些技术不应该开发给开发人员,否则会给系统带来灾难,比较典型的例子就是地址重定向,如果这个技术放到开发语言中,这无疑是一场灾难;
- 在一些反复调用的函数中,对性能有巨大的提升,在这种情况下,哪怕几条指令也有着巨大的效率,这种情况下也会使用;
系统初始化和windows 开发人员关系不大,所以主要面对的是剩下两个需求,但第二个需求看起来就非常危险,所以一般最容易遇到的就是最后一类需求。
实际使用过的是内联汇编,利用内联汇编程序,可以直接在 C 和 C++ 源程序中嵌入汇编语言指令,而无需执行额外的汇编和链接步骤。 内联汇编程序生成到该编译器中,因此不需要一个单独的汇编程序,例如 Microsoft Macro Assembler (MASM)。
由于内联汇编程序不需要单独的程序集和链接步骤,因此它比单独的汇编程序更方便。 内联汇编代码可以使用范围中的任何 C 或 C++ 变量或函数名称,因此,将其与程序的 C 和 C++ 代码结合非常容易。 由于汇编代码可以与 C 和 C++ 语句结合,因此它可以完成单独用 C 或 C++ 难以完成或无法完成的任务。
__asm 关键字将调用内联汇编程序并且可在 C 或 C++ 语句合法的任何位置出现。 它不能单独出现。 它必须后跟一个程序集指令、一组括在大括号中的指令或者至少一对空大括号。 此处的术语“__asm 块”指任何指令或指令组(无论是否在大括号中)。
以下代码是括在大括号中的简单 __asm 块。 (此代码是一个自定义函数 prolog 序列。)
// asm_overview.cpp
// processor: x86
void __declspec(naked) main()
{
// Naked functions must provide their own prolog...
__asm {
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE
}
// ... and epilog
__asm {
pop ebp
ret
}
}
或者,还可以将 __asm 放在每个程序集指令前面:
__asm push ebp
__asm mov ebp, esp
__asm sub esp, __LOCAL_SIZE
// 注意,也可以写在一行内
__asm push ebp __asm mov ebp, esp __asm sub esp, __LOCAL_SIZE
注意:
具有内联汇编代码的程序不能完全移植到其他硬件平台。 如果要针对可移植性进行设计,请避免使用内联汇编程序;
ARM 和 x64 处理器不支持内联汇编程序;
实际上第二点非常麻烦,这导致内联汇编技术非常冷门,但是这个限制是可以绕过的,早期有人提出了单独编写汇编文件,然后将它单独编译为64位的OBJ文件参与项目最后的链接部分,后来微软索性开放了这个技术并为之提供一定的支持,这个技术在未来的文档中会单独介绍。