C语言进阶-汇编和C
9unk Lv5

案例:计算两个变量差的绝对值

一、简化代码的过程

16位汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
;程序名:test.asm
;功能:演示16位汇编如何转化成C语言

;数据定义:16位汇编 db dw dd
assume cs:code,ds:data

data segment
a dw -2
b dw -3
abs dw ? ;绝对值
data ends

code segment
start:
mov ax,data
mov ds,ax

mov ax,a
mov bx,b
cmp ax,bx
jl L1 ;为什么不是 jb L1

sub ax,bx
mov abs,ax
jmp L2
L1:
sub bx,ax
mov abs,bx
L2:
mov ax,4c00h
int 21h
code ends
end start

32位汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
;---------------------------
;程序名:test.asm
;功能:演示16位汇编如何转化成32位汇编
;作者:9unk
;编写时间:2023-8-28
;----------------------------

TITLE 演示程序

.386
.model flat,stdcall
option casemap:none

include kernel32.inc
includelib kernel32.lib
include masm32.inc
includelib masm32.lib

.data
a SDWORD -2
b SDWORD -3
abs DWORD ?

.code
main PROC
mov eax,a
mov ebx,b

.if eax<b
;
sub ebx,eax
mov abs,ebx
;
.else
;
sub eax,ebx
mov abs,eax
;
.endif

INVOKE ExitProcess,0
main ENDP
END main

编译器新增功能

  1. 段由完整定义(data segment...data ends)改为简化定义(.data)
  2. 32位汇编开始习惯使用 main PROC...main ENDP 来区别程序执行的主函数和其它函数。
  3. 变量定义出现了 “有符号变量” 和 “无符号变量”
  4. 将 cmp 指令简化为宏指令 .if.else.endif
  5. 根据定义的变量是否为有符号变量自动选择相应的 “转移指令”
  6. 为符合人类的常规逻辑来执行相应的语句块,使用 <>….等符号来表示大小等,实际汇编未达到这种效果需要使用的是相反的跳转指令。
  7. call 指令用 INVOKE 伪指令代替。
    1
    2
    3
    4
    5
    6
    7
    ;INVOKE ExitProcess,0
    push 0
    call ExitProcess

    相当于
    mov ax,4c00h
    int 21h

C语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*程序名:test.c*/
/*
演示32位汇编如何转为C语言
*/

#define _CRT_SECURE_NO_DEPRECATE
#include <time.h>
#include <stdio.h>

int main(void)
{
int a = -2;
int b = -3;
unsigned int abs;

if (a < b)
abs = b - a;
else
abs = a - b;

return 0;
}

编译器优化后的功能

  1. 在C语言中隐藏了程序的执行细节(消除了内存地址、寄存器和汇编指令),语法简化且详细,学习的内容。
  2. 有无符号的变量定义从 “SDOWRD” 变成 “int”,”DWORD” 变成 “unsigned int”。
  3. main PROC...main ENDP...END main 函数结构被 main(){...} 代替。
  4. .if...else...endif 语句,简化为 if...else 语句。

总结

  1. C语言是建立在汇编语言基础上,翻译成汇编语言,然后在机器上执行高级语言
  2. C语言比汇编语言隐藏了程序执行的细节,再就是语法规则比汇编语言规定的更详细。
  3. C语言保留了汇编语言操作内存的特性
  4. C语言更接近于人类语言,代码编写的效率大大提高,更有利于进行更大规模的软件开发。

这一切都是建立在更为强大的编译器的基础之上

二、指针和变量

32位汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
;---------------------------
;程序名:ys.asm
;功能:演示16位汇编如何转化成32位汇编
;作者:9unk
;编写时间:2023-8-28
;----------------------------

TITLE 演示程序

;.386
;.model flat,stdcall
;option casemap:none

;include kernel32.inc
;includelib kernel32.lib
;include masm32.inc
;includelib masm32.lib

INCLUDE Irvine32.inc

.data
a db 1
b dword 2,3,4,5,6
message byte "hello welcome to c !",0
temp dword 0

.code
main PROC
mov eax,0

;printf("%d\n", a);
movsx eax,[a]
call WriteInt
call Crlf

;printf("%p\n", &a);
lea eax,[a]
call WriteHex
call Crlf

;printf("%d\n\n", *(&a));
movsx eax,[a]
call WriteInt
call Crlf
call Crlf

;printf("%p\n", b);
mov eax,offset b
call WriteHex
call Crlf

;printf("%d\n", *b);
mov eax,[b+TYPE b*0]
call WriteInt
call Crlf

;printf("%d\n", *(b + 1));
mov eax,[b+TYPE b*1]
call WriteInt
call Crlf

;printf("%d\n", *(b + 2));
mov eax,[b+TYPE b*2]
call WriteInt
call Crlf

;printf("%p\n\n", &b);
lea eax,[b]
call WriteHex
call Crlf
call Crlf

;printf("%s\n", c);
mov edx,offset message
call WriteString
call Crlf

;printf("%c\n", *c);
mov al,[message+TYPE message*0]
call WriteChar
call Crlf

;printf("%c\n", *(c + 1));
mov al,[message+TYPE message*1]
call WriteChar
call Crlf

;printf("%c\n", *(c + 2));
mov al,[message+TYPE message*2]
call WriteChar
call Crlf

;printf("%p\n\n", &c);
lea eax,[message]
call WriteHex
call Crlf
call Crlf

INVOKE ExitProcess,0
main ENDP
END main

C语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*程序名:test.c*/
/*
演示32位汇编内存地址如何转为C语言指针
*/

#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>

int main(void)
{
int a = 1;
int b[5] = { 2,3,4,5,6 };
char c[] = "hello welcome to c!";

/*变量*/
printf("%d\n", a);
printf("%p\n", &a);
printf("%d\n\n", *(&a));

/*数组*/
printf("%p\n", b);
printf("%d\n", *b);
printf("%d\n", *(b + 1));
printf("%d\n", *(b + 2));
printf("%p\n\n", &b);

/*字符串*/
printf("%s\n", c);
printf("%c\n", *c);
printf("%c\n", *(c + 1));
printf("%c\n", *(c + 2));
printf("%p\n\n", &c);

return 0;
}

总结

  1. 汇编语言中变量a既表示地址,也表示地址存放的内容。
    C语言中:变量名a表示内存地址,&a表示地址

  2. 汇编语言中的数组名b既表示数组的起始地址,也表示该数组第一个元素的值。
    C语言中:数组名b表示地址,&b表示地址,*b表示数组的第一个元素值。

  3. 汇编语言中字符串变量名message表示该字符串起始地址,也表示字符串首字符。
    C语言中:字符串变量名c和&c都表示字符串数组的起始地址,*c表示字符串数组的第一个元素的值。

总结:C语言规定更为详细,将汇编语言中的地址和地址对应的内容做了区分;C语言中的指针就是内存地址,对指针的操作,就是直接对内存地址的操作。

三、函数与过程

汇编语言

寄存器传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
;---------------------------
;程序名:test2.asm
;功能:函数与过程-寄存器传参
;作者:9unk
;编写时间:2023-8-28
;----------------------------

TITLE 演示程序

INCLUDE Irvine32.inc

.data
a dword 5
b dword 6


.code

;函数原型声明
AddTwo PROTO,
x:DWORD,y:DWORD

main PROC
mov eax,a
mov ebx,b

INVOKE AddTwo,eax,ebx

call WriteInt
call Crlf

exit
main ENDP

;-------------------
;过程名:AddTwo
;功能:求和
;入口参数:eax,ebx
;出口参数:eax
;-------------------
AddTwo PROC USES ebx,
x:DWORD,y:DWORD
add eax,ebx
ret
AddTwo ENDP

END main

堆栈传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
;---------------------------
;程序名:test3.asm
;功能:函数与过程-堆栈传参
;作者:9unk
;编写时间:2023-8-28
;----------------------------

TITLE 演示程序

INCLUDE Irvine32.inc

.data
a dword 5
b dword 6


.code

;函数原型声明
AddTwo PROTO,
x:DWORD,y:DWORD

main PROC
INVOKE AddTwo,a,b

call WriteInt
call Crlf

exit
main ENDP

;-------------------
;过程名:AddTwo
;功能:求和
;入口参数:eax,ebx
;出口参数:eax
;-------------------
AddTwo PROC USES ebx,
x:DWORD,y:DWORD

mov eax,[ebp+8]
mov ebx,[ebp+12]
add eax,ebx

ret
AddTwo ENDP

END main

这里我尽量用伪代码,这样和C语言对比看起来更清晰。需要注意的是这里的伪代码对应的汇编是什么需要清楚。

C语言

有返回值函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*程序名:test2.c*/
/*
函数与过程
*/

#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>

int addtwo(int x, int y); //函数声明

int main(void)
{
int a = 5;
int b = 6;
int sum;

/*函数调用*/
sum = addtwo(a, b);
printf("a+b=%d\n", sum);

return 0; //程序结束
}

int addtwo(int x, int y)
{
return x + y;
}

无返回值函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*程序名:test3.c*/
/*
参数的传递:是否需要return返回值
*/

#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>

void addtwo(int* x, int* y, int* z); //函数原型声明

int main(void)
{
int a = 5;
int b = 6;
int sum;

/*函数调用*/
addtwo(&a, &b, &sum);
printf("a+b=%d\n", sum);

return 0; //程序结束
}

void addtwo(int* x, int* y, int* z)
{
*z = *x + *y;
}

总结

  1. 在汇编中函数的参数入栈顺序可由程序编写者定义,后续为了规范操作定义了函数的调用约定,以此来规范函数写法。

  2. 函数名即函数的地址,在call调用函数时会push压入call的下一条指令的地址。而函数中的ret指令,返回到之前压入的地址处,从而达到函数调用的效果。在后面编译器的优化中,call指令可使用伪指令INVOKE代替,但使用 INVOKE 伪指令时就需要使用伪指令 PROTO 声明函数原型。

  3. 利用 mov 指令将参数传到寄存器中,表示寄存器传参;利用push指令将参数传到堆栈中,表示堆栈传参。入栈n个参数,函数就需要 ret n*4 或者在call函数后添加 add esp,n*4,这就是堆栈平衡(保证堆栈执行后续代码时还原成当前函数执行之前的状态)。

四、创建多模块程序-外部函数与过程的调用

外部函数与过程的调用(传统汇编)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
;---------------------------------------------
;程序名:test4.asm
;功能:创建多模块程序-外部函数与过程的调用(传统汇编)
;作者:9unk
;编写时间:2023-8-28
;----------------------------------------------

TITLE 演示程序

INCLUDE Irvine32.inc

;如果是外部函数
extern AddTwo@0:PROC

AddTwo EQU AddTwo@0

.data
a dword 5
b dword 6

.code
;函数原型声明
;AddTwo PROTO,
; x:DWORD,y:DWORD

main PROC
mov eax,a
mov ebx,b

push eax
push ebx
call AddTwo@0

call WriteInt
call Crlf

exit
main ENDP
END main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
;---------------------------------------------
;程序名:_AddTwo.asm
;功能:创建多模块程序-外部函数与过程的调用(传统汇编)
;作者:9unk
;编写时间:2023-8-28
;----------------------------------------------
INCLUDE Irvine32.inc

PUBLIC a,b

.data
a dword 5
b dword 6

.code
;-------------------
;过程名:AddTwo
;功能:求和
;入口参数:eax,ebx
;出口参数:eax
;-------------------
AddTwo PROC USES ebx

mov ebp,esp

mov eax,[ebp+8]
mov ebx,[ebp+12]
add eax,ebx

mov esp,ebp
ret
AddTwo ENDP
END

知识点回顾:

  1. PUBLIC 可将变量和过程模块(函数)设为公共的,方便外部调用。其次,默认情况下masm中所有的过程名(函数)都是公共的,这使得同一程序的其他任意模块都可以调用这些过程。

  2. EXTERN 问指令用于在调用当前模块之外的过程时使用。它可以指定外部过程的名字和外部过程堆栈的大小。

  3. 过程名的后缀 @n 确定了已声明参数占用的堆栈空间总量。如果使用的是基本 PROC 伪指令,没有声明参数,那么 EXTERN 中的每个过程名后缀都为 @0。如果声明过程中使用的是带参数的扩展的 PROC 伪指令,那么每个参数占用 4 字节。

  4. EXTERN 访问外部变量:EXTERN [变量名]:[变量类型]

外部函数与过程的调用(高级汇编)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
;---------------------------------------------
;程序名:test5.asm
;功能:创建多模块程序-外部函数与过程的调用(高级汇编)
;作者:9unk
;编写时间:2023-8-28
;----------------------------------------------

TITLE 演示程序

;INCLUDE test.inc
INCLUDE Irvine32.inc

;函数原型声明
AddTwo PROTO,
x:dword,
y:dword,
z:dword

.data
a dword 5
b dword 6
sum dword ?

.code
;函数原型声明
;AddTwo PROTO,
; x:DWORD,y:DWORD

main PROC
INVOKE AddTwo,a,b,sum

call WriteInt
call Crlf

exit
main ENDP
END main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;---------------------------------------------
;程序名:_AddTwo.asm
;功能:创建多模块程序-外部函数与过程的调用(高级汇编)
;作者:9unk
;编写时间:2023-8-28
;----------------------------------------------
INCLUDE Irvine32.inc

.code
;-------------------
;过程名:AddTwo
;功能:求和
;入口参数:eax,ebx
;出口参数:eax
;-------------------
AddTwo PROC USES ebx,
x:DWORD,
y:DWORD,
z:DWORD

mov eax,[ebp+8]
mov ebx,[ebp+12]
add eax,ebx
mov z,eax

ret
AddTwo ENDP
END

知识点回顾:

  1. 多模模块程序可以使用 MASM 的高级伪指令 INVOKE、PROTO 和 扩展PROC伪指令创建。与使用call和EXTERN的传统方式相比,其主要优点在于能够匹配INVOKE伪指令中传递的参数列表和PROC伪指令声明的参数列表检查是否一致。

总结

C语言函数的外部调用和32位汇编使用高级伪代码进行外部调用是一样,只是C中更加人性化了,看起来更加简洁。

五、段的定义

汇编和C的段的定义

  1. 16位汇编:段的完整定义

  2. 32位汇编:段的简化定义

  3. C语言:段的简化定义

汇编和C的段的划分

  1. C语言中分为5个区域:堆区、栈区、代码区、静态区(全局变量),常量区(常量值,包括字符串)
  2. 对应汇编中的5个区域:堆区、栈区、代码区、初始化数据与未初始化数据区

汇编和C的各个段的区别

汇编语言:

  1. 堆区:程序员自己申请的内存分配,完成特定的任务,或者存放数据
  2. 栈区:系统分配的内存
  3. 代码区:代码
  4. 数据段(已分配)
  5. 未初始化的数据段BSS:未初始化全局变量,未初始化全局静态变量

C语言

  1. 堆区:一样
  2. 栈区:一样,局部变量,函数参数
  3. 代码区:代码
  4. 静态区:已初始化的全局变量、已初始化的全局静态变量、局部静态变量、常量数据、全局未初始化的变量、静态未初始化变量
  5. 常量区:字符串常量

堆区和栈区在汇编中统称为堆栈,这两个区没有分别就是一块内存。后面在 C 中将它们区分开来,在C中栈区主要用来存放局部变量(存放小的数据块),当我们个人需要一块大的内存时,我们就需要自己申请,这就是堆区。栈区是由系统来维护,而堆区是由程序员来维护。

  • 本文标题:C语言进阶-汇编和C
  • 本文作者:9unk
  • 创建时间:2023-08-28 16:21:00
  • 本文链接:https://9unkk.github.io/2023/08/28/c-yu-yan-jin-jie-hui-bian-he-c/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!