对于新的签名方案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. 开始接触QM(Quality Management)
  2. 原生ajax表单交互
  3. 六间房与花椒直播重组首次交割完成
  4. ASP.NET防止按F5键造成表单重复提交
  5. c语言程序设保安值班,保安值班系统
  6. 如何自定义SAP Spartacus店铺的界面颜色风格
  7. 湘潭大学oracle考试,湘潭大学07级《oracle数据库》A卷及其部分参考答案
  8. Python2.7.16安装(Win10)
  9. 网络虚拟化基础一:linux名称空间Namespaces
  10. SD卡在SPI模式下的初始化和详细的代码分析
  11. oracle 表空间配置
  12. tab栏切换制作(原生js版本)
  13. mac 安装mysql
  14. sum() over (order by )
  15. 是什么让你萌发了跳槽的念头?
  16. RFC chinese
  17. LINUX OpenGL简单测试代码
  18. 淘宝客怎么推广?学会这三招,赚钱不再愁
  19. 调查问卷或量表数据的一般处理与SPSS统计分析
  20. 柯特斯公式的matlab代码,牛顿-柯特斯公式C语言的实现.pdf

热门文章

  1. 怎样压缩图片大小?好用的图片压缩技巧分享
  2. 戴尔Win10笔记本蓝牙突然不能用且设备管理器中找不到蓝牙
  3. 如何控制局域网网速_ipv4和ipv6哪个网速快
  4. Redisson分布式锁快速入门教程
  5. 【电子画册免费制作】云展网教程 | 添加注释按钮
  6. 自定义laravel validate request 返回错误信息格式
  7. 《去哪网》编写到上线总结
  8. 百度向海龙走后大搜妖魔化了
  9. SVN撤销已提交的代码
  10. DROO demo_on_off.py