首先我们来看一下咱们 这个程序的主体 首先第一个吧 虽然我们没有详细的讲解过 一个完整的汇编程序的结构 但是从这张图上我们可以基本上 猜得出来看到程序入口地址在哪 -main 因为-main 是个int C里面的一个main C程序的入口函数是main 所以这个对应的汇编函数 它的入口地址也是main main里面我们看每行干了些什么事
首先看一下 第一行和第三行 push 后边一个move 这个完全非常熟悉了 因为它还是一个C程序吗 C程序函数被调用的时候 它的入口地方呢会建立这个栈帧 就是push ebp和mov esp到 ebp 这个我们非常的熟悉
那么当中这一行mov 16 到eax 是干什么的呢 这个我也不知道 这个我也不知道 我们回头看 后来这个代码会被优化掉
后来再往下走 再往下这一条指令 这是一条减法指令 把esp减8 那这个呐 我们应该也熟悉了 就是给当前main函数一个分配 8字节的一个栈空间 esp减8吗
那这个是干嘛的呢 后来又加了一个and 为什么会有一个与操作呢 实际上把esp跟负16去 与一下 的话 就使得什么 因为负16大家都知道 用补码表示的话 它的最低四位是0 全零 它跟esp 与一下的话 就确保这个栈顶地址是16字节对齐 这一般来说 在C程序里头 我们分配栈的话 一般来说 要保这个栈顶地址要16字节对齐 对一个函数而言 经常有这样的一个约定 但这个约定是一个软件约定 可以修改的
接下来两个call 对应两个函数 这两个函数都不认识 其实这两个呢 当然跟我们汇编编程 应该关系不是特别太大 这两个函数分别都是 与C运行时库初始化相关的 这大家知道就是说 我们写个C程序吧 一般来说 C程序默认去调的libc这个库 C运行时库 你所调的一些C调用 都是在C运行时库里实现的 那么这两个函数呢 关于C运行时库初始化相关的 其中第一个呢 跟栈的分配相关 这咱们就不展开说了 跟我们关系不是太大
再往下看 似成相识 因为我们在这个原来C代码里 就调了一个printf 最后在退出时 调了一个exit 0 exit 0 好说了 就是程序退出吗 就是程序退出这个调用 那么这样的话 相当于就是说 mov 0 括号esp 实际上就设置了这个exit 这个零的 这个参数 这我们回想一下 就相当于mov零到栈顶 然后call 返回地址压栈 就去进入exit 实际上这个地方就做了一个过程调用参数 而其我们看到就是说 call(exit)之后呢 renturn(0)那个代码 在这里头没有体现 为什么呢 已经调用了exit 编译前是很聪明的 exit之后 这个代码就不给你编译了 renturn(0)就给它skip掉了 虽然前面已经写了一个了
再往下看的话呢 call puts 注意啊 我们原来调的那个函数 叫call printf用的是printf 那这个地方呢 我认为这个编译器比较聪明 printf里头我们只是用了什么 把它输出一个字符串 没有做任何格式的转化 所以它用一个puts来替换 那这样简单一点了
然后这个呢 因为相当于就是说 我们要打个hello world出发 出来 所以puts唯一的一个参数 也就是说它要打印出来的 字符串的地址 所以我们就把这个地址 lc0放到栈顶callputsok然后就把这个helloworld可以打印出来了printf打印出来ok那么lc0表示什么呢就表示上面helloworld这个字符串的地址因为它是一个常量它是一个常量虽然我们目前在汇编或者编译的时候还不知道这个常量的地址是什么但是我们用助记符来表示吗用lc0这个助记符来表示表示它是一个地址所以它前面要加一个 l c 0 放 到 栈 顶 c a l l p u t s o k 然 后 就 把 这 个 h e l l o w o r l d 可 以 打 印 出 来 了 p r i n t f 打 印 出 来 o k 那 么 l c 0 表 示 什 么 呢 就 表 示 上 面 h e l l o w o r l d 这 个 字 符 串 的 地 址 因 为 它 是 一 个 常 量 它 是 一 个 常 量 虽 然 我 们 目 前 在 汇 编 或 者 编 译 的 时 候 还 不 知 道 这 个 常 量 的 地 址 是 什 么 但 是 我 们 用 助 记 符 来 表 示 吗 用 l c 0 这 个 助 记 符 来 表 示 表 示 它 是 一 个 地 址 所 以 它 前 面 要 加 一 个 号 表示它是一个常量 是一个地址的常量 把它放到栈顶去 就作为参数压栈 然后就call这个函数把它打印出来
再往上呢 就是堆 堆是什么意思呢 你刚才提到的那些 看到的那些 main malloc 就是动态分配 动态分配出来的那些数据 就放在堆上头 堆呢 就是在data 据段的上头 自底往上涨
首先就是说我们linux情况下 怎么去怎么把一个汇编程序 最后转换成一个运行程序
那首先就学学命令看 相关的汇编指令 汇编命令是什么 命令行的命令是什么 as就是assembler这个命令 gcc下面默认的这个命令 然后呢hello world.s 就是你写了一个名字hello world.s 用as把它汇编 转换成一个对象文件object 那么我们这边写的是32位 默认是32位环境 但如果你是64位环境呢 就添加命令行参数–32 也就是说.o之后呢
ld ld也就是link 就是把你这个汇编程序生成出来的.o 最后生成你的exe的执行程序 你的执行程序 你可以把多个.o 连接起来 生成一个最终的你的执行程序 那么如果是64位环境下呢 来生成一个32位的执行文件 加一个参数 大概就是这个样子
如果你在某些linux版本下 如果发现一些错误 比如说乌班图之类的 而且发现什么东西没找着 执行exe或者ld时候 有些报错的话 建议啊 你就打这个命令 就是有问题的话 你看一下你是不是要安装 这个包安装上 就ok了
去处理命令行参数的示例 就是什么意思呢 就是说汇编程序啊 它启动的时候你也可以在命令行里头 给它传入参数 那么这个参数是怎么实现的 就是你在C 汇编代码里边怎么去处理这个参数
text globl start 入口地址不说了 popl ecx popl ecx 这是干嘛呀 其实我们结合后面的说明 看也看得出来 猜也猜的到 就是当一个程序通过命令行启动时 传输给它的参数 还是被保存到它的运行栈里头 这个就是跟咱们实际上跟C程序是一样的 C程序调用的时候相互调来调去 也是通过栈来传参 那么一个汇编程序 或者说一个程序启动的时候 它启动的时候 实际上系统传给它的命令行参数 也是放到它的运行栈里头的 那么这样的话它的第一个操作 比如说 我要取参数的话 第一个参数的话 我说pop ecx 相当于我把 它的参数也是 从命令行来看 也是从右往左压栈 从右往左压栈 第一个压的是什么呢 argc 如果大家写过c程序的话就非常熟了 int main函数 第一个函数就是int argc函数 表示 你这 main函数 里面一共有多少个参数 argc 所以说这里也是一样 popl ecx 把第一个参数取出来 第一个参数 从右往左压栈 它这个应该是最后一个压栈 第一个popl出来 popl出来之后呢 我就知道了 ecx里面存有当前 一共有多少个参数的数值 就在这里头
然后呢 看看 进入一个循环 继续popl 现在我就知道一共多少个 我再popl下一个 下一个什么意思呢 argv main函数再回忆一下 C里面main函数什么样子 char ** argv 实际上里面就是指向了一系列的参数 指向参数表 每个参数在里面都有一个用字符串形式来 进行存储的 我们就指向它这个地址 那然后怎么办呢 如果说ecx是空指针是null 那就结束 就说明我们没有 没有其他的参数 就是相当于 如果text就相当于把它作为and 说明它是空的话 那就直接退出了 退出的话呢 我们说过了eax是$1是一号调用 exit 然后呢 exit 0 ebx退出代码 实际上就是0了 自己跟自己做个异或嘛 就退出
那么如果不是空指针呢 那就一行行来 实际上就是movl ecx ebx 实际上edx清0 这个什么意思呢 大家看看 实际上就是说这个时候啊 ecx 事实上这时候呢 它里面存储的是什么 就存储了这个 参数列表里面的当前这个的起始地址 这个地址就放在ebx里面去 放在ebx里面去了 放完之后呢 我们就一个一个的把它的这个 就是当前这个字符串参数 这个参数 它里面的每个byte一个个都给它取出来 因为我们知道就是说 命令行参数最终都转成一个字符串的形式 一个字符串的形式 把它取出来 就是movl b ebx取第一个 放到al里面去 这是取的byte 然后呢看一下 这是我的 这里edx相当于什么相当于计数 看你当前这个字符串参数 它里面有多少个byte 它长度是多少 加加 ebx也是 加加 相当于指针往后挪了一下 地址往后挪了一下 那么如果al是0的话 就是text如果是0的话 就说明什么呢 就说明这个时候 如果是0就说明当前这个字符串 参数就到尾巴上了 这个字符串参数就结束了 如果不结束的话 还是反复 我们可以猜出来这是干什么 就相当于就是说我把当前这个argv 相当于这个参数列表里面的当前这个参数 字符串参数 字符串的个数 或者说长度给它计算出来给它计算出来了 计算出来这个长度值 放到edx里面 我就在里面填了一个换行符 因为我这个另一行参数程序的作用就是 把一个个一命令行参数打出来 那既然我现在知道到你尾巴上了 然后就是说怎么办呢 ebx减1把 10挪进去 10 挪 进 去 10 正好是换行符 换行键嘛 就把它放到你这个字符串尾巴上 本来这个地方是0的换成$10
然后呢我通过系统调用 这个write系统调用 在标准输出上面 把你的当前命令行参数给它打出来 打出来之后呢 ok 再跳回去 就开始 就相当于把一个一个的命令行 里面的参数一条一条的给它打出来 实际上这里面就做了这么个事
下面给个例子 就是讲汇编如何调用C的库函数示例 其实在我们以前课程当中一再讲到过 就是从C语言出发来看看 它对应的汇编程序是什么样子 以此来帮助大家理解 程序在机器层面的表示以及运行的流程 但是反过来说的话呢 我们看到一个汇编程序 你实际上很难区分 就会变成是到底手写出来的 还是拿C编译过来的 那么既然这样的话 如果C能够调用这个lib_c
的库函数 这个没问题 那么汇编当然也能够调用 这个lib_c
的库函数了
首先我们从头说一遍 这个咱们不讲了 这个printf里头一样的了 我们要把这个字符串打出来 唯一有一点区别是什么呢 这里面不是用ascii a s c i i那个说明 a s c i z 什么意思啊 就是说这还是一个字符串 字符串类型的 但是我默认的自动的 在这个字符串后面加一个0 这个z就是这个意思 因为我们想想看它为什么呢 因为我们后面用printf 把它这个东西打出来 printf C里面字符串 输出的时候是默认找0作为结尾 所以这里面自动给你填个0
…
接下来呢我们就做一个简单的一个小节 程序结构 汇编程序的结构 主要会包括三个常用的段 最重要的正文段 就不用说了 程序指令
然后呢 这两个都是数据段 但是数据段和数据段不一样 .data 就声明带有初始值的数据 .bss 也是数据段 它是声明无需初始化的数据 但还有rdata之类的只读数据 这个咱们不细说了
这两者为什么要区分开呢 实际上大家可以想想看 实际上是这样子 就是说你带有初始值的数据 它的生成对象文件或者执行文件 这个段是实打实占多少空间的 我举个例子比如说 你声明了一个1024的一个数组 每个数组你都不厌其烦的 把初始化值都填上了 那最后生成这个对象文件 或者执行文件的时候 那这个数据的要实打实的要占 1024个空间的 数据在里头嘛 那如果是.bss段呢 比如我们刚才说的那个 这个东西 就像这个 我声明一个12字节的buffer 但这buffer没有初始化呀 没有初始化的话呢 也就是我在段里面只要描述一下 我这里有一个长度为12字节的 buffer就行了 我不需要实打实的占那么多的空间 因为没有初始化过嘛 所以这两端把它分开是有道理的
再往下的话就是程序入口地址 程序入口地址就是_start 它要有.globl 有.globl的属性 好像C里面的main函数一样 最终汇编就从这里边进去
汇编当中的过程调用 实际上也应该是 就相当于你比如你写一个函数 人家来调用过程人家来写 这个要说明一下 就是要说 它里面肯定涉及到传参嘛 那么如果你的汇编 严格来说汇编之间的 过程调用没有一个特定限制 说你必须得这么传参 或者那么传参 跟C不一样 但是你反正得定义一个规范了 从互操作性上来讲 你肯定得规范 那这规范怎么定呢 就是我们还是选择 用默认的C函数的传参规范 来进行作为我们自己的 一个写汇编上的规范 这样的一个好处显而易见 因为你汇编程序可能去调C C也会去调用你这个汇编 所以大家遵循一样的 传参的方式 包括你栈恢复的方式 以及过程的返回参数的方式的话呢 C跟汇编之间的互调性会非常好 所以说呢 一句话就是说 你反正得选择一个规范 就不如选一个现成的规范 现成规范就是C语言的规范
那这里面为什么说还要保存ebp操作呢 我们说过了 因为我们在汇编里面 实际上在这里头 汇编也遵循C里面的过程操作 所以说你这里面还得 把ebp给它保存一下
这里面会定义几个 在汇编里出现的一些符号 实际上就是一些伪码操作类似于 这就是什么意思呢 有点像咱们C里面的常量 就像什么什么等于嘛 这个名字 就是说这个常量啊 设置成factor等于3 把factor设置成3 factor这个标号表示3 linux sys call 实际上就是call int X86 把它用做助记符来表示 这个在C里面经常用 这样的话 我们要避免使用一些绝对的数据 因为它的可读性不好嘛 增加它的可读性 当然了 设置之后呢 数据符号值是不能在程序当中更动的 因为它们是常量 就不能在里面更动的 相当于C里面的 一个define什么的宏变成一个常量 就这个意思 当然不能做一些改动了 那这样的话 实际上就可以 增加咱们程序的可读性
实际上看一下这个例子 就是我们在这个程序里面 大量的使用.equ 实际是一种宏 指明什么意思呢 就是说我里面定义了 一系列系统调用的call numbers 我们说过exit是1吗 write是4 我们以前都用过 那这个动画 如果你直接写1写4的话 一般来说 可能大家不一定记得不那么清楚 你这么一写的话 用sys exit 来替换这个1 可读性就好多了
刚才我们讲了很多例子 这里边实际就是说每个程序 只有一个模块 就是你只写了个.s文件 .s文件生成那个.o 这.o生成一个exe执行文件
现在我们讲什么 就是说 能不能在一个程序里面有多个.s文件 也就是多个模块 多个模块连起来之后 形成最终一个目标的执行文件 那么这里边我们会分别介绍三个程序 就读 写 修改数据记录文件 有那几个基本函数 读 写记录 从文件里边读写记录 读或写都是一个单独的汇编.s文件 还有两个单独常量定义文件 它有什么意思呢 就跟刚才说过的一样 我们把系统调用相关的 标准输入输出相关的 还是些别的什么东西 就定义到两个.s里边去
最后我们就是把这几个程序连接在一起 因为刚才提到过了 这个程序是由两个.o构成 一个是records.s 一个是刚才调用的write-record 那个函数所对应的.s 所以分别拿它们汇编出来一个.o 把两个用ld命令link 把两个.o连接起来 生成最终的执行文件 最终执行文件
链接,顾名思义就是把多个程序的模块,装配成一个最终执行文件的大致这么一个流程。
在讲这节课当中 我想大家也许会 多多少少意识到有点问题 什么呢
比如你把一个.c编译成.S吧 实际最终是.O .s是一个中间结果 转变成汇编程序的时候 大家可以看到 里头出现了一些标号 这个标号呢 有可能是一个函数的 或者说过程的一个地址 也有可能一个变量的 一般都是指全局变量的地址 那么也许大家多多少少会问 就是说 你这个标号表示这个地址 但你在编译出来的程序里头 .S里头 你写的还是标号的形式 但这个地方应该是地址 那么这个地址 什么时候才能真正转变为一个 实际的一个地址
大家应该想过这个问题 因为你直接把一个.c转换一个.s的时候 这个时候是不大可能知道 这个标号的地址的 想想看
最简单的想法是什么呢 大家知道就是说 你可能把多个.c 要装配成为一个执行程序 大家都有自己的代码 就是函数 也都有自己的全局的变量 这些全局变量大家拼到一块 这些代码或者正文拼到一块 形成一个连续的一个空间 这个时候才有可能把每个函数的地址 或者说每个全局变量应该存的地址 才有可能得到 所以光光这个.c到.o 或者说到.s的转变 这个过程 也就是编译过程 是不可能知道最终以 标号所处的位置的 那么这个位置什么时候才知道呢 是应该链接才知道
什么叫符号解析?就是说你这个C模块里面,声明了哪些、用了哪些,另外一个C模块里面声明了哪些、用了哪些,相互对一对,能不能对得上,就是你用我的、我用你的,对上了这个事情就OK嘛,解析完了嘛。
堆是什么呢 就是程序运行的时候 你动态分配的那些空间 就是分配在堆的上面 什么叫动态分配 就是你们写过C函数的话 就是什么什么alloc calloc malloc之类的 一些内存 分配出来的 都放在堆上面的 也就是程序动态分配的 c语言当中的这些函数 就是操作这块区域的 那么当前堆的地址上限被称为 system break 就是堆 自底往上涨
内存分配容易造成的一个问题是什么呢?
会造成内存的碎片化。
参考文献:
1. 汇编语言程序设计 - 清华大学 - 学堂在线。