Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1206709
  • 博文数量: 56
  • 博客积分: 400
  • 博客等级: 一等列兵
  • 技术积分: 2800
  • 用 户 组: 普通用户
  • 注册时间: 2010-03-30 13:08
个人简介

一个人的差异在于业余时间

文章分类

全部博文(56)

文章存档

2023年(1)

2019年(1)

2018年(1)

2017年(1)

2016年(2)

2015年(20)

2014年(10)

2013年(7)

2012年(12)

2011年(1)

分类: 网络与安全

2015-09-22 11:27:27

    关于代码实现,首先要说的是在帧的接收和发送的时候都讲到了qos. 在实际的应用中发送用的最多,而接收的控制需要其他辅助性的工作. linux下qos需要iproute2集成的tc命令,以及iptables命令等的支持和配合,它和iptables/netfilters有点类似都需要用户空间和内核空间的配合.
   QOS的控制分为Ingress 和Egress。这里主要分析出口.
   调试需要iproute2的tc :

点击(此处)折叠或打开

  1. Linux Traffic Control is configured with the utility tc. It is part of the iproute2 package. Some Linux distributions already include tc, but it may be an old version, without support for Diffserv.

点击(此处)折叠或打开

  1. 调试版本iproute2-4.2.0
  2. 那么编译iproute2需要依赖的东西:
  3. Bison
  4. Flex
  5. Libdb-dev
找到tc的源码后,我们先看看tc命令的主程序 tc.c 

点击(此处)折叠或打开

  1. int main(int argc, char **argv)
  2. {
  3.     int ret;
  4.     char *batch_file = NULL;

  5.     while (argc > 1) {
  6.         if (argv[1][0] != '-')
  7.             break;
  8.         if (matches(argv[1], "-stats") == 0 ||
  9.              matches(argv[1], "-statistics") == 0) {
  10.             ++show_stats;
  11.         } else if (matches(argv[1], "-details") == 0) {
  12.             ++show_details;
  13.         } else if (matches(argv[1], "-raw") == 0) {
  14.             ++show_raw;
  15.         } else if (matches(argv[1], "-pretty") == 0) {
  16.             ++show_pretty;
  17.         } else if (matches(argv[1], "-graph") == 0) {
  18.             show_graph = 1;
  19.         } else if (matches(argv[1], "-Version") == 0) {
  20.             printf("tc utility, iproute2-ss%s\n", SNAPSHOT);
  21.             return 0;
  22.         } else if (matches(argv[1], "-iec") == 0) {
  23.             ++use_iec;
  24.         } else if (matches(argv[1], "-help") == 0) {
  25.             usage();
  26.             return 0;
  27.         } else if (matches(argv[1], "-force") == 0) {
  28.             ++force;
  29.         } else if (matches(argv[1], "-batch") == 0) {
  30.             argc--;    argv++;
  31.             if (argc <= 1)
  32.                 usage();
  33.             batch_file = argv[1];
  34.         } else if (matches(argv[1], "-netns") == 0) {
  35.             NEXT_ARG();
  36.             if (netns_switch(argv[1]))
  37.                 return -1;
  38.         } else if (matches(argv[1], "-names") == 0 ||
  39.                 matches(argv[1], "-nm") == 0) {
  40.             use_names = true;
  41.         } else if (matches(argv[1], "-cf") == 0 ||
  42.                 matches(argv[1], "-conf") == 0) {
  43.             NEXT_ARG();
  44.             conf_file = argv[1];
  45.         } else {
  46.             fprintf(stderr, "Option \"%s\" is unknown, try \"tc -help\".\n", argv[1]);
  47.             return -1;
  48.         }
  49.         argc--;    argv++;
  50.     }

  51.     if (batch_file)
  52.         return batch(batch_file);

  53.     if (argc <= 1) {
  54.         usage();
  55.         return 0;
  56.     }

  57.     tc_core_init();
  58.     if (rtnl_open(&rth, 0) < 0) {
  59.         fprintf(stderr, "Cannot open rtnetlink\n");
  60.         exit(1);
  61.     }

  62.     if (use_names && cls_names_init(conf_file)) {
  63.         ret = -1;
  64.         goto Exit;
  65.     }

  66.     ret = do_cmd(argc-1, argv+1);
  67. Exit:
  68.     rtnl_close(&rth);

  69.     if (use_names)
  70.         cls_names_uninit();

  71.     return ret;
  72. }
1. 先处理 –xxx 选项;从解析来看“-”选项必须在tc 命令的开始。
2. Batch的处理,可以把批处理的命令放到文件里,让其自动解析加载(参数 -batch  filename 指定)
3. tc_core_init 、rtnl_open 和 cls_names_init 
tc_core_init设置了tick之类的东西;
rtnl_open建立rth->fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, protocol); // protocol : NETLINK_ROUTE
设置send buf 和rec buf,附带开启netlink通信.
// NETLINK_ROUTE 内核的建立:(相关netlink的使用参见之前分析的文章),rtnetlink.c
5. do_cmd 
我们重点还是看这里

点击(此处)折叠或打开

  1. static int do_cmd(int argc, char **argv)
  2. {
  3.     if (matches(*argv, "qdisc") == 0)
  4.         return do_qdisc(argc-1, argv+1);
  5.     if (matches(*argv, "class") == 0)
  6.         return do_class(argc-1, argv+1);
  7.     if (matches(*argv, "filter") == 0)
  8.         return do_filter(argc-1, argv+1);
  9.     if (matches(*argv, "actions") == 0)
  10.         return do_action(argc-1, argv+1);
  11.     if (matches(*argv, "monitor") == 0)
  12.         return do_tcmonitor(argc-1, argv+1);
  13.     if (matches(*argv, "exec") == 0)
  14.         return do_exec(argc-1, argv+1);
  15.     if (matches(*argv, "help") == 0) {
  16.         usage();
  17.         return 0;
  18.     }

  19.     fprintf(stderr, "Object \"%s\" is unknown, try \"tc help\".\n",
  20.         *argv);
  21.     return -1;
  22. }
很清晰的分类处理,我们就从一个最简单的例子开始我们的分析之旅吧!

点击(此处)折叠或打开

  1. Tc qdisc del dev eth0 root
  2. tc qdisc add dev eth0 root handle 1: htb r2q 1
  3. tc class add dev eth0 parent 1: classid 1:1 htb rate 20mbit ceil 100mbit
  4. tc class add dev eth0 parent 1:1 classid 1:10 htb rate 200kbit ceil 250kbit
  5. tc filter add dev eth0 parent 1: protocol ip prio 16 u32 match ip dst 192.168.1.0/24 flowid 1:10
我们先从第二行开始,新建一个队列规则. do_cmd(argc-1, argv+1); // 跳过程序自身的名字tc , 匹配qdisc/class/filter等
do_qdisc(argc-1, argv+1); 区分何种操作后 add/change/replace/link/delete--->(统一都会调用tc_qdisc_modify)

点击(此处)折叠或打开

  1. tc_qdisc_modify(RTM_NEWQDISC, NLM_F_EXCL|NLM_F_CREATE, argc-1, argv+1);
具体tc qdisc add dev eth0 root handle 1: htb r2q 1对应的参数传递:
Cmd=RTM_NEWQDISC
Flags=NLM_F_EXCL|NLM_F_CREATE
Argc
Argv--->dev
显然这个函数利用netlink的消息机制下发给内核,循环解析dev 后续的参数.
NEXT_ARG:

点击(此处)折叠或打开

  1. Utils.h (include):#define NEXT_ARG() do { argv++; if (--argc <= 0) incomplete_command(); } while(0)
  2. Utils.h (include):#define NEXT_ARG_OK() (argc - 1 > 0)
在tc_qdisc_modify中解析流程:
1. dev  --->d=eth0
2. 参数root
req.t.tcm_parent = TC_H_ROOT;
3.参数handle:这个handle即传递的1:(1值)
req.t.tcm_handle = handle;
handle句柄参数后边一般跟qdisc的具体算法.
4. htb --->进入默认处理:
q=get_qdisc_kind
它查询qdisc_list 把每个id和算法名比较如果一样则找到返回,然后查找是否存在对于的相关库等如果没有则创建这个q,q是什么呢?

点击(此处)折叠或打开

  1. struct qdisc_util {
  2.     struct qdisc_util *next;
  3.     const char *id;
  4.     int    (*parse_qopt)(struct qdisc_util *qu, int argc, char **argv, struct nlmsghdr *n);
  5.     int    (*print_qopt)(struct qdisc_util *qu, FILE *f, struct rtattr *opt);
  6.     int     (*print_xstats)(struct qdisc_util *qu, FILE *f, struct rtattr *xstats);

  7.     int    (*parse_copt)(struct qdisc_util *qu, int argc, char **argv, struct nlmsghdr *n);
  8.     int    (*print_copt)(struct qdisc_util *qu, FILE *f, struct rtattr *opt);
  9. };
算法后续的参数解析通过qdisc的opts来操作; 具体需要接口addattr_l,解析后续参数以消息的方式组织填充

点击(此处)折叠或打开

  1. struct {
  2.         struct nlmsghdr     n;
  3.         struct tcmsg         t;
  4.         char             buf[TCA_BUF_MAX];
  5.     } req;
保持消息头4字节对齐.在函数中一开始已经初始化了req.n消息的一些字段 。其实本质就是把消息内容放到了req.buf里
具体buf里消息内容的组织方式为  struct rtattr XXX +  data; ...+ struct rtattr XXX+data;
例如htb:

点击(此处)折叠或打开

  1. struct rtattr {
  2.     unsigned short    rta_len;
  3.     unsigned short    rta_type; //TCA_KIND
  4. };
+data内容即“htb”.
5. 后续判断qdisc算法,利用自身的函数解析具体的算法参数。Q->parse_qopt
但是我们知道不论是否找到q,它都返回为真,(1.在动态库里找到,2创建),对于创建的q自然解析不成功.默认的parse_qopt不支持参数解析.
刚才我们加了(htb)rtattr,接着是算法选项参数,例子htb_parse_opt
Htb  r2q 1 同理以rtattr+data+…+rtattr+data组织方式添加options信息.
6. 如果配置了stab则处理stab参数。(设置表大小的吧)详细请参考:

点击(此处)折叠或打开

  1. struct tc_sizespec {
  2.     unsigned char    cell_log;
  3.     unsigned char    size_log;
  4.     short        cell_align;
  5.     int        overhead;
  6.     unsigned int    linklayer;
  7.     unsigned int    mpu;
  8.     unsigned int    mtu;
  9.     unsigned int    tsize;
  10. };

点击(此处)折叠或打开

  1. struct {
  2.         struct tc_sizespec    szopts;
  3.         __u16            *data;
  4.     } stab;
7.接着处理d,即设备名。Rth为之前打开的rtnetlink 句柄,找到设备的索引值
req.t.tcm_ifindex = idx;
发送消息给rtnl,下发配置 .   其实Class、filter的操作调用和qdisc的一样(tc_class_modify)
1.之前我们已经创建了socket即 Rtnl_open开启socket 设置发送buf 和接收buf
2. Ll_init_map初始化rtnetlink
首先发送消息请求:
nlh = nlmsg_hdr(skb);
nlmsg_type=RTM_GETLINK 
nlmsg_flags=NLM_F_REQUEST | NLM_F_DUMP
之后rtnl_dump_filter_l主要做了接收返回消息的工作。
3.最后就是rtnl_talk下发真正的配置信息.
关于rtnetlink也是netlink的一种,之前已经经过netlink的具体用法,既然用户空间socket已经建立,那么必然内核也需要对应的工作:

点击(此处)折叠或打开

  1. static int __net_init rtnetlink_net_init(struct net *net)
  2. {
  3.     struct sock *sk;
  4.     struct netlink_kernel_cfg cfg = {
  5.         .groups = RTNLGRP_MAX,
  6.         .input = rtnetlink_rcv,
  7.         .cb_mutex = &rtnl_mutex,
  8.         .flags = NL_CFG_F_NONROOT_RECV,
  9.     };

  10.     sk = netlink_kernel_create(net, NETLINK_ROUTE, &cfg);
  11.     if (!sk)
  12.         return -ENOMEM;
  13.     net->rtnl = sk;
  14.     return 0;
  15. }
sk = netlink_kernel_create(net, NETLINK_ROUTE, &cfg); // 注册到了nl_table全局表. 
但是我们还发现发送消息时那么多命令分类,对应不同的消息类型,又是如何区分和处理的呢?qdisc相关的,class相关的,filter相关的,在内核里我们会看到sch_api.c:

点击(此处)折叠或打开

  1. static int __init pktsched_init(void)
  2. {
  3.     int err;

  4.     err = register_pernet_subsys(&psched_net_ops);
  5.     if (err) {
  6.         pr_err("pktsched_init: "
  7.          "cannot initialize per netns operations\n");
  8.         return err;
  9.     }

  10.     register_qdisc(&pfifo_qdisc_ops);
  11.     register_qdisc(&bfifo_qdisc_ops);
  12.     register_qdisc(&pfifo_head_drop_qdisc_ops);
  13.     register_qdisc(&mq_qdisc_ops);

  14.     rtnl_register(PF_UNSPEC, RTM_NEWQDISC, tc_modify_qdisc, NULL, NULL);
  15.     rtnl_register(PF_UNSPEC, RTM_DELQDISC, tc_get_qdisc, NULL, NULL);
  16.     rtnl_register(PF_UNSPEC, RTM_GETQDISC, tc_get_qdisc, tc_dump_qdisc, NULL);
  17.     rtnl_register(PF_UNSPEC, RTM_NEWTCLASS, tc_ctl_tclass, NULL, NULL);
  18.     rtnl_register(PF_UNSPEC, RTM_DELTCLASS, tc_ctl_tclass, NULL, NULL);
  19.     rtnl_register(PF_UNSPEC, RTM_GETTCLASS, tc_ctl_tclass, tc_dump_tclass, NULL);

  20.     return 0;
  21. }
filter的在cls_api.c:

点击(此处)折叠或打开

  1. static int __init tc_filter_init(void)
  2. {
  3.     rtnl_register(PF_UNSPEC, RTM_NEWTFILTER, tc_ctl_tfilter, NULL, NULL);
  4.     rtnl_register(PF_UNSPEC, RTM_DELTFILTER, tc_ctl_tfilter, NULL, NULL);
  5.     rtnl_register(PF_UNSPEC, RTM_GETTFILTER, tc_ctl_tfilter,
  6.          tc_dump_tfilter, NULL);

  7.     return 0;
  8. }
rtnl_register则是注册一个rtnetlink消息类型,它存放在全局rtnl_msg_handlers中。怎么说呢 它这个机制算是rtnetlink所特有的。

点击(此处)折叠或打开

  1. rtnl_register(PF_UNSPEC, RTM_GETLINK, rtnl_getlink,
  2.          rtnl_dump_ifinfo, rtnl_calcit);

点击(此处)折叠或打开

  1. /**
  2.  * rtnl_register - Register a rtnetlink message type
  3.  *
  4.  * Identical to __rtnl_register() but panics on failure. This is useful
  5.  * as failure of this function is very unlikely, it can only happen due
  6.  * to lack of memory when allocating the chain to store all message
  7.  * handlers for a protocol. Meant for use in init functions where lack
  8.  * of memory implies no sense in continuing.
  9.  */
  10. void rtnl_register(int protocol, int msgtype,
  11.          rtnl_doit_func doit, rtnl_dumpit_func dumpit,
  12.          rtnl_calcit_func calcit)
  13. {
  14.     if (__rtnl_register(protocol, msgtype, doit, dumpit, calcit) < 0)
  15.         panic("Unable to register rtnetlink message handler, "
  16.          "protocol = %d, message type = %d\n",
  17.          protocol, msgtype);
  18. }
family=AF_UNSPEC(等同PF_UNSPEC).
说了这么多,还需要看看哪里关联起来了. 内核里rtnetlink的接收函数为:

点击(此处)折叠或打开

  1. static void rtnetlink_rcv(struct sk_buff *skb)
  2. {
  3.     rtnl_lock();
  4.     netlink_rcv_skb(skb, &rtnetlink_rcv_msg);
  5.     rtnl_unlock();
  6. }
它就不具体分析流程了.它根据具体的消息类型和flags做出相应的处理.
如果消息类型为NLM_F_REQUEST则调用到 err = cb(skb, nlh); 这个cb即

点击(此处)折叠或打开

  1. /* Process one rtnetlink message. */

  2. static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
  3. {
  4.     struct net *net = sock_net(skb->sk);
  5.     rtnl_doit_func doit;
  6.     int sz_idx, kind;
  7.     int min_len;
  8.     int family;
  9.     int type;
  10.     int err;

  11.     type = nlh->nlmsg_type;
  12.     if (type > RTM_MAX)
  13.         return -EOPNOTSUPP;

  14.     type -= RTM_BASE;

  15.     /* All the messages must have at least 1 byte length */
  16.     if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(struct rtgenmsg)))
  17.         return 0;

  18.     family = ((struct rtgenmsg *)NLMSG_DATA(nlh))->rtgen_family;
  19.     sz_idx = type>>2;
  20.     kind = type&3;

  21.     if (kind != 2 && !ns_capable(net->user_ns, CAP_NET_ADMIN))
  22.         return -EPERM;

  23.     if (kind == 2 && nlh->nlmsg_flags&NLM_F_DUMP) {
  24.         struct sock *rtnl;
  25.         rtnl_dumpit_func dumpit;
  26.         rtnl_calcit_func calcit;
  27.         u16 min_dump_alloc = 0;

  28.         dumpit = rtnl_get_dumpit(family, type);
  29.         if (dumpit == NULL)
  30.             return -EOPNOTSUPP;
  31.         calcit = rtnl_get_calcit(family, type);
  32.         if (calcit)
  33.             min_dump_alloc = calcit(skb, nlh);

  34.         __rtnl_unlock();
  35.         rtnl = net->rtnl;
  36.         {
  37.             struct netlink_dump_control c = {
  38.                 .dump        = dumpit,
  39.                 .min_dump_alloc    = min_dump_alloc,
  40.             };
  41.             err = netlink_dump_start(rtnl, skb, nlh, &c);
  42.         }
  43.         rtnl_lock();
  44.         return err;
  45.     }

  46.     memset(rta_buf, 0, (rtattr_max * sizeof(struct rtattr *)));

  47.     min_len = rtm_min[sz_idx];
  48.     if (nlh->nlmsg_len < min_len)
  49.         return -EINVAL;

  50.     if (nlh->nlmsg_len > min_len) {
  51.         int attrlen = nlh->nlmsg_len - NLMSG_ALIGN(min_len);
  52.         struct rtattr *attr = (void *)nlh + NLMSG_ALIGN(min_len);

  53.         while (RTA_OK(attr, attrlen)) {
  54.             unsigned int flavor = attr->rta_type & NLA_TYPE_MASK;
  55.             if (flavor) {
  56.                 if (flavor > rta_max[sz_idx])
  57.                     return -EINVAL;
  58.                 rta_buf[flavor-1] = attr;
  59.             }
  60.             attr = RTA_NEXT(attr, attrlen);
  61.         }
  62.     }

  63.     doit = rtnl_get_doit(family, type);
  64.     if (doit == NULL)
  65.         return -EOPNOTSUPP;

  66.     return doit(skb, nlh, (void *)&rta_buf[0]);
  67. }
关键部分在这个函数的最后即doit,它会根据之前说的消息类型(userspace--->req)+rtnetlink_register找到对应的doit.
继续看看用户空间下发的req的消息:
req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));// 消息头4字节对齐
req.n.nlmsg_flags = NLM_F_REQUEST|flags; //NLM_F_EXCL|NLM_F_CREATE
req.n.nlmsg_type = cmd; //RTM_NEWQDISC
req.t.tcm_family = AF_UNSPEC;
需要提醒的是在之前ll_init_map初始化的时候的req和这个还是有不同的:

点击(此处)折叠或打开

  1. 在ll_init_map中情况是不同的:
  2. int rtnl_wilddump_req_filter(struct rtnl_handle *rth, int family, int type,
  3.              __u32 filt_mask)
  4. {
  5.     struct {
  6.         struct nlmsghdr nlh;
  7.         struct ifinfomsg ifm;
  8.         /* attribute has to be NLMSG aligned */
  9.         struct rtattr ext_req __attribute__ ((aligned(NLMSG_ALIGNTO)));
  10.         __u32 ext_filter_mask;
  11.     } req;
对于用户配置下发的消息类型为  RTM_NEWQDISC,即在
/* Process one rtnetlink message. */
static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)

点击(此处)折叠或打开

  1. doit = rtnl_get_doit(family, type);
  2.     if (doit == NULL)
  3.         return -EOPNOTSUPP;

  4.     return doit(skb, nlh, (void *)&rta_buf[0]);
  5. }
tc的配置参数在用户空间解析以rtattr的方式组织,同样到了内核则传递给了rta_buf来处理。(它是rtattr数组)
对于RTM_NEWQDISC则直接调用doit函数即:tc_modify_qdisc,所以我们只需要全力分析这个函数即可
tc_modify_qdisc
1. 找到接口 
dev = __dev_get_by_index(net, tcm->tcm_ifindex);
2. 获取管理控制信息
tcm = nlmsg_data(n);
ubuntu14 系统默认的队列规则pfifo_fast 
#tc qdisc show
#qdisc pfifo_fast 0: dev eth0  root refcnt 2 bands  3 priomap  1 2 2 2 1  2 0 0 1 1 1 1  3.查询设备qdisc  对比handle值,如果没找到则创建
qdisc_create,然后绑定通过qdisc_graft ,这个是add qdisc的流程。
那么add  class/ filter  呢? 通过tc_ctl_tclass/ tc_ctl_tfilter(前面说的对应的doit)

在之前我们已经提到过,根据算法名查找qdisc_util,即通过get_qdisc_kind中 dlopen(NULL,XXX)
它开了程序有所的有的库,
然后查找dlsym符号所在的地址。很明显我们之前看到tc在编译连接的时候,例如q_htb.c里的内容。
找到后 ,添加到qdisc_list中. 如果没有则创建一个.
关于dlopen和dlsym这里补充下知识:
我们知道机器可执行的是machine code, 而我们使用的高级语言编程, 并不是利用晦涩的机器码, 而是用human-readable的变量名, 函数名等, 这些名字就是symbolic name. 编译器
在编译时收集symbol信息, 并储存在object file的.symtab和.dynsym中. symbols是linker和debugger所必需的信息, 如果没有symbols, 试想debugger如何能展示给用户调试信息了?
如果没有symbol, 而只有地址(相对于object file的offset), linker如何能够链接多个object file了?
 一般地, 生成的可执行文件都是包含symbols, 不过这些信息不是程序执行所必需的, 可以通过strip(Discard symbols from object files)去除
不过对于.dynsym的内容段的内容是程序运行必须的 ,strip也去不掉
查看符号的工具:debugger/nm/readelf等

Nm  test 
Readelf  -s  test
如dlopen("/lib/libmy.so",RTLD_LAZY)。返回对象/lib/libmy.so的句柄。
如果你把pathname输入为NULL,则返回的是一个全局的对象表,包括你在load前的进程镜像表。这种方式很少用,英文解释为:

点击(此处)折叠或打开

  1. If pathname is NULL, dlopen() provides a handle to the running process's global symbol object. This provides access to the symbols from the original program image file,
  2. the dependencies it loaded at startup, plus any objects opened with dlopen() calls using theRTLD_GLOBAL flag. This set of symbols can change dynamically if the
  3. application subsequently calls dlopen() using RTLD_GLOBAL.
那么这个全局表是什么呢?我们在查看.systab 和.dysym的时候会看到 “GLOBAL  OBJECT” 的标示.
但是在看tc的时候 看到dlopen(null,…); 百思不得其解,为什么它就找到了符号htb_qdisc_util呢?(原因为自己测试程序不成功,但是tc工具是ok的)
看了tc的makefile也是仅仅把q_htb.c连接进去,并没有直接调用,而它却在查找动态库符号的时候找到了。我自己随便写一个验证程序是不行,后来发现编译的时候要指定参数即:
-Wl, -export-dynamic 这个参数会把symbol里的符号都导入到dynsym中.
测试代码我就不贴了,用readelf –a 查看符号的时候,是有的,我们strip后,在.dynsym里已经没有了,所以dlsym里肯定找不到.

点击(此处)折叠或打开

  1. --export-dynamic
  2.            When creating a dynamically linked executable, add all symbols to the dynamic symbol table. The
  3.            dynamic symbol table is the set of symbols which are visible from dynamic objects at run time.
  4.            If you do not use this option, the dynamic symbol table will normally contain only those symbols
  5.            which are referenced by some dynamic object mentioned in the link.
  6.            If you use "dlopen" to load a dynamic object which needs to refer back to the symbols defined by the
  7.            program, rather than some other dynamic object, then you will probably need to use this option when
  8.            linking the program itself.
动态库如何编译这里不说了,还有elf的文档资料.



































阅读(9805) | 评论(0) | 转发(0) |
0

上一篇:linux下QOS--理论篇

下一篇:浅谈socket

给主人留下些什么吧!~~