汇编语言笔记C语言的汇编实现
汇编语言笔记C语言的汇编实现
0x00 结语
王爽老师在书的结尾设立了一个综合研究部分,为了深入研究有关问题:
针对①c语言中能不使用变量吗?②c语言可以不用main函数吗③如何不定参数的函数如何实现?这三个我们在学习c语言时常被忽视的问题作者进行了追问,并警醒读者:
都在用,我们就非得用吗?
规定了,我们就只知道遵守吗?
司空见惯,我们就不怀疑了吗?
作为一个学者,或者说技术爱好者,学习时必定要有这种打破砂锅问到底的研究劲头,否则只是按图索骥,难有真的收获和深入的体验,如果是作为工作,那是另一种情况,在此不论。
下面就针对这三个问题展开研究
0x01 搭建dos下的精简的c语言环境
根据系统的不同,c语言有不同的集成开发环境,dos系统下的IDE(intergrated development environment)是turbo c,我是用turbo c 2.0和dosbox(没有用bochs运行dos太麻烦了)
tc2.0目录下文件很多
为了知道哪些程序和文件用于解决我们的问题,在d盘新建一个文件夹minic,然后一个文件一个文件地复制到minc的方法,然后用命令mount c d:/minic 把minc挂在成dos的c盘,找到最小的tc开发环境。
执行命令c: 进入minc文件加,windows下把tc.exe复制到minc,然后dos下执行命令tc.exe
然后清楚options中directions里的所有路径(这都是开发环境的依赖文件,都删除,即设置为当前路径,也就是和tc同一目录),写下如图代码并在compile中compile to obj
然后我们试着link链接,报错了
根据提示导入c0s.obj再次尝试,仍然报错,于是依次导入缺少文件
终于在添加cs.lib,c0s.obj,emu.lib,graphics.lib,maths.lib后链接成功得到test.exe
运行成功
0x02 c语言中使用寄存器
汇编中我们把数据储存到寄存器或者内存空间,我们试着在c中使用,支持下图寄存器,几乎所有了
编写程序
main()
{
_AX=1;
_BX=1;
_CX=2;
_AX=_BX+_CX;
_AH=_BL+_CL;
_AL=_BH+_CH;
}
1编译链接后用debug运行(需要把debug也导入到minic文件夹),u命令观察编译后的汇编代码,我看了下,从cs:0到出现第一个ret,中间好多代码啊。。
2思考main函数的代码在什么段中,用debug怎么找到url.exe中的main函数? main函数作为代码当然在代码段 段地址和debug中的cs对应,但是怎么找到我就不会了
3用下面的方法打印出test.exe被加载运行时,main函数在代码段中的偏移地址:
main()
{
prinf("%x\n",main);
}
上图时cs:1fa对应的代码,可以看到01fa是作为一个常数地址常数出现(是不是和汇编里的地址标号很相似?)的,所以我推测上面代码之所以能打印mian的地址,是编译器把main作为一个地址标号处理,编译时直接作为一个地址常数,所以pirntf里放的mian不是一个变量,是一个地址常数
结尾的ret告诉我们,c语言里的mian函数在汇编里是子程序
研究下面的代码,验证上面的推测
void f(void);
main(){
_AX=1;
_BX=1;
_CX=2;
f();
}
void f(void){
_AX=_BX+_CX;
}
图一对应main函数 图二对应f函数
0x03 分析内存空间
//买菜去了,待更新
汇编语言访问内存需要指明数据的地址和长度,c语言是类似的,要访问内存都需要给出地址(确切的说是空间首地址)和空间存储的数据类型。C语言使用指针类型来表示内存空间的地址和内存空间存储数据的类型。
*(char * )0x2000=‘a’ 向偏移地址为2000h 一个字节空间内写入字符’a’(注意这种写法针对8086平台上的c语言)
当然也可以用给出段地址和偏移地址的方法访问内存空间
*(char far *)0x20000000=’a’ far指明该地址是段地址和偏移地址 0x2000是段地址 0x0000是偏移地址
不过直接用地址访问内存空间是不安全的,因为这些地址并不是分配给我们使用的,可能更改了别的程序的代码或者数据(os环境下)
实验一:
main(){
*(char *)0x2000='a';
*(int *)0x2000=0xf;
*(char far *)0x20000000='a'
_AX=0x2000;
*(char *)_AX='b'
_BX=0x1000;
*(char *)(_BX+_BX)='a';
*(char far *)(0x20001000+_BX)=*(char *)_AX
}
这是上述代码编译后的汇编代码,很好理解
实验二:编一个程序,在屏幕中间显示一个绿色字符’a’
main(){
*(char far *)0xb80007d0='a';
*(char far *)0xb80007d1=2;
}
实验三:分析下面程序所有函数的汇编代码,思考c语言将全局变量存放在哪里,局部变量存放在哪里?每个函数开头的push bp mov bp sp有何意义
int a1,a2,a3;
void f(void);
main(){
int b1,b2,b3;
a1=0xa1;a2=0xa2;a3=0xa3;
b1=0xb1;b2=0xb2;b3=0xb3;
}
void f(void){
int c1,c2,c3;
a1=0x0fa1;a2=0x0fa2;a3=0x0fa3;
c1=0xc1;c2=0xc2;c3=0xc3;
}
从上面可以看出,对于全局变量,c语言的编译器将其放在ds段内,也就是数据段,而对于局部变量,则是放在栈区,每个函数开头的push bp,mov bp,sp都是为了保存bp sp的值,然后通过让sp减小,修改栈顶,扩大栈内空间,来储存局部变量。所以sp减小的大小和该函数内的局部变量总大小相等,最后函数返回之前,mov sp,bp,pop bp 恢复回来。值得注意的是,通过r命令发现程序的ds和ss寄存器的值相同,也就是数据区和栈区共用一个段,通过上面的汇编代码,全局变量 偏移地址按定义顺序增大,局部变量也是按定义顺序增大(同一作用域时)。
实验四:c语言将函数的返回值存放在哪里?
int f(void);
int a,b,ab;
main(){
int c;
c=f();
}
int f(void){
ab=a+b;
return ab;
}
上述分别是main函数和f函数编译后的汇编代码,main函数里mov [BP-2],AX,前者是局部变量c,后面的AX即使函数的返回值,f里的汇编代码也应证了这一点。即函数的返回值通过寄存器传递
实验五:下面的程序将向安全的内存空间写入从’a’到’h’的8个字符,理解其运行过程
#define Buffer ((char *)*(int far *)0x200)
main(){
Buffer=(char*)malloc(20);
Buffer[10]=0;
while(Buffer[10]!=8){
Buffer[Buffer[10]]='a'+Buffer[10];
Buffer[10]++;
}
free(Buffer);
}
仅从代码分析,define定义了一个宏,Buffer 实际是((char *) * (int far *)0x200),看起来有点复杂,那我们分开来看 右边是 (int far *)0x200 进一步 int far *是干嘛的呢? 是说右边是一个int类型的指针,那什么又是指针呢?指针就是右边的数作为一个内存空间的地址,地址里面存放的数据是int far类型的(32位)。所以说 (int far *)0x200即0x200是个指针,而 *(int far *)0x200,即指0x200存放的int整数的值, 而左边的cha * 即说明这个int far整数又被看作一个地址。
而第一句Buffer=(char*)malloc(20),实际上是修改了0x200内的整数,由malloc给出,malloc是操作系统从内存里找一个连续的20字节的空间,返回其空间的首地址,由于是int far,所以返回是的段地址和偏移地址。
然后就简单了,Buffer [10]即是 *(Buffer+10),即刚刚的首地址+10,这个地址作为首地址,char一个字节大小的内存空间的值,被设置0,作为循环的变量,条件就是不等于8,最后的free(Buffer),则是向操作系统声明解除对首地址为Buffer的20个字节的内存空间的占用。
下面是其编译后的汇编代码
汇编代码和我上面描述的差不多,比较惨的是,Buffer是一个char指针,本身是一个 段地址+偏移地址的 int far ,所以每次给他指向的内容赋值都是 xor bx,bx mov es,bx mov bx,0200h mov byte ptr es:[bx],xx 同时我们看到,当参数通过寄存器的方式传入函数之前,都会被push保存,出函数后,都是pop cx。。。
CBW指令
cbw指令是符号扩展指令,只对al起作用,什么是符号扩展指令呢?如比十进制8用8位有符号数表示,为00001000b 放在al里,对其使用cbw指令,会根据al的第7位(最高位)是0,给ah全0填充,这样ax是 00000000 00001000b 仍然是16位有符号数8。 再如十进制 -8 用8位有符号数表示,为11111000b 放在al里,使用 cbw ,根据al最高位为1,给ah全1填充,这样ax的值是 11111111 11111000b 仍然是16为有符号数 -8。 可见 CBW可以让根据al的值 设置ah 最终让ax和al表示同一个有符号数,但是位数扩展了
0x04 不用main函数编程
step1
f(){
*(char far *)(0xb8000000+160*10+80)='a';
*(char far *)(0xb8000000+160*10+80)=2;
}
该程序编译时无报错,连接时报错。
使用之前的link.exe对编译后的obj文件连接可以成功生成exe文件,对其进行debug:
发现与有main函数,由tc连接的exe文件不同,这个文件被载入内存后f函数在代码段开头,偏移为0,和我们之前的汇编有点类似,而tc编译连接的exe载入后main都在1fa偏移地址处
由于该程序f函数的偏移地址是0,直接被执行,但是之前没有call操作,所以最后ret时,必然时pop ip pop cs导致程序跳转错误,不能正确返回
通过link连接的f函数有541byte大小
step2
main(){
*(char far *)(0xb8000000+160*10+80)='a';
*(char far *)(0xb8000000+160*10+80)=2;
}
先将该代码直接用tc 编译 连接,
发现main函数的偏移地址为1fah
而且该main函数是被调用的,所以可以正确返回(ret)
通过tc编译链接的main函数有4200多byte大了好多。
step3
通过tc编译,但是用link连接,还是上面的main函数
大小和偏移地址又变回来了
step4
我们发现,通过tc连接时,需要main函数,没有就会报错,报错中提示cos module中’_main’找不到,我们写的代码在代码段有偏移,而且编译后的exe文件比link大很多,而link 一样的函数内容大小就相同,而且偏移都是0。所以,推测tc中的把c0s.obj和我们程序的obj连接在一起了,c0s中的代码要求有main,并且从main执行我们的代码
我们同样可以用汇编里的link单独对c0s.obj进行单独连接生产exe 并用debug查看其汇编代码
即使报错了,但是仍然生成了c0s.exe,debug观察其汇编代码,发现和tc编译连接生成的程序开头一样
step5
如何不使用main函数编写c语言程序
因此,我们自己编写的c0s.obj如下:
assume cs:code,ss:stack
stack segment
db 128 dup (0)
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,128
call s
mov ax,4c00h
int 21h
s: nop;执行完nop后会继续执行被链接的程序代码
code ends
end start
使用masm编译,然后替换minc目录下的c0s.obj,再在tc里尝试编译连接那个没有main的f函数
可以看到,基于我们自己给出的C0S.obj文件,我们成功编译连接了没有main函数的代码
并且生成的exe文件运行也正确
0x05 函数如何接受不定数量的参数
固定数量的参数
void showchar(char a,int b);
main(){
showchar('a',2);
}
void showchar(char a,int b){
*(char far *)(0xb8000000+160*10+80)=a;
*(char far *)(0xb8000000+160*10+81)=b;
}
对上述代码编译连接 然后debug到main函数里,发现在进入调用函数前,先把参数列表里的所有参数push进栈,然后出栈到cx,保持栈平衡,而在被调用的函数内部:
被调用的函数通过直接访问栈中的参数(需要清楚在栈中的位置,不难的,进入被调用的函数前先push了参数,再由call push ip 再进函数函数自己也push,那么可以推算出参数的位置)。
不定数量的参数
void showchar(int,int,...);
main(){
showchar(8,2,'a','b','c','d','e','f','g','h');
}
void showchar(int n,int color,...){
int a;
for(a=0;a!=n;a++){
*(char far *)(0xb8000000+160*10+80+a+a)=*(int*)(_BP+8+a+a);
*(char far *)(0xb8000000+160*10+81+a+a)=color;
}
}
上图是main函数里,参数被从右往左push进栈,下面则是被调用函数内部
参数2进栈->参数8进栈 ->ip进栈->bp进栈-> mov bx,sp 也就是ss:[bp] 执行的内容是bp自己 ,所以[bp+4]即使参数8 ,不定参数是通过让左边第一个参数记录参数个数来实现的,有些语言比如python或者js没有传递参数的个数,实际上是由解释器来记录并传递给被调用函数访问的