2

80x86下汇编与C语言的混合编程

 3 years ago
source link: https://www.viseator.com/2018/07/02/80x86_asm_c/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在汇编课程中的实验中要求了我们在80x86下实现C语言与汇编代码的混合编程,虽然80x86时代离现代有些久远,但我们仍可以把80x86当作x86的一个简化版本来学习一些重要的概念。

从一个例子开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
extern int test_fun(void *param);
extern int var_from_asm;
int global_init = 10;
int global;
static int static_init = 1000;
static int static_var;
char *string = "test string";
int main() {
int value = 666;
test_fun(&value);
printf("var from asm:%d\n", var_from_asm);
printf("assign from asm:%d\n", global);
printf("value passed by stack:%d\n", global_init);
return 0;
}

可以看到我们在C语言中分别定义了几种类型的变量:初始化过的全局变量、未初始化的全局变量、初始化过的静态变量、未初始化的静态变量与字符串。定义这些变量是为了查看编译后各变量所处的数据段与存放形式。

同时也声明了一个外部函数test_fun()与一个外部变量var_from_asm,用来测试C语言对asm声明的符号的引用。

现在我们编译这个C语言文件到汇编文件,使用TC2.0的命令行工具tcc,使用-S参数,即可在同目录生成同名的ASM文件:

8086asmc1.png

我们打开文件,删除一些debug信息后的结果如下:

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
102
103
104
105
106
107
108
109
110
111
112
_TEXT	segment byte public 'CODE'
_TEXT ends
DGROUP group _DATA,_BSS
assume cs:_TEXT,ds:DGROUP
_DATA segment word public 'DATA'
_DATA ends
_BSS segment word public 'BSS'
_BSS ends
_DATA segment word public 'DATA'
_global_init label word
db 10
db 0
static_init label word
db 232
db 3
_string label word
dw DGROUP:s@
_DATA ends
_TEXT segment byte public 'CODE'
;
; int main() {
;
assume cs:_TEXT
_main proc near
push bp
mov bp,sp
sub sp,2
;
; int value = 666;
;
mov word ptr [bp-2],666
;
; test_fun(&value);
;
lea ax,word ptr [bp-2]
push ax
call near ptr _test_fun
pop cx
;
; printf("var from asm:%d\n", var_from_asm);
;
push word ptr DGROUP:_var_from_asm
mov ax,offset DGROUP:s@+12
push ax
call near ptr _printf
pop cx
pop cx
;
; printf("assign from asm:%d\n", global);
;
push word ptr DGROUP:_global
mov ax,offset DGROUP:s@+29
push ax
call near ptr _printf
pop cx
pop cx
;
; printf("value passed by stack:%d\n", global_init);
;
push word ptr DGROUP:_global_init
mov ax,offset DGROUP:s@+49
push ax
call near ptr _printf
pop cx
pop cx
;
; return 0;
;
xor ax,ax
jmp short @1@58
@1@58:
;
; }
;
mov sp,bp
pop bp
ret
_main endp
_TEXT ends
_BSS segment word public 'BSS'
static_var label word
db 2 dup (?)
_global label word
db 2 dup (?)
_BSS ends
_DATA segment word public 'DATA'
s@ label byte
db 'test string'
db 0
db 'var from asm:%d'
db 10
db 0
db 'assign from asm:%d'
db 10
db 0
db 'value passed by stack:%d'
db 10
db 0
_DATA ends
_TEXT segment byte public 'CODE'
_TEXT ends
public _main
public _string
_static_var equ static_var
_static_init equ static_init
public _global
public _global_init
extrn _var_from_asm:word
extrn _test_fun:near
extrn _printf:near
_s@ equ s@
end

首先可以发现几个数据段:

1
2
3
_TEXT	segment byte public 'CODE'
_DATA segment word public 'DATA'
_BSS segment word public 'BSS'

是不是很熟悉?虽然是80386,但是现代ELF中仍可以见到这几个节的身影。
DGROUP group _DATA,_BSS DGROUP表示_DATA_BSS段合成的段标号。
分析这几个段中的内容,可以发现_TEXT段即运行时的CS段,存放着代码。
_DATA段中存放着下列内容:

1
2
3
4
5
6
7
8
_global_init	label	word
db 10
db 0
static_init label word
db 232
db 3
_string label word
dw DGROUP:s@

前两个即已经初始化的全局变量与静态变量。第三个是一个别名,找到它的定义:

1
2
3
4
5
6
7
8
9
10
11
12
s@	label	byte
db 'test string'
db 0
db 'var from asm:%d'
db 10
db 0
db 'assign from asm:%d'
db 10
db 0
db 'value passed by stack:%d'
db 10
db 0

我们不但在s@处发现了字符串常量test string,而且发现这里存放着printf中使用的格式化字符串。

_BSS(意为Block Started by Symbol)中存放着为尚未初始化或初始化为零的全局或静态变量预留的空间,在这里,它存放着如下内容:

1
2
3
4
static_var	label	word
db 2 dup (?)
_global label word
db 2 dup (?)

你可能已经发现,我们在C语言中定义的变量在生成的汇编代码中被加上了下划线前缀,其实不光是变量名,函数名也会被编译器做相同的处理:

1
_main	proc	near

在文件的末尾还有如下内容:

1
2
3
4
5
6
7
8
9
10
	public	_main
public _string
_static_var equ static_var
_static_init equ static_init
public _global
public _global_init
extrn _var_from_asm:word
extrn _test_fun:near
extrn _printf:near
_s@ equ s@

可以看到使用public关键字声明了全局变量stringglobalglobal_init与函数main,以便外部去引用他们。同时也使用extrn关键字声明了外部定义的_var_from_asm_test_fun_printf,在链接时会解析这些标记完成偏移地址的修改。

到这里我们已经分析完了test.c编译后的内容。

下面是测试使用的汇编程序t.asm

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
public _test_fun
public _var_from_asm
extrn _global:byte, _global_init:byte

_DATA segment use16 word public 'DATA'
_var_from_asm label word
db 10
db 0
_DATA ends

_TEXT segment use16 byte public 'CODE'
assume CS:_TEXT, DS:_DATA

_test_fun proc near
push bp
mov bp, sp
push di
mov ax, 6[bp]
mov word ptr _global_init, ax
mov word ptr _global, 100
pop di
pop bp
ret
_test_fun endp
_TEXT ends
end

程序比较简单,在头部同样地声明了对外的符号与引用的外部的符号。

同时,为了实现与C程序的相互引用,我们使用相同的标识定义了_DATA段与_TEXT段,为了使C语言可以以var_from_asm的形式使用汇编中定义的变量,所以在_DATA段中使用_var_from_asm声明了两个字节并初始化为10的空间。

同理,在_TEXT段中使用_test_fun作为子程序名定义了函数test_fun

而在test_fun中,我们先将bp入栈,将sp赋给bp后,将bp+6位置的值赋给了ax,在函数调用的时候,会先将参数入栈,然后将CSIP入栈,占用了4个字节的栈空间,函数内调用push bp又占用了2个字节的栈空间,所以传入的参数应该在bp+6的位置上。我们将这个参数值写回到C语言定义的全局变量_global_init中,下一行将_global赋上了100

现在,就可以进行编译链接步骤了。

一般地,我们可以使用C编译器编译test.c生成目标文件,使用TASM汇编编译器编译t.asm生成目标文件,再使用tlink将生成的目标文件与库提供的目标文件进行链接,但是这样做略显麻烦,tcc会调用TASM编译汇编文件,也会将生成的目标文件和库文件一起链接并生成最后的可执行文件,所以我们只要简单地执行tcc test.c t.asm就可以了。

8086asmc2.png

直接运行生成的test.exe,查看结果:

8086asmc3.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK