对于新的签名方案APK Signature Scheme v2,在这篇文章中已经有详细的介绍http://www.tuicool.com/articles/bURRVrj。从这篇文章中可以知道,新的签名方案与旧的签名方案之间的对比是:

图1

新的签名方案生成与旧的签名方案相比,在zip文件中新增了一个APK Signing Block区块。使用新的签名方案以后,在apk下所有的文件中所做的修改,都会导致在android7.0系统上安装失败。那么android7.0上的签名校验过程是怎样的呢?为什么apk下任意文件修改都会导致签名校验失败呢?网上很多文章都对7.0之前的签名校验过程进行了讲解,想了解7.0之前的签名校验过程可以参考文章http://blog.csdn.net/roland_sun/article/details/42029019。 但是很少有文章对7.0及更高版本上的签名校验过程进行分析,所以在这篇文章中,主要针对7.0及以上的签名校验过程进行分析,通过分析7.0系统源码的方式,来向大家描述一下这一过程。
(1) Android平台上所有应用程序安装都是由PackageManangerService(代码位于frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java)来管理的,apk的安装流程与签名验证相关的步骤位于installPackageLI函数中:
    private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {  ……  PackageParser pp = new PackageParser();  ……  try {  pp.collectCertificates(pkg, parseFlags);  pp.collectManifestDigest(pkg);  } catch (PackageParserException e) {  res.setError("Failed collect during installPackageLI", e);  return;  }  ……  
(2)  在这个函数中,会用到PackageParser这个类 (代码位于 frameworks\base\core\java\android\content\pm\PackageParser.java ,编译后存在于 framework.jar 文件中)是一个apk包的解析器,接下来我们来看其 collectCertificates 函数的实现:
     public static void collectCertificates(Package pkg, int parseFlags)throws PackageParserException {collectCertificatesInternal(pkg, parseFlags);final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0;for (int i = 0; i < childCount; i++) {Package childPkg = pkg.childPackages.get(i);childPkg.mCertificates = pkg.mCertificates;childPkg.mSignatures = pkg.mSignatures;childPkg.mSigningKeys = pkg.mSigningKeys;}}
(3)  在 collectCertificates函数中,调用了   collectCertificatesInternal函数。所以接下来我们看看   collectCertificatesInternal的函数实现:
    private static void collectCertificatesInternal(Package pkg, int parseFlags)throws PackageParserException {pkg.mCertificates = null;pkg.mSignatures = null;pkg.mSigningKeys = null;Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates");try {collectCertificates(pkg, new File(pkg.baseCodePath), parseFlags);if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) {for (int i = 0; i < pkg.splitCodePaths.length; i++) {collectCertificates(pkg, new File(pkg.splitCodePaths[i]), parseFlags);}}} finally {Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);}}
(4)在函数 collectCertificatesInternal中调用了 collectCertificates函数的重载函数,在7.0之前,是在 collectCertificates函数中直接调用其重载函数,中间是没有 collectCertificatesInternal函数的。 collectCertificates的重载函数是一个很重要的函数,接下来我们看看在这个函数中做了什么操作:
            private static void collectCertificates(Package pkg, File apkFile, int parseFlags)
1            throws PackageParserException {
1151        final String apkPath = apkFile.getAbsolutePath();
1152
1153        // Try to verify the APK using APK Signature Scheme v2.
1154        boolean verified = false;
1155        {
1156            Certificate[][] allSignersCerts = null;
1157            Signature[] signatures = null;
1158            try {
1159                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
1160                allSignersCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
1161                signatures = convertToSignatures(allSignersCerts);
1162                // APK verified using APK Signature Scheme v2.
1163                verified = true;
1164            } catch (ApkSignatureSchemeV2Verifier.SignatureNotFoundException e) {
1165                // No APK Signature Scheme v2 signature found
1166            } catch (Exception e) {
1167                // APK Signature Scheme v2 signature was found but did not verify
1168                throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
1169                        "Failed to collect certificates from " + apkPath
1170                                + " using APK Signature Scheme v2",
1171                        e);
1172            } finally {
1173                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
1174            }……
在这个函数中,首先对签名apk做了v2方式的签名校验(代码从1154-1174)。也就是说首先用针对v2方式的签名方式来做签名校验,如果校验成功 verified   = true。如果在校验的过程中抛出了异常,那么有两种可能:1.apk没有用v2签名方式进行签名;2.apk用了v2签名方式进行签名,但是签名检验没有成功。
(5)我们接下来继续看  private  static  void  collectCertificates( Package  pkg,  File  apkFileint  parseFlags)函数中的内容:
            StrictJarFile jarFile = null;
1199        try {
1200            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
1201            // Ignore signature stripping protections when verifying APKs from system partition.
1202            // For those APKs we only care about extracting signer certificates, and don't care
1203            // about verifying integrity.
1204            boolean signatureSchemeRollbackProtectionsEnforced =
1205                    (parseFlags & PARSE_IS_SYSTEM_DIR) == 0;
1206            jarFile = new StrictJarFile(
1207                    apkPath,
1208                    !verified, // whether to verify JAR signature
1209                    signatureSchemeRollbackProtectionsEnforced);
1210            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
1211
1212            // Always verify manifest, regardless of source
1213            final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
1214            if (manifestEntry == null) {
1215                throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
1216                        "Package " + apkPath + " has no manifest");
1217            }
1218
1219            // Optimization: early termination when APK already verified
1220            if (verified) {
1221                return;
1222            }……
(6)接下来,用到了  StrictJarFile这个类(代码位于/ frameworks/ base/ core/ java/ android/ util/ jar/ StrictJarFile.java)。可以看到在实例化该类时,传入的参数中有 ! verified。
          public StrictJarFile(String fileName,
74            boolean verify,
75            boolean signatureSchemeRollbackProtectionsEnforced)
76                    throws IOException, SecurityException {
77        this.nativeHandle = nativeOpenJarFile(fileName);
78        this.raf = new RandomAccessFile(fileName, "r");
79
80        try {
81            // Read the MANIFEST and signature files up front and try to
82            // parse them. We never want to accept a JAR File with broken signatures
83            // or manifests, so it's best to throw as early as possible.
84 if (verify) {
85                HashMap<String, byte[]> metaEntries = getMetaEntries();
86                this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
87                this.verifier =
88                        new StrictJarVerifier(
89                                fileName,
90                                manifest,
91                                metaEntries,
92                                signatureSchemeRollbackProtectionsEnforced);
93                Set<String> files = manifest.getEntries().keySet();
94                for (String file : files) {
95                    if (findEntry(file) == null) {
96                        throw new SecurityException(fileName + ": File " + file + " in manifest does not exist");
97                    }
98                }
99
100                isSigned = verifier.readCertificates() && verifier.isSignedJar();
101            } else {
102                isSigned = false;
103                this.manifest = null;
104                this.verifier = null;
105            }
106        } catch (IOException | SecurityException e) {
107            nativeClose(this.nativeHandle);
108            IoUtils.closeQuietly(this.raf);
109            throw e;
110        }
进入 StrictJarFile中可以看出,当v erified = true时(要记得在外面传入的是! verified,所以这里的v erified = true表示的是v2签名校验验没有成功),所以在if( verify)判断条件成立的情况下(即v2签名校验失败),后面使用了 StrictJarVerifie类,对apk使用7.0之前的签名校验方式进行签名校验,之后的流程就是7.0之前的签名校验过程了,这里就不在多说了;当v erified = false时,只做了一些赋值操作。
然后回后到   private  static  void  collectCertificates( Package  pkg,  File  apkFileint  parseFlags)函数中的1212行,对apk中的manifest文件是否为空进行了校验,如果manifest校验通过。接下来v erified = true(说明v2签名校验成功)就退出了该函数,这一次的校验结束。
到这里可以对7.0系统中的签名校验过程先做一个小结:7.0系统对apk进行签名校验,先对apk进行v2签名方案的签名校验,如果校验成功,之后还会再对manifest进行校验,签名校验到此结束; 如果v2签名校验没有成功,会走7.0之前的签名校验流程,之后还会再对manifest进行校验。
(7)在上面我们一直说,7.0上会先对apk做v2签名校验,那么接下来,我们就详细介绍一下v2签名方式校验的流程:
首先在  private  static  void  collectCertificates( Package  pkg,  File  apkFileint  parseFlags)方法中调用了 ApkSignatureSchemeV2Verifier. verify方法来进行v2签名校验,我以我们先来看卡 ApkSignatureSchemeV2Verifier中 verify这个方法:
       /**
97     * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
98     * associated with each signer.
99     *
100     * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
101     * @throws SecurityException if a APK Signature Scheme v2 signature of this APK does not verify.
102     * @throws IOException if an I/O error occurs while reading the APK file.
103     */104    public static X509Certificate[][] verify(String apkFile)
105            throws SignatureNotFoundException, SecurityException, IOException {
106        try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
107            return verify(apk);
108        }
109    }
在这个方法中,读取apk文件以后,其实是调用了 verif y的重载方法 verif y( RandomAccessFile  apk),并将其结果直接返回,所以接下来我们看看 verif y( RandomAccessFile  apk)方法的实现。

(8)

       private static X509Certificate[][] verify(RandomAccessFile apk)
121            throws SignatureNotFoundException, SecurityException, IOException {
122        SignatureInfo signatureInfo = findSignature(apk);
123        return verify(apk.getFD(), signatureInfo);
124    }
在这个方法中先通过  findSignatur e方法获取了apk的签名信息,  findSignatur e这个方法的返回值是一个  SignatureInfo 类型的对象,通过  SignatureInfo 这个我们来看一下  findSignatur这个方法获取的签名信息中都包含哪些内容:

(9)

        /**
127     * APK Signature Scheme v2 block and additional information relevant to verifying the signatures
128     * contained in the block against the file.
129     */130    private static class SignatureInfo {
131        /** Contents of APK Signature Scheme v2 block. */132        private final ByteBuffer signatureBlock;
133
134        /** Position of the APK Signing Block in the file. */135        private final long apkSigningBlockOffset;
136
137        /** Position of the ZIP Central Directory in the file. */138        private final long centralDirOffset;
139
140        /** Position of the ZIP End of Central Directory (EoCD) in the file. */141        private final long eocdOffset;
142
143        /** Contents of ZIP End of Central Directory (EoCD) of the file. */144        private final ByteBuffer eocd;
145
146        private SignatureInfo(
147                ByteBuffer signatureBlock,
148                long apkSigningBlockOffset,
149                long centralDirOffset,
150                long eocdOffset,
151                ByteBuffer eocd) {
152            this.signatureBlock = signatureBlock;
153            this.apkSigningBlockOffset = apkSigningBlockOffset;
154            this.centralDirOffset = centralDirOffset;
155            this.eocdOffset = eocdOffset;
156            this.eocd = eocd;
157        }
158    }
signatureInfo中包括 整个签名块的内容,签名块的位置信息,核心目录块的的位置信息,目录结束标识块的位置及内容。获取apk文件中个区块信息之后,又调用了verify的另一个重载函数:

(10)

        private static X509Certificate[][] verify(
203            FileDescriptor apkFileDescriptor,
204            SignatureInfo signatureInfo) throws SecurityException {
205        int signerCount = 0;
206        Map<Integer, byte[]> contentDigests = new ArrayMap<>();
207        List<X509Certificate[]> signerCerts = new ArrayList<>();
208        CertificateFactory certFactory;
209        try {
210            certFactory = CertificateFactory.getInstance("X.509");
211        } catch (CertificateException e) {
212            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
213        }
214        ByteBuffer signers;
215        try {
216            signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
217        } catch (IOException e) {
218            throw new SecurityException("Failed to read list of signers", e);
219        }
220        while (signers.hasRemaining()) {
221            signerCount++;
222            try {
223                ByteBuffer signer = getLengthPrefixedSlice(signers);
224                X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory);
225                signerCerts.add(certs);
226            } catch (IOException | BufferUnderflowException | SecurityException e) {
227                throw new SecurityException(
228                        "Failed to parse/verify signer #" + signerCount + " block",
229                        e);
230            }
231        }
232
233        if (signerCount < 1) {
234            throw new SecurityException("No signers found");
235        }
236
237        if (contentDigests.isEmpty()) {
238            throw new SecurityException("No content digests found");
239        }
240
241        verifyIntegrity(
242                contentDigests,
243                apkFileDescriptor,
244                signatureInfo.apkSigningBlockOffset,
245                signatureInfo.centralDirOffset,
246                signatureInfo.eocdOffset,
247                signatureInfo.eocd);
248
249        return signerCerts.toArray(new X509Certificate[signerCerts.size()][]);
250    }
在该函数中,会从签名块信息中获取sigers信息放在缓冲区中,sigers中可能包含多个signerBlock,循环调用 getLengthPrefixedSlice (sigers)方法从signers中获取signerBlock,获取signerBlock以后,会调用  verifySigner方法获取si gnerBlock 中的签名数据、签名和公钥,以下是  verifySigner方法 中获取签名数据、签名和公钥的代码。
              ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
verifySigner中还对签名信息 signatures 进行验证。并且由于 signedData中包含摘要信息,所以在  verifySigner方法中还会从signerBlock的signedData 中获取摘要信息放在 contentDigests中,以及获取并返回signerBlock的证书。
contentDigests是一个 Map< Integer,  byte[]>类型,其中使用以下函数 通过key值可以找到该摘要所使用的算法 ,byte[]是摘要。 contentDigests 将在  verifyIntegrity方法中用得到。
       private static String getSignatureAlgorithmJcaKeyAlgorithm(int sigAlgorithm) {
704        switch (sigAlgorithm) {
705            case SIGNATURE_RSA_PSS_WITH_SHA256:
706            case SIGNATURE_RSA_PSS_WITH_SHA512:
707            case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
708            case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
709                return "RSA";
710            case SIGNATURE_ECDSA_WITH_SHA256:
711            case SIGNATURE_ECDSA_WITH_SHA512:
712                return "EC";
713            case SIGNATURE_DSA_WITH_SHA256:
714                return "DSA";
715            default:
716                throw new IllegalArgumentException(
717                        "Unknown signature algorithm: 0x"
718                                + Long.toHexString(sigAlgorithm & 0xffffffff));
719        }
接下来在 verify(  FileDescriptor  apkFileDescriptor, SignatureInfo  signatureInfo ) 方法校验了signerBlock的个数(不能小于1)和摘要信息(不能为空)。之后调用  verifyIntegrity方法进行继续校验,下面我们来看看  verifyIntegrity的实现过程:

(11)

       private static void verifyIntegrity(
389            Map<Integer, byte[]> expectedDigests,
390            FileDescriptor apkFileDescriptor,
391            long apkSigningBlockOffset,
392            long centralDirOffset,
393            long eocdOffset,
394            ByteBuffer eocdBuf) throws SecurityException {
395
396        if (expectedDigests.isEmpty()) {
397            throw new SecurityException("No digests provided");
398        }
399
400        // We need to verify the integrity of the following three sections of the file:
401        // 1. Everything up to the start of the APK Signing Block.
402        // 2. ZIP Central Directory.
403        // 3. ZIP End of Central Directory (EoCD).
404        // Each of these sections is represented as a separate DataSource instance below.
405
406        // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to
407        // avoid wasting physical memory. In most APK verification scenarios, the contents of the
408        // APK are already there in the OS's page cache and thus mmap does not use additional
409        // physical memory.
410        DataSource beforeApkSigningBlock =
411                new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset);
412        DataSource centralDir =
413                new MemoryMappedFileDataSource(
414                        apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);
415
416        // For the purposes of integrity verification, ZIP End of Central Directory's field Start of
417        // Central Directory must be considered to point to the offset of the APK Signing Block.
418        eocdBuf = eocdBuf.duplicate();
419        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
420        ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
421        DataSource eocd = new ByteBufferDataSource(eocdBuf);
422
423        int[] digestAlgorithms = new int[expectedDigests.size()];
424        int digestAlgorithmCount = 0;
425        for (int digestAlgorithm : expectedDigests.keySet()) {
426            digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
427            digestAlgorithmCount++;
428        }
429        byte[][] actualDigests;
430        try {
431            actualDigests =
432                    computeContentDigests(
433                            digestAlgorithms,
434                            new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
435        } catch (DigestException e) {
436            throw new SecurityException("Failed to compute digest(s) of contents", e);
437        }
438        for (int i = 0; i < digestAlgorithms.length; i++) {
439            int digestAlgorithm = digestAlgorithms[i];
440            byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
441            byte[] actualDigest = actualDigests[i];
442            if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
443                throw new SecurityException(
444                        getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
445                                + " digest of contents did not verify");
446            }
447        }
448    }
从注释可以看出,该函数 会对除APK Signing Block以外的其他三个模块进行完整性校验,函数开始处分别获得了apk中其他三个模块的数据,第431行,会调用 computeContentDigests 方法来计算其他三个模块的摘要信息;获取摘要信息以后,到438行,有一个for循环,在for 循环中会获取保存在 APK Signing Block模块中的每个模块的摘要信息,即第(10)步中说到的 contentDigests的 value信息和计算得来的每个模块的摘要信息进行比较,如果不想等则完整性校验没有通过。
这个过程说明,在 APK Signing Block中会保存Contents of ZIP entries 、Central Directory和End of Central Directory这三个模块的摘要信息,完整性校验,其实就是再计算一遍个模块的摘要信息和保存在 APK Signing Block中的摘要信息进行比较,如果都相等说明apk没有被修改。
从这个过程也可以看出,没有对 APK Signing Block模块进行完整性校验,所以对 APK Signing Block模块进行修改不会影响apk在安装时的签名校验过程。这也就是为什么可以在改模块上加入渠道信息。
上面说到,会重新计算Contents of ZIP entries 、Central Directory和End of Central Directory这三个模块的摘要信息,下面简单说一下照耀信息的计算过程:
  1. 每个部分的内容分成连续的1 MB大小的块。最后一大块将是较短的,if段长度不是1 MB的倍数。没有块是为空(零长度)段制作的。
  2. 计算每个块的摘要信息
  3. 并将所有部分的摘要按照顺序连起来。

apk签名怎么兼容7.0的v2签名方式,及6.0及以下的签名方式:

APK 签名方案 v2 是在 Android 7.0 (Nougat) 中引入的。为了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。

为了能够更好的理解android7.0签名校验过程,这里对zip文件的格式进行简单的介绍,想详细的了解zip的格式,可以点击以下网址 https://my.oschina.net/dubenju/blog/514969进行详细的了解。
zip文件的结构:压缩源文件数据区+压缩源文件目录区( Central directory 核心目录 )+压缩源文件目录结束标志( End of central directory record(EOCD) 目录结束标识 )
具体点的zip格式:
[文件头+文件数据+数据描述符]{此处可重复n次}+核心目录+目录结束标识
每个块都有一个开始的标志及压缩后的大小:
文件头标识,值固定(0x04034b50)
核心目录文件header标识=(0x02014b50)
核心目录结束标记(0x06054b50)
在ZIP文件的第一部分,会保存每个文件的信息,所以修改apk中的任意文件,都会导致第一个模块完整性校验通不过,apk安装失败。
而且可以知道zip文件每个区块的数据是独立的。新版的v2签名,是在“压缩源文件数据区”这个块之后又增加了一个新的块,因为各模块是是独立的,以至于在 APK Signing Block区块中增加内容不会对其他区块造成影响,所以在 7.0系统上对其他三个模块做完整性校验是能够通过的。
若有不正之处请多多谅解,并欢迎批评指正。
请尊重作者劳动成果,转载请标明原文链接

android7.0及以上版本签名校验过程详解相关推荐

  1. Android签名与校验过程详解

    原文:https://blog.csdn.net/gulinxieying/article/details/78677487 目 录 一.签名与校验原理概要    2 1.数字签名简介    2 2. ...

  2. mysql8.0.20 64位安装教程_MySQL8.0.20压缩版本安装教程图文详解

    1.mysql下载地址: http://ftp.ntu.edu.tw/mysql/downloads/mysql-cluster-8.0/ 2.解压以后放在一个文件夹里面,创建my.ini配置文件: ...

  3. 【甘道夫】HBase(0.96以上版本)过滤器Filter详解及实例代码

    说明: 本文参考官方Ref Guide,Developer API和众多博客,并结合实测代码编写,详细总结HBase的Filter功能,并附上每类Filter的相应代码实现. 本文尽量遵从Ref Gu ...

  4. Faster-RCNN.pytorch的搭建、使用过程详解(适配PyTorch 1.0以上版本)

    Faster-RCNN.pytorch的搭建.使用过程详解 引言 faster-rcnn pytorch代码下载 faster-rcnn pytorch配置过程 faster-rcnn pytorch ...

  5. Android签名机制之---签名验证过程详解

    一.前言 今天是元旦,也是Single Dog的嚎叫之日,只能写博客来祛除寂寞了,今天我们继续来看一下Android中的签名机制的姊妹篇:Android中是如何验证一个Apk的签名.在前一篇文章中我们 ...

  6. MySQL8.0二进制免编译部署过程详解(二)

    一.背景介绍 本文主要介绍MySQL二进制免编译软件包的安装过程详解,之所以选择二进制安装包部署MySQL8.0,是因为官方版本已经内置所有功能,在安装的时候可以指定数据库安装路径. 目前官网MySQ ...

  7. Android 7.0 Audio的Resample过程详解

    Android 7.0 Audio的Resample过程详解 Qidi 2017.02.23 (Markdown & Haroopad) [前言] 处理过音频文件的工程师都知道音频数据存在采样 ...

  8. Android中mesure过程详解 (结合Android 4.0.4 最新源码)

    如何遍历并绘制View树?之前的文章Android中invalidate() 函数详解(结合Android 4.0.4 最新源码)中提到invalidate()最后会发起一个View树遍历的请求,并通 ...

  9. Android中layout过程详解 (结合Android 4.0.4 最新源码)

    上一篇文章Android中mesure过程详解 (结合Android 4.0.4 最新源码)介绍了View树的measure过程,相对与measure过程,本文介绍的layout过程要简单多了,正如l ...

最新文章

  1. 我是LinkedIn的SRE,我把LinkedIn搞挂了
  2. github使用的一点记录。
  3. Python中dict用法详解
  4. UA MATH575B 数值分析下 计算统计物理例题1
  5. 【深度学习】Batch Normalization(BN)超详细解析
  6. 新手入门python的注意事项_【新手入门Python语言的方法】
  7. css background 一半_CSS---阴阳图
  8. 华人团队再获ACL最高奖,这次来自字节跳动的NLP基础研究
  9. 地铁译:Spark for python developers --- 搭建Spark虚拟环境 4...
  10. ubuntu上常用的软件安装
  11. 知识点记录:李群李代数,微分流形,微分几何,图论
  12. 数学建模—一元回归分析
  13. 米思齐(Mixly)初体验—触摸式开关
  14. 微信卡券开发错误自排查参考文档
  15. sgu 309 Real Fun
  16. Css、less和Sass(SCSS)的区别
  17. PLSQL连接登录失败
  18. Solver 配置详解
  19. win10 启动自动修复失败
  20. 关于友盟9.3.8版本集成QQ无效问题

热门文章

  1. zk下载文件中文乱码解决方案
  2. 上海市高等学校计算机试卷,2016年上海市高等学校计算机等级考试试卷
  3. 每周精品之理财20190325
  4. Ubuntu分区方案100G
  5. 连接数据库,OleDbDataReader读取mdb的数据
  6. 【数据结构Note5】- 树和二叉树(知识点超细大全-涵盖常见算法 排序二叉树 线索二叉树 平衡二叉树 哈夫曼树)
  7. Java 时间复杂度和空间复杂度
  8. LeetCode 2017. Grid Game【前缀和/数组】中等
  9. ElasticSearch从0到1
  10. 『忘了再学』Shell基础 — 19、使用declare命令声明变量类型