从getaddrinfo看Glibc的nss
一、问题的引入
在使用IPV6转化的过程中,为了兼容IPV4和V6版本,一个兼容的方法就是使用这个getaddrinfo函数,这个函数在windows下同样存在,应该是一个跨平台的接口。
这个接口的一个特殊之处就是这个接口可以传入hostname和servername,这两个变量都是可以为一个字符串的名称,而这个字符串的名称是如何转换为内核识别且仅识别的一个数字或者数字数组呢?另一方面,这个函数又是如何解析或者自动识别是一个IPV4还是IPV6的地址呢?
二、函数的实现
从getservbyname开始说起。这个函数的定义位于getsrvbynm.c文件中,但是由于系统中这类函数的相似度比较高,所以很多地方都是通过相同的文件进行了一些处理,也就是使用了预处理的方式。
该函数的定义为
#define LOOKUP_TYPE struct servent
#define FUNCTION_NAME getservbyname
#define DATABASE_NAME services
#define ADD_PARAMS const char *name, const char *proto
#define ADD_VARIABLES name, proto
#define BUFLEN 1024
#include "../nss/getXXbyYY.c"
可以看到,它直接包含了其中的getXXbyYY.c,从而将其中的宏替换为特殊的一个名字,其它的很多函数也是通过这个接口完成的。
在getXXbyYY.c函数中
/* To make the real sources a bit prettier. */
#define REENTRANT_NAME APPEND_R (FUNCTION_NAME)
#define APPEND_R(name) APPEND_R1 (name)
#define APPEND_R1(name) name##_r
#define INTERNAL(name) INTERNAL1 (name)
#define INTERNAL1(name) __##name
LOOKUP_TYPE *
FUNCTION_NAME (ADD_PARAMS)
…………
while (buffer != NULL
&& (INTERNAL (REENTRANT_NAME) (ADD_VARIABLES, &resbuf, buffer,
buffer_size, &result H_ERRNO_VAR)
== ERANGE)
其实这里的操作比较简单,也就是将一个函数名字转换为__xxx_r的形式,也就是在原始的函数前面加上双下划线,在最后加上_r,从而转发给另一个可冲入的函数。
可重入函数的定义
getsrvbynm_r.c,在该函数中再次定义了对于函数的一个派生实现,从而完成了可冲入函数的定义。
#define LOOKUP_TYPE struct servent
#define FUNCTION_NAME getservbyname
#define DATABASE_NAME services
#define ADD_PARAMS const char *name, const char *proto
#define ADD_VARIABLES name, proto
#include "../nss/getXXbyYY_r.c"
在getXXbyY_r.c中
/* To make the real sources a bit prettier. */
#define REENTRANT_NAME APPEND_R (FUNCTION_NAME)
#define APPEND_R(name) APPEND_R1 (name)
#define APPEND_R1(name) name##_r
#define INTERNAL(name) INTERNAL1 (name)
#define INTERNAL1(name) __##name
#define NEW(name) NEW1 (name)
no_more = DB_LOOKUP_FCT (&nip, REENTRANT_NAME_STRING, &fct.ptr);
这里就是将一个函数转换为
int
INTERNAL (REENTRANT_NAME) (ADD_PARAMS, LOOKUP_TYPE *resbuf, char *buffer,
size_t buflen, LOOKUP_TYPE **result H_ERRNO_PARM)
{
static bool startp_initialized;
static service_user *startp;
static lookup_function start_fct;
service_user *nip;
注意,这里的名称还是非常简单的,就是在原始的名字之后加上一个_r,而没有其它的任何额外操作,这样的原因是以后这个函数将会作为基础来在多个so文件中查找,从而要组合出不同的函数名称。例如__nss_files_getsrvbynm_r函数等。
在这个函数中,将会不断的尝试找到一个函数的定义,这个函数的实现有一个循环,这个循环用来在nsswitch.conf中指定的优先级及动作之间进行运作。这个解析在XXX-lookup.c:__nss_database_lookup函数中完成,在XXX-lookup.c文件中中,其中为每个函数都定义了一个单独的函数查找名字:
#ifndef DB_LOOKUP_FCT
# define DB_LOOKUP_FCT CONCAT3_1 (__nss_, DATABASE_NAME, _lookup)
# define CONCAT3_1(Pre, Name, Post) CONCAT3_2 (Pre, Name, Post)
# define CONCAT3_2(Pre, Name, Post) Pre##Name##Post
#endif
这样,文件中的DB_LOOKUP_FCT宏就会变成__nss_service_lookup,然后就将会执行后面的函数
int
internal_function
DB_LOOKUP_FCT (service_user **ni, const char *fct_name, void **fctp)
{
if (DATABASE_NAME_SYMBOL == NULL
&& __nss_database_lookup (DATABASE_NAME_STRING, ALTERNATE_NAME_STRING,
DEFAULT_CONFIG, &DATABASE_NAME_SYMBOL) < 0)
return -1;
*ni = DATABASE_NAME_SYMBOL;
return __nss_lookup (ni, fct_name, fctp);
}
在函数nsswitch.c:__nss_database_lookup中
#define _PATH_NSSWITCH_CONF "/etc/nsswitch.conf"
service_table = nss_parse_file (_PATH_NSSWITCH_CONF);
将会解析这个文件的配置,从而找到其中对于某项服务的说明,其中的该函数的第一个参数即为对应的database项的名称,在这里为services。
然后该函数将会解析nsswitch.conf文件的配置,确定查找files db nis nisplus的顺序,并从中找到对应的函数名称,从而可以进行模块的加载与执行。
不管怎样,我们将会执行到
return __nss_lookup (ni, fct_name, fctp);
在该函数中
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->library->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name);
构造动态库的名称 libnss_files.so.version,动态加载一个文件,从而可以得到函数中的符号,进而执行对应的查找和解析函数。
…………
/* Construct the function name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
ni->library->name),
"_"),
fct_name);
拼凑出__nss_files_getsrvbynm_r字符串
/* Look up the symbol. */
result = __libc_dlsym (ni->library->lib_handle, name);
__nss_files_getsrvbynm_r函数的定义
files-service.c函数中
/* Return the next entry from the database file, doing locking. */
enum nss_status
CONCAT(_nss_files_get,ENTNAME_r) (struct STRUCTURE *result, char *buffer,
size_t buflen, int *errnop H_ERRNO_PROTO)
{
/* Return next entry in host file. */
enum nss_status status = NSS_STATUS_SUCCESS;
…………
#define DB_LOOKUP(name, keysize, keypattern, break_if_match, proto...) \
enum nss_status \
_nss_files_get##name##_r (proto, \
struct STRUCTURE *result, char *buffer, \
size_t buflen, int *errnop H_ERRNO_PROTO) \
{ \
enum nss_status status; \
同样是files-service.c函数的实现
DB_LOOKUP (servbyname, 2 + strlen (name) + (proto ? strlen (proto) : 0),
(".%s/%s", name, proto ?: ""),
{
/* Must match both protocol (if specified) and name. */
if (proto != NULL && strcmp (result->s_proto, proto))
continue;
LOOKUP_NAME (s_name, s_aliases)
},
const char *name, const char *proto)
从而拼凑出这个__nss_files_getservbyname_r的定义。
然后该函数的实现中:
status = internal_setent (keep_stream); \
/* Open database file if not already opened. */
static enum nss_status
internal_setent (int stayopen)
{
enum nss_status status = NSS_STATUS_SUCCESS;
if (stream == NULL)
{
stream = fopen (DATAFILE, "re");
#define DATAFILE "/etc/" DATABASE
#define ENTNAME servent
#define DATABASE "services"
所以也就是从 /etc/services文件中读取servent内容,从而完成系统配置的读取。
其中对应的parse_line将一个字符串转换为xxxent结构
# define parser_stclass /* Global */
# define parse_line CONCAT(_nss_files_parse_,ENTNAME)
# ifdef IS_IN_libc
files-parse.c
# define parse_line CONCAT(_nss_files_parse_,ENTNAME)
# ifndef TRAILING_LIST_MEMBER
# define TRAILING_LIST_PARSER /* Nothing to do. */
# else
# define TRAILING_LIST_PARSER \
{ \
char **list = parse_list (line, data, datalen, errnop); \
if (list) \
result->TRAILING_LIST_MEMBER = list; \
else \
return -1; /* -1 indicates we ran out of space. */ \
}
三、IPV4和IPV6地址的解析
通过getaddrinfo,最后通过inet_pton函数进行,由于函数参数中要求指明协议的类型,所以这里没什么好围观的了。
而且其中的AI_PASSIVE只有在主机名为空的时候,决定是否返回loopback地址(inet4的127.0.0.1和INET6的::1)。