作者purpose (purpose)
看板ASM
标题[心得] 个人的 x86 组合语言观念笔记
时间Wed Oct 13 17:02:20 2010
※ [本文转录自 C_and_CPP 看板 #1Cis6A7c ]
作者: purpose (purpose) 看板: C_and_CPP
标题: Re: [问题] C/C++ 中的 asm 该如何学起?
时间: Tue Oct 12 03:12:07 2010
发篇笔记
一、[简介] 机器语言与80x86
二、[观念] 组合语言—Intel Style 与 AT&T Style、MASM 与 NASM
三、[教学] 简单连结范例—NASM 组合语言 与 C/C++ (Windows 平台)
-------------------------------------------------------------
一、[简介] 机器语言与80x86
大家家里用的计算机器叫做个人电脑 (PC)。
可以拿来安装 Windows、Linux,甚至 Mac OS X...等作业系统。
个人电脑的 CPU 演变历史,可以说就是 Intel 的历史。从最早的16位元CPU:
「8088/8086 -> 80286」,再演化到32位元的 80386、80486...
後来因为商标不能用数字注册,Intel 不使用 80586 命名,从586开始,
改名为历史上的 Pentium CPU。
AMD 也差不多是在 Pentium 时代开始慢慢成为 Intel 在个人电脑处理器上的
竞争者。
可以想见 Intel 就是个人电脑处理器的「唯一制定者」,Intel自己做新的 CPU
也要向後相容以前的东西,就像 Windows 7 也得要能执行 Windows XP 的程式一般。
你在 286 写的程式,拿去给 486 的 CPU 也要能跑。
所以「个人电脑 CPU = x86 家族」...好啦,可能有人不认同这句话。
你跟美国人讲话就要讲英文、跟法国人讲法文;
跟 x86 家族的处理器讲话,就要讲「x86 机器语言」;
跟 Intel 8051 单晶片处理器沟通,就讲「8051 机器语言」;
在算盘本里面介绍的处理器是「MIPS」家族,就用「MIPS 机器语言」跟其沟通。
所有处理器里面,x86家族功能当然是最强,每一代都有增加新功能,又要向後相容,
所以其实该语言最复杂、不规则、不好学。但只用些基本的功能的话,还是过得去的。
二、[观念] 组合语言—Intel Style 与 AT&T Style、MASM 与 NASM
机器语言因为电路关系,原始形式就是 010101 这种二进位形式,但你喜欢也可以
转成十六进位写出来给别人看。
下面这是一个 x86 机器语言指令 (instruction):
05 0A 00 00 00 (十六进位表示)
用人类说法就是你告诉某颗 x86 家族的 CPU:
「把你的 eax 暂存器内容取出,将其跟10相加,再把结果写回 eax 暂存器」
用C语言表示法就是:
「eax += 10;」
机器语言形式显然太麻烦了。
於是发明了「助忆符号」,比如用 add 代表「相加这个运算动作」;
减法动作,用符号 sub 标记;
将资料从A处复制过去B处的动作,就用 mov 助忆符号标记。
add, sub, mov...等是运算子,而 eax 暂存器跟 10 是运算参与单元 (运算元)。
如果综合以上讲的运算子跟运算元,想要写出完整指令时,还会有一个问题!
若有 eax, ecx 两个运算元,想要把 eax 的值取出,复制到 ecx 去
到底该写 mov eax, ecx 还是 mov ecx, eax ?
哪边来源?哪边目的?
AT&T、Intel 各自有一套语法惯例。
详细资料参考这里:
http://www.ibm.com/developerworks/library/l-gas-nasm.html
C语言 Intel AT&T
指派运算子的 靠最左边的运算元 靠最右边的运算元
左边是目的地 是目的地。 是运算结果放置处。
int eax = 4; mov eax, 4 movl $4, %eax
(暂存器名称前,需加 % 符号;
而且4这个立即数值前,需加 $ 符号;
且用 movl 表示 move long 这麽长)
西瓜靠大边,跟大家一起用 Intel 惯例的写法就好。
像上面 mov eax, 4 这样子的指令形式,都叫「组合语言」,说穿了只是把当初
的「x86 机器语言」写成比较容易看懂的形式而已。
既然这样,那 x86 机器语言就一套,助忆符号跟暂存器也固定那几个。
为什麽最後却搞出 MASM、TASM、NASM、FASM...这麽多种组合语言呢?
MASM,软体界霸主微软推行的组合语言 (虽然微软最近变心去搞 MSIL 的样子?)
NASM,在台湾是仅次於微软的选择方案,而且跨多个作业系统平台。文件完整、
有中文书籍在讲它,而且状态稳定。
※ FASM,较新,类似於NASM,听说比较快?
※ TASM,老牌子,现在很少人用了,但是有 Turbo Debugger 很强大,可以
对 16 位元执行档做侦错,偶尔也值得一用。
上面提到的组合语言都是 Intel Style,而 AT&T 会看到的地方,就是使用「gcc -S」
功能时会出现。但是可以用 objdump 去看 Intel Style 的组语。
如果是 gdb 侦错则直接就有选项 disassembly-flavor intel 可以切换到
Intel 风格组语。
分别用 VC 跟 GCC ,一样写个 C 语言动态函式库,把某个函数输出,
我用 VC 时,可以在函数前面加上 __declspec(dllexport) 告知 VC 将该函数输出。
也可以写个「模组定义档」(*.def) 去记载哪些函数要输出。
但是这两个方法都是 VC 特有,不是 C 语言规定的。
同样状况在组合语言亦同,MASM 也有一些组译器指令是其专有,而 NASM 没有。
甚至在语法上,两者也有差异。
详细资料:
http://www.nasm.us/doc/nasmdoc2.html#section-2.2
NASM 对於「LABEL 符号」是有分大小写的,MASM 没有分。
(这不包含暂存器名称,没必要非写 eax 而不写 EAX,
也不包含 NASM 的假指令,比如 SECTION 大小写都可以。)
而且 MASM 有些遭诟病的语法规范,NASM 有对其改进之。
对於 MASM
「某符号」要拿来「当成记忆体位址」用时,需加上 offset 修饰。
对於 NASM
觉得微软这样太麻烦,直接写就一定是当位址用,不用加 offset。
MASM 的毛病是
如果写组译器指令如下,去定义两个符号:
(MASM、NASM 都支援这两个假指令)
foo equ 1
bar dw 2
equ 指令很类似 #define pi 3.141421356 (...偷用别人的梗)
dw 这个假指令,是告诉组译器把现在这个地方,所对应的记忆体位址,
取别名叫 bar,并且写入 double-word (4位元组) 到此处,
存放值为 0x 00 00 00 02。
此时,如果 MASM 有两个指令如下:
mov eax, foo
; foo 因为是 equ 设定出来的值,所以 eax 会得到 1
mov eax, bar
; bar 因为是「组合语言变数」,故 eax 得到值是 2
上面两个指令,语法格式看起来完全一致,
但如果没去观看 foo 跟 eax 的假指令定义,就不能判定机器码该翻成哪个。
如果是用 NASM,因为他强制规定只要是「间接取值」者,一律需加上中括号 []。
这个间接取值,意思是第一次取到的值,不是我要的,第二次取到的值才是我要的。
可以想成「间接取值 = 二次取值」。
换言之
mov eax, foo
mov eax, bar
对於 NASM 来说,不需要去看假指令定义,
因为 bar 跟 foo 都没有用中括号,所以两个都做一次取值就好。
亦即 mov eax, bar 会得到 bar 对应的记忆体位址,而不是 bar 的存放内容 2。
可是 MASM 很没规律,因为 foo 是 equ 所定义,所以 eax 得到 1 (没间接取值);
因为 bar 是一个 dw 定义的「组合语言变数」,所以将会做「间接取值」,先取得
bar 所对应的记忆体位址後,再到该位址再取一次值。
抓4位元组得到 2 来传给 eax。
三、[教学] 简单连结范例—NASM 组合语言 与 C/C++ (Windows 平台)
存成档案 xx.c
---------------------------------------
#include <stdio.h>
int plusTen(int val);
int plusEleven(int x) {
return x+11;
}
int main() {
printf("return = %d\n", plusEleven(1));
printf("return = %d\n", plusTen(0x00123456));
return 0;
}
---------------------------------------
用 VC 编译器的话,执行指令 cl /c xx.c 可以获得对应的目的档「xx.obj」
从 Visual Studio 200X 命令提示字元,去下这个 cl 指令,以省略环境变数的设定
存成档案 fun.asm
---------------------------------------
section .text
global _plusTen
_plusTen:
push ebp ;函数初始化工作
mov ebp, esp ;函数初始化工作
mov eax, 0
mov ax, word [ebp+8]
add eax, 10
pop ebp ;函数结尾工作
ret ;返回呼叫函数 (caller),eax 是存放返回值用
---------------------------------------
去下载 NASM
http://www.nasm.us/pub/nasm/releasebuilds/2.09.02/win32/nasm-2.09.02-win32.zip
解压缩後,执行指令 nasm -f win32 D:\Desktop\fun.asm
就能获得一样是 COFF 格式的目的档 fun.obj。
简单来说就是这个目的档跟 cl /c 得到的目的档有一样格式。
最後再去「Visual Studio 200X 命令提示字元」下指令 link xx.obj fun.obj
就能得到 xx.exe 完成 C/C++ 跟 NASM 函数的连结了。
※对原理有兴趣可以参考《程式设计师的自我修养》一书。
section .text
是 NASM 假指令,表示从这行指令以下的内容,翻译成机器码後
都要放到 .text 区去。每个 *.obj、*.exe 内部都有 .text 区段。
global
是 NASM 假指令,在这里表示要把 _function 标签包括的机器码
视为全域函数。
在 C/C++ 你预设写个函数,像上面的 plusEleven() 就自然会是
这里说的这种「全域函数」。
使用 dumpbinGUI 工具,跳去 xx.obj、fun.obj 查看符号表,就可
看到 external 字眼。表示 xx.obj 可以调用 fun.obj 里面写
external 的符号,反之则反。
mov ax, word [ebp+8]
这个 ebp+8 是代表 plusTen() 函数的「参数1」
查一下 stack frame 的观念,再用侦错软体观察「函数呼叫」
进入前後的堆叠、暂存器变化,应该就能理解。
要说明的是,[ebp+8] 是「二次取值」,当第一次取值得到
ebp+8 位址假设是 0x0012FF74,接着要在这个地方做二次取值,在
C/C++ 要取几个位元组的值是看该指标的资料型态。
如果是在 MASM,则是在中括号前写 word ptr [ebp+8] 代表 2位元组;
若写 byte ptr [ebp+8] 则代表 1位元组。
而 NASM 跟 MASM 差不多,但必须拿掉 ptr 字眼,否则会组译错误。
因C语言还没公开前,就已经留下一堆目的档、一堆函数,他们都用正常的命名,
一些好记的名字都被用过了,所以 C 语言在使用函数时,其实一律自动加 _ 来命名。
因此原本的 C 函数呼叫虽然是 plusTen,但在 fun.asm 里输出的全域函数要写成
_plusTen 才能让 xx.c 可以连结到。
------------------------------------
关於「目的档」,参考这篇:
http://en.wikipedia.org/wiki/Object_file
都只讲微软平台
在 DOS 时代目的档名称也都是 *.obj;执行档名称也都是 *.exe,
但这里的 *.obj 其实是 OMF 格式 (Relocatable Object Module Format)。
跟你在 Windows 用 VC 编译出来的目的档 *.obj 不同。
用 nasm -f obj fun.asm 应该就是产生 OMF 格式的目的档?
用 nasm -f win32 fun.asm 则是产生 COFF 格式的目的档。
更精确来说这个 COFF 格式是微软修改过的变种 COFF 格式。
你也可以叫它 PE/COFF 格式,甚至叫他 PE 格式也行,要解读 PE 格式,其
第一选择当然也是微软提供 dumpbin,而软体 dumpbinGUI 是图形介面的前端。
真要说的话 dumpbin 也是一个前端,其实是呼叫 VC 的 link.exe,给隐藏选项
link /DUMP /ALL xx.obj。
http://en.wikipedia.org/wiki/Portable_Executable
PE 格式不一定是 *.exe 执行档,也可以是 *.dll 也可以是 *.obj...等。
因为 VC 编译出的目的档都是 PE 格式,而 VC 的 link.exe 不能处理古早的
目的档 OMF 格式,所以上面需要叫 nasm 用 -f win32 选项去产生 PE 目的档。
也许会有某个很强的连结器,可以把 OMF 跟 PE 目的档连结成 PE 执行档吧?
甚至把 gcc 编出来的目的档跟 PE 目的档 link 成 PE 执行档?
看有没有人知道罗
------------------------------------
MASM 组译器指令清单
http://msdn.microsoft.com/en-us/library/8t163bt0.aspx
刚好看到,补充上来。
------------------------------------
(怕以後忘记,再写篇记录,上文不变动,新增内容於下)
16位元记忆体模型—Segment:Offset (分段记忆体模型)
32位元记忆体模型—Flat Memory Model 加 Paging
(
http://en.wikipedia.org/wiki/X86_architecture )
个人电脑 x86 家族的 CPU,在 16 位元时代是 8088/8086/80286 这三位;
而 x86 家族第一个 32 位元始祖是划时代的 80386。
8088跟8086的位址汇流排都有20条线,每条线都是一端连接记忆体,一端连接
处理器,在高低电位变化下 (0、1),总共可有 2^20 种控制变化。
换言之,依照每个记忆体位址对应一个位元组的惯例,可以定位 2^20 大小的
记忆体位址;
80286 则进化到 24 条位址线,定址能力达 2^24,即 16M 记忆体。
省麻烦,把它当成跟 8088/8086 一样,只能定址到 1M 记忆体就好。
※ 在 x86 的术语中,记忆体位址可以分成三种:
逻辑位址、线性位址、真实位址(物理位址)
必须先「逻辑位址→线性位址」,然後接着才是「线性位址→真实位址」。
自从32位元 CPU 出现 (自 80386 後),记忆体 Model 变成 Flat Memory,
逻辑位址就已经等於线性位址了。
然後是因为有「分页机制」武力介入,所以需要先透过分页机制转换,线性位址
才会变成真正的物理位址。
而分页机制是从 80386 开始使用 (保护模式的完整版也是从 80386 开始)。
那为什麽 16 位元处理器,不使用 Flat Memory Model?为什麽当初的逻辑位址
要先经过转换才会变成线性位址?
因为16位元CPU内部,参与运算的暂存器当时都还停留在16位元
(如:ax, bx, cx, dx),甚至最重要的指令暂存器 (IP) 也是 16 位元,
故只有定位到 0~65535 也就是 64K 记忆体的能力。
Intel 用额外提供的四个「分段暂存器」(CS、DS、ES、SS),搭配其他暂存器後
,使得每次记忆体定址方式其实是 Segment:Offset,此时这种位址表达法叫
逻辑位址。
CS 是 Code Segment、DS 是 Data Segment、SS 是 Stack Segment ...
逻辑位址→线性位址,公式是:「Segment Register * 0x10 + Offset」
假设我们有个程式,里面的「全域变数」(不是放在堆叠的那种区域变数),
有个很大的整数阵列,总共有 128K。当程式执行时,这些资料区段假设放在
「线性位址=物理位址」的 0x0 ~ 0x1FFFF 这段连续的记忆体空间里。
在逻辑位址(以写程式的角度去观看的位址),这 128K 资料会被分成两段,
第一段是 DS=0 且 offset = 0x0000~0xFFFF,第二段是 DS=1 且 offset
= 0x0000~0xFFFF。
换言之,如果我要把某阵列元素移到 ax 暂存器,指令可能长这样
mov ax, word ptr[0x1234]
如果执行这行时 DS=0,则会取到物理记忆体位址 0x01234 处 (第一段);
如果执行这行时 DS=1,则会取道物理记忆体位址 1*0x10 + 0x1234 = 0x11234。
若要读取最後一个位元组到ax,只要执行以下指令即可:
mov ds, 1
mov al, byte ptr[0xFFFF]
因为不知道当时的 DS 值为多少,所以保险点,先设定 DS,然後因为 ax 是
16 位元,不必用到这麽大。所以用 al 存放即可。 al 就是 ax 暂存器
低 8 位元别名。
(ax 在 32 位元以上的 CPU 时,其实也是 eax 暂存器的低16位元处别名)
现今的执行档,比如 PE 执行档,往往内部都有分 .text (.code)、.data,
可能就是承袭当初的记忆体分段机制?
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 124.8.140.240
1F:推 JUSTLOVEAYU:大推~ 10/12 03:15
2F:→ xam:gcc 吃 at&t 喔, intel 未必比较大边.. 10/12 03:16
3F:→ tropical72:大推大推..不过我想我需要一些时间吸收这篇文章吧.. 10/12 03:43
4F:→ tropical72:看完後觉得想看的书似乎又更多了.. 10/12 03:43
5F:→ king19880326:有啥好大不大边的, 写 assembly 第一件事就是看 10/12 03:44
6F:→ king19880326:assembler 的 document. 10/12 03:44
7F:→ tropical72:所以..要学组语的话,"程式设计师的自我修养"、算盘书 10/12 03:49
8F:→ tropical72: 将会是我的第一步? 10/12 03:49
9F:推 nand:赞啦!! 10/12 04:07
10F:推 loveflames:原po如果发到ASM板的话,就能m起来了 10/12 04:16
11F:推 xatier:大推!学了很多<(_ _)> 10/12 07:26
12F:推 VictorTom:推一下:) 10/12 07:41
13F:推 bill42362:推! 10/12 09:53
14F:推 snoopy0907:推推推~~ 10/12 09:55
15F:推 linjack:推荐这篇文章 10/12 10:04
16F:→ purpose:学组语就买教组语的书来学。你可以多买几本,交叉对照。 10/12 10:25
17F:→ purpose:我对 AT&T 的偏见太深了...感谢指正 10/12 10:26
18F:→ purpose:补充一下,我第一次修x86组语被当,第一本买的组语书完全 10/12 10:29
19F:→ purpose:看不懂,不是每个人都是一次学懂,脑子累积的东西够多,回 10/12 10:30
20F:→ purpose:去看以前不会的组语,就开窍了,书多买多对照一样有帮助。 10/12 10:30
21F:推 stupid0319:我都用ollydbg学组语,很容易看数值怎麽跑 10/12 10:40
22F:推 loveflames:但是ollydbg只能看ring 3 10/12 11:16
23F:→ purpose:Win 三大侦错软体:IDA、OD、WinDbg (功能最强介面最囧) 10/12 11:34
24F:推 stupid0319:有没有可以在Nprotect下跑的debugger? 10/12 11:56
25F:推 stupid0319:以一般的程式设计师需要看ring0吗? 10/12 12:01
26F:→ stupid0319:功能最强介面最囧是SoftIce吧 10/12 12:04
27F:→ purpose:搞硬体、搞系统、搞XX的会需要ring0。Softice听说不能在 10/12 12:17
28F:→ purpose:新版 Windows 跑了,至於有没有人修正就不知道罗 10/12 12:17
29F:推 richliu:其实 MASM 很强, 当年写 MASM 可以写到像是 C 语言 10/12 12:55
30F:推 loveflames:if跟while都有,else忘了有没有 10/12 13:00
31F:推 loveme00835:所以我不算错啊 0.0 程式设计师的自我修养就是这样写 10/12 13:05
32F:→ purpose:PE 其实大家都很随便在讲,广义认定跟狭义认定的区别而已 10/12 13:16
33F:→ purpose:有 if、while 喔...现在才知道 10/12 13:18
34F:推 herman602:是有if跟 while, 可是好像是假指令吧(?) 10/12 17:54
35F:→ herman602:组语上机考的时候老师说禁止使用xd 10/12 17:55
36F:→ loveflames:我跟楼上相反,结果没几个人用jxx 10/12 18:04
37F:→ tropical72:非常感谢purpose与各位的解释,小弟我上网search tool, 10/12 19:26
39F:→ tropical72:突然觉得很方便,不然p大所提供的tool info.我会再去学 10/12 19:28
40F:→ tropical72:怎用的,谢谢各位的指教,感激不尽!! 10/12 19:28
41F:推 tropical72:网址里点进去还有Crystal FLOW for C(++),似乎很有趣.. 10/12 19:37
42F:→ purpose:程式码打好,或者贴上去,按下「emulate」就开始执行 10/12 19:42
43F:→ purpose:第一行写假指令 org 100h 是 *.COM 的格式,意思是当一个 10/12 19:43
44F:→ purpose:*.COM 被执行,DOS会把他放置到 IP = 0x100 的地方 10/12 19:44
46F:→ purpose:这个软体只能模拟8086 CPU,我以前写boot sector也用过 10/12 19:45
47F:推 suhorng:推耶! 10/12 19:48
48F:推 loveflames:*.com也没分code段跟data段 10/12 19:50
49F:→ purpose:没有保护模式,没有虚拟记忆体的...美好时代? 10/12 19:51
50F:推 richliu:有 segment 的概念.. 还有 IO 其实 x86 指令很鸟.... 10/12 23:30
51F:推 hpo14:我超弱,看得好头痛 10/13 01:34
※ 编辑: purpose 来自: 124.8.128.171 (10/13 12:00)
52F:推 tropical72:请问,89C51在设计时需要额外的ROM IC存放程式码,到 10/13 12:08
53F:→ tropical72:89S51时放置程式於同一IC中,至於x86所谓的code seg.与 10/13 12:11
54F:→ tropical72:data seg.现行应都做於同一IC?? 另,8051系列似乎都可进 10/13 12:12
55F:→ tropical72:行烧录之动作,这些资料都算多,那x86市面上是否有提供烧 10/13 12:13
56F:→ tropical72:录器与可程式化IC可购? 10/13 12:13
57F:→ purpose:code&data seg. 都是在主记忆体上。什麽烧录器可程式化IC 10/13 12:29
58F:→ purpose:我这辈子都没碰过,不知道那什麽,你可以去电机版或组语版 10/13 12:29
59F:推 tropical72:我在ASM发问,#1CjJU9xN,得到 WolfLord 以下解释 10/13 13:59
60F:→ tropical72:186-xxxx家族是SOC 然後也有一些厂商用X86CORE作SOC 10/13 13:59
61F:→ tropical72:不过X86本身LICENSE就贵,作SOC其实很不合算,因此并 10/13 13:59
62F:→ tropical72:不常见,而X86的SOC要ISP都要颇贵的特殊工具而且仅能 10/13 14:00
63F:→ tropical72:洽原厂 #end#, 最後谢谢purpose解决我许多疑惑. 10/13 14:00
64F:→ purpose:去看了看不懂。现在才发现loveflames大是ASM板板主,失敬 10/13 16:57
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 124.8.128.171
65F:→ purpose:错误的地方,还请各位专业的前辈不吝批评、指正,感谢 10/13 17:02
66F:推 afai:感谢! 推~ 10/13 23:26
67F:推 sorkayi:不常用x86 组语 还是推一个 10/17 09:31
68F:推 Ross0916:mov ds, 1 是不行的 顶多是 pop ds 不过这是硬挑的XD 10/18 07:15
69F:推 loveflames:要mov ds,ax才行 10/18 07:23
70F:→ purpose:mov ds,1 没验证过就打了出来,谢谢楼上大大指正 10/18 11:50
71F:→ purpose:回想起来,我之前也这样写过,被组译器挡了下来改过就忘了 10/18 11:53
72F:→ eva19452002:所有处理器里面,x86家族功能当然是最强? 10/19 11:14
73F:→ eva19452002:就算功能最强,也不表示执行效能最好 10/19 11:14
74F:→ eva19452002:你是不是把复杂跟功能强画上等号啊? 复杂不等於功能强 10/19 11:17
75F:→ purpose:>10/19 11:14 小弟见识浅薄,没见过什麽世面,仅能评论自 10/19 12:12
76F:→ purpose:己见识过的处理器,让前辈们看笑话了,感谢指正 10/19 12:13
77F:→ purpose:>10/19 11:17 我觉得x86处理器跟Windows API状况很像,因 10/19 12:14
78F:→ purpose:为有旧有包袱,为了相容而把一些事情搞得很复杂,但是他们 10/19 12:14
79F:→ purpose:每一代还是会推出些更好的新功能,比如新一代处理器的SIMD 10/19 12:15
80F:→ purpose:指令集(如:SSE),一次可以处理多个资料,让效率提昇很多 10/19 12:15
81F:→ purpose:因为这些新功能的不断提供,让我觉得他们的功能很强 10/19 12:16
82F:推 asususer: \⊙▽⊙/~by PTTNOW~ 04/14 01:51