循环引用?在系统调用的实现内部发现了包含此系统调用的中间函数:符号解析,c语言头文件与makefile在声明函数签名与选择同签名函数的不同实现上的作用

问题复现

在MIT 6.828 Lab3中,需要在syscall.c中实现OS的系统调用功能。在熟悉源码时,我发现一个名为cprintf的函数调用让人非常疑惑:

// Print a string to the system console.
// The string is exactly 'len' characters long.
// Destroys the environment on memory errors.
static void
sys_cputs(const char *s, size_t len) {
      cprintf("%.*s", len, s);
}

对于实现系统调用来说,OS在实际的系统调用实现中应该直接与底层硬件进行互动,而非调用一个抽象层次较高的格式化输出函数。也许是这个函数内部实现了底层IO功能?通过头文件中的声明,查看cprintf函数实现的位置:

// lib/printf.c
int	cprintf(const char *fmt, ...);
int	vcprintf(const char *fmt, va_list);

查看lib/printf.c中的cprintf函数的实现:

// Implementation of cprintf console output for user environments,
// based on printfmt() and the sys_cputs() system call.
static void
putch(int ch, struct printbuf *b)
{
	b->buf[b->idx++] = ch;
	if (b->idx == 256-1) {
		sys_cputs(b->buf, b->idx);
		b->idx = 0;
	}
	b->cnt++;
}

int
vcprintf(const char *fmt, va_list ap)
{
	struct printbuf b;

	b.idx = 0;
	b.cnt = 0;
	vprintfmt((void*)putch, &b, fmt, ap);
	sys_cputs(b.buf, b.idx);

	return b.cnt;
}

int
cprintf(const char *fmt, ...)
{
	va_list ap;
	int cnt;

	va_start(ap, fmt);
	cnt = vcprintf(fmt, ap);
	va_end(ap);

	return cnt;
}

什么情况,头部注释说cprintf的实现使用了sys_cputs的系统调用,但是sys_cputs本身就调用了这个函数。出现了符号循环引用,但是编译器没有报错。系统设计角度上,系统调用的实现不应该包含一个更高层次的抽象;程序运行上,编译器没有报错。这说明可能是在程序编译链接方面欠缺了知识。

项目Makefile

在查找资料之前,我在这部分项目文件的Makefile中找到了这样一段代码:

KERN_SRCFILES :=	kern/entry.S \
			kern/entrypgdir.c \
			kern/init.c \
			kern/console.c \
			kern/monitor.c \
			kern/pmap.c \
			kern/env.c \
			kern/kclock.c \
			kern/picirq.c \
			kern/printf.c
		
...

# How to build kernel object files
# 内核目标文件都由内核目录中文件构建
$(OBJDIR)/kern/%.o: kern/%.c $(OBJDIR)/.vars.KERN_CFLAGS

首先,发现在/kern目录下也有printf.c这个文件,并且包含签名完全相同的cprintf函数,其中实现与目录/lib不同,更加底层:

static void
putch(int ch, int *cnt)
{
	cputchar(ch);
	*cnt++;
}

...

void
cputchar(int c)
{
	cons_putc(c);
}

...

// output a character to the console
static void
cons_putc(int c)
{
	serial_putc(c);
	lpt_putc(c);
	cga_putc(c);
}

其次,在makefile中规定了内核目标文件的构建方式,即符号定义都来源于/kern目录下的源码文件,那么系统调用中的函数使用了kern/printf.c中的实现,而非头文件中lib/printf.c的函数实现。

链接技术角度解释

在C语言的项目中,如果包含了声明了函数原型的头文件,但是在多个目录中都存在该函数的实现(定义),最终链接时使用哪一个实现取决于在编译链接过程中的指定。如上文提到的makefile中的一行:

$(OBJDIR)/kern/%.o: kern/%.c $(OBJDIR)/.vars.KERN_CFLAGS

通过这样的方式,指定了正确版本的函数实现。

之前学习CSAPP的时候,有接触过链接器算法的内容,回去复习了一遍,发现符号解析部分内容确实与今天的问题有关。一般情况下,编译器和链接器会按照你在命令行(或Makefile中)指定的顺序来搜索和链接对应实现。

链接器维护三个集合:
可重定位目标文件集合E
一个未解析符号集合U
一个在前面输入文件已经定义的符号集D
初始时,各集合全空;若扫描到目标文件f,则链接器将f添加到E,修改U和D来反应f中的符号定义和引用。
若扫描到存档文件(静态库)f,则链接器尝试匹配U中未定义的符号和由存档文件定义的符号。若某文件m定义了U中的符号,则将m添加至E,并修改U和D。对所有目标文件,依次执行上述过程。
若扫描所有输入文件后,U是非空的,则抛出异常。否则,合并和重定位E中目标文件,生成可执行目标文件。

通过链接器算法,也更好理解外部变量在项目中解析的过程,即本地文件没有定义的符号会加入集合U,希望在其他文件中寻找定义,转移到集合D。

总结

在头文件中声明的函数原型,可以通过在makefile中指定不同的文件,使最终的可执行文件选择不同的同签名函数实现。头文件中的声明并没有指定某一种函数实现的能力。

对于项目文件,我们可以更加方便地进行项目架构设计。如对于不同的权限,我们可以灵活使用相同的目录结构和声明文件,但是编译时通过不同的Makefile指定函数的实现,得到更统一的项目接口规范。

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值