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

写在前面

在汇编课程中的实验中要求了我们在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文件:

我们打开文件,删除一些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就可以了。

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