error: RPC failed; result=22, HTTP code = 404

在进行jenkins checkout git中的项目时,报了个错:error: RPC failed; result=22, HTTP code = 404

直接用git clone工程时,也报了同样的错误。

那到底是什么原因导致了git报这样的错误呢?网上搜索也是各种说法都有,但是这种是正确的:是centos自带的版本太低了。

1,查看git的版本
git --version

2,centos自带的yum库版本太低了,需要增加 wandisco 的版本库:

vim /etc/yum.repos.d/wandisco-git.repo

[wandisco-git]
name=Wandisco GIT Repository
baseurl=http://opensource.wandisco.com/centos/7/git/$basearch/
enabled=1
gpgcheck=1
gpgkey=http://opensource.wandisco.com/RPM-GPG-KEY-WANdisco

rpm --import http://opensource.wandisco.com/RPM-GPG-KEY-WANdisco

3,然后再升级git
yum install -y git

升级好后再看git的版本,OK了。

然后 git clone 看看,这个错误消失了。

Auto-configuration的规范

尽管springboot的官方文档很糟糕,但是关于Auto-configuration还是写得比较好的,位于springboot reference中的Creating Your Own Auto-configuration。

至于为什么要用 Auto-configuration,可以见我写得这一篇文章:@SpringBootApplication中几点解惑

springboot官方提供的一个样例,非常不错,看了以后,auto-configuration就学会了:Master Spring Boot auto-configuration

auto-configuration的代码分层架构

官方推荐分成2个工程,一个starter,另一个autoconfigure。
但是我个人认为就那么一个starter就好了,何必分成2个工程,感觉多此一举的,因为starter纯粹就是一个空工程。
不过,话反过来说,学会了2个分开的工程,合成一个工程也是超级简单的,所以无所谓一个工程还是2个工程。

命名常规

官方的auto-configuration,比如yyy来举例,那么应该是 spring-boot-starter-yyy.jar,spring-boot-autoconfigure-yyy.jar
非官方的auto-configuration,比如xxx来举例,那么应该是 xxx-spring-boot-starter.jar,xxx-spring-boot-autoconfigure.jar
上面已经描述很清晰了,不再解释了。

spring-factories

对于auto-configuration工程来说,已经取消了Component auto scan的功能,代而取之的是采用spring-factories的方式,这个spring-factories文件位于autoconfigure这个工程中。

spring-provides

这个文件在springboot的官方文档中是没有提到的,那这个spring-provides文件用来干什么的?我查了有个人这么解释的:

It's for tooling. STS (and other IDEs if they chose to) can index those files and make autocomplete suggestions based on stuff that isn't yet on the classpath.

哦,原来是给STS,或者IDEA用的,这个文件放置在starter这个工程中,那就不用去管这个文件了。

好了,springboot的 auto-configuration就这么简单。

在vscode下搭建golang的开发环境

golang的下载、安装、设置在这篇文章中就不叙述了。

本文讲述的是在 centos7 下面的 vscode 的 golang 开发环境搭建。

vscode的安装

vscode的安装非常简单,最好的方式就是前往官网:https://code.visualstudio.com/,那是最新的最权威的。
这里做个摘抄:

rpm --import https://packages.microsoft.com/keys/microsoft.asc
sh -c 'echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/vscode.repo'
yum check-update
yum install code

vscode的启动

很简单,直接在console中敲入 code 即可。但是假如以root身份运行的话,需要执行:
code --user-data-dir

vscode的汉化

打开vscode后,按下 ctrl + shift + p,会打开一个输入框,输入 configure 即可,会出现一个 Configure Display Language的选项,点下它进行安装简体中文,重启vscode后,就能看到汉化了。

更改颜色主题

默认的vscode是黑色主题,我一直喜欢白色的主题,对于黑色的主题,对我来说,很累眼睛(有些人认为黑色不累眼睛)。那么在vscode更改颜色主题的步骤很简单:左下角有个齿轮状的按钮,点击后出现:颜色主题,点击后出现一个输入框,找到淡色的白色主题即可。

取消迷你地图

我到现在也没有感觉到迷你地图用来干什么的,取消吧,占地方。点击菜单的”查看”,去掉“显示迷你地图”前面的勾即可。

设置快捷键

点击左下角的齿轮状的按钮,点击键盘快捷方式,出现许多的键盘快捷方式,进行调整。
我进行更改的几个快捷键如下:

删除一行  ctrl + d
后退  alt + <-
前进  alt + ->

安装golang的插件

在 vscode 中,按下 ctrl + shift + p,会打开一个输入框,输入 install 即可,会出现一个 Extensions: Install Extensions的选项,点下它后,在左侧会出现插件商店,在插件商店的输入框中输入:go 即可找到 go 的官方插件,点击后找到 install 即可。
golang插件装好以后,随便打开一个golang的工程,这个时候,vscode会在右下角连续弹出很多窗口提示安装golang的组件,逐个点击安装即可。因为我平时就已经把这些组件通过go get的方式安装好了,所以弹出的组件安装很少,更不用说遇到墙的事情了。假如被强了,自己可以到其他网站查找相关安装方法。
总的来说,vscode下的golang安装组件可以说是傻瓜式的,非常简单。

设置编辑区字体大小

默认编辑区的字体太小,点击左下角的齿轮状的按钮,点击“设置”,找到“文本编辑器”–>“字体”,找到 Font Size,改为 18 即可。

设置golang的大纲显示

打开golang的工程后,左侧的上半部分是文件及文件夹,左侧的下半部分是包或者类的大纲。我常用的开发视图是左侧是文件及文件夹,右侧是类的大纲,这样的界面显示能快速总览全局,也方便打开文件和类。但是在vscode中是没有办法把左上部分和左下部分进行分离(分离的效果是左侧文件及文件夹,右侧是类的大纲)。于是我们可以安装go outliner这个组件来加强这个功能,不过go outliner也没有办法单独显示在右侧。但是go outliner比vscode自带的那个大纲要强大很多,你们装上后就能发现两者的不同。在扩展商店中,找到go outliner,安装好后,进行manage,务必要在Extend Default Explorer tab with additinal section containing Go symbols的前面打勾。如此能在最左侧的能出现go outliner,点击后,比较完美的大纲出现了。就是因为不能在右侧,所以需要进行切换比较麻烦。

查找方法的引用和实现

在面向对象的语言中,对于查找某个方法的引用和实现是非常实用的一个技能。golang也提供了这一功能,这个功能是有guru这个组件来实现的。这个组件可以说非常的牛,具体可以看文档Using Go Guru,假如被墙的话,那么可以到这里来看Go Guru(golang 代码导航工具) 的使用。具体用guru来查找方法的引用和实现,只需要在方法上点击右键,然后点击“查找引用”和“查找实现”即可。但是这里千万千万要注意,在vscode中的,右键点击方法,然后选择“查找实现”是找不到结果的,正确的方法应该是:在接口类上点击右键,然后“查找实现”,这样guru会给你找出实现这个接口的实现类,切记切记,我尝试了很多次才试出来的!

delve的配置

在debug显示变量的时候,有些字符串的长度比较长,delve显示的结果是”xxx … +xx more”,怎么样让delve显示完整的字符串呢?在 settings.json中进行相应设置。(by the way,如何打开 settings.json?点击左下角的齿轮状按钮,然后点击“设置”,随便找一个“在 settings.json中编辑”点击即可)。进入settings.json后,输入 go.delveConfig,vscode马上会弹出可以设置选项给你,找到后,直接回车马上就能显示所有的delveConfig的选项,找到 maxStringLen,设为 9999 即可,如下:

"go.delveConfig": {
        "dlvLoadConfig": {
            "followPointers": true,
            "maxVariableRecurse": 1,
            "maxStringLen": 99999,
            "maxArrayValues": 64,
            "maxStructFields": -1
        },
        "apiVersion": 2,
        "showGlobalVariables": true
    }

工程中launch.json设置

在工程中,当你按下F5进行debug的时候,会发现vscode默认是当前所选文件作为入口点开始进行debug,往往当前文件不是main的入口文件,所以就会报错,所以我们要修改 launch.json 这个配置。点击菜单,找到“调试”,点击“打开配置”,即可打开 launch.json 这个文件,找到 program 这个配置项,设置为 “program”: “${workspaceRoot}/your-main-file.go”,其中的 ${workspaceRoot} 指得是打开的工程所在的文件夹,这些变量大家可以参考 vscode Variables Reference

golang的编辑器选择

很早就开始接触golang这门语言了,之间也用了不少不同的IDE来开发golang,但是每一种IDE都有这种那种的问题。

先说一下现在的结果吧:我现在用 vscode 来作为 golang 的开发工具,vscode 是我目前为止用过的最好的 golang 开发工具。

sublime

sublime是个伟大的产品,可以说vscode百分百是参照它来做的,很多套路基本上一样。不说sublime别的,就说golang在sumlime上的插件,做得就不是特别好,在golang代码的追踪上存在一些问题。用sublime来写golang那是很久以前的事情了,可能现在有了改观。但是随着vscode出来,就算有改观,我也不会再去尝试了,因为sublime要钱。

goland

jetbrains出品,要钱的,只对于我来说,我是对jetbrains的系列产品都是比较反感的,我不习惯它的这种开发风格,不适应。

goclipse

是eclipse中的一个插件,凭借这eclipse这个平台,在goclipse中开发golang,带来了太多熟悉的感觉,上手非常快,但是goclipse已经不再维护了,所以新的go版本在goclipse下面会报错。还有很多的go的组件都出问题了。这个非常可惜。强烈建议有能力的、有时间的人继续做下去。

liteide

在用vscode之前,我用的一直是liteide,大而全,基本上都有,界面也还行、代码跟踪采用 guru 也不错。但是 liteide 有各种各样的莫名其妙的问题,特别严重的是在 debug 这个环节,很多module的代码没有办法跟,即便用 delve 也是如此。本来我以为golang的debug在有些包下面会出现这样的问题(跟不进去的问题),等到我用了vscode后,才发现在 vscode 下很容易的跟踪进去,分析变量的值。于是,我毅然抛弃了 liteide,投向了 vscode的怀抱。

vscode

在vscode下安装golang的环境特别特别的方便,可以说算是傻瓜式的吧。vscode的golang开发环境可以说到目前为止最为优秀了,基本上需要的一些功能都能找到。对于vscode的golang开发,见另一篇博文《在vscode下搭建golang的开发环境》

如何在tomcat中禁用JSESSIONID

首先这是一个“伪话题”,原因有下面2点:
1. 现在的应用部署越来越多采用分布式集群部署,从而对于session的使用越来越少。采用session来存储数据的方式逐渐都转移到了缓存中实现。
2. 前后端分离的应用开发模式越来越多,而 jsp 的使用也是越来越少。

但是,我为什么还是要把这个话题拿出来在这里再叙述一遍呢?原因是看到外面大量关于这个话题的文章,基本上都是错的,为了防止读者受到错误影响,我写下了这篇文章,希望对你有所帮助。

JSESSIONID的产生机制

tomcat只有在程序中使用了 xxx.getSession() 的时候才会创建session(JSESSIONID是它的标识),其他任何时候都不会主动去创建。

这个时候有人问了,我的程序里明确没有调用 xxx.getSession(),为什么还会创建session?原因是用到了jsp作为渲染机制,你去看一下在tomcat的工作目录中所对应的jsp编译好的.class或者也有.java文件,能够看到:session = pageContext.getSession();这样一段代码。这个session也就是jsp几个内置对象中含有session对象的来源。所以,只要你用到了 jsp,那么就会默认产生session。

那不用jsp,而用 servlet 的话,会不会主动创建 session 呢?不会的,因为对于Controller或者Action来说,本质上还是servlet。不会的,除非你自己写 xxx.getSession()。

那我也不用jsp,用其他的模板引擎,比如 freemarker 或者 Thymeleaf,会不会主动创建 session 呢?不会的,除非这个模板引擎中也主动调用xxx.getSession,否则不会。

那我既不用jsp,也不用servlet,用的是 spring的Controller或者 struts 的Action,那会不会主动创建 session 呢?不会的,因为对于Controller或者Action来说,本质上还是servlet。

禁用tomcat的session

通过上面的机制分析,很明显禁用tomcat的session的最好途径就是告诉jsp,不要主动创建session,那么方式是在每个jsp页面中加入:
<%@ page session="false" %>
加好以后,创新一下该jsp页面,会发现编译好的.class或者.java中 session = pageContext.getSession(); 这句话已经消失不见了。
加好以后,在浏览器中清除cookie后,再看一下 JSESSIONID 已经不再出现。
那在每个页面中加入 <%@ page session="false" %>,太麻烦了,一般在jsp工程中,大家都会在开始就用 <%@ include file="xxx.jsp"%> 这样来引入一些每个页面中都需要的元素,那么就把 <%@ page session="false" %> 加入到 xxx.jsp 中。

另一种做法:禁用tomcat的session

上一种方法是非常不错的,但是会出现一些纰漏的地方:
1. 会不会有个别jsp页面没有加入 <%@ page session="false" %>
2. 在servlet中,会不会某个程序员随心写了句 session = request.getSession(),但是 session 却没有去用
为了堵住这些纰漏的地方,还有一种采用连根拔起的方式方法。

import java.io.IOException;

import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Session;
import org.apache.catalina.session.ManagerBase;

public class SessionManager extends ManagerBase implements Lifecycle {

    private Session globalSession;

    @Override
    protected synchronized void startInternal() throws LifecycleException {
        super.startInternal();
        setState(LifecycleState.STARTING);
    }

    @Override
    protected synchronized void stopInternal() throws LifecycleException {
        setState(LifecycleState.STOPPING);
    }

    @Override
    public void load() throws ClassNotFoundException, IOException {
    }

    @Override
    public void unload() throws IOException {
    }

    @Override
    public Session createSession(String sessionId) {
        if(globalSession == null) {
            globalSession = super.createSession(sessionId);
        } else {
            globalSession.setValid(true);
        }

        return globalSession;
    }

    @Override
    public Session createEmptySession() {
        if(globalSession == null) {
            globalSession = super.createSession(sessionId);
        } else {
            globalSession.setValid(true);
        }

        return globalSession;
    }

}

SessionManager的原理其实是用同一个 globalSession 来代替每次创建新的 session,不创建这个 session 的话,那么会在访问 jsp 页面的时候报错:Page needs a session and none is available。

还需要注意这个globalSession需要进行激活,否则在tomcat的session失效后,会将此session标注为失效,那么尽管globalSession还是存在的,但是是失效的状态,还是会报错说session不存在。所以globalSession.setValid(true);这句很重要。

然后,在tomcat的server.xml中进行设置,大致如下:

<Context docBase="D:\test" path="/test" reloadable="false" sessionCookieName="yoursessionname">
    <Manager className="xxx.xxx.SessionManager" />
</Context>

上面的sessionCookieName默认是JSESSIONID,其实这个是有点点缺陷的,能够让别人知道你的应用是用java开发的,所以,换个其他的名字会更好一些。

springboot中使用上面例子的SessionManager

上面这个方法是用传统方式开发应用,那么在springboot下,因为内嵌了tomcat,使得更加简便

@Configuration
public class TomcatConfig {

    @Bean
    public EmbeddedServletContainerFactory servletContainer(){
        TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
        factory.addContextCustomizers(new TomcatContextCustomizer() {
        @Override
        public void customize(Context context) {
            SessionManager sessionManager = new SessionManager();
            context.setManager(sessionManager);
            context.setSessionCookieName("yoursessionname");
        }
    });
        return factory;
    }
}

这种方式,最好做成 Auto-configuration 的jar,在pom.xml中引入,更加方便,具体可以参考一下这篇文章:@SpringBootApplication中几点解惑

综述

  1. 最好在jsp页面中加入 <%@ page session="false" %>
  2. 用上面的SessionManager方式
    最后说一下,现代的java应用开发,几乎不考虑 session 了,所以这篇文章对您可能没有多大用处,对一些传统应用还是有些帮助的。

tags在golang编译中的作用

golang没有条件编译,但是可以在编译的时候用tags参数来实现条件编译的效果。

这个参数的官方解释如下:

-tags 'tag list'
    a list of build tags to consider satisfied during the build.
    For more information about build tags, see the description of
    build constraints in the documentation for the go/build package.

等于没有说,说让去看 go/build package中的build constraints的描述,这又是在哪里?

在这里:go/build package文档

这个tags参数用法,可以参考这两篇文章,tags参数因为本身很简单,然后这两篇文章也写得好:
golang项目中使用条件编译
How to use conditional compilation with the go build tool

比较郁闷,在搜索引擎中搜索:golang tags,搜出来的基本上是tags在struct中的使用.

ldflags在golang编译中的2个作用

golang在编译的时候,可以传入一些参数,其中有 -ldflags 参数,这个参数的官方解释如下:

-ldflags 'flag list'
    arguments to pass on each 5l, 6l, 8l, or 9l linker invocation.

意思大概是:这个参数将影响链接这个过程。

对于链接所涉及的一些相关概念,请参考这篇文章静态链接,动态链接,静态库,共享库这些概念的详解

第一个用法

ldflags用于链接过程,这个具体的用法,这篇文章写得非常到味,比我写得好,我就不再写了,大家可以到这里查看,也谈Go的可移植性

第二个用法

ldflags在编译golang的时候,可以传入一些值用来配置golang的应用。这个用法简单,可以查看 golang在编译时用ldflags设置变量的值

第二个用法目前大多数用于应用程序的版本信息,本人认为第二个用法在应用程序假如有配置文件的话,那就没有太多意义了,完全可以用配置文件来替代这个功能。但是假如只是一个纯净的应用程序,没有配置文件的话,这个用法相当有意思了。

静态链接,动态链接,静态库,共享库这些概念的详解

静态链接,动态链接,静态库,共享库,这些名词的资料在网上一大堆,但是我看了这些资料,总觉得有些地方怪怪的,有些文章描述某个概念特别好,但是在描述其他的概念的时候就有些凌乱。于是,我把这些资料给整理一下,厘清这些名词的真正含义。

本文一开始将各个概念全部罗列一遍(一开始的时候不讲各个之间的关系,或者比较少讲),然后才将各个概念联系起来(这就是外面那些资料缺少的地方,那些资料都没有讲清楚)

编译过程

要了解这些概念,编译过程是前提条件。
编译过程分成下面几个过程:预处理–>汇编–>编译–>链接。这些概念大家也可以到网上去搜索,一大堆。
本文中的这些概念就是发生在最后一个环节:链接。千万要记住这一点,很重要。

静态链接

关于静态链接和动态链接,网上这篇文章写得通俗易懂:静态链接和动态链接的区别,这里对这篇文章进行了摘抄:
1. 什么是静态链接:主函数中调用了库中的某个函数(one()),并且把该函数编码进了可执行文件中。见下图:

2. 静态链接存在的第一个问题:若需要调用库中的函数过多,会造成可执行文件体积巨大。见下图:

3. 静态链接存在的第二个问题:因为库中的函数代码嵌入了可执行程序,所以如果库更新了,想要更新可执行文件中库部分的代码,就只能重新编译。见下图:

动态链接

为了解决静态链接的以上2个问题,可以让库中的函数不要直接编译进可执行程序,而是放在内存中,可执行程序在使用(one)函数时,直接读内存的地址就可以用了。这样既减小了可执行程序的大小,又可以方便的使用库函数更新。这种方式即为——动态链接。见下图:

动态链接的优点:
1. 执行文件很小。
2. 当依赖的库函数发生升级了,执行文件不用重新编译。
动态链接的缺点:
一个可执行应用,在A服务器上进行了编译–>链接操作,在链接的时候所依赖的那些函数库是存在A服务器上的。当把这个可执行应用拿到B服务器上去运行,因为在B服务器上该可执行应用所依赖的函数库可能缺少或者位置不正确,而导致报错。所以,在使用这个可执行应用之前,需要在B服务器上进行make一下生成所对应的可执行应用才可以。这也就是 configure–>make–>make install的真正含义所在。

静态库和共享库(动态库,动态链接库)概述

  1. 静态库,在windows的后缀为 .lib(library),在linux的后缀为 .a(archive)
  2. 共享库,又称为动态库,又称为动态链接库,(这3个叫法在网上到处混着乱飞,就是指同一个),在windows的后缀为 .dll(dynamic link library),在linux的后缀为 .so(shared object)

在讲下面之前,借用《C编程一站式学习》中的“链接详解”这个章节中的例子来举例说明,顺便说一下《C编程一站式学习》这本书写得不错,但是“链接详解”这个章节还是没有说清楚,反而越说越糊涂。

/* stack.c */
char stack[512];
int top = -1;
/* push.c */
#include <stdio.h>

extern char stack[512];
extern int top;
void push(char c)
{
    printf("....");
    stack[++top] = c;
}
/* pop.c */
extern char stack[512];
extern int top;
char pop(void)
{
return stack[top--];
}
/* is_empty.c */
extern int top;
int is_empty(void)
{
return top == -1;
}
/* stack.h */
#ifndef STACK_H
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#endif
/* stack.h */
#ifndef STACK_H
#define STACK_H
extern void push(char);
extern char pop(void);
extern int is_empty(void);
#endif
/* main.c */
#include <stdio.h>
#include "stack.h"
int main(void)
{
push('a');
return 0;
}

上面这个例子也是很浅显易懂的。

静态库

gcc -c stack.c push.c pop.c is_empty.c
//上面这个步骤是compile,还不涉及link,结果生成了各自的.o,通过反汇编能看到,凡是使用printf()这个函数的地方都用了 0X0 这样的占位符先占着位置(printf()是出自libc.so这个大名鼎鼎的共享库),凡是彼此之间相互用到的也都用了0X0 这样的占位符先占着位置。
ar cr libstack.a stack.o push.o pop.o is_empty.o //创建libstack.a这个静态库

ar 这个命令仅仅是压缩,什么都不涉及,不会对那些.o有任何修改,记住就是压缩!既然是压缩,那么原来怎么样就是怎么样了。
通过上面ar生成的 libstack.a 就是静态库,那为什么要取这样的名字 lib + stack + .a,这个是规范,要给下面讲到的链接所用,死记即可。
静态库的特点如下:
1,仅仅是.o的打包而已
2,.o中假如是 .c之间相互用到的,那么是0X0占位符
3,.o中假如有来自共享库(见下面,这里暂时不管)的,那么也是0X0占位符

静态库和静态链接、动态链接的结合

这个部分就是外面网上好多没有提到的,这样就导致了看到后面一头雾水,就是压根不明白到底什么是什么。

默认“动态链接”

gcc main.c -L. -lstack -Istack -o main  #这是一步compile和link合在一起的步骤

-L:大写的L,表示静态库的文件夹(看清每个字,理解每个字,是文件夹,而且是静态库)
-l:小写的l,表示链接的时候要链接 libstack.so或者libstack.a 这个文件,具体找到共享库(.so)还是静态库(.a),libstack.so先找到就用这个共享库,找不到则在-L指定的文件夹下面找libstack.a这个静态库

那是不是找到.so动态库就是动态链接?找到.a静态库就是静态链接?
否,否,否!!!
千万不要被他们之间摸棱两可的命名给混淆了,接着看:

gcc的链接,默认的情况下采用“动态链接”,比如上面的那条命令。那么按照“动态链接”的概念来进行链接,main.o当链接到“静态库”的时候,假如只是单纯的把main.o中的push()替换成“静态库”中的push()函数地址,因为libstack.a是不可能更改的,里面的那些彼此之间的函数调用以及调用的printf()仅仅是0X0地址,那肯定不对的。所以,尽管是“动态链接”,但是遇到libstack.a这个静态库的时候,只有华山一条路,只能采用“静态链接”的方式,将所调用的push()函数给拷贝到应用程序main中,同时调整拷贝过来的push()中所用到的libc.so中的printf()函数地址为间接地址(也就是说printf()还是“动态链接”的)。这个过程想想都能明白的。

总结一下:
“动态链接”,当链接到“静态库”的时候,采用“静态链接”,全部拷贝过来。但是对于main自己本身所用到的“动态库”和“静态库”中所用到的“动态库”则采用“动态链接”。

强制使用“静态链接”

gcc -static main.c -L. -lstack -Istack -o main

-static,表示使用“静态链接”的方式。那么不管是遇到“静态库”还是“动态库”,还是“静态库”中用到“动态库”,将一律拷贝到应用程序中,更改调用地址,也就是说一律采用“静态链接”的方式。

总结一下:
“静态链接”,不管遇到“静态库”还是“动态库”,统统采用“静态链接”的方式。

共享库(动态库,动态链接库)

gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
//compile成.o,通过反汇编可以看到调用的函数地址为0x0(%ebx),本质上还是仅仅占位符,可以看成0X0
gcc -shared -o libstack.so stack.o push.o pop.o is_empty.o
//将上面生成的.o编译成共享库libstack.so,通过反汇编可以看到上面的0x0已经被替换成间接地址了。

到这里为止,可以看到“共享库”和“静态库”有本质差别的,差别就在于调用函数的地址上,“共享库”提供了一个简介地址可以在后面的链接中以及运行中找到调用函数。

共享库和静态链接、动态链接的结合

还是和上面一样,“共享库”和“静态链接”、“动态链接”没有本质上的瓜葛。

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

从前面讲到的知道,这是“动态链接”,而且链接的是“动态库”libstack.so,而且还要链接printf()的动态库libc.so。那么最终生成的main这个可执行程序中所涉及的函数全部都是间接寻址。

当执行./main的时候报错,说libstack.so找不到,这个时候要记得:.L是指的“静态库”的文件夹,而对于“动态库”的寻找,一般是在/usr/lib下面,所以我们还需要将生成的libstack.so放在/usr/lib下面,这也就是 make install 的作用,将生成的“动态库”放在 /usr/lib下面。

假如我们还是想用“静态链接”来链接,那么就改用下面的来执行:

gcc -static main.c -L. -lstack -Istack -o main

这样,尽管都是“动态库”,但是还是强制使用了“静态链接”。

全部总结一下:
当“动态链接”遇到“静态库”,则采用“静态链接”。
当“动态链接”遇到“动态库”,则采用“动态链接”。
当“静态链接”遇到“静态库”、“动态库”,一律采用“静态链接”。

本文结束,希望你能彻底搞清这些名词之间的关系,“静态链接”和“静态库”,都有“静态”2个字,其实是2个方面,2者严格意义上完全不一样。“动态链接”和“动态链接库”,更加具有混淆性,也是2个方面,千万别被字面上的含义弄混淆。

顺便说一下:ldd和nm是2个用来分析链接的常用工具。

参考资料

C编程一站式学习

Makefile疑难解答和技巧合集

Makefile文档

Makefile是GNU make中最重要的一个文件,官方的文档:GNU make,当然也是比较复杂和难懂的。

Makefile教程

很早以前,咱们国人写过这么一篇《跟我一起写 Makefile》,这篇文章写得是比较不错的,通过通俗易懂的例子教你怎么写Makefile,但是是一页一页的html,看起来累死,你也可以到网上去搜索相应的pdf,下载下来后,在写作和分析别人写得Makefile的时候,可以作为参考手册。

Makefile基础

Makefile的大致框架如下:

target(目标) ... : prerequisites(依赖目标) ...
    <TAB>command(命令)

上述的翻译:目标、依赖目标、命令 是来自《跟我一起写 Makefile》,大家在看这本书的时候,一定要记住,千万要看清目标和依赖目标,千万别看错了。我个人觉得prerequisites还是翻译成“前置条件”比较好,尽管从意义上来看在这里的prerequisites确实是依赖的目标,但是翻译成了“前置条件”不至于把读者给看晕和看错。

Makefile的调试

Makefile挺难的,更难的是那些变量值、那些调用过程,一个复杂点的Makefile会把你给完全绕晕。这个时候,对于Makefile的调试就非常重要了。对于Makefile的调试,主要看这篇文章,写得相当不错:Makefile常用调试方法
我调试的时候,常用下面的技巧:
1. make的时候,用参数 “-n” “--just-print” “--dry-run” “--recon”,这4个参数是完全一样的。比如 make -n
2. 但是上面只能看到具体执行了哪个命令,假如Makefile相当庞大和复杂,那么就不知道这个命令是由哪个规则调用的,这个时候就需要用下面的参数: --debug=basic,也就是完整的命令是:make -n --debug=basic,出来的结果非常不错,强烈推荐这个。
3. 对于某个变量的值是多少,可以用$(error, ….) $(warning, ….)这2个函数来打印。

Mafefile的函数参考

《跟我一起写 Makefile》这本书写得比较早,有些函数在它上面找不到了,需要在官网http://www.gnu.org/software/make/manual/make.html#Syntax-of-Functions参考,官网是全面的。

Makefile的模式规则

其实挺简单的,就是类似正则表达式,比如下面的:

all: bin/a bin/b
bin/%: ...

那么 bin/a 就会匹配 bin/%,bin/b 也会匹配 bin/%

再比如:

all: bin/a/image bin/b/image
bin/%/image: ...

那么 bin/a/image 就会匹配 bin/%/image,bin/b 也会匹配 bin/%/image

再来一个例子:

all: bin/a/image bin/b/image

bin/a/image: ...

bin/%/image: ...

那么 bin/a/image 会匹配 bin/a/image 和 bin/%/image 这2个目标,这2个目标都执行!

Makefile的中间规则文件

隐含规则,可以参见《跟我一起写 Makefile》,其中上面所说到的模式规则也是属于隐含规则。那么上面所提到的bin/a,bin/a/image所匹配的bin/%, bin/%/image这些就是属于中间规则,假如这些中间规则结果是个文件,那么就叫做中间规则文件。正常情况下,中间规则文件是在规则执行完后要被删除的,比如:

%.txt: foo.log
    # pass

%.log:
    # pass

执行结果如下:

$ make a.txt -n
# pass
# pass
rm foo.log

上面的foo.log这个就是中间规则文件,所以要被删除的。为了防止中间规则文件被删除,那么用下面的来定义,这样就不会删除该中间规则文件了:
.PRECIOUS: foo.log
或者
.SECONDARY: foo.log