`
wwn15wwn
  • 浏览: 16283 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

动静库

 
阅读更多

动静库
2010年12月16日
  1,  Linux下动态库查看方法:nm -D libavformat.so 
  Linux下静态库查看方法:ar -t libavformat.a 
  2, 
  基本概念 
  库有动态与静态两种, 
  动态通常用.so为后缀,静态用.a为后缀。例如:libhello.so libhello.a 
  为了在同一系统中使用不同版本的库,可以在库文件名后加上版本号为后缀,例如: libhello.so.1.0, 
  由于程序连接默认以.so为文件后缀名。所以为了使用这些库,通常使用建立符号连接的方式。 ln -s libhello.so.1.0 libhello.so.1 ln -s libhello.so.1 libhello.so 
  使用库 
  当要使用静态的程序库时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这种拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。 
  然而,对动态库而言,就不是这样。动态库会在执行程序内留下一个标记'指明当程序执行时,首先必须载入这个库。 
  由于动态库节省空间,linux下进行连接的缺省操作是首先连接动态库,也就是说,如果同时存在静态和动态库,不特别指定的话,将与动态库相连接。 
  现在假设有一个叫hello的程序开发包,它提供一个静态库libhello.a 一个动态库libhello.so,一个头文件hello.h,头文件中提供sayhello()这个函数 /* hello.h */ void sayhello(); 另外还有一些说明文档。这一个典型的程序开发包结构 
  1.与动态库连接 linux默认的就是与动态库连接,下面这段程序testlib.c使用hello库中的sayhello()函数 /*testlib.c*/ #include #include int main() { sayhello(); return 0; } 
  使用如下命令进行编译 $gcc -c testlib.c -o testlib.o 
  用如下命令连接: $gcc testlib.o -lhello -o testlib 在连接时要注意,假设libhello.o 和libhello.a都在缺省的库搜索路径下/usr/lib下,如果在其它位置要加上-L参数与与静态库连接麻烦一些,主要是参数问题。 
  还是上面的例子: $gcc testlib.o -o testlib -WI,-Bstatic -lhello 注:这个特别的"-WI,-Bstatic"参数,实际上是传给了连接器ld. 指示它与静态库连接,如果系统中只有静态库当然就不需要这个参数了。 
  如果要和多个库相连接,而每个库的连接方式不一样,比如上面的程序既要和libhello进行静态连接,又要和libbye进行动态连接,其命令应为: $gcc testlib.o -o testlib -WI,-Bstatic -lhello -WI,-Bdynamic -lbye 
  3.动态库的路径问题 
  为了让执行程序顺利找到动态库,有三种方法: 
  (1)把库拷贝到/usr/lib和/lib目录下。 
  (2)在LD_LIBRARY_PATH环境变量中加上库所在路径。例如动态库libhello.so在/home/ting/lib目录下,以bash 为例,使用命令: $export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ting/lib 
  (3) 修改/etc/ld.so.conf文件,把库所在的路径加到文件末尾,并执行ldconfig刷新。这样,加入的目录下的所有库文件都可见、 
  4.查看库中的符号 
  有时候可能需要查看一个库中到底有哪些函数,nm命令可以打印出库中的涉及到的所有符号。 
  库既可以是静态的也可以是动态的。 
  nm列出的符号有很多, 
  常见的有三种, 
  一种是在库中被调用,但并没有在库中定义(表明需要其他库支持),用U表示; 
  一种是库中定义的函数,用T表示,这是最常见的; 
  另外一种是所谓的"弱态"符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖,用W表示。 
  例如,假设开发者希望知道上央提到的hello库中是否定义了 printf(): $nm libhello.so |grep printf U printf U表示符号printf被引用,但是并没有在函数内定义,由此可以推断,要正常使用hello库,必须有其它库支持,再使用ldd命令查看hello依赖于哪些库: $ldd hello libc.so.6=>/lib/libc.so.6(0x400la000) /lib/ld-linux.so.2=>/lib/ld-linux.so.2 (0x40000000) 从上面的结果可以继续查看printf最终在哪里被定义,有兴趣可以go on 生成库第一步要把源代码编绎成目标代码。 
  以下面的代码为例,生成上面用到的hello库: /* hello.c */ #include void sayhello() { printf("hello,world "); } 用gcc编绎该文件,在编绎时可以使用任何全法的编绎参数,例如-g加入调试代码等: gcc -c hello.c -o hello.o 
  1.连接成静态库 连接成静态库使用ar命令,其实ar是archive的意思 $ar cqs libhello.a hello.o 
  2.连接成动态库 生成动态库用gcc来完成,由于可能存在多个版本,因此通常指定版本号: $gcc -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0 hello.o 另外再建立两个符号连接: $ln -s libhello.so.1.0 libhello.so.1 $ln -s libhello.so.1 libhello.so 这样一个libhello的动态连接库就生成了。最重要的是传gcc -shared 参数使其生成是动态库而不是普通执行程序。 -Wl 表示后面的参数也就是-soname,libhello.so.1直接传给连接器ld进行处理。 
  实际上,每一个库都有一个soname,当连接器发现它正在查找的程序库中有这样一个名称,连接器便会将soname嵌入连结中的二进制文件内,而不是它正在运行的实际文件名,在程序执行期间,程序会查找拥有 soname名字的文件,而不是库的文件名,换句话说,soname是库的区分标志。这样做的目的主要是允许系统中多个版本的库文件共存,习惯上在命名库文件的时候通常与soname相同 libxxxx.so.major.minor 其中,xxxx是库的名字,major是主版本号,minor 是次版本号 
  3, 
  一、链接器的基本知识 
  在说库之前,先简单介绍一下链接器的原理。 
  编译的时候,每个.c(这里简单一点,只考虑C程序,C++原理类似)源程序会被编译成.o文件。在C源代码中声明的符号,如函数,全局变量等等,编译器在当前的C源代码中能够找到定义的会被直接编译,而不能找到定义,只有声明的会作为外部符号,存放在目标文件的导入表中。(有人问,如果是又没声明又没定义的符号呢?废话,当然通不过编译了-_-)嗯…………编译器没有办法确定这个未知符号的定义,但它相信这个定义是肯定存在的,自己既然无能为力,就只能交给链接器去办啦。而对于C源程序中定义的全局函数、变量等等,编译器会把它放到目标文件的导出表中,因为它相信,这些符号也许其他模块需要的。 
  轮到链接器上场的时候,简单的讲,它会把这一堆的目标文件"组合"起来组装成一个可执行文件(Linux是ELF、COFF或者 a.out格式的文件,Windows是PE或者LE文件)。组装的过程是什么?简单的讲,它从这一堆的目标文件中抽出机器码部分,把这些机器码组合起来,然后生成输出文件的文件头等等结构,最后拼装成最终的可执行文件。这个过程是比较复杂的,我们暂时不深究。但是,我们对这个过程中链接器处理符号的方式比较感兴趣:它是怎么处理上面提到的那些导入和导出的符号的?答案是,链接器从目标文件中取出符号表并进行合并操作,具体就是,如果目标文件甲中声明了导出符号abcd,而目标文件乙中声明了同名的导入符号abcd,两个符号会被合并,导入符号会从最终文件的导入表中去除。处理机器码时,链接器会把目标文件乙中所有对外部符号abcd的引用替换成目标文件甲中定义的符号abcd的地址。当然,链接器不可能处理完所有的符号,如果它还有导入符号无法处理,但是它相信操作系统的装载器能够处理的话,它会把这些符号保留在最终可执行文件的导入表中,交由操作系统的装载器(可以认为包含一个链接器)去处理。当然如果它发现操作系统仍然无法处理这个导入符号的话,就是大家熟悉的错误 啦:ld error xxxx: yyyy.o: Cannot resolve external symbol zzzzzzz。对于导出符号,可 以原样放在导出表中以供其他模块使用,也可以去除。 
  最终的可执行文件产生了,是不是链接任务完成了呢?没有!生成的可执行文件中仍然会有导入和导出表,需要在执行的时候链接,不过这个任务杀操作系统的加载器完成的。 
  总结一下,链接主要分以下两种:编译时链接:GCC的ld链接器完成的任务运行时链接:操作系统的加载器完成的任务 
  以上就是链接器的基本原理,它对于理解静态和动态库的原理很重要。 
  FAQ: 为什么我的程序编译能通过,链接的时候报错:xxxx :Cannot resolve/find symbol/function/variable yyyy? 初学者提问频率比较高的问题之一,如果你看完上面一段的话就不难自己回答这个问题了。 
  TIP:查看nm命令和objdump命令的man手册………… 
  二、静态库问题:如果我有一些可复用的代码,用什么方式"复用"?(这里的"复用代码"是模块级的,而非代码片断) 
  当然,如果每写一个新程序就把那些可复用的代码源程序原样拷贝过来,随新程序一起编译链接的话,当然可以达到目的,不过这样每次构建都得把那些复用代码一起编译链接,如果复用代码很大的话,费时又费力,要是能够把这些代码预先编译好,一定能够节省不少时间和精力。可是,如果分别把源代码编译成目标文件,会产生大量目标文件,不便使用和管理。有什么好办法?静态库就是解决方案。 
  创建静态库用ar命令,库文件习惯以lib打头,扩展名.a 
  TIP: 使用man ar查看ar的说明 
  [案例分析] 一个简单的程序程序由a.c, b.asm, main.c组成。a.c中实现了加法函数add(),b.asm是用汇编语言实现的减法运算(用汇编语言的目的是为了说明链接原理和编程语言无关)substract()函数。main.c使用了以上两个函数 
  a.c 
  Code view plainprint?  int add(int a, int b) { return a + b; }  b.asm  Code view plainprint?  section .text global substract ;声明导出符号substract substract: push ebp mov ebp, esp push ebx mov eax, dword [ebp + 8] ;a mov ebx, dword [ebp + 12] ;b sub eax, ebx ;result pop ebx leave ret 
  main.c 
  Code view plainprint?  假设a.c b.asm是复用的代码 
  编译: 
  gcc -c -g -o a.o a.c yasm -g dwarf2 -f elf -o b.o b.asm gcc -c -g -o main.o main.c 
  链接: 
  gcc -g -o main a.o b.o main.o 
  使用nm查看符号: > nm a.o 00000000 T add 
  nm b.o 00000000 T substract 
  nm main.o  00000000 T main  可以看到标着U(Undefined)的符号是导入符号,而T(Text,定义在.text段中的符号)是导出符号。 
  现在我们用ar命令创建一个库: ar rc libdemo.a a.o b.o 
  怎么使用它? 
  gcc -g -o main main.o -L./ -ldemo 
  -L指明附加库的路径(这里是当前目录),-l 指明附加库名,这里链接demo库,文件名是libdemo.a,就是我们刚才用ar命令创建的。 
  这是什么原理?答案是,ar命令只是简单的把目标文件打包,链接时,链接器从打包的库中取出各个目标文件和程序产生的目标文件链接。链接的原理请看文章第一部分 
  TIP: 使用ar t libdemo.a查看库中包含的目标文件 
  > ar t libdemo.a a.o b.o 
  二、共享库 Linux支持共享库已经有很久远的历史了,它十分类似于Windows操作系统中的dll文件,但是提供了更加完备的机制来防止出现一些Windows系统中老大难的问题。 
  问题:为什么要动态链接的共享库?静态库不是很好了么?举C库为例,如果C库做成静态的当然没有问题,但是如果系统中有100个进程的映像文件链接的时候都使用了C库,内存中就会有100份相同代码的拷贝。这是一种极大的浪费!如果让这100个程序共享同一个C库,自然就可以节约不少内存空间了。 
  如何编写和使用共享库? 
  [案例分析] 程序同静态库部分的,2个C文件加一个asm汇编文件。 
  现在按照共享库的方式编译 gcc -c -fpic -g -o a.o a.c gcc -c -fpic -g -o main.o main.c yasm -f elf -g dwarf2 -o b.o b.asm 
  -fpic生成与加载地址无关的代码(可重定位) 
  链接库: gcc -shared -g -fpic -o libdemo.so a.o b.o 
  关键是一个-shared参数,让gcc产生共享库 
  TIPS: man gcc看看这个参数的详细解释 
  链接程序: gcc -g -o main main.o -L./ -ldemo 
  现在运行,嗯,怎么不能运行? > ./main ./main: error while loading shared libraries: libdemo.so: cannot open shared object file: No such file or directory 
  什么?找不到?不就在当前目录下嘛,哦,想起来了,Linux的惯例,不会到自动到当前目录下查找文件的,我们必须手动设置一下。设置什么?LD_LIBRARY_PATH环境变量, 这个环境变量告诉装载器到哪里去找共享库. 
  > export LD_LIBRARY_PATH=./ 
  > ./main 
  这下可以了。 
  我们看一下可执行文件中extern节的内容  extern:804A028 ; extern extern:804A028 extrn __libc_start_main@@GLIBC_2_0:near extern:804A02C extrn scanf@@GLIBC_2_0:near extern:804A030 extrn printf@@GLIBC_2_0:near extern:804A034 extrn add:near  ; CODE XREF: _add j extern:804A034  ; DATA XREF: .got.plt:off_804A010 o extern:804A038 extrn __libc_start_main:near ; CODE XREF: ___libc_start_main j extern:804A038  ; DATA XREF: .got.plt:off_804A004 o extern:804A03C ; int scanf(const char *,...) extern:804A03C extrn scanf:near  ; CODE XREF: _scanf j extern:804A03C  ; DATA XREF: .got.plt:off_804A008 o extern:804A040 ; int printf(const char *,...) extern:804A040 extrn printf:near  ; CODE XREF: _printf j extern:804A040  ; DATA XREF: .got.plt:off_804A00C o extern:804A044 extrn __gmon_start__ ; weak ; DATA XREF: .got:08049FF0 o extern:804A044  ; .got.plt:off_804A000 o extern:804A048 extrn _Jv_RegisterClasses ; weak extern:804A04C extrn substract  ; DATA XREF: .got.plt:off_804A014 o  可以看到熟悉的add 和substract ^_^ 
  这种方式是运行时静态链接,装载器在装入可执行程序的时候会一并把库也装入并且做好链接 
  怎么?没有下文啦?共享库不会这么简单吧,当然不会。共享库最大的魅力还没有介绍呢──动态载入 
  前面提到链接的方式有两种,实际上,还有一种链接方式也是很常用的,那就是运行时动态链接。补上前面两种,链接方式有: 1.编译时静态链接 2.运行时静态链接 3.运行时动态链接 
  下面介绍第3种链接方式 
  [案例分析] 程序需要之前给出的libdemo.so库 
  dymlnk.c 
  Code view plainprint?   1. #include  2. #include  3. #include  4. 5. 6. int main(int argc, char *argv[]) { 7. 8. char *errmsg; // error message 9. void *lib; // shared library handle 10. int (*add)(int a, int b); // add() function 11. int (*substract)(int a, int b); // substract() function 12. 13. // OPen the library 14. lib = dlopen("./libdemo.so", RTLD_LAZY); 15. if (!lib) { 16. fprintf(stderr, dlerror()); 17. exit(-1); 18. } 19. 20. // get export symble address 21. add = dlsym(lib, "add"); 22. errmsg = dlerror(); 23. if (errms 24. 25. substract = dlsym(lib, "substract"); 26. errmsg = dlerror(); 27. if (errmsg != NULL) { 28. fprintf(stderr, errmsg); 29. exit(-1); 30. } 31. 32. // the same as the old one 33. int a, b, c, tmp; 34. 35. printf("Input 3 numbers:"); 36. scanf("%d%d%d", &a, &b, &c); 37. tmp = add(a, b); 38. tmp = substract(tmp, c); 39. printf("result:%d\n", tmp); 40. 41. // close lib 42. dlclose(lib); 43. 44. return 0; 45. } 
  include  #include  #include  int main(int argc, char *argv[]) { char *errmsg; // error message void *lib; // shared library handle int (*add)(int a, int b); // add() function int (*substract)(int a, int b); // substract() function // OPen the library lib = dlopen("./libdemo.so", RTLD_LAZY); if (!lib) { fprintf(stderr, dlerror()); exit(-1); } // get export symble address add = dlsym(lib, "add"); errmsg = dlerror(); if (errms substract = dlsym(lib, "substract"); errmsg = dlerror(); if (errmsg != NULL) { fprintf(stderr, errmsg); exit(-1); } // the same as the old one int a, b, c, tmp; printf("Input 3 numbers:"); scanf("%d%d%d", &a, &b, &c); tmp = add(a, b); tmp = substract(tmp, c); printf("result:%d\n", tmp); // close lib dlclose(lib); return 0; } 
  程序编译: gcc -o dymlnk dymlnk.c -ldl 需要链接库dl 
  这个程序有点复杂,简单解释一下程序中首先调用dlopen打开一个库,这里就是我们刚才写的库, 这个函数会返回一个共享库的"handle"(怎么象Windows了?句柄?) 然后调用dlsym获取add和substract函数的指针计算的代码和之前的main.c一样最后关闭共享库 
  当然这个程序运行的时候不需要LD_LIBRARY_PATH变量,因为是全路径指明共享库路径的. 
  TIP: man dlopen可以查到以上函数的详细说明 
  4, 
  静态连接库(扩展名为 .a)是.o文件的简单集合。在 linux/unix下,使用 ar 命令生成静态连接库。 
  动态连接库(扩展名为.so) 是将.o文件集合,并增加了导出表。导出表是一个函数名、函数索引、函数地址的数组。因此,应用程序可以装载(使用 ldopen函数)后,根据函数名,导出函数的索引位置来调用函数。 
  动态连接库的优点在于:程序可以独立于连接库,即不需要包含头文件。 
  两种连接库都可以减少模块间的依赖。 
  两种连接库的文件名都必须有 lib前缀。 
  可以使用 nm 命令查看连接库有哪些导出选项。 
  可以使用 ldd 命令查看应用程序需要哪些连接库。 
  可以一次性地指定编译当前目录下的所有 .cpp 文件为一个 连接库文件:在makefile 中 
  OBJ = $(shell find . -name "*.cpp" -printf "%p " | sed -e "s/.cpp/.o/g" ) 
  find 参数解释: 
  . 当前目录及所有子目录。如果要指定只有当前目录,则配合使用 参数-maxdepth 1 
  -name 文件名。可使用通配符。 
  -printf表示不加换行符地输出, 格式化参数 "%p"表示输出完整的文件名及路径。如果不需要使用路径,可使用 "%f"。 
  sed 参数解释: 
  -e 表示使用正则表达式 
  s 表示执行文本替换,后面的两个参数表示把 .cpp 替换为 .o , g表示全部替换( global replace )。如果没有 g ,则只替换一次。 
  5, 
分享到:
评论

相关推荐

    C语言链接库

    【C语言链接库】主要涉及两种类型:静态库和动态库。它们都是为了代码复用和模块化设计,但有着显著的区别。 1. **静态库**:在编译时,静态库(通常以`.lib`文件形式存在)的代码会直接合并到目标程序中,形成一个...

    C语言编译,链接,内存布局,动静链接库的使用

    C语言编译,链接,内存布局,动静链接库的使用

    互联网动静分离架构

    互联网动静分离架构 互联网架构中,静态页面和动态页面是两个不同的概念。静态页面是指互联网架构中几乎不变的页面,例如首页、HTML页面、JS/CSS样式文件、jpg/apk等资源文件。这些页面变化频率很低,适合使用静态...

    可视化图表组件库-包含动静两种图表.rplib

    可视化图表组件库-包含动静两种图表,可导入组合库直接使用,亲测Axure8和9均可使用。

    库文件介绍及区别

    库文件介绍及区别 本文档详细介绍了动态链接库文件、静态链接库文件和可执行文件之间的区别,并对这三种文件之间的联系和区别进行了综合分析。 知识点: 1. 动态链接库(Dynamic Link Library,缩写为 DLL) ...

    行业文档-设计装置-串联式动静智能称重平台.zip

    串联式动静智能称重平台是一种在工业生产和物流领域广泛应用的高科技设备,它结合了静态称重和动态称重的优点,能够高效、精确地对物体进行重量测量。这种平台设计巧妙,适应性强,尤其适合需要连续作业和高精度称重...

    动静分离案例所需要用的样式(css/js/img)

    在"static/js"目录下,你可以放置各种JavaScript文件,包括主应用脚本、库文件(如jQuery、React等)以及自定义的函数和模块。在HTML文件中,通过`<script>`标签引入这些JS文件,确保它们能在页面加载时正确执行。 ...

    部署Nginx+Apache动静分离的实例详解

    - 安装必要的支持软件,如GCC编译器和开发库。 - 创建Nginx运行的用户和组。 - 下载并编译Nginx,配置时指定用户、安装目录等参数。 - 编写Nginx配置文件,设置动静分离的规则。例如: ```nginx server { ...

    一种基于Java虚拟机的动静结合自适应优化方法.pdf

    针对题目中给出的内容,以下是对【一种基于Java虚拟机的动静结合自适应优化方法.pdf】文档中所涉及的知识点的详细说明: 1. Java虚拟机(JVM)基础: Java虚拟机是运行Java程序的核心引擎,负责将Java字节码转换为...

    Nginx介绍,安装、反向代理、负载均衡、动静分离。 Nginx安装需要用到的资源。

    在安装Nginx之前,我们需要准备一些基础库。这里提到了四个压缩包文件: 1. **pcre-8.21.tar.gz**: PCRE(Perl Compatible Regular Expressions)是Nginx用来解析和执行正则表达式的一个库。Nginx的重写规则和URL...

    分库分表入门级-lzg

    2. **垂直分表**:针对大表,将表中的字段按常用程度划分,将不常用的字段分离,减小单行数据大小,提高缓存利用率,同时便于实现动静分离。然而,垂直分表同样无法解决并发量过大的问题。 3. **水平分表**:依据...

    Arch-03-15- Nginx+tomcat 配置负载均衡动静分离

    标题“Arch-03-15- Nginx+tomcat 配置负载均衡动静分离”涉及的是在Web服务器架构中使用Nginx与Tomcat的集成,通过配置实现负载均衡和动静态资源分离。这样的架构可以提高系统的可用性和响应速度,减轻后端应用...

    Python库 | DoorPi-2.4.0.7-py2.7.egg

    2. **传感器集成**:DoorPi可以连接各种传感器,如PIR(被动红外)运动传感器,用于检测周围环境的动静,触发相应的安全响应。 3. **GPIO控制**:由于DoorPi针对树莓派设计,因此它能充分利用树莓派的GPIO(通用...

    车库智能照明设计方案【精选文档】.doc

    1. 红外动静感应控制:通过在车位安装红外感应器,系统能够自动识别车辆和人员的进出,实现"人(车)来灯亮"、"人(车)走灯灭"的智能控制,有效节约电能。 2. 定时控制:利用7寸真彩触摸屏,可预设不同时间段的照明场景...

    Nginx动静分离、压缩、缓存、黑白名单、跨域、高可用、性能优化详解.docx

    4. 安装Nginx依赖库和包:yum install gcc-c++ pcre pcre-devel zlib zlib-devel openssl openssl-devel 5. 编译和安装Nginx:./configure && make && make install 四、Nginx高级特性 1. 动静分离配置:使用Nginx...

    新高压电工证试题库.doc

    4.断路器距离:10kV 真空断路器动静触头之间的断开距离一般为5-10mm。 5.电工刀使用:电工刀不能用于带电作业。 6.绝缘档板试验:绝缘档板一般每12个月进展一次绝缘试验。 7.真空断路器寿命:真空断路器每次分...

    iphone-static-library-project工具类源码_ios源码

    3. 动静分离:根据功能需求和性能考虑,合理选择静态库和动态库(Dynamic Library)的使用。 总结,"iphone-static-library-project-template"是iOS开发中的实用工具,它简化了静态库的创建流程,让开发者能够更...

    基于单片机的货物仓库防盗报警系统设计与实现(设计报告+源代码+PCB仿真+开题报告+中期报告).zip

    1. **传感器技术**:可能使用了红外、微波或磁感应等类型的传感器来检测仓库内的动静,及时捕捉非法入侵行为。 2. **信号处理**:单片机需要对传感器收集到的信号进行解析和处理,确保报警系统只在必要时触发。 3...

    赛元SC92L8X3X低功耗动静态触控库+资料+demo

    在"赛元SC92L8X3X低功耗动静态触控库+资料+demo"中,我们可以找到一系列关键资源来理解和开发基于该芯片的项目。首先,触控库是实现触控功能的核心组件,它包含了一系列预编程的算法和函数,用于处理SC92L8X3X的输入...

Global site tag (gtag.js) - Google Analytics