4. 共享库

4.1. 编译、链接、运行

组成共享库的目标文件和一般的目标文件有所不同,在编译时要加-fPIC选项,例如:

$ gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c

-f后面跟一些编译选项,PIC是其中一种,表示生成位置无关代码(Position Independent Code)。那么用-fPIC生成的目标文件和一般的目标文件有什么不同呢?下面分析这个问题。

我们知道一般的目标文件称为Relocatable,在链接时可以把目标文件中各段的地址做重定位,重定位时需要修改指令。我们先不加-fPIC选项编译生成目标文件:

$ gcc -c -g stack/stack.c stack/push.c stack/pop.c stack/is_empty.c

由于接下来要用objdump -dS把反汇编指令和源代码穿插起来分析,所以用-g选项加调试信息。注意,加调试信息必须在编译每个目标文件时用-g选项,而不能只在最后编译生成可执行文件时用-g选项。反汇编查看push.o

$ objdump -dS push.o 
push.o:     file format elf32-i386

Disassembly of section .text:

00000000 <push>:
/* push.c */
extern char stack[512];
extern int top; void push(char c)
{
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 04 sub $0x4,%esp
6: 8b 45 08 mov 0x8(%ebp),%eax
9: 88 45 fc mov %al,-0x4(%ebp)
stack[++top] = c;
c: a1 00 00 00 00 mov 0x0,%eax
11: 83 c0 01 add $0x1,%eax
14: a3 00 00 00 00 mov %eax,0x0
19: 8b 15 00 00 00 00 mov 0x0,%edx
1f: 0f b6 45 fc movzbl -0x4(%ebp),%eax
23: 88 82 00 00 00 00 mov %al,0x0(%edx)
}
29: c9 leave
2a: c3 ret

指令中凡是用到stacktop的地址都用0x0表示,准备在重定位时修改。再看readelf输出的.rel.text段的信息:

Relocation section '.rel.text' at offset 0x848 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
0000000d 00001001 R_386_32 00000000 top
00000015 00001001 R_386_32 00000000 top
0000001b 00001001 R_386_32 00000000 top
00000025 00001101 R_386_32 00000000 stack

标出了指令中有四处需要在重定位时修改。下面编译链接成可执行文件之后再做反汇编分析:

$ gcc -g main.c stack.o push.o pop.o is_empty.o -Istack -o main
$ objdump -dS main
...
080483c0 <push>:
/* push.c */
extern char stack[];
extern int top; void push(char c)
{
80483c0: push %ebp
80483c1: e5 mov %esp,%ebp
80483c3: ec sub $0x4,%esp
80483c6: 8b mov 0x8(%ebp),%eax
80483c9: fc mov %al,-0x4(%ebp)
stack[++top] = c;
80483cc: a1 a0 mov 0x804a010,%eax
80483d1: c0 add $0x1,%eax
80483d4: a3 a0 mov %eax,0x804a010
80483d9: 8b a0 mov 0x804a010,%edx
80483df: 0f b6 fc movzbl -0x4(%ebp),%eax
80483e3: a0 mov %al,0x804a040(%edx)
}
80483e9: c9 leave
80483ea: c3 ret
80483eb: nop
...

原来指令中的0x0被修改成了0x804a010和0x804a040,这样做了重定位之后,各段的加载地址就定死了,因为在指令中使用了绝对地址。

现在看用-fPIC编译生成的目标文件有什么不同:

$ gcc -c -g -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
$ objdump -dS push.o push.o: file format elf32-i386 Disassembly of section .text: <push>:
/* push.c */
extern char stack[];
extern int top; void push(char c)
{
: push %ebp
: e5 mov %esp,%ebp
: push %ebx
: ec sub $0x4,%esp
: e8 fc ff ff ff call <push+0x8>
c: c3 add $0x2,%ebx
: 8b mov 0x8(%ebp),%eax
: f8 mov %al,-0x8(%ebp)
stack[++top] = c;
: 8b mov 0x0(%ebx),%eax
1e: 8b mov (%eax),%eax
: 8d lea 0x1(%eax),%edx
: 8b mov 0x0(%ebx),%eax
: mov %edx,(%eax)
2b: 8b mov 0x0(%ebx),%eax
: 8b mov (%eax),%ecx
: 8b mov 0x0(%ebx),%edx
: 0f b6 f8 movzbl -0x8(%ebp),%eax
3d: 0a mov %al,(%edx,%ecx,)
}
: c4 add $0x4,%esp
: 5b pop %ebx
: 5d pop %ebp
: c3 ret Disassembly of section .text.__i686.get_pc_thunk.bx: <__i686.get_pc_thunk.bx>:
: 8b 1c mov (%esp),%ebx
: c3 ret

指令中用到的stacktop的地址不再以0x0表示,而是以0x0(%ebx)表示,但其中还是留有0x0准备做进一步修改。再看readelf输出的.rel.text段:

Relocation section '.rel.text' at offset 0x94c contains  entries:
Offset Info Type Sym.Value Sym. Name
R_386_PC32 __i686.get_pc_thunk.bx
0000000e 0000130a R_386_GOTPC _GLOBAL_OFFSET_TABLE_
0000001a R_386_GOT32 top
R_386_GOT32 top
0000002d R_386_GOT32 top
R_386_GOT32 stack

topstack对应的记录类型不再是R_386_32了,而是R_386_GOT32,有什么区别呢?我们先编译生成共享库再做反汇编分析:

$ gcc -shared -o libstack.so stack.o push.o pop.o is_empty.o
$ objdump -dS libstack.so
...
0000047c <push>:
/* push.c */
extern char stack[];
extern int top; void push(char c)
{
47c: push %ebp
47d: e5 mov %esp,%ebp
47f: push %ebx
: ec sub $0x4,%esp
: e8 ef ff ff ff call <__i686.get_pc_thunk.bx>
: c3 6c 1b add $0x1b6c,%ebx
48e: 8b mov 0x8(%ebp),%eax
: f8 mov %al,-0x8(%ebp)
stack[++top] = c;
: 8b f4 ff ff ff mov -0xc(%ebx),%eax
49a: 8b mov (%eax),%eax
49c: 8d lea 0x1(%eax),%edx
49f: 8b f4 ff ff ff mov -0xc(%ebx),%eax
4a5: mov %edx,(%eax)
4a7: 8b f4 ff ff ff mov -0xc(%ebx),%eax
4ad: 8b mov (%eax),%ecx
4af: 8b f8 ff ff ff mov -0x8(%ebx),%edx
4b5: 0f b6 f8 movzbl -0x8(%ebp),%eax
4b9: 0a mov %al,(%edx,%ecx,)
}
4bc: c4 add $0x4,%esp
4bf: 5b pop %ebx
4c0: 5d pop %ebp
4c1: c3 ret
4c2: nop
4c3: nop
...

和先前的结果不同,指令中的0x0(%ebx)被修改成-0xc(%ebx)-0x8(%ebx),而不是修改成绝对地址。所以共享库各段的加载地址并没有定死,可以加载到任意位置,因为指令中没有使用绝对地址,因此称为位置无关代码。另外,注意这几条指令:

 494:	8b 83 f4 ff ff ff    	mov    -0xc(%ebx),%eax
49a: 8b 00 mov (%eax),%eax
49c: 8d 50 01 lea 0x1(%eax),%edx

和先前的指令对比一下:

 80483cc:       a1 10 a0 04 08          mov    0x804a010,%eax
80483d1: 83 c0 01 add $0x1,%eax

可以发现,-0xc(%ebx)这个地址并不是变量top的地址,这个地址的内存单元中又保存了另外一个地址,这另外一个地址才是变量top的地址,所以mov -0xc(%ebx),%eax是把变量top的地址传给eax,而mov (%eax),%eax才是从top的地址中取出top的值传给eaxlea 0x1(%eax),%edx是把top的值加1存到edx中,如下图所示:

图 20.3. 间接寻址

topstack的绝对地址保存在一个地址表中,而指令通过地址表做间接寻址,因此避免了将绝对地址写死在指令中,这也是一种避免硬编码的策略。

现在把main.c和共享库编译链接在一起,然后运行:

$ gcc main.c -g -L. -lstack -Istack -o main
$ ./main
./main: error while loading shared libraries: libstack.so: cannot open shared object file: No such file or directory

结果出乎意料,编译的时候没问题,由于指定了-L.选项,编译器可以在当前目录下找到libstack.so,而运行时却说找不到libstack.so。那么运行时在哪些路径下找共享库呢?我们先用ldd命令查看可执行文件依赖于哪些共享库:

$ ldd main
linux-gate.so.1 => (0xb7f5c000)
libstack.so => not found
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7dcf000)
/lib/ld-linux.so.2 (0xb7f42000)

ldd模拟运行一遍main,在运行过程中做动态链接,从而得知这个可执行文件依赖于哪些共享库,每个共享库都在什么路径下,加载到进程地址空间的什么地址。/lib/ld-linux.so.2是动态链接器,它的路径是在编译链接时指定的,我们在第 2 节 “main函数和启动例程”讲过gcc在做链接时用-dynamic-linker指定动态链接器的路径,它也像其它共享库一样加载到进程的地址空间中。libc.so.6的路径/lib/tls/i686/cmov/libc.so.6是由动态链接器ld-linux.so.2在做动态链接时搜索到的,而libstack.so的路径没有找到。linux-gate.so.1这个共享库其实并不存在于文件系统中,它是由内核虚拟出来的共享库,所以它没有对应的路径,它负责处理系统调用。总之,共享库的搜索路径由动态链接器决定,从ld.so(8)的Man Page可以查到共享库路径的搜索顺序:

  1. 首先在环境变量LD_LIBRARY_PATH所记录的路径中查找。

  2. 然后从缓存文件/etc/ld.so.cache中查找。这个缓存文件由ldconfig命令读取配置文件/etc/ld.so.conf之后生成,稍后详细解释。

  3. 如果上述步骤都找不到,则到默认的系统路径中查找,先是/lib然后是/usr/lib。

先试试第一种方法,在运行main时通过环境变量LD_LIBRARY_PATH把当前目录添加到共享库的搜索路径:

$ LD_LIBRARY_PATH=. ./main

这种方法只适合在开发中临时用一下,通常LD_LIBRARY_PATH是不推荐使用的,尽量不要设置这个环境变量,理由可以参考Why LD_LIBRARY_PATH is bad(http://www.visi.com/~barr/ldpath.html)。

再试试第二种方法,这是最常用的方法。把libstack.so所在目录的绝对路径(比如/home/akaedu/somedir)添加到/etc/ld.so.conf中(该文件中每个路径占一行),然后运行ldconfig

$ sudo ldconfig -v
...
/home/akaedu/somedir:
libstack.so -> libstack.so
/lib:
libe2p.so. -> libe2p.so.2.3
libncursesw.so. -> libncursesw.so.5.6
...
/usr/lib:
libkdeinit_klauncher.so -> libkdeinit_klauncher.so
libv4l2.so. -> libv4l2.so.
...
/usr/lib64:
/lib/tls: (hwcap: 0x8000000000000000)
/usr/lib/sse2: (hwcap: 0x0000000004000000)
...
/usr/lib/tls: (hwcap: 0x8000000000000000)
...
/usr/lib/i686: (hwcap: 0x0008000000000000)
/usr/lib/i586: (hwcap: 0x0004000000000000)
...
/usr/lib/i486: (hwcap: 0x0002000000000000)
...
/lib/tls/i686: (hwcap: 0x8008000000000000)
/usr/lib/i686/cmov: (hwcap: 0x0008000000008000)
...
/lib/tls/i686/cmov: (hwcap: 0x8008000000008000)

ldconfig命令除了处理/etc/ld.so.conf中配置的目录之外,还处理一些默认目录,如/lib/usr/lib等,处理之后生成/etc/ld.so.cache缓存文件,动态链接器就从这个缓存中搜索共享库。hwcap是x86平台的Linux特有的一种机制,系统检测到当前平台是i686而不是i586i486,所以在运行程序时使用i686的库,这样可以更好地发挥平台的性能,也可以利用一些新的指令,所以上面ldd命令的输出结果显示动态链接器搜索到的libc/lib/tls/i686/cmov/libc.so.6,而不是/lib/libc.so.6。现在再用ldd命令查看,libstack.so就能找到了:

$ ldd main
linux-gate.so.1 => (0xb809c000)
libstack.so => /home/akaedu/somedir/libstack.so (0xb806a000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7f0c000)
/lib/ld-linux.so.2 (0xb8082000)

第三种方法就是把libstack.so拷到/usr/lib/lib目录,这样可以确保动态链接器能找到这个共享库。

其实还有第四种方法,在编译可执行文件main的时候就把libstack.so的路径写死在可执行文件中:

$ gcc main.c -g -L. -lstack -Istack -o main -Wl,-rpath,/home/akaedu/somedir
-Wl,-rpath,/home/akaedu/somedir表示-rpath /home/akaedu/somedir是由gcc传递给链接器的选项。可以看到readelf的结果多了一条rpath记录: $ readelf -a main
...
Dynamic section at offset 0xf10 contains entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libstack.so]
0x00000001 (NEEDED) Shared library: [libc.so.]
0x0000000f (RPATH) Library rpath: [/home/akaedu/somedir]
...

还可以看出,可执行文件运行时需要哪些共享库也都记录在.dynamic段中。当然rpath这种办法也是不推荐的,把共享库的路径定死了,失去了灵活性。

4.2. 动态链接的过程 请点评

本节研究一下在main.c中调用共享库的函数push是如何实现的。首先反汇编看一下main的指令:

$ objdump -dS main
...
Disassembly of section .plt: 080483a8 <__gmon_start__@plt-0x10>:
80483a8: ff 35 f8 9f 04 08 pushl 0x8049ff8
80483ae: ff 25 fc 9f 04 08 jmp *0x8049ffc
80483b4: 00 00 add %al,(%eax)
...
080483d8 <push@plt>:
80483d8: ff 25 08 a0 04 08 jmp *0x804a008
80483de: 68 10 00 00 00 push $0x10
80483e3: e9 c0 ff ff ff jmp 80483a8 <_init+0x30> Disassembly of section .text:
...
080484a4 <main>:
/* main.c */
#include <stdio.h>
#include "stack.h" int main(void)
{
80484a4: 8d 4c 24 04 lea 0x4(%esp),%ecx
80484a8: 83 e4 f0 and $0xfffffff0,%esp
80484ab: ff 71 fc pushl -0x4(%ecx)
80484ae: 55 push %ebp
80484af: 89 e5 mov %esp,%ebp
80484b1: 51 push %ecx
80484b2: 83 ec 04 sub $0x4,%esp
push('a');
80484b5: c7 04 24 61 00 00 00 movl $0x61,(%esp)
80484bc: e8 17 ff ff ff call 80483d8 <push@plt>
...

第 3 节 “静态库”链接静态库不同,push函数没有链接到可执行文件中。而且call 80483d8 <push@plt>这条指令调用的也不是push函数的地址。共享库是位置无关代码,在运行时可以加载到任意地址,其加载地址只有在动态链接时才能确定,所以在main函数中不可能直接通过绝对地址调用push函数,也是通过间接寻址来找push函数的。对照着上面的指令,我们用gdb跟踪一下:

$ gdb main
...
(gdb) start
Breakpoint 1 at 0x80484b5: file main.c, line 7.
Starting program: /home/akaedu/somedir/main
main () at main.c:7
7 push('a');
(gdb) si
0x080484bc 7 push('a');
(gdb) si
0x080483d8 in push@plt ()
Current language: auto; currently asm

跳转到.plt段中,现在将要执行一条jmp *0x804a008指令,我们看看0x804a008这个地址里存的是什么:

(gdb) x 0x804a008
0x804a008 <_GLOBAL_OFFSET_TABLE_+20>: 0x080483de

原来就是下一条指令push $0x10的地址。继续跟踪下去:

(gdb) si
0x080483de in push@plt ()
(gdb) si
0x080483e3 in push@plt ()
(gdb) si
0x080483a8 in ?? ()
(gdb) si
0x080483ae in ?? ()
(gdb) si
0xb806a080 in ?? () from /lib/ld-linux.so.2

最终进入了动态链接器/lib/ld-linux.so.2,在其中完成动态链接的过程并调用push函数,我们不深入这些细节了,直接用finish命令返回到main函数:

(gdb) finish
Run till exit from #0 0xb806a080 in ?? () from /lib/ld-linux.so.2
main () at main.c:8
8 return 0;
Current language: auto; currently c

这时再看看0x804a008这个地址里存的是什么:

(gdb) x 0x804a008
0x804a008 <_GLOBAL_OFFSET_TABLE_+20>: 0xb803f47c
(gdb) x 0xb803f47c
0xb803f47c <push>: 0x53e58955

动态链接器已经把push函数的地址存在这里了,所以下次再调用push函数就可以直接从jmp *0x804a008指令跳到它的地址,而不必再进入/lib/ld-linux.so.2做动态链接了。

4.3. 共享库的命名惯例

你可能已经注意到了,系统的共享库通常带有符号链接,例如:

$ ls -l  /lib
...
-rwxr-xr-x root root -- : libc-2.8..so
lrwxrwxrwx root root -- : libcap.so. -> libcap.so.1.10
-rw-r--r-- root root -- : libcap.so.1.10
lrwxrwxrwx root root -- : libcap.so. -> libcap.so.2.10
-rw-r--r-- root root -- : libcap.so.2.10
...
lrwxrwxrwx root root -- : libc.so. -> libc-2.8..so
...
$ ls -l /usr/lib/libc.so
-rw-r--r-- root root -- : /usr/lib/libc.so

按照共享库的命名惯例,每个共享库有三个文件名:real name、soname和linker name。真正的库文件(而不是符号链接)的名字是real name,包含完整的共享库版本号。例如上面的libcap.so.1.10libc-2.8.90.so等。

soname是一个符号链接的名字,只包含共享库的主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序的.dynamic段只记录共享库的soname,只要soname一致,这个共享库就可以用。例如上面的libcap.so.1libcap.so.2是两个主版本号不同的libcap,有些应用程序依赖于libcap.so.1,有些应用程序依赖于libcap.so.2,但对于依赖libcap.so.1的应用程序来说,真正的库文件不管是libcap.so.1.10还是libcap.so.1.11都可以用,所以使用共享库可以很方便地升级库文件而不需要重新编译应用程序,这是静态库所没有的优点。注意libc的版本编号有一点特殊,libc-2.8.90.so的主版本号是6而不是2或2.8。

linker name仅在编译链接时使用,gcc-L选项应该指定linker name所在的目录。有的linker name是库文件的一个符号链接,有的linker name是一段链接脚本。例如上面的libc.so就是一个linker name,它是一段链接脚本:

$ cat /usr/lib/libc.so
/* GNU ld script
Use the shared library, but some functions are only in
the static library, so try that secondarily. */
OUTPUT_FORMAT(elf32-i386)
GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux.so.2 ) )

下面重新编译我们的libstack,指定它的soname:

$ gcc -shared -Wl,-soname,libstack.so.1 -o libstack.so.1.0 stack.o push.o pop.o is_empty.o

这样编译生成的库文件是libstack.so.1.0,是real name,但这个库文件中记录了它的soname是libstack.so.1

$ readelf -a libstack.so.1.0
...
Dynamic section at offset 0xf10 contains 22 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000e (SONAME) Library soname: [libstack.so.1]
...

如果把libstack.so.1.0所在的目录加入/etc/ld.so.conf中,然后运行ldconfig命令,ldconfig会自动创建一个soname的符号链接:

$ sudo ldconfig
$ ls -l libstack*
lrwxrwxrwx 1 root root 15 2009-01-21 17:52 libstack.so.1 -> libstack.so.1.0
-rwxr-xr-x 1 akaedu akaedu 10142 2009-01-21 17:49 libstack.so.1.0

但这样编译链接main.c却会报错:

$ gcc main.c -L. -lstack -Istack -o main
/usr/bin/ld: cannot find -lstack
collect2: ld returned 1 exit status

注意,要做这个实验,你得把先前编译的libstack共享库、静态库都删掉,如果先前拷到/lib或者/usr/lib下了也删掉,只留下libstack.so.1.0libstack.so.1,这样你会发现编译器不认这两个名字,因为编译器只认linker name。可以先创建一个linker name的符号链接,然后再编译就没问题了:

$ ln -s libstack.so.1.0 libstack.so
$ gcc main.c -L. -lstack -Istack -o main

linux c 链接详解4-共享库的更多相关文章

  1. linux c 链接详解3-静态库

    3静态库 摘自:Linux C编程一站式学习 透过本节可以学会编译静态链接库的shell脚本! 有时候需要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库,我们在不同的程 ...

  2. linux c 链接详解5-虚拟内存管理

    5. 虚拟内存管理 我们知道操作系统利用体系结构提供的VA到PA的转换机制实现虚拟内存管理.有了共享库的基础知识之后,现在我们可以进一步理解虚拟内存管理了.首先分析一个例子: $ ps PID TTY ...

  3. linux ln链接详解

    1.序 Linux具有为一个文件起多个名字的功能,称为链接.被链接的文件可以存放在相同的目录下,但是必须有不同的文件名,而不用在硬盘上为同样的数据重复备份.另外,被链接的文件也可以有相同的文件名,但是 ...

  4. linux c 链接详解1-多目标文件链接

    1. 多目标文件的链接 摘自:linux c编程一站式学习 http://learn.akae.cn/media/index.html 可以学会在linux下将多个c语言文件一起编译. 现在我们把例  ...

  5. linux c 链接详解2-定义和声明

    2定义和声明 摘自:linux c编程一站式学习 可以学会extern和static用法,头文件知识. 2.1. extern和static关键字 在上一节我们把两个程序文件放在一起编译链接,main ...

  6. Linux 软硬链接详解

    软链接 软链接: 类似于windows的快捷方式,—>文本文件,但是包含了真实文件的地址               源文件删除,则软连接也删除               软链接可以放在任何文 ...

  7. Linux 链接详解----静态链接实例分析

    由Linux链接详解(1)中我们简单的分析了静态库的引用解析和重定位的内容, 下面我们结合实例来看一下静态链接重定位过程. /* * a.c */ ; void add(int c); int mai ...

  8. linux lsof命令详解

    linux lsof命令详解 简介 lsof(list open files)是一个列出当前系统打开文件的工具.在linux环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访 ...

  9. [转贴]linux lsof命令详解

    linux lsof命令详解 https://www.cnblogs.com/sparkbj/p/7161669.html 简介 lsof(list open files)是一个列出当前系统打开文件的 ...

随机推荐

  1. rabbiitmq非阻塞调用

    https://blog.csdn.net/panxianzhan/article/details/50755409 https://blog.csdn.net/u013946356/article/ ...

  2. Vue-鼠标按键修饰符

    left .right .middle 这些修饰符会限制处理函数仅响应特定的鼠标按钮. 如下例子 <div id="app"> <input type=" ...

  3. Python3.5-20190530-unittest模块

    >>> dir(unittest) #所有的属性和方法 ['BaseTestSuite', 'FunctionTestCase', 'SkipTest', 'TestCase', ' ...

  4. pycharm远程连接的步骤(有一项需特别注意)

    1.设置远程服务器,在这里配置服务器地址等: 要注意下边的这个mappings设置好,跟编译器的path mapping设置为同一个,这里我没设置为同一个,后边就出现了点问题. 2.配置远程的编译器 ...

  5. Linux命令"ls"进阶说明

    pwd:the current working directory cd -: return to the previous working directory Filenames that begi ...

  6. v-for中的key的使用【key的作用主要是是为了高效的更新虚拟DOM】

    vue中列表循环需加:key="唯一标识" 唯一标识可以是item里面id index等,因为vue组件高度复用增加Key可以标识组件的唯一性,为了更好地区别各个组件 key的作用 ...

  7. 纯JSP简单登录实例

    记一下,免得以后忘记了,又要去查. 文件共有四个web.xml.login.jsp.logout.jsp.welcome.jsp四个文件 测试环境:Tomcat 6.0.x 假设项目名称是LoginS ...

  8. MongoDB笔记【2】——基本概念和基本指令

    - 基本概念 数据库(database) 集合(collection) 文档(document) - 在MongoDB中,数据库和集合都不需要手动创建,当我们创建文档时,如果文档所在的集合或数据库不存 ...

  9. MySQL索引优化之双表示例

    select * from tableA a left join tableB b on a.f_id = b.id; 索引建tableB表上面, 因为left join 注定左表全都有,所以应该关心 ...

  10. SQL Server DBA日常检查常用SQL

    .数据库 --所有数据库的大小 exec sp_helpdb --所有数据库的状态 select name, user_access_desc, --用户访问模式 state_desc, --数据库状 ...