前言:最近学习了汇编语言,想通过反汇编TC2.0的程序,了解C语言的各项元素到底是如何实现的。先做TC2.0的研究,DOS下的C语言研究透彻了。打好基础,学完Windows编程后,再对VC6的东西进行反汇编。 变量:C语言的局部变量一般定义在堆栈中,不同的类型占用不同的字节数。 以下定义了一个int整型变量: push bp 将bp寄存器压栈,在后续程序中做为基址寄存器使用。 mov bp, sp sub sp, 2 在栈中分配空间存储局部变量 mov [bp-2], 100h为局部变量赋值 mov sp, bp 恢复sp寄存器,将局部变量占用堆栈恢复,之前不能改变bp寄存器内容,否则程序无法正常返回 pop bp retn 弹出ip寄存器内容,函数返回 a) 整型变量:long 之外其他的整型,均分配16位,占用两个字节;long整型占用32位,占用四个字节,一般会放在ax,dx寄存器中。 b) 浮点型变量:比较复杂,暂未分析。 c) 字符变量:使用8位(即一个字节)存储字符,但C语言编译器在栈中分配2个字节的存储空间(或许是因为分配两个字节效率高)。 |
C语言程序: int a = 0x100; int b = 0x200; int c,d,e,f,g; c = a + b; d = b - a; e = a * b; f = b / a; g = b % a; 对应的汇编语句: push bp mov bp, sp sub sp, 0Ah 分配堆栈 push si 保存函数中要用到的寄存器,C语言偏向使用si,di寄存器 push di mov si, 100h mov di, 200h mov ax, si add ax, di mov [bp-8], ax c = a + b mov ax, di sub ax, si mov [bp-6], axd = b - a mov ax, si mul di mov [bp-4], axe = a * b mov ax, di cwd idiv si mov [bp-2], ax f = b / a mov ax, di cwd idiv si mov [bp-0Ah], dxg = b % a pop di pop si 恢复函数中使用的寄存器 mov sp, bp 恢复堆栈指针 pop bp retn 返回程序pop ip 呵呵。。。写的比较简单,但是基本上反应了C语言编译器的意图。确实很佩服写TC2.0编译器的那些前辈,反汇编的代码精炼的很。C语言的执行效率就是通过简练的汇编代码保证的。 |
C语言程序: int a = 0x100; int b = 0x200; int c = 0x300; int d = a * b + c + 'a'; 对应的汇编语句: push bp mov bp, sp sub sp, 4 push si push di mov si, 100h mov di, 200h mov [bp-4], 300h 变量C mov ax, si mul di a * b add ax, [bp-4] a * b + c add ax, 61h ; 'a' a * b + c + ‘a’ mov [bp-2], ax 将计算结果赋给d变量 pop di pop si mov sp, bp pop bp retn 写的都比较简单,但是原理都是一样的。 |
C语言程序: int x = 1; int y = 10; int i = 0x99; x++; x = x + 1; y--; y = y - 1; i += 1; i += 2; 对应的汇编语句: push bp mov bp, sp sub sp, 2 push si push di mov si, 1 变量x mov di, 0Ah 变量y mov [bp-2], 99h 变量i inc si x++ mov ax, si inc ax mov si, ax x = x + 1 dec di y-- mov ax, di dec ax mov di, ax y = y - 1 inc [bp-2] i += 1 add [bp+var_2], 2i +=2 pop di pop si mov sp, bp pop bp retn 后续还有更深入一些的研究。 |
1、大于号的使用 C语言程序: int x = 1; int y = 10; if( x > y) x = 0x10; else y = 0x100; 对应的汇编语句: push bp mov bp, sp push si push di mov si, 1 mov di, 0Ah x=1;y=10对x,y赋值 cmp si, di 比较x,y jle short loc_100A6小于或等于转移至y=0x100 mov si,10h jmp short loc_100A9跳至返回部分代码 loc_100A6: mov di, 100h loc_100A9: pop di pop si pop bp retn 2、大于等于号的使用 C语言程序: int x = 1; int y = 10; if( x >= y) x = 0x10; else y = 0x100; 汇编关键部分: cmp si, di jl short loc_100A6小于转移至y=0x100 mov si, 10h jmp short loc_100A9 loc_100A6: mov di, 100h loc_100A9: pop di pop si pop bp retn 3、小于号的使用 C语言部分 if( x < y) x = 0x10; else y = 0x100; 汇编关键部分: cmp si, di jge short loc_100A6大于或等于转移至y=0x100 mov si, 10h jmp short loc_100A9 loc_100A6: mov di, 100h loc_100A9: pop di pop si pop bp retn 4、小于等于的使用 C语言部分 if( x <= y) x = 0x10; else y = 0x100; 汇编关键部分: cmp si, di jg short loc_100A6大于转移至y=0x100 mov si, 10h jmp short loc_100A9 loc_100A6: mov di, 100h loc_100A9: pop di pop si pop bp retn 5、等于的使用 C语言部分 if( x == y) x = 0x10; else y = 0x100; 汇编关键部分: cmp si, di jnz short loc_100A6不等于则转移至y=0x100 mov si, 10h jmp short loc_100A9 loc_100A6: mov di, 100h loc_100A9: pop di pop si pop bp retn 6、不等于的使用 C语言部分 if( x != y) x = 0x10; else y = 0x100; 汇编关键部分: cmp si, di jz short loc_100A6等于则转移至y=0x100 mov si, 10h jmp short loc_100A9 loc_100A6: mov di, 100h loc_100A9: pop di pop si pop bp retn 小结:综上所述,TC编译器将关系运算符转变为相反的关系运算,保持了源程序的顺序,即相反的比较跳转至else部分,而正常的跳转至if后语句。 |
一、逻辑运算符: 1、与运算符的使用 C语言程序: int x = 1; int y = 10; if( x && y) x = 0x10; else y = 0x100; 对应的汇编语句: push bp mov bp, sp push si push di mov si, 1 mov di, 0Ah or si, si 求或运算,若为零则跳转至y=0x100 jz short loc_100AA or di, di 求或运算,若为零则跳转至y=0x100 jz short loc_100AA mov si, 10h jmp short loc_100AD loc_100AA: mov di, 100h loc_100AD: pop di pop si pop bp retn 注意:与运算时,只要有一个为0则结果就为0,所以判断x是否为0,为0则跳转至y=0x100;若不为零,再判断y是否为0,若为零则亦跳转至y=0x100。 2、或运算符的使用 C语言程序: int x = 1; int y = 10; if( x || y) x = 0x10; else y = 0x100; 对应的汇编语句: push bp mov bp, sp push si push di mov si, 1 mov di, 0Ah or si, si jnz short loc_100A5 求或运算,若不为零则跳转至x=0x10 or di, di jz short loc_100AA 求或运算,若为零则跳转至y=0x100 mov si, 10h jmp short loc_100AD loc_100A5: mov si, 10h jmp short loc_100AD loc_100AA: mov di, 100h loc_100AD: pop di pop si pop bp retn 注意:求或运算时,先判断x是否为0,不为0则直接x=0x100;若为0,则再判断y是否为0,为0则直接y=0x100。 3、非运算符的使用 C语言程序: int x = 1; int y = 10; if(!y) x = 0x10; else y = 0x100; 对应的汇编语句: push bp mov bp, sp push si push di mov si, 1 mov di, 0Ah or di, di jnz short loc_100A6若y不为零则执行y=0x100 mov di, 10h 若为零则执行x=0x10 jmp short loc_100A9 loc_100A6: mov si, 100h loc_100A9: pop di pop si pop bp retn 注意:求非运算时,先判断y是否为0,若不为0非y后,结果为假,则直接y=0x100,不为0则直接x=0x100。 综上所述:可以看到 TC 编译器,处理逻辑运算符时,先判断左边第一个参数,后判断接下来的参数,所以在使用时应该将最有可能出现的情况放在左边做判断,效率会更高一些。 |
If语句 C语言程序: int x = 1; int y = 10; if( x > y) x=0x10; else { if(x==y) y = 0x100; else y =0x102; } 汇编程序: push bp mov bp, sp push si push di mov di, 1 mov si, 0Ah cmp di, si jle short loc_100A6若x<=y则跳转至loc_100A6进入else部分 mov di, 10h x=0x10 jmp short loc_100B2 loc_100A6: cmp di, si jnz short loc_100AF mov si, 100h jmp short loc_100B2 loc_100AF: mov si, 102h loc_100B2: pop di pop si pop bp retn |
Switch语句 C语言程序: int x = 1; int y = 10; switch(x) { case 1:y = 0x100; case 2:y = 0x101; case 3:y = 0x102; default:y = 0x110; } 汇编程序: push bp mov bp, sp push si push di mov di, 1 mov si, 0Ah mov ax,1 将di(即变量x)赋给ax cmp ax,1 case 1,x与1比较 jz short loc_100B0 y=0x100 cmp ax,2 case 2,x与2比较 jz short loc_100B3 y=0x101 cmp ax,3 case 3,x与3比较 jz short loc_100B6 y=0x102 jmp short loc_100B9y=0x110 loc_100B0:mov si,100h loc_100B3:mov si,101h loc_100B6:mov si,102h loc_100B9: mov si,110h pop di pop si pop bp retn 总结:switch语句将会转变为类似多个if语句的结构。以下为上面的switch语句对应的C语言if语句描述,若不加break语句,则每条if语句顺序执行。 If(x==1) y=0x100; If(x==2) y=0x101; If(x==3) y=0x102; Y=0x103; |
C语言的各种循环语句 1、While循环 C语言程序: int x = 1; while(x<10) { x++; } 对应的汇编语句: push bp mov bp, sp push si mov si, 1 jmp short loc_1009C loc_1009B: inc si loc_1009C: cmp si,0Ah 判断x是否大于10 jl short loc_1009B小于则跳至inc si,x++ pop si pop bp retn 理解:先判断while语句后的条件,若是真,则执行后续语句,若为假则退出。 2、for循环 C语言程序: int x = 0; int i; for(i=0;i<10;i++) { x++; } 对应的汇编语句: push bp mov bp, sp push si push di xor di,di 设置x=0 xor si,si 设置i=0 jmp short loc_1009F loc_1009D: inc di inc si loc_1009F: cmp si,0Ah 比较i是否大于10 jl short loc_1009D pop di pop si pop bp retn 理解:先判断条件,若条件为真,则执行i++,执行下列语句;若条件为假,则退出循环。 3、do while循环 C语言程序: int x = 1; do { x++; } while (x<10); 对应的汇编语句: push bp mov bp, sp push si mov si,1 loc_10099: inc si cmp si,0Ah jl short loc_10099 pop si pop bp retn 理解:先执行后续语句,后判断条件。换句话说,后续语句最少可以执行一次。 4、break,continue语句 理解: break 语句用于 switch 或循环中,退出循环; continue 语句用于结束此次循环。 |
数组的定义使用 C语言程序: int x[] = {1,2}; int i = x[0]+x[1]; 汇编程序代码: Pushbp movbp, sp subsp, 6 用到三个局部变量,其中数组2个,所以分配6个字节的堆栈空间 push ss lea ax, [bp-6] pushax push ds movax, 194h push ax 以上压栈操作给子程序传递参数 movcx, 4 数组占用字节长度 callSCOPY@注意:此处为远调用,压入CS、IP寄存器 movax, [bp-6]ax寄存器内容x[1] addax, [bp-4] x[0]+x[1] mov[bp-2], ax 将结果付给[bp-2],即变量i movsp, bp popbp retn 以上语句为返回子程序 子程序SCOPY@ pushbp movbp, sp pushsi pushdi pushds 保存子程序中用到的寄存器 lds si, [bp+6] ds:si为调用程序中的DS,Ax的值,初始化字符串复制源地址 les di, [bp+0Ah]es:di为堆栈空间的地址 cld shr cx, 1 rep movsw 将数据段中定义的数组内容复制到堆栈中分配的空间 adccx, cx rep movsb 此处有疑问,不太清楚。 popds popdi popsi popbp retf8 堆栈平衡,清空子程序调用前压入的参数。 总结:C语言处理数组是通过在数据段定义数组内容,并且一般以16字节为单位,若不够则用“0”补齐,然后将DS及数组偏移地址,SS及堆栈开始地址压入堆栈供子程序调用,子程序负责将数据段中定义的数组内容复制到堆栈中。 一定要注意复制数组内容的子程序为远调用,曾经在此处分析时,出现问题。 多维数组其实就是按行存放,先放第一行的元素,再存放第二行的元素。 例:int i[2][2]内存中为:i[0][0],i[0][1],i[1][0],i[1][1]。 |
这个的内容比较多,有点重量级。呵呵。。 C语言源码: int add(int,int); fun() { int a = 0x10; int b = 0x100; int c = add(a,b); } int add(int a,int b) { int c; c = a + b; return c; } 汇编程序代码: push bp movbp, sp 压栈bp,bp寄存器将在后续部分作为基址寄存器使用 subsp, 2 在堆栈中分配空间给变量C push si push di movsi, 10h 变量a=0x10 movdi, 100h 变量b=0x100 push di push si 压栈b,a做为参数传递给add函数 callsub_100B0 popcx popcx 平衡堆栈,pop cx机器指令较短,执行效率高,相当于add sp,4 mov[bp-2], ax将返回值付给变量C,C语言中通过ax寄存器返回函数值 popdi popsi movsp, bp popbp retn 至此函数fun返回 sub_100B0: push bp movbp, sp push si movsi, [bp+4]c = a addsi, [bp+6]c = a + b movax, si 将结果传给ax寄存器 popsi popbp retn add子程序返回 总结:函数进入时基本上都有push bp,mov bp,sp语句,此两条语句将bp寄存器压栈,过后做为基址寄存器使用。 调用函数时,先将参数从右到左依次压栈,执行call指令时,将ip压栈,接着将bp压栈,所以在16位的CPU中,取参数时,bp+4取得第一个参数,bp+6取得第二个参数,依次类推。局部变量使用使用sub sp,2指令在堆栈中开辟空间存储,所以取局部变量时,通过bp-2取得第一个局部变量,bp-4取得第二个局部变量,函数若有返回值一般通过ax寄存器返回。最后要执行mov sp,bp,pop bp指令平衡局部变量占用的堆栈空间,实际的参数并未清空。Ret后弹出ip值,返回调用程序执行的地址。 一般情况下由调用函数的程序平衡堆栈,所以上例pop cx执行两次,相当于add sp,4平衡堆栈。但数组中传送字符串的函数使用了ret n,由子程序本身进行了平衡堆栈的工作。 从上可以看到,在调用函数时,调用参数并未在子函数中更改,即在子函数中使用了参数值,但并未真正更改参数的值。使用指针做为参数时的情况,后续再研究。 |
C语言源码: void change(int[]); fun() { int a[] = {1,2}; add(a); } void change(int a[]) { int tmp; tmp=a[0]; a[0]=a[1]; a[1]=tmp; } 汇编代码: push bp movbp, sp subsp, 4 ;开辟堆栈空间,保存数组数据 push ss lea ax, [bp-4] push ax ;将es:di压栈 movax,0 push ax push ds ;将ds:si压栈 movcx,4 ;设置数组数据所占字节数,即需复制字节数 callsub_100C7 lea ax, [bp-4] ;此命令就是将数组的首地址传给调用的函数 push ax ;将数组的首地址压栈传递给子程序change callsub_100C6 popcx ;平衡堆栈 movsp, bp popbp retn sub_100C6: ;change函数 push bp movbp, sp push si push di ;tmp局部变量 movsi, [bp+4] ;将数组首地址读入si寄存器 movdi, [si] ;si保存的是一个地址,这个地址中存储就是a[0],即指针tmp = a[0] movax, [si+2] ;ax = a[1] mov[si], ax ;a[0]=ax a[0]=a[1]通过ax寄存器中转 mov[si+2],di ;a[1]=tmp popdi popsi popbp retn sub_100C7: ;复制数据段数组数据至开辟的堆栈空间中 push bp movbp, sp push si push di push ds lds si, [bp+6] les di, [bp+0Ah] cld shr cx, 1 rep movsw adccx, cx rep movsb popds popdi popsi popbp retf8 总结:传递数组其实就是传递数组的首地址,可以理解为传递的是数组指针,对数组的操作,将会影响数组中的数据。 |
C语言源码: int a = 0x99; int g = 0x100; fun() { int a = 0x10; g = a; fun1(); } fun1() { int c = 0x11; a = c; g = a; } 汇编代码: push bp mov bp, sp push si mov si, 0010 ;局部变量int a = 0x10; mov ds:[0002], si ;对全局变量赋值g = a; call 0023 pop si pop bp ret push bp mov bp, sp push si mov si, 0011 ;局部变量int c = 0x11; mov ds:[0000], si ;对全局变量赋值a = c; mov ax, word ptr ds:[0000] mov word ptr ds:[0002], ax ;以上两句完成g = a的操作 pop si pop bp ret 总结:C语言将局部变量存储于堆栈中,在函数结束时自动清除,全局变量存储于定义的数据段中,在程序中全局变量定义后的函数中均可访问。 若定义了全局变量和局部变量,在子函数中若存在与全局变量同名的局部变量,则使用子函数中的局部变量,而不使用全局变量。 |
静态、动态存储变量的区别 C语言源码: fun() { int i = 0x100; static int c = 0x200; i = c; } 汇编代码: push bp movbp, sp push si movsi, 100h ;定义动态变量i = 0x100 movsi, ds:[0000];对静态变量赋值i = c;ds:[0000]内存储的是静态变量C popsi popbp retn 总结: 其实静态变量类似于全局变量,实现方式类似。 |
指针的定义及存储方式 C语言源码: f() { int i = 0x100; int *p; p= &i; } 汇编代码: enter 4, 0 ;分配四个字节空间存储局部变量 mov[bp-4], 100h ;int i = 0x100 lea ax, [bp-4] ;将变量i的地址赋给ax mov[bp-2], ax ;int *p=&i leave retn 总结:指针即地址,指针变量存储的是i的地址。 C语言源码: f() { int a = 0x100; int b = 0x200; int *p,*p1,*p2; p1 = &a; p2 = &b; p = p1; p1 = p2; p2 = p; } 汇编代码: enter 6, 0 push si push di mov[bp-6], 100h ;变量a int a = 0x100; mov[bp-4], 200h;变量b int b = 0x200; lea si, [bp-6] ;变量p1 int *p1 = &a,现si中存储的是变量a的地址 lea di, [bp-4] ;变量p2 int *p2 = &b,现di中存储的是变量b的地址 mov[bp-2], si ;p = p1 movsi, di ;p1 = p2 movdi, [bp-2] ;p2 = p1 popdi popsi leave retn 总结: lea 命令是取地址的操作命令。 |
C语言源码: void swap(int *p1,int *p2); f() { int a = 0x100; int b = 0x200; int *p1 = &a; int *p2 = &b; swap(p1,p2); } void swap(int *p1,int *p2) { int p; p = *p1; *p1 = *p2; *p2 = p; } 汇编代码: enter 4, 0 push si push di mov[bp-4], 100h ;局部变量aa = 0x100 mov[bp-2], 200h ;局部变量bb = 0x200 lea si, [bp-4] ;局部变量p1 p1 = &a lea di, [bp-2] ;局部变量p2 p2 = &b push di ;将p2压栈 push si ;将p1压栈 callsub_100B3 ;调用函数swap popcx popcx ;堆栈平衡 popdi popsi leave retn sub_100B3: push bp movbp, sp push si ;变量p movbx, [bp+4] ;取得变量p1,bx中现存放的是地址 movsi, [bx] ;取得*p1,p = *p1 movbx, [bp+6] ;取得变量p2,bx中现存放的是地址 movax, [bx] movbx, [bp+4] mov[bx], ax ;以上三句完成*p1 = *p2 movbx, [bp+6] mov[bx], si ;以上两句完成*p2 = p popsi popbp retn 总结:函数参数为指针,即将指针即地址压入栈中,传递给函数。从上可以看到,函数指针参数并未发生变化,但指针指向的内存空间发生了变化。 |
C语言代码: fun() { int i[] = {'a','b'}; int *p = i; int i1= *++p; } 汇编代码: enter 6,0 ;开辟空间存储数组元素 push si push ss lea ax,[bp-6] push ax push ds mov ax,0 push ax mov cx,4 call 000A:0029 ;以上为数组元素复制函数准备参数 lea si,[bp-6] ;将数组首地址赋给si。即int p* = i inc si inc si ;以上两句完成++p mov ax,[si] mov [bp-2],ax ;以上两句完成int i1= *++p; pop si leave ret C语言代码: fun() { char c[] = {'a','b'}; char *p = c; char c1= *p++; } 汇编代码: enter 4,0 ;开辟空间存储数组元素 push si push ss lea ax,[bp-4] push ax push ds mov ax,0 push ax mov cx,2 call 000A:0029 ;以上为数组元素复制函数准备参数 lea si,[bp-4] ;将数组首地址赋给si。即char p* = c mov al,[si] ;[bp-3]中存放字符串'b',[bp-2]未使用 mov [bp-1],al ;以上两句完成char c1= *p inc si ;p++ pop si leave ret 总结:以上均省略了数组元素复制的代码。通过以上代码可以发现C语言编译器可以根据定义的指针的类型,对++操作进行指定的字节后移,char类型占用一个字节,则p++对应为inc si;int类型占用两个字节,则p++对应为inc si两次。 指针多使用lea命令将地址赋值给相应的指针变量。另外可以看到p++,++p的差别,p++是先操作后加,++p为先加后操作。 |
C语言代码: fun() { char *c = "I love you!"; char *c1 = "I love you!"; } 汇编代码: push bp movbp, sp subsp, 4 mov[bp-4], 0 ;char *c = "I love you!" mov[bp-2], 0Ch ;char *c1 = "I love you!" movsp, bp popbp retn 总结:字符指针初始化时,将字符存于数据段中,而只给字符指针一个引用地址。字符数组必须定义为static,其实也是存于数据段中。 |
C语言代码: main() { int max(); int (*p)(); int a,b,c; a = 0x10; b = 0x20; p = max; c = (*p)(a,b); } max(int a,int b) { int z; if(a > b) z = a; else z = b; return b; } 汇编代码: push bp movbp, sp subsp, 4 ;分配变量c和b。其中c-[bp-2],b-[bp-4] push si push di movdi, 10h ;变量a = 0x10 mov[bp-4], 20h ;变量b = 0x20 movsi, 21Eh ;p = max即函数的地址,此处21Eh为函数的地址 push [bp-4] push di callsi ; sub_1021E ;压栈b,a跳转至si指向的函数的地址 popcx popcx ;堆栈平衡 mov[bp-2], ax ;c = (*p)(a,b),一般函数通过ax返回值。 popdi popsi movsp, bp popbp retn sub_1021E: ;max函数开始,地址为21Eh push bp movbp, sp push si push di movsi, [bp+6] ;变量b movax, [bp+4] ;变量a cmpax, si ;if语句开始 jle short loc_10232 ;a <= b则z = b,否则z = a movdi, [bp+4] ;z = a jmpshort loc_10234 loc_10232: movdi, si ;z = b loc_10234: movax, si ;将返回值赋给ax寄存器 popdi popsi popbp retn 总结:函数的指针其实就是函数的开始地址。注意函数的参数必须一致,否则在调用实际的函数时,就会出现问题。 另外:C语言编译器发现局部变量较少时,使用di,si寄存器存储局部变量,这样可以提高程序的运行速度。 |