您的位置: > 奇异游戏 >

手机md5校验工具(20 款萌小工具合集)

导读 手机md5校验工具文章列表:1、20 款萌小工具合集2、看完这篇,帮你彻底搞懂Android动态加载so3、此软着实强大!各路大神级开发者纷纷对其出招!强上加强4、技术岗怎样做工作交接5

手机md5校验工具文章列表:

手机md5校验工具(20 款萌小工具合集)

20 款萌小工具合集

Moe Tools 萌工具箱 是一款集合了 20 款小工具的「萌」工具箱,包含了程序员、数据编码、文本处理、科学计算、网络通信、影音娱乐、图像处理这几个类别的实用小工具,特色功能有在线端口扫描、服务器压力测试、网易云音乐下载。@Appinn

其实在摘录 Moe Tools 大分类的同时,青小蛙看着这几个类别就笑出了声,仔细看这几个类别:程序员、数据编码、文本处理、科学计算、网络通信、影音娱乐、图像处理。像不像是这个喜欢二次元的程序员全部的生活了:编程写代码的同时,需要做一些数据编码、文字处理,然后就是看看图、看看片、听听歌。

这样的程序员妹子们喜欢么?

哦还有科学计算,当青小蛙以为会是什么算法的时候,没想到居然是温度换算

照惯例,来列一下所有功能:

MD5 校验计算

进制转换

CSS 代码优化

JSON/XML 格式化

在线 HTML 测试

社会主义加密

Base 64 加密/解密

dhdj encode 加密?

汉字转拼音

字数统计

温度换算*

IP 地址信息查询

在线端口扫描

服务器压力测试

网易云音乐下载链接

表情包生成

OCR 文字识别

名片生成

LOGO 生成

Base 64 转图片

dhdj encode 是什么梗没看懂,温度换算暂时失联,已经反馈给开发者,应该已经修复了。更多可以在这里和开发者讨论,Moe Tools 萌工具箱 官网在这里。

还记得前几天推荐的 MikuTools 吗?拥有 58 款小工具,包括视频下载、磁力搜索、实用工具、文字处理等。

看完这篇,帮你彻底搞懂Android动态加载so

作者:Pika

对于一个普通的android应用来说,so库的占比通常都是居高不下的,因为我们无可避免的在开发中遇到各种各样需要用到native的需求,所以so库的动态化可以减少极大的包体积,自从2020腾讯的bugly团队发布关于动态化so的相关文章后,已经过去两年了,经过两年的考验,实际上so动态加载也是非常成熟的一项技术了,但是很遗憾,许多公司都还没有这方面的涉略又或者说不知道从哪里开始进行,因为so动态其实涉及到下载,so版本管理,动态加载实现等多方面,我们不妨抛开这些额外的东西,从最本质的so动态加载出发吧!这里是本次的例子,我把它命名为sillyboy,欢迎pr还有后续点赞呀!

https://cloud.tencent.com/developer/article/1592672

https://github.com/TestPlanB/SillyBoy

一、so动态加载介绍

动态加载,其实就是把我们的so库在打包成apk的时候剔除,在合适的时候通过网络包下载的方式,通过一些手段,在运行的时候进行分离加载的过程。这里涉及到下载器,还有下载后的版本管理等等确保一个so库被正确的加载等过程,在这里,我们不讨论这些辅助的流程,我们看下怎么实现一个最简单的加载流程。

1、从一个例子出发

我们构建一个native工程,然后在里面编入如下内容,下面是cmake。

# For more information about using CMake with Android Studio, read the# documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_required(VERSION 3.18.1)# Declares and names the project.project("nativecpp")# Creates and names a library, sets it as either static# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds them for you.# gradle automatically packages shared libraries with your APK.add_library( # Sets the name of the library. nativecpp # Sets the library as a shared library. SHARED # Provides a relative path to your source File(s). native-lib.cpp)add_library( nativecpptwo SHARED test.cpp)# Searches for a specified prebuilt library and stores the path as a# variable. Because CMake includes system libraries in the search path by# default, you only need to specify the name of the public NDK library# you want to add. CMake verifies that the library exists before# completing its build.find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log)# Specifies libraries CMake should link to your target library. You# can link multiple libraries, such as libraries you define in this# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library. nativecpp # Links the target library to the log library # included in the NDK. ${log-lib})target_link_libraries( # Specifies the target library. nativecpptwo # Links the target library to the log library # included in the NDK. nativecpp ${log-lib})

可以看到,我们生成了两个so库一个是nativecpp,还有一个是nativecpptwo(为什么要两个呢?我们可以继续看下文) 这里也给出最关键的test.cpp代码。

#include <JNI.h>#include <string>#include<android/log.h>extern "C"JNIEXPORT void JNICALLjava_com_example_nativecpp_MainActivity_clickTest(JNIEnv *env, jobject thiz) { // 在这里打印一句话 __android_log_print(ANDROID_LOG_INFO,"hello"," native 层方法");}

很简单,就一个native方法,打印一个log即可,我们就可以在java/kotin层进行方法调用了,即:

public native void clickTest();

2、so库检索与删除

要实现so的动态加载,那最起码是要知道本项目过程中涉及到哪些so吧!不用担心,我们gradle构建的时候,就已经提供了相应的构建过程,即构建的task【 mergeDebugNativeLibs】,在这个过程中,会把一个project里面的所有native库进行一个收集的过程,紧接着task【stripDebugDebugSymbols】是一个符号表清除过程,如果了解native开发的朋友很容易就知道,这就是一个减少so体积的一个过程,我们不在这里详述。所以我们很容易想到,我们只要在这两个task中插入一个自定义的task,用于遍历和删除就可以实现so的删除化了,所以就很容易写出这样的代码。

ext { deleteSoName = ["libnativecpptwo.so","libnativecpp.so"]}// 这个是初始化 -配置 -执行阶段中,配置阶段执行的任务之一,完成afterEvaluate就可以得到所有的tasks,从而可以在里面插入我们定制化的数据task(dynamicSo) {}.doLast { println("dynamicSo insert!!!! ") //projectDir 在哪个project下面,projectDir就是哪个路径 print(getRootProject().findAll()) def file = new File("${projectDir}/build/intermediates/merged_native_libs/debug/out/lib") //默认删除所有的so库 if (file.exists()) { file.listFiles().each { if (it.isDirectory()) { it.listFiles().each { target -> print("file ${target.name}") def compareName = target.name deleteSoName.each { if (compareName.contains(it)) { target.delete() } } } } } } else { print("nil") }}afterEvaluate { print("dynamicSo task start") def customer = tasks.findByName("dynamicSo") def merge = tasks.findByName("mergeDebugNativeLibs") def strip = tasks.findByName("stripDebugDebugSymbols") if (merge != null || strip != null) { customer.mustRunAfter(merge) strip.dependsOn(customer) }}

可以看到,我们定义了一个自定义task dynamicSo,它的执行是在afterEvaluate中定义的,并且依赖于mergeDebugNativeLibs,而stripDebugDebugSymbols就依赖于我们生成的dynamicSo,达到了一个插入操作。那么为什么要在afterEvaluate中执行呢?那是因为android插件是在配置阶段中才生成的mergeDebugNativeLibs等任务,原本的gradle构建是不存在这样一个任务的,所以我们才需要在配置完所有task之后,才进行的插入,我们可以看一下gradle的生命周期。

通过对条件检索,我们就删除掉了我们想要的so,即ibnativecpptwo.so与libnativecpp.so。

3、动态加载so

根据上文检索出来的两个so,我们就可以在项目中上传到自己的后端中,然后通过网络下载到用户的手机上,这里我们就演示一下即可,我们就直接放在data目录下面吧。

真实的项目过程中,应该要有校验操作,比如md5校验或者可以解压等等操作,这里不是重点,我们就直接略过啦!

那么,怎么把一个so库加载到我们本来的apk中呢?这里是so原本的加载过程,可以看到,系统是通过classloader检索native目录是否存在so库进行加载的,那我们反射一下,把我们自定义的path加入进行不就可以了吗?这里采用tinker一样的思路,在我们的classloader中加入so的检索路径即可,比如:

https://cs.android.com/android/platform/superproject/ /master:libcore/ojluni/src/main/java/java/lang/Runtime.java;l=1?q=Runtime.java

private static final class V25 { private static void install(ClassLoader classLoader, File folder) throws Throwable { final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList"); final Object dexPathList = pathListField.get(classLoader); final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories"); List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList); if (origLibDirs == null) { origLibDirs = new ArrayList<>(2); } final iterator<File> libDirIt = origLibDirs.iterator(); while (libDirIt.hasNext()) { final File libDir = libDirIt.next(); if (folder.equals(libDir)) { libDirIt.remove(); break; } } origLibDirs.add(0, folder); final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories"); List<File> origSystemLibDirs = (List<File>) SystemNativeLibraryDirectories.get(dexPathList); if (origSystemLibDirs == null) { origSystemLibDirs = new ArrayList<>(2); } final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() origSystemLibDirs.size() 1); newLibDirs.addAll(origLibDirs); newLibDirs.addAll(origSystemLibDirs); final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class); final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs); final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements"); nativeLibraryPathElements.set(dexPathList, elements); }}

我们在原本的检索路径中,在最前面,即数组为0的位置加入了我们的检索路径,这样一来claaloader在查找我们已经动态化的so库的时候,就能够找到!

4、结束了吗?

一般的so库,比如不依赖其他的so的时候,直接这样加载就没问题了,但是如果存在着依赖的so库的话,就不行了!相信大家在看其他的博客的时候就能看到,是因为Namespace的问题。具体是我们动态库加载的过程中,如果需要依赖其他的动态库,那么就需要一个链接的过程对吧!这里的实现就是Linker,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libxxx.so 文件(当前的so)能找到,而依赖的so 找不到的情况。bugly文章

https://cloud.tencent.com/developer/article/1592672">

很多实现都采用了Tinker的实现,既然我们系统的classloader是这样,那么我们在合适的时候把这个替换掉不就可以了嘛!当然bugly团队就是这样做的,但是笔者认为,替换一个classloader显然对于一个普通应用来说,成本还是太大了,而且兼容性风险也挺高的,当然,还有很多方式,比如采用Relinker这个库自定义我们加载的逻辑。

为了不冷饭热炒,嘿嘿,虽然我也喜欢吃炒饭(手动狗头),这里我们就不采用替换classloader的方式,而是采用跟relinker的思想,去进行加载!具体的可以看到sillyboy的实现,其实就不依赖relinker跟tinker,因为我把关键的拷贝过来了,哈哈哈,好啦,我们看下怎么实现吧!不过在此这前,我们需要了解一些前置知识。

5、ELF文件

我们的so库,本质就是一个elf文件,那么so库也符合elf文件的格式,ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

那么我们so中,如果依赖于其他的so,那么这个信息存在哪里呢!?没错,它其实也存在elf文件中,不然链接器怎么找嘛,它其实就存在.dynamic段中,所以我们只要找打dynamic段的偏移,就能到dynamic中,而被依赖的so的信息,其实就存在里面啦 我们可以用readelf(ndk中就有toolchains目录后) 查看,readelf -d nativecpptwo.so 这里的 -d 就是查看dynamic段的意思。

这里面涉及到动态加载so的知识,可以推荐大家一本书,叫做程序员的自我修养-链接装载与库这里就画个初略图 。

我们再看下本质,dynamic结构体如下,定义在elf.h中。

typedef struct{Elf32_Sword d_tag;union{Elf32_Addr d_ptr;....}}

当d_tag的数值为DT_NEEDED的时候,就代表着依赖的共享对象文件,d_ptr表示所依赖的共享对象的文件名。看到这里读者们已经知道了如果我们知道了文件名,不就可以再用System.load去加载这个不就可以了嘛!不用替换classloader就能够保证被依赖的库先加载!我们可以再总结一下这个方案的原理,如图:

比如我们要加载so3,我们就需要先加载so2,如果so2存在依赖,那我们就先加载so1,这个时候so1就不存在依赖项了,就不需要再调用Linker去查找其他so库了。我们最终方案就是,只要能够解析对应的elf文件,然后找偏移,找到需要的目标项(DT_NEED)就可以了。

public List<String> parseNeededDependencies() throws IOException { channel.position(0); final List<String> dependencies = new ArrayList<String>(); final Header header = parseHeader(); final ByteBuffer buffer = ByteBuffer.allocate(8); buffer.order(header.bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); long numProgramHeaderEntries = header.phnum; if (numProgramHeaderEntries == 0xFFFF) { /** * Extended numbering * * If the real number of program header table entries is larger than * or equal to PN_XNUM(0xffff), it is set to sh_info field of the * section header at index 0, and PN_XNUM is set to e_phnum * field. Otherwise, the section header at index 0 is zero * initialized, if it exists. **/ final SectionHeader sectionHeader = header.getSectionHeader(0); numProgramHeaderEntries = sectionHeader.info; } long dynamicSectionOff = 0; for (long i = 0; i < numProgramHeaderEntries; i) { final ProgramHeader programHeader = header.getProgramHeader(i); if (programHeader.type == ProgramHeader.PT_DYNAMIC) { dynamicSectionOff = programHeader.offset; break; } } if (dynamicSectionOff == 0) { // No dynamic linking info, nothing to load return Collections.unmodifiableList(dependencies); } int i = 0; final List<Long> neededOffsets = new ArrayList<Long>(); long vStringTableOff = 0; DynamicStructure dynStructure; do { dynStructure = header.getDynamicStructure(dynamicSectionOff, i); if (dynStructure.tag == DynamicStructure.DT_NEEDED) { neededOffsets.add(dynStructure.val); } else if (dynStructure.tag == DynamicStructure.DT_STRTAB) { vStringTableOff = dynStructure.val; // d_ptr union } i; } while (dynStructure.tag != DynamicStructure.DT_NULL); if (vStringTableOff == 0) { throw new IllegalStateException("String table offset not found!"); } // Map to file offset final long stringTableOff = offsetFromVma(header, numProgramHeaderEntries, vStringTableOff); for (final Long strOff : neededOffsets) { dependencies.add(readString(buffer, stringTableOff strOff)); } return dependencies;}

6、扩展

我们到这里,就能够解决so库的动态加载的相关问题了,那么还有人可能会问,项目中是会存在多处System.load方式的,如果加载的so还不存在怎么办?比如还在下载当中,其实很简单,这个时候我们字节码插桩就派上用场了,只要我们把System.load替换为我们自定义的加载so逻辑,进行一定的逻辑处理就可以了,嘿嘿,因为笔者之前就有写一个字节码插桩的库的介绍,所以在本次就不重复了,可以看Sipder,同时也可以用其他的字节码插桩框架实现,相信这不是一个问题。

https://github.com/TestPlanB/Spider

总结

看到这里的读者,相信也能够明白动态加载so的步骤了,最后源代码可以在SillyBoy,当然也希望各位点赞呀!当然,有更好的实现也欢迎评论!!

https://github.com/TestPlanB/SillyBoy

此软着实强大!各路大神级开发者纷纷对其出招!强上加强

今天推荐一款非常强大的软件,堪称PC骨灰级用户的钟爱神器!各路大神也纷纷对其进行升级,屋主挑选其中优化比较好的分享给大家~相信大家使用后就会发现它的强悍,从瞧不上到离不开,可能只是试一试那么简单

Total Commander 是一款PC电脑端使用的 全能文件资源管理器,同时也是一款超越资源管理器的小神器。通过该软件用户可以对电脑的工具栏、菜单、快捷键 等功等进行自定义设置。在文件管理方面,它能够进行文件的快速复制,移动,删除,搜索 或者是 名称修改等操作。

其它功能方面,Total Commander 内置了 压缩/解压功能(Zip/TAR/GZ/TGZ),可以直接打开 ARJ/CAB/RAR/LZH/ACE/UC2 等格式的压缩包。还支持 文件批量重命名,文件 分割/合并,同步文件夹,文件内容比较,创建/检查文件校验 (MD5/SFV) 等功能,搜索功能,无论是文件还是内容,同样支持在这些压缩包中进行。

Total Commander 功能简介:

1、软件的很大一个特点就是布局设计。该软件界面 由两个列表窗口组成,这种设计不仅从视觉效果来说一目了然,而且避免了windows资源管理器目录树在文件操作的一系列弊端。

2、完全的 压缩/解压缩功能。软件具有普通压缩软件的所有功能,包括:创建自解压缩包、分卷压缩包、一次解压多个压缩包等。支持Zip、RAR、ACE等多种格式和各种压缩软件生成的自解压缩文件,完全可以替代各种压缩软件!

3、文件 分割/合并。Total Commander 使用DOS命令“Copy”就可以把文件合并,并且它会在合并的同时创建 一个校验文件,这个校验文件的作用就是检验合并后的文件是否和原文件相同。

4、文件改名批量处理。软件拥有功能全面的文件改名功能,功能强大到很多专业的工具都自叹不如。文件改名需要的各种功能它几乎都有提供,不论是 文件名称加数字,替换指定字符,名称后缀修改,还是大小写转换,甚至是 文件目录名称或者时间软件加入到文件名称中,都能够实现!

5、FTP功能。如果你是一个站长,或者经常与服务器打交道,那么你对 FTP 上传/下载类的工具必不陌生。Total Commander 内置的FTP功能支持 同时连接多个服务器,可以完成自动保持在线,定制 本地/远程路径,支持文件续传操作,而且还可以设置代理服务器。总之,它虽然不是专业的FTP工具,但是你需要的功能,它几乎都有!

6、非常强大的搜索功能!作为一款文件资源管理器,面对海量的各类文件,搜索功能可以说必不可少!软件提供的搜索功能不仅支持指定查找文件的日期、大小、属性,更强大的是它还可以在压缩包中搜索!这个功能可以说罕有软件能够实现。此外,它还支持搜索多个文本文件中指定的文字,即便是压缩包中文本文件的内容它都能够搜索!搜索设置还可以保存起来以便下次搜索时再次使用。

Total Commander 的功能还有很多,例如:丰富的插件资源,文件编码与解码,文件过滤与定位,文件间内容比较等,这里就不在一一介绍……

增强超级版 特色:

增强超级版是基于官方版的优化升级,提供了更多强大的功能!

1、集成更多实用且强大的软件!如:Everything、Notepad2、MyHash、SwitchOFF 等工具,可实现文件快速搜索、文本编辑、校验和计算、智能关机等功能,用过这几款软件的人都知道这些软件的强大;

2、软件界面优化,功能菜单精心打造。增强版从 字体、颜色、图标、大小等多个方面进行了修改优化,并且对 菜单、工具栏、文件夹等优化升级,集成中文版文档及插件,可以进行拼音首字母定位。

3、其它各种优化N多,不再多做叙述

技术岗怎样做工作交接

技术岗怎样做工作交接

本文节选择《Netkiller Management 手札》作者:netkiller

在国内工作交接如同家常便饭,有数据显示员工服务于公司的平均时间,其中80后平均是3年时间,90后平均为2~2.5,95后约半年~1.5年。如此频繁地更换工作不仅仅是员工自身问题,公司用人单位也存在一定问题,这里就不讨论了。

在早期管理学还未在中国普及的时候,50/60/70后沿用了自古以来的老旧的管理思想,搞仁制和驭人术那套,如今已经是2022年了,管理学已经非常成熟,大学有专门管理课,社会上有专业的管理培训,但仍有大量公司在采用中式管理,且管理层搞得不亦乐乎,并未有改变的想法。

国内公司存在的问题:

管理混乱,换个领导就意味着换套新流程

口头管理,没有形成流程和文档化

驭人术,讲忠诚和服从性

所以在工作中,工作交接是不可避免的,无论是用人单位找到你,还是你主动应聘岗位,工作交接都是一项极具挑战的工作,俗称“擦屁股”,我们怎样做好工作交接呢?

工作交接过程

任何工作交接都会丢失80%内容,交接者只能接受20%的工作内容。

丢失的内容有可能是交接者没有想到,交接者故意隐瞒,交接者故意挖坑,交接者说了但是接受者没听懂或忘记了,总之工作不可能达到 100% 的交接。

尤其是国内企业和雇员紧张的劳动关系,几乎大部分的工作交接都不太顺利。

同时离职员工去下家公司也同样面临严峻挑战的工作交接过程,似乎这是一个无解的死循环。

即使是我们口中常说的大厂也存在上述问题。

工作交接过程中可能遇到的问题:

没有足够的书面资料

口头只给你说一次

交接者故意隐瞒信息

交接者故意给你挖坑

交接者不主动、敷衍、不配合

没有给你足够的时间学习、理解、吸收

你不问,对方不会主动给你讲

你能做的是尽量获得更多交接文档,很多事你要先想到,主动问对方,交接者没有责任和义务帮你,他想尽快甩锅走人,你只能求菩萨保佑它跟用人带单位没有纠纷。

这导致很多时候工作交接要靠我们自己,同时我们离职的时候也会用这些办法对付接收我们工作的人,呵呵!!!

确认入口

以技术工作交接为例,如何快速摸清系统,掌握各系统模块之间的关系呢?

互联网入口都有哪些:

网站入口

手机App入口

小程序/公众号/第三方入口

TCP/UDP Socket 入口

域名检查

使用 nslookup/dig 等工具检查域名

A 记录,主机解析

MX 记录,邮件交换记录

TXT 记录,一些特定服务

AAAA 记录,IPv6 主机解析

NS 记录

然后在登陆的DNS管理界面查漏补缺,这里重点是 MX 记录,很多时候会忽略,导致邮件服务不能用。

IP地址检查

网站入口通常是域名或IP地址,例如我的域名是 www.netkiller.cn,使用 ping 工具查看其IP地址

[root@netkiller ~]# ping www.netkiller.cn

手机App链接的API地址需要抓包工具

网络设备

备份配置文件

登陆网络设备,备份配置文件,然后分析网络拓扑,如果有交接文档,就按照文档核对 vlan和每个路由器和交换机端口功能。

Cisco 网络设备使用 show running-config 查看配置文件

Router#show running-configRouter#show startup-config

H3C 网络设备使用 display current-configuration 查看配置文件

[WA2220E-AG]display current-configuration# version 5.20, Release 1110P01# sysname WA2220E-AG# domain default enable system# telnet server enable# port-security enable#vlan 1#radius scheme system#domain system access-limit disable state active idle-cut disable self-service-url disable

Juniper 网络设备,使用 get config 查看配置文件

[root@netkiller ~]# ssh neo@192.168.1.1neo@192.168.3.1's password:Remote Management Consolefirewall-> get configTotal Config size 17376:set clock ntpset clock timezone 7set vrouter trust-vr sharableset vrouter "untrust-vr"exitset vrouter "trust-vr"unset auto-route-exportexit.........exitset vrouter "untrust-vr"exitset vrouter "trust-vr"exitfirewall->

这些网络设备如何使用,请参考《Netkiller Network 手札》

绘制网络拓扑图

几乎90%(可以我说少了)公司,在IT这块的管理十分混乱,走进机房,网线乱如麻,你根本不知道那根网线链接了什么设备,我们需要先理清重点设备的链接拓扑图。

这里以 H3C 为例,其他设备操作方式大同小异,下面是找到接口对等设备的IP地址

<H3C>display arp interface GE1/0/32 Type: S-Static D-Dynamic O-Openflow R-Rule M-Multiport I-InvalidIP address MAC address VLAN/VSI name Interface Aging Type172.16.200.75 b07b-2523-5326 200 GE1/0/32 501 D

通过 mac 地址找接口

<H3C>display arp all | include 6227-d242-269e172.16.0.68 6227-d242-269e 10 GE1/0/3 1085 D

通过 ip 地址找接口

<H3C>display arp all | include 172.16.200.102172.16.200.102 001e-67fa-df92 200 GE1/0/36 282 D

服务器系统检查

当我们知道服务器IP地址之后就可以登录服务器了

[root@netkiller ~]# ssh root@www.netkiller.cn

登陆服务器之后首先快速检查是否有安全问题

检查用户权限

[root@netkiller ~]# cat /etc/passwdroot:x:0:0:root:/root:/bin/bashbin:x:1:1:bin:/bin:/sbin/nologindaemon:x:2:2:daemon:/sbin:/sbin/nologinadm:x:3:4:adm:/var/adm:/sbin/nologinlp:x:4:7:lp:/var/spool/lpd:/sbin/nologinsync:x:5:0:sync:/sbin:/bin/syncshutdown:x:6:0:shutdown:/sbin:/sbin/shutdownhalt:x:7:0:halt:/sbin:/sbin/haltmail:x:8:12:mail:/var/spool/mail:/sbin/nologinoperator:x:11:0:operator:/root:/sbin/nologingames:x:12:100:games:/usr/games:/sbin/nologinftp:x:14:50:FTP User:/var/ftp:/sbin/nologinnobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologindbus:x:81:81:System message bus:/:/sbin/nologinsystemd-coredump:x:999:997:systemd Core Dumper:/:/sbin/nologinsystemd-resolve:x:193:193:systemd Resolver:/:/sbin/nologintss:x:59:59:Account used for TPM access:/dev/null:/sbin/nologinpolkitd:x:998:996:User for polkitd:/:/sbin/nologinunbound:x:997:993:Unbound DNS resolver:/etc/unbound:/sbin/nologinlibstoragemgmt:x:996:992:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologinsetroubleshoot:x:995:991::/var/lib/setroubleshoot:/sbin/nologincockpit-ws:x:994:990:User for cockpit web service:/nonexisting:/sbin/nologincockpit-wsinstance:x:993:989:User for cockpit-ws instances:/nonexisting:/sbin/nologinsssd:x:992:988:User for sssd:/:/sbin/nologinchrony:x:991:987::/var/lib/chrony:/sbin/nologinsshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologintcpdump:x:72:72::/:/sbin/nologinnscd:x:28:28:NSCD Daemon:/:/sbin/nologinpostfix:x:89:89::/var/spool/postfix:/sbin/nologindocker:x:986:986:Container Administrator:/home/docker:/bin/bashgit:x:1000:1000::/home/git:/bin/bashcaddy:x:985:984:Caddy web server:/var/lib/caddy:/sbin/nologinnginx:x:984:983:Nginx web server:/var/lib/nginx:/sbin/nologin

通常除了 root 用户,小于1000的UID的用户 shell 应该是 /sbin/nologin

重点检查拥有 shell 权限的用户,非必要全部改为 /sbin/nologin

[root@netkiller ~]# cat /etc/passwd | grep bashroot:x:0:0:root:/root:/bin/bashdocker:x:986:986:Container Administrator:/home/docker:/bin/bashgit:x:1000:1000::/home/git:/bin/bash

检查用户是否激活,并设置密码

[root@netkiller ~]# cat /etc/shadowroot:$6$XyW4o/lZWdu$VsjNYW8nVcE63cWwAxLxBe5s22vt4hQCOivzxo5RQ3rGbcBkfWnc/ATiy073D7/aIHQaDZplIJWz51s4Pzkn0.:19173:0:99999:7:::bin:*:18505:0:99999:7:::daemon:*:18505:0:99999:7:::adm:*:18505:0:99999:7:::lp:*:18505:0:99999:7:::sync:*:18505:0:99999:7:::shutdown:*:18505:0:99999:7:::halt:*:18505:0:99999:7:::mail:*:18505:0:99999:7:::operator:*:18505:0:99999:7:::games:*:18505:0:99999:7:::ftp:*:18505:0:99999:7:::nobody:*:18505:0:99999:7:::dbus:!!:19136::::::systemd-coredump:!!:19136::::::systemd-resolve:!!:19136::::::tss:!!:19136::::::polkitd:!!:19136::::::unbound:!!:19136::::::libstoragemgmt:!!:19136::::::setroubleshoot:!!:19136::::::cockpit-ws:!!:19136::::::cockpit-wsinstance:!!:19136::::::sssd:!!:19136::::::chrony:!!:19136::::::sshd:!!:19136::::::tcpdump:!!:19136::::::nscd:!!:19136::::::postfix:!!:19136::::::docker:$6$6.KJvlfezlr7q09N$Xo83rIqChY0sLrvyCGnNYqPpCsREk1h1En7eNwZBV9sjN6XVOO9xgjuKGn..tAhElYK1SLI6mvZa6pxsCcuqb/:19177:0:99999:7:::git:$6$EcduJ7Z1HAZWhRPN$wyG7OOpQaw84FgvAsXBPUKr.dZWEDIaWhMKeoEuNRvpfzvoeKo4gei028lSCm./nAvFqRpos/2Ng4ARHR5FRj.:19173:0:99999:7:::caddy:!!:19177::::::nginx:!!:19192::::::

!! 和 * 是未设置密码,该用户无法登陆系统,$6$ 表示该用户有密码

这三个用户是有密码的

[root@netkiller ~]# cat /etc/shadow | grep '$6$'root:$6$XyW4o/lZWdu$VsjNYW8nVcE63cWwAxLxBe5s22vt4hQCOivzxo5RQ3rGbcBkfWnc/ATiy073D7/aIHQaDZplIJWz51s4Pzkn0.:19173:0:99999:7:::docker:$6$6.KJvlfezlr7q09N$Xo83rIqChY0sLrvyCGnNYqPpCsREk1h1En7eNwZBV9sjN6XVOO9xgjuKGn..tAhElYK1SLI6mvZa6pxsCcuqb/:19177:0:99999:7:::git:$6$EcduJ7Z1HAZWhRPN$wyG7OOpQaw84FgvAsXBPUKr.dZWEDIaWhMKeoEuNRvpfzvoeKo4gei028lSCm./nAvFqRpos/2Ng4ARHR5FRj.:19173:0:99999:7:::

锁定/解锁用户

发现可疑用户,我们可以先把它锁掉,让他登陆不了。使用 passwd -l 给用户加锁,-u 解锁。

[root@netkiller ~]# passwd --helpUsage: passwd [OPTION...] <accountName> -k, --keep-tokens keep non-expired authentication tokens -d, --delete delete the password for the named account (root only); also removes password lock if any -l, --lock lock the password for the named account (root only) -u, --unlock unlock the password for the named account (root only) -e, --expire expire the password for the named account (root only) -f, --force force operation -x, --maximum=DAYS maximum password lifetime (root only) -n, --minimum=DAYS minimum password lifetime (root only) -w, --warning=DAYS number of days warning users receives before password expiration (root only) -i, --inactive=DAYS number of days after password expiration when an account becomes disabled (root only) -S, --status report password status on the named account (root only) --stdin read new tokens from stdin (root only)Help options: -?, --help Show this help message --usage Display brief usage message

例如我们要禁止 git 用户登陆系统

[root@netkiller ~]# passwd -l gitLocking password for user git.passwd: Success

恢复 git 用户登陆权限

[root@netkiller ~]# passwd -u gitUnlocking password for user git.passwd: Success

实际的加锁与解锁的过程就是在 shadow 文件中,密码前面增加 !! 符号。

[root@netkiller ~]# passwd -l gitLocking password for user git.passwd: Success[root@netkiller ~]# cat /etc/shadow | grep 'git'git:!!$6$EcduJ7Z1HAZWhRPN$wyG7OOpQaw84FgvAsXBPUKr.dZWEDIaWhMKeoEuNRvpfzvoeKo4gei028lSCm./nAvFqRpos/2Ng4ARHR5FRj.:19173:0:99999:7:::[root@netkiller ~]# passwd -u gitUnlocking password for user git.passwd: Success[root@netkiller ~]# cat /etc/shadow | grep 'git'git:$6$EcduJ7Z1HAZWhRPN$wyG7OOpQaw84FgvAsXBPUKr.dZWEDIaWhMKeoEuNRvpfzvoeKo4gei028lSCm./nAvFqRpos/2Ng4ARHR5FRj.:19173:0:99999:7:::

检查端口

非法进入服务器必须某种服务端口,我们确认用户安全后,接下来要做的就是扫描端口,看看服务器上开了哪些端口。

[root@netkiller ~]# dnf install -y nmap

nmap 使用方法请看我的《Netkiller Linux 手札》本文只教方法论。

[root@netkiller ~]# nmap www.netkiller.cnStarting Nmap 7.70 ( https://nmap.org ) at 2022-07-23 16:23 CSTNmap scan report for chat.netkiller.cn (8.219.73.35)Host is up (0.00041s latency).Not shown: 993 filtered portsPORT STATE SERVICE22/tcp open ssh80/tcp open http443/tcp open https3389/tcp closed ms-wbt-server8000/tcp closed http-alt8080/tcp closed http-proxy9000/tcp closed cslistenerNmap done: 1 IP address (1 host up) scanned in 25.40 seconds

通过端口找进程,可以使用 ss 和 netstat 两个工具完成

[root@netkiller ~]# netstat -antup|grep 80tcp 0 0 172.22.119.108:20507 100.103.15.60:80 ESTABLISHED 1490/AliYunDun tcp6 0 0 :::80 :::* LISTEN 1629762/caddy [root@netkiller ~]# ss -lntpState Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1189,fd=5)) LISTEN 0 1024 127.0.0.1:2019 0.0.0.0:* users:(("caddy",pid=1629762,fd=3)) LISTEN 0 1024 *:80 *:* users:(("caddy",pid=1629762,fd=8)) LISTEN 0 1024 *:443 *:* users:(("caddy",pid=1629762,fd=7))

这里可以看到 80 端口是 1629762 进程监听的,该进程是 caddy 它是一个 web 服务器

通过进程找进程ID

[root@netkiller ~]# pgrep caddy1629762

检查进程

Linux 系统进程带有中括号的是内核进程,除此之外都需要一一排查

[root@netkiller ~]# ps ax | grep '[' 2 ? S 0:01 [kthreadd] 3 ? I< 0:00 [rcu_gp] 4 ? I< 0:00 [rcu_par_gp] 6 ? I< 0:00 [kworker/0:0H-events_highpri] 9 ? I< 0:00 [mm_percpu_wq] 10 ? S 0:00 [rcu_tasks_rude_] 11 ? S 0:00 [rcu_tasks_trace] 12 ? S 0:01 [ksoftirqd/0] 13 ? I 9:14 [rcu_sched] 14 ? S 0:00 [migration/0] 15 ? S 0:00 [watchdog/0] 16 ? S 0:00 [cpuhp/0] 17 ? S 0:00 [cpuhp/1] 18 ? S 0:00 [watchdog/1] 19 ? S 0:00 [migration/1] 20 ? S 0:00 [ksoftirqd/1] 22 ? I< 0:00 [kworker/1:0H-events_highpri] 23 ? S 0:00 [cpuhp/2] 24 ? S 0:01 [watchdog/2] 25 ? S 0:00 [migration/2] 26 ? S 0:00 [ksoftirqd/2] 28 ? I< 0:00 [kworker/2:0H-events_highpri] 29 ? S 0:00 [cpuhp/3] 30 ? S 0:01 [watchdog/3] 31 ? S 0:00 [migration/3] 32 ? S 0:00 [ksoftirqd/3] 34 ? I< 0:00 [kworker/3:0H-events_highpri] 35 ? S 0:00 [cpuhp/4] 36 ? S 0:01 [watchdog/4] 37 ? S 0:00 [migration/4] 38 ? S 0:00 [ksoftirqd/4] 40 ? I< 0:00 [kworker/4:0H-events_highpri] 41 ? S 0:00 [cpuhp/5] 42 ? S 0:01 [watchdog/5] 43 ? S 0:00 [migration/5] 44 ? S 0:00 [ksoftirqd/5] 46 ? I< 0:00 [kworker/5:0H-events_highpri] 47 ? S 0:00 [cpuhp/6] 48 ? S 0:01 [watchdog/6] 49 ? S 0:00 [migration/6] 50 ? S 0:00 [ksoftirqd/6] 52 ? I< 0:00 [kworker/6:0H-events_highpri] 53 ? S 0:00 [cpuhp/7] 54 ? S 0:02 [watchdog/7] 55 ? S 0:00 [migration/7] 56 ? S 0:00 [ksoftirqd/7] 58 ? I< 0:00 [kworker/7:0H-events_highpri] 67 ? S 0:00 [kdevtmpfs] 68 ? I< 0:00 [netns] 69 ? S 0:00 [kauditd] 70 ? S 0:01 [khungtaskd] 71 ? S 0:00 [oom_reaper] 72 ? I< 0:00 [writeback] 73 ? S 0:00 [kcompactd0] 74 ? SN 0:00 [ksmd] 75 ? SN 0:18 [khugepaged] 76 ? I< 0:00 [crypto] 77 ? I< 0:00 [kintegrityd] 78 ? I< 0:00 [kblockd] 79 ? I< 0:00 [blkcg_punt_bio] 80 ? I< 0:00 [tpm_dev_wq] 81 ? I< 0:00 [md] 82 ? I< 0:00 [edac-poller] 83 ? S 0:00 [watchdogd] 84 ? I< 0:01 [kworker/0:1H-kblockd] 118 ? S 0:00 [kswapd0] 224 ? I< 0:00 [kthrotld] 225 ? I< 0:00 [acpi_thermal_pm] 226 ? I< 0:00 [kmpath_rdacd] 227 ? I< 0:00 [kaluad] 229 ? I< 0:02 [kworker/4:1H-kblockd] 230 ? I< 0:00 [ipv6_addrconf] 232 ? I< 0:00 [kstrp] 313 ? I< 0:01 [kworker/5:1H-kblockd] 319 ? I< 0:01 [kworker/2:1H-kblockd] 347 ? I< 0:14 [kworker/7:1H-xfs-log/vda1] 350 ? I< 0:01 [kworker/3:1H-kblockd] 355 ? I< 0:01 [kworker/1:1H-kblockd] 361 ? I< 0:01 [kworker/6:1H-kblockd] 501 ? I< 0:00 [ata_sff] 502 ? S 0:00 [scsi_eh_0] 503 ? I< 0:00 [scsi_tmf_0] 504 ? S 0:00 [scsi_eh_1] 505 ? I< 0:00 [scsi_tmf_1] 533 ? I< 0:00 [xfsalloc] 535 ? I< 0:00 [xfs_mru_cache] 536 ? I< 0:00 [xfs-buf/vda1] 537 ? I< 0:00 [xfs-conv/vda1] 538 ? I< 0:00 [xfs-cil/vda1] 539 ? I< 0:00 [xfs-reclaim/vda] 540 ? I< 0:00 [xfs-blockgc/vda] 542 ? I< 0:00 [xfs-log/vda1] 543 ? S 2:57 [xfsaild/vda1] 880 ? I< 0:00 [nfit]1636473 ? I 0:01 [kworker/5:2-events_power_efficient]1636635 ? I 0:07 [kworker/7:0-events]1636884 ? I 0:00 [kworker/6:0-events]1636886 ? I 0:00 [kworker/2:3-events]1637134 ? I 0:00 [kworker/1:0-events]1637224 ? I 0:00 [kworker/2:0]1637273 ? I 0:00 [kworker/0:3-mm_percpu_wq]1637275 ? I 0:00 [kworker/u16:0-events_unbound]1637290 ? I 0:00 [kworker/6:2-mm_percpu_wq]1637344 ? I 0:00 [kworker/1:1]1637352 ? Ss 0:00 sshd: root [priv]1637362 ? I 0:00 [kworker/0:2-events]1637501 ? I 0:00 [kworker/4:1-events]1637953 ? I 0:00 [kworker/3:0-cgroup_pidlist_destroy]1637956 ? I< 0:00 [ib-comp-wq]1637957 ? I< 0:00 [kworker/u17:0]1637958 ? I< 0:00 [ib-comp-unb-wq]1637960 ? I< 0:00 [ib_mcast]1637961 ? I< 0:00 [ib_nl_sa_wq]1637965 ? I 0:00 [kworker/5:0]1637976 ? I 0:00 [kworker/4:0-xfs-sync/vda1]1637979 ? I 0:00 [kworker/7:2]1637981 ? I 0:00 [kworker/3:3-cgroup_destroy]1637986 ? I 0:00 [kworker/u16:1-events_unbound]1638007 pts/0 S 0:00 grep --color=auto [

使用 grep -v 排除所有内核进程,这样看着舒服一些。

[root@netkiller ~]# ps ax | grep -v '[' PID TTY STAT TIME COMMAND 1 ? Ss 3:08 /usr/lib/systemd/systemd --switched-root --system --deserialize 17 649 ? Ss 0:21 /usr/lib/systemd/systemd-journald 681 ? Ss 0:02 /usr/lib/systemd/systemd-udevd 711 ? S<sl 0:04 /sbin/auditd 716 ? S< 0:01 /usr/sbin/sedispatch 759 ? Ss 0:03 /usr/bin/lsmd -d 769 ? Ss 0:00 /usr/sbin/smartd -n -q never 770 ? Ssl 0:01 /usr/lib/polkit-1/polkitd --no-debug 779 ? Ss 0:14 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only 788 ? Ss 0:00 /usr/sbin/mcelog --ignorenodev --daemon --foreground 794 ? S 0:08 /usr/sbin/chronyd 974 ? Ss 0:08 /usr/lib/systemd/systemd-logind 1023 ? Ssl 1:03 /usr/sbin/NetworkManager --no-daemon 1027 ? Ssl 76:19 /usr/libexec/platform-python -Es /usr/sbin/tuned -l -P 1189 ? Ss 0:01 /usr/sbin/sshd -D -oCiphers=aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes256-ctr,aes256-cbc,aes128-gcm@openssh.com,aes128-ctr,aes128-cbc -oMACs=hmac-sha2-256-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha1,umac-128@openssh.com,hmac-sha2-512 -oGSSAPIKexAlgorithms=gss-curve25519-sha256-,gss-nistp256-sha256-,gss-group14-sha256-,gss-group16-sha512-,gss-gex-sha1-,gss-group14-sha1- -oKexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1 -oHostKeyAlgorithms=ecdsa-sha2-nistp256,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-512-cert-v01@openssh.com,ssh-rsa,ssh-rsa-cert-v01@openssh.com -oPubkeyAcceptedKeyTypes=ecdsa-sha2-nistp256,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-512-cert-v01@openssh.com,ssh-rsa,ssh-rsa-cert-v01@openssh.com -oCASignatureAlgorithms=ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-256,rsa-sha2-512,ssh-rsa 1201 ? Ss 0:00 /usr/sbin/atd -f 1233 tty1 Ss 0:00 /sbin/agetty -o -p -- u --noclear tty1 linux 1234 ttyS0 Ss 0:00 /sbin/agetty -o -p -- u --keep-baud 115200,38400,9600 ttyS0 vt220 1461 ? S<sl 22:04 /usr/local/aegis/aegis_update/AliYunDunUpdate 1490 ? S<sl 412:12 /usr/local/aegis/aegis_client/aegis_11_25/AliYunDun 1652 ? Ss 0:02 /usr/sbin/crond -n 1702 ? Ssl 1:25 /usr/sbin/rsyslogd -n 4186 ? Ssl 35:58 /usr/bin/containerd 4200 ? Ssl 26:56 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock 221773 ? Ss 0:01 /usr/sbin/sssd -i --logger=files 221775 ? S 0:09 /usr/libexec/sssd/sssd_be --domain implicit_files --uid 0 --gid 0 --logger=files 221776 ? S 3:32 /usr/libexec/sssd/sssd_nss --uid 0 --gid 0 --logger=files 298641 ? Ssl 31:55 /usr/local/share/aliyun-assist/2.2.3.309/aliyun-service 298799 ? Ssl 5:34 /usr/local/share/assist-daemon/assist_daemon1629762 ? Ssl 0:23 /usr/bin/caddy run --environ --config /etc/caddy/Caddyfile1637356 ? Ss 0:00 /usr/lib/systemd/systemd --user1637358 ? S 0:00 (sd-pam)1637366 ? S 0:00 sshd: root@pts/01637367 pts/0 Ss 0:00 -bash1638004 pts/0 R 0:00 ps ax

对于可疑进程一律找到它所在位置,先kill掉,然后去掉可执行权限,归档备份。

例如我们对 caddy 下手,首先查看进程位置 /usr/bin/caddy

[root@netkiller ~]# ps ax |grep caddy1629762 ? Ssl 0:23 /usr/bin/caddy run --environ --config /etc/caddy/Caddyfile1637983 pts/0 S 0:00 grep --color=auto caddy

看看是否有系统启动服务

systemctl stop caddysystemctl disable caddy

现在就禁用了,如果这个程序是手工安装没有systemd,我们就需要 chmod -x /usr/bin/caddy

以上是 caddy 为例,其他服务也都如此。

检查配置文件

确认服务器使用的是什么操作系统,然后用这个版本安装一个干净的环境,在新服务器和当前要检查的服务器上分别执行下面命令

[root@netkiller ~]# find /etc/ > etc.lst[root@netkiller ~]# find /usr/etc/ > usr.etc.lst[root@netkiller ~]# find /usr/local/etc/ > usr.etc.lst

然后使用文件比较工具比较文件列表,差异的部分,就是新增的文件。你也可以讲两个服务器的 etc 目录复制出来,使用工具比较他们的差异。

容器的检查

目前许多公司都在使用 Docker / Kubernetes 部署系统,其实容器并非安全,在容器镜像中藏点私货是非常容易的,Docker 官方提供了安全扫描工具

[root@netkiller ~]# docker scanDocker Scan relies upon access to Snyk, a third party provider, do you consent to proceed using Snyk? (y/N)

我们安装物理机所用的系统通常是去官网下载,很多容器的制作并为使用官方镜像,同时会做 md5sum 校验,所以是安全的。容器镜像安全更多是来自无序的继承,经过多次继承我们不知道里面做了那些变化,所以选择基础镜像非常重要。

[root@netkiller ~]# cat Dockerfile | grep FROMFROM nginx:latest

如果不是官方镜像,需要格外注意

云安全检查

云平台通常网络分成WLAN和LAN两个部分,有些平台WLAN 是挂载到 eth0 上,有些平台云主机只有内网IP,WLAN是映射到内网IP上。

采用的手段跟上面一样,扫描端口,检查安全组规则,弹性IP和SLB策略。

原则是,WLAN外网只允许通过特定端口例如80/443,剩下所有服务在LAN上通信。

工作交接期间,只开放80/443端口,完成交接后,并做了安全排查之后,逐渐开放需要的端口,建议限制来源IP地址,例如只允许从办公室访问。

系统备份

以上检查的所有项的配置文件都需要备份一份,以防万一,这个万一存在很多不确定性因素,你懂的

绘制网络和服务器/容器拓扑图

使用visio等软件绘制网络和服务器/容器拓扑图

部署运作机制

这个步骤是搞清楚代码从开发到生产环境是怎么部署的,如果使用了CI/CD技术,就从持续集成和部署着手看,可以一目了然。如果是手工部署,要亲力亲为自己做一遍,同时将CI/CD就此机会做起来。

举一个例子,比如有一个项目教 api.netkiller.cn

代码库中找到这个项目

编译和打包

目前国内最主流搭配是前端 node 后端 java,当然也有少量项目使用 PHP、Python和Go,总体大同小异。

如果是 node.js 项目通常是 webpack 无非就是下面两步

npm install --registry=https://registry.npm.taobao.orgnpm run build:prod

Java 项目

mvn clean package

搞清楚配置文件

前端 NodeJS 项目配置通常只链接后端,配置涉及较少,相对容易掌握。

Java 项目配置文件放在 src/main/resources/ 中 Springboot 项目配置文件是 src/main/resources/application.yml,Springcloud 的配置文件是 src/main/resources/bootstrap.yml

也有使用 application.properties 和 bootstrap.properties

我们既要检查配置文件中都链接了那些服务,还要检查配置是否合理,例如链接池的最大链接数量,链接超时时间等等。

如果使用配置中心,需要进入配置中心查看配置文件

绘制业务关系拓扑图

走一遍业务逻辑,从域名进入网站到数据保存到数据,这中间经过了那些服务,他们的调用关系是什么,画一张图,让你一清二楚。

如果有链路追踪能让你更快完成这步工作,如果没有,这也是未来要做的工作,这时就需要结合各种配置文件,或者询问各种相关部门和人员来完成这个拓扑图。

备份

备份代码

备份配置文件

将备份过程放在CI/CD过程中,让备份自动化

接手代码

程序猿常常自嘲是粪海狂蛆!陈年的粪缸,经过发酵、沉淀、分解已经没有屎尿的味道,你千万别去动它。

国内大部分项目都是平均三年经验的开发人写的,同时90后平均不到两年就会换工作,你别期望代码质量有多高,你别更别期望问同事能得到答案。

常态是,大部分情况下,只要修改代码就产生新bug!延伸阅读《程序猿说的「优化」是什么意思?》

代码审查

代码审查只是一句口号历来如此,很多团队都说在做,实际情况你懂的。但是此时一些对外的服务真的有必要做一次 code review,我的职业生涯中,曾经遇到过离职人员埋后门的情况。

经过前面的服务器排查,通过SSH登陆服务器搞破坏的可能性是极小的,更多的漏洞是代码漏洞和业务漏洞。

代码扫描

使用代码扫描工具,例如 SonarQube 扫描lib和引用第三方包的安全漏洞,及时升级第三方包,修复存在安全的代码。

如果你期待代码扫描能帮你找出业务漏洞,纯属扯淡,我们使用代码扫描更多是发现 pom.xml 中引用第三方库已知的 CVE 漏洞。

收缩技术栈

接手代码之后,首要解决的是能让它工作起来,然后着手优化代码,绝大多数人的思维是加法思维,他们会使用许多技术来完成一件事,这是错误的。

正确的做法是使用一种技术完成所有工作。

深入理解JVM虚拟机——java字节码技术及其命令剖析

参考文档:

深入理解JVM虚拟机——JVM运行时栈结构和方法调用

深入理解JVM虚拟机——JVM是怎么实现invokedynamic的

深入理解JVM虚拟机——类的加载机制
深入理解JVM虚拟机——JIT编译器


开篇

Java 是一门面向对象,静态类型的语言,具有跨平台的特点,与 C,C 这些需要手动管理内存,编译型的语言不同,它是解释型的,具有跨平台和自动垃圾回收的特点,那么它的跨平台到底是怎么实现的呢?

我们知道计算机只能识别二进制代码表示的机器语言,所以不管用的什么高级语言,最终都得翻译成机器语言才能被 cpu 识别并执行,对于 C 这些编译型语言来说是直接一步到位转为相应平台的可执行文件(即机器语言指令),而对 java 来说,则首先由编译器将源文件编译成字节码,再在运行时由虚拟机(JVM)解释成机器指令来执行,我们可以看下下图

也就是说 Java 的跨平台其实是通过先生成字节码,再由针对各个平台实现的 JVM 来解释执行实现的,JVM 屏蔽了 OS 的差异,我们知道 Java 工程都是以 Jar 包分发(一堆 class 文件的集合体)部署的,这就意味着 jar 包可以在各个平台上运行(由相应平台的 JVM 解释执行即可),这就是 Java 能实现跨平台的原因所在

这也是为什么 JVM 能运行 Scala、Groovy、Kotlin 这些语言的原因,并不是 JVM 直接来执行这些语言,而是这些语言最终都会生成符合 JVM 规范的字节码再由 JVM 执行,不知你是否注意到,使用字节码也利用了计算机科学中的分层理念,通过加入字节码这样的中间层,有效屏蔽了与上层的交互差异。

JVM 是怎么执行字节码的

在此之前我们先来看下 JVM 的整体内存结构,对其有一个宏观的认识,然后再来看 JVM 是如何执行字节码的

JVM 内存结构

JVM 在内存中主要分为「栈」,「堆」,「非堆」以及 JVM 自身,堆主要用来分配类实例和数组,非堆包括「方法区」、「JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)」、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码

我们主要关注栈,我们知道线程是 cpu 调度的最小单位,在 JVM 中一旦创建一个线程,就会为其分配一个线程栈,线程会调用一个个方法,每个方法都会对应一个个的栈帧压到线程栈里,JVM 中的栈内存结构如下

JVM 栈内存结构

至此我们总算接近 JVM 执行的真相了,JVM 是以栈帧为单位执行的,栈帧由以下四个部分组成

返回值

局部变量表(Local Variables):存储方法用到的本地变量

动态链接:在字节码中,所有的变量和方法都是以符号引用的形式保存在 class 文件的常量池中的,比如一个方法调用另外的方法,是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用,这么说可能有人还是不理解,所以我们先执行一下 javap -verbose Demo.class命令来查看一下字节码中的常量池是咋样的

注意:以上只列出了常量池中的部分符号引用

可以看到 Object 的 init 方法是由 #4.#16 表示的,而 #4 又指向了 #19,#19 表示 Object,#16 又指向了 #7.#8,#7 指向了方法名,#8 指向了 ()V(表示方法的返回值为 void,且无方法参数),字节码加载后,会把类信息加载到元空间(Java 8 以后)中的方法区中,动态链接会把这些符号引用替换为调用方法的直接引用,如下图示

那为什么要提供动态链接呢,通过上面这种方式绕了好几个弯才定位到具体的执行方法,效率不是低了很多吗,其实主要是为了支持 Java 的多态,比如我们声明一个 Father f = new Son()这样的变量,但执行 f.method() 的时候会绑定到 son 的 method(如果有的话),这就是用到了动态链接的技术,在运行时才能定位到具体该调用哪个方法,动态链接也称为后期绑定,与之相对的是静态链接(也称为前期绑定),即在编译期和运行期对象的方法都保持不变,静态链接发生在编译期,也就是说在程序执行前方法就已经被绑定,java 当中的方法只有final、static、private和构造方法是前期绑定的。而动态链接发生在运行时,几乎所有的方法都是运行时绑定的

举个例子来看看两者的区别,一目了然

class Animal{ public void eat(){ System.out.println("动物进食");}}class Cat extends Animal{ @Override public void eat() { super.eat();//表现为早期绑定(静态链接) System.out.println("猫进食"); }}public class AnimalTest { public void showAnimal(Animal animal){ animal.eat();//表现为晚期绑定(动态链接)} }

操作数栈(Operand Stack):程序主要由指令和操作数组成,指令用来说明这条操作做什么,比如是做加法还是乘法,操作数就是指令要执行的数据,那么指令怎么获取数据呢,指令集的架构模型分为基于栈的指令集架构和基于寄存器的指令集架构两种,JVM 中的指令集属于前者,也就是说任何操作都是用栈来管理,基于栈指令可以更好地实现跨平台,栈都是是在内存中分配的,而寄存器往往和硬件挂钩,不同的硬件架构是不一样的,不利于跨平台,当然基于栈的指令集架构缺点也很明显,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈),而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢不少,这也是为了跨平台而做出的一点性能牺牲,毕竟鱼和熊掌不可兼得。

1 Java 字节码简介

注意线程中还有一个「PC 程序计数器」,是每个线程独有的,记录着当前线程所执行的字节码的行号指示器,也就是指向下一条指令的地址,也就是将执行的指令代码。由执行引擎读取下一条指令。我们先来看下看一下字节码长啥样。假设我们有以下 Java 代码

package com.mahai;public class Demo { private int a = 1; public static void foo() { int a = 1; int b = 2; int c = (a b) * 5; }}

执行 javac Demo.java 后可以看到其字节码如下

字节码是给 JVM 看的,所以我们需要将其翻译成人能看懂的代码,好在 JDK 提供了反解析工具 javap ,可以根据字节码反解析出 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等信息。我们执行以下命令来看下根据字节码反解析的文件长啥样(更详细的信息可以执行 javap -verbose 命令,在本例中我们重点关注 Code 区是如何执行的,所以使用了 javap -c 来执行

javap -c Demo.class

转换成这种形式可读性强了很多,那么aload_0,invokespecial 这些表示什么含义呢, javap 是怎么根据字节码来解析出这些指令出来的呢

首先我们需要明白什么是指令,指令=操作码 操作数,操作码表示这条指令要做什么,比如加减乘除,操作数即操作码操作的数,比如 1 2 这条指令,操作码其实是加法,1,2 为操作数,在 Java 中每个操作码都由一个字节表示,每个操作码都有对应类似 aload_0,invokespecial,iconst_1 这样的助记符,有些操作码本来就包含着操作数,比如字节码 0x04 对应的助记符为 iconst_1, 表示 将 int 型 1 推送至栈顶,这些操作码就相当于指令,而有些操作码需要配合操作数才能形成指令,如字节码 0x10 表示 bipush,后面需要跟着一个操作数,表示 将单字节的常量值(-128~127)推送至栈顶。以下为列出的几个字节码与助记符示例

字节码

助记符

表示含义

0x04

iconst_1

将int型1推送至栈顶

0xb7

invokespecial

调用超类构建方法, 实例初始化方法, 私有方法

0x1a

iload_0

将第一个int型本地变量推送至栈顶

0x10

bipush

将单字节的常量值(-128~127)推送至栈顶

至此我们不难明白 javap 的作用了,它主要就是找到字节码对应的的助记符然后再展示在我们面前的,我们简单看下上述的默认构造方法是如何根据字节码映射成助记符并最终呈现在我们面前的:

最左边的数字是 Code 区中每个字节的偏移量,这个是保存在 PC 的程序计数中的,比如如果当前指令指向 1,下一条就指向 4

另外大家不难发现,在源码中其实我们并没有定义默认构造函数,但在字节码中却生成了,而且你会发现我们在源码中定义了private int a = 1;但这个变量赋值的操作却是在构造方法中执行的(下文会分析到),这就是理解字节码的意义:它可以反映 JVM 执行程序的真正逻辑,而源码只是表象,要深入分析还得看字节码!

接下来我们就来瞧一瞧构造方法对应的指令是如何执行的,首先我们来看一下在 JVM 中指令是怎么执行的。

    首先 JVM 会为每个方法分配对应的局部变量表,可以认为它是一个数组,每个坑位(我们称为 slot)为方法中分配的变量,如果是实例方法,这些局部变量可以是 this, 方法参数,方法里分配的局部变量,这些局部变量的类型即我们熟知的 int,long 等八大基本,还有引用,返回地址,每个 slot 为 4 个字节,所以像 Long , Double 这种 8 个字节的要占用 2 个 slot, 如果这个方法为实例方法,则第一个 slot 为 this 指针, 如果是静态方法则没有 this 指针

    分配好局部变量表后,方法里如果涉及到赋值,加减乘除等操作,那么这些指令的运算就需要依赖于操作数栈了,将这些指令对应的操作数通过压栈,弹栈来完成指令的执行

比如有 int i = 69 这样的指令,对应的字码节指令如下

0: bipush 692: istore_0

其在内存中的操作过程如下

可以看到主要分两步:第一步首先把 69 这个 int 值压栈,然后再弹栈,把 69 弹出放到局部变量表 i 对应的位置,istore_0 表示弹栈,将其从操作数栈中弹出整型数字存储到本地变量中,0 表示本地变量在局部变量表的第 0 个 slot

理解了上面这个操作,我们再来看一下默认构造函数对应的字节码指令是如何执行的

首先我们需要先来理解一下上面几个指令

aload_0:从局部变量表中加载第 0 个 slot 中的对象引用到操作数栈的栈顶,这里的 0 表示第 0 个位置,也就是 this

invokespecial:用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及 可见的超类方法,在此例中表示调用父类的构造器(因为 #1 符号引用指向对应的 init 方法)

iconst_1:将 int 型 1推送至栈顶

putfield:它接受一个操作数,这个操作数引用的是运行时常量池里的一个字段,在这里这个字段是 a。赋给这个字段的值,以及包含这个字段的对象引用,在执行这条指令的时候,都会从操作数栈顶上 pop 出来。前面的 aload_0 指令已经把包含这个字段的对象(this)压到操作数栈上了,而后面的 iconst_1 又把 1 压到栈里。最后 putfield 指令会将这两个值从栈顶弹出。执行完的结果就是这个对象的 a 这个字段的值更新成了 1。

接下来我们来详细解释以上以上助记符代表的含义

第一条命令 aload_0,表示从局部变量表中加载第 0 个 slot 中的对象引用到操作数栈的栈顶,也就是将 this 加载到栈顶,如下

第二步 invokespecial #1,表示弹栈并且执行 #1 对应的方法,#1 代表的含义可以从旁边的解释(# Method java/lang/Object."":()V)看出,即调用父类的初始化方法,这也印证了那句话:子类初始化时会从初始化父类

之后的命令 aload_0,iconst_1,putfied #2 图解如下

可能有人有些奇怪,上述 6: putfield #2命令中的 #2 怎么就代表 Demo 的私有成员 a 了,这就涉及到字节码中的常量池概念了,我们执行 javap -verbose path/Demo.class 可以看到这些字面量代表的含义,#1,#2 这种数字形式的表示形式也被称为符号引用,程序运行期会将符号引用转换为直接引用

由此可知 #2 代表 Demo 类的 a 属性,如下

从最终的叶子节点可以看出 #2 最终代表的是 Demo 类中类型为 int(I 代表 int 代表 int 类型),名称为 a 的变量

我们再来用动图看一下 foo 的执行流程,相信你现在能理解其含义了

唯一需要注意的此例中的 foo 是个静态方法,所以局部变量区是没有 this 的。

相信你不难发现 JVM 执行字节码的流程与 CPU 执行机器码步骤如出一辙,都经历了「取指令」,「译码」,「执行」,「存储计算结果」这四步,首先程序计数器指向下一条要执行的指令,然后 JVM 获取指令,由本地执行引擎将字节码操作数转成机器码(译码)执行,执行后将值存储到局部变量区(存储计算结果)中

最后关于字节码我推荐两款工具

一个是 Hex Fiend,一款很好的十六进制编辑器,可以用来查看编辑字节码

一款是 Intellij Idea 的插件 jclasslib Bytecode viewer,能为你展示 javap -verbose 命令对应的常量池,接口, Code 等数据,非常的直观,对于分析字节码非常有帮忙,如下

2. 获取字节码指令清单

可以用 javap 工具来获取 class 文件中的指令清单。 javap 是标准 JDK 内置的一款工具, 专门用于反编译 class 文件。

让我们从头开始, 先创建一个简单的类,后面再慢慢扩充。

public class HelloByteCode { public static void main(String[] args) { HelloByteCode obj = new HelloByteCode(); }}

代码很简单, main 方法中 new 了一个对象而已。然后我们编译这个类:

javac demo/jvm0104/HelloByteCode.java

使用 javac 编译 ,或者在 IDEA 或者 Eclipse 等集成开发工具自动编译,基本上是等效的。只要能找到对应的 class 即可。

javac 不指定 -d 参数编译后生成的 .class 文件默认和源代码在同一个目录。

注意: javac 工具默认开启了优化功能, 生成的字节码中没有局部变量表(LocalVariableTable),相当于局部变量名称被擦除。如果需要这些调试信息, 在编译时请加上 -g 选项。有兴趣的同学可以试试两种方式的区别,并对比结果。

JDK 自带工具的详细用法, 请使用: javac -help 或者 javap -help 来查看; 其他类似。

然后使用 javap 工具来执行反编译, 获取字节码清单:

javap -c demo.jvm0104.HelloByteCode# 或者: javap -c demo/jvm0104/HelloByteCodejavap -c demo/jvm0104/HelloByteCode.class

javap 还是比较聪明的, 使用包名或者相对路径都可以反编译成功, 反编译后的结果如下所示:

Compiled from "HelloByteCode.java"public class demo.jvm0104.HelloByteCode { public demo.jvm0104.HelloByteCode(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class demo/jvm0104/HelloByteCode 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: return}

OK,我们成功获取到了字节码清单, 下面进行简单的解读。

3 解读字节码清单

可以看到,反编译后的代码清单中, 有一个默认的构造函数 public demo.jvm0104.HelloByteCode(), 以及 main 方法。

刚学 Java 时我们就知道, 如果不定义任何构造函数,就会有一个默认的无参构造函数,这里再次验证了这个知识点。好吧,这比较容易理解!我们通过查看编译后的 class 文件证实了其中存在默认构造函数,所以这是 Java 编译器生成的, 而不是运行时JVM自动生成的。

自动生成的构造函数,其方法体应该是空的,但这里看到里面有一些指令。为什么呢?

再次回顾 Java 知识, 每个构造函数中都会先调用 super 类的构造函数对吧? 但这不是 JVM 自动执行的, 而是由程序指令控制,所以默认构造函数中也就有一些字节码指令来干这个事情。

基本上,这几条指令就是执行 super() 调用;

public demo.jvm0104.HelloByteCode(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return

至于其中解析的 java/lang/Object 不用说, 默认继承了 Object 类。这里再次验证了这个知识点,而且这是在编译期间就确定了的。

继续往下看 c,

public static void main(java.lang.String[]); Code: 0: new #2 // class demo/jvm0104/HelloByteCode 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: return

main 方法中创建了该类的一个实例, 然后就 return 了, 关于里面的几个指令, 稍后讲解。

4 查看 class 文件中的常量池信息

常量池 大家应该都听说过, 英文是 Constant pool。这里做一个强调: 大多数时候指的是 运行时常量池。但运行时常量池里面的常量是从哪里来的呢? 主要就是由 class 文件中的 常量池结构体 组成的。

要查看常量池信息, 我们得加一点魔法参数:

javap -c -verbose demo.jvm0104.HelloByteCode

在反编译 class 时,指定 -verbose 选项, 则会 输出附加信息。

结果如下所示:

ClassFile /XXXXXXX/demo/jvm0104/HelloByteCode.class Last modified 2019-11-28; size 301 bytes MD5 checksum 542cb70faf8b2b512a023e1a8e6c1308 Compiled from "HelloByteCode.java"public class demo.jvm0104.HelloByteCode minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #4.#13 // java/lang/Object."<init>":()V #2 = Class #14 // demo/jvm0104/HelloByteCode #3 = Methodref #2.#13 // demo/jvm0104/HelloByteCode."<init>":()V #4 = Class #15 // java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 main #10 = Utf8 ([Ljava/lang/String;)V #11 = Utf8 SourceFile #12 = Utf8 HelloByteCode.java #13 = NameAndType #5:#6 // "<init>":()V #14 = Utf8 demo/jvm0104/HelloByteCode #15 = Utf8 java/lang/Object{ public demo.jvm0104.HelloByteCode(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #2 // class demo/jvm0104/HelloByteCode 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: return LineNumberTable: line 5: 0 line 6: 8}SourceFile: "HelloByteCode.java"

其中显示了很多关于 class 文件信息: 编译时间, MD5 校验和, 从哪个 .java 源文件编译得来,符合哪个版本的 Java 语言规范等等。

还可以看到 ACC_PUBLIC 和 ACC_SUPER 访问标志符。 ACC_PUBLIC 标志很容易理解:这个类是 public 类,因此用这个标志来表示。

但 ACC_SUPER 标志是怎么回事呢? 这就是历史原因, JDK 1.0 的 BUG 修正中引入 ACC_SUPER 标志来修正 invokespecial 指令调用 super 类方法的问题,从 Java 1.1 开始, 编译器一般都会自动生成ACC_SUPER 标志。

有些同学可能注意到了, 好多指令后面使用了 #1, #2, #3 这样的编号。

这就是对常量池的引用。 那常量池里面有些什么呢?

Constant pool: #1 = Methodref #4.#13 // java/lang/Object."<init>":()V #2 = Class #14 // demo/jvm0104/HelloByteCode #3 = Methodref #2.#13 // demo/jvm0104/HelloByteCode."<init>":()V #4 = Class #15 // java/lang/Object #5 = Utf8 <init>......

这是摘取的一部分内容, 可以看到常量池中的常量定义。还可以进行组合, 一个常量的定义中可以引用其他常量。

比如第一行: #1 = Methodref #4.#13 // java/lang/Object."<init>":()V, 解读如下:

#1 常量编号, 该文件中其他地方可以引用。

= 等号就是分隔符.

Methodref 表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类指向的 #4, 方法签名指向的 #13; 当然双斜线注释后面已经解析出来可读性比较好的说明了。

同学们可以试着解析其他的常量定义。 自己实践加上知识回顾,能有效增加个人的记忆和理解。

总结一下,常量池就是一个常量的大字典,使用编号的方式把程序里用到的各类常量统一管理起来,这样在字节码操作里,只需要引用编号即可。

5 查看方法信息

在 javap 命令中使用 -verbose 选项时, 还显示了其他的一些信息。 例如, 关于 main 方法的更多信息被打印出来:

public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1

可以看到方法描述: ([Ljava/lang/String;)V:

其中小括号内是入参信息/形参信息;

左方括号表述数组;

L 表示对象;

后面的java/lang/String就是类名称;

小括号后面的 V 则表示这个方法的返回值是 void;

方法的访问标志也很容易理解 flags: ACC_PUBLIC, ACC_STATIC,表示 public 和 static。

还可以看到执行该方法时需要的栈(stack)深度是多少,需要在局部变量表中保留多少个槽位, 还有方法的参数个数: stack=2, locals=2, args_size=1。把上面这些整合起来其实就是一个方法:

public static void main(java.lang.String[]);

注:实际上我们一般把一个方法的修饰符 名称 参数类型清单 返回值类型,合在一起叫“方法签名”,即这些信息可以完整的表示一个方法。

稍微往回一点点,看编译器自动生成的无参构造函数字节码:

public demo.jvm0104.HelloByteCode(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return

你会发现一个奇怪的地方, 无参构造函数的参数个数居然不是 0: stack=1, locals=1, args_size=1。 这是因为在 Java 中, 如果是静态方法则没有 this 引用。 对于非静态方法, this 将被分配到局部变量表的第 0 号槽位中, 关于局部变量表的细节,下面再进行介绍。

有反射编程经验的同学可能比较容易理解: Method#invoke(Object obj, Object... args); 有JavaScript编程经验的同学也可以类比: fn.apply(obj, args) && fn.call(obj, arg1, arg2);

6 线程栈与字节码执行模型

想要深入了解字节码技术,我们需要先对字节码的执行模型有所了解。

JVM 是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM stack),用于存储栈帧(Frame)。每一次方法调用,JVM都会自动创建一个栈帧。栈帧 由 操作数栈, 局部变量数组 以及一个class 引用组成。class 引用 指向当前方法在运行时常量池中对应的 class)。

我们在前面反编译的代码中已经看到过这些内容。

局部变量数组 也称为 局部变量表(LocalVariableTable), 其中包含了方法的参数,以及局部变量。 局部变量数组的大小在编译时就已经确定: 和局部变量 形参的个数有关,还要看每个变量/参数占用多少个字节。操作数栈是一个 LIFO 结构的栈, 用于压入和弹出值。 它的大小也在编译时确定。

有一些操作码/指令可以将值压入“操作数栈”; 还有一些操作码/指令则是从栈中获取操作数,并进行处理,再将结果压入栈。操作数栈还用于接收调用其他方法时返回的结果值。

7 方法体中的字节码解读

看过前面的示例,细心的同学可能会猜测,方法体中那些字节码指令前面的数字是什么意思,说是序号吧但又不太像,因为他们之间的间隔不相等。看看 main 方法体对应的字节码:

0: new #2 // class demo/jvm0104/HelloByteCode 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: return

间隔不相等的原因是, 有一部分操作码会附带有操作数, 也会占用字节码数组中的空间。

例如, new 就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。

因此,下一条指令 dup 的索引从 3 开始。

每个操作码/指令都有对应的十六进制(HEX)表示形式, 如果换成十六进制来表示,则方法体可表示为HEX字符串。例如上面的方法体百世成十六进制如下所示:

甚至我们还可以在支持十六进制的编辑器中打开 class 文件,可以在其中找到对应的字符串:

(此图由开源文本编辑软件Atom的hex-view插件生成)

粗暴一点,我们可以通过 HEX 编辑器直接修改字节码,尽管这样做会有风险, 但如果只修改一个数值的话应该会很有趣。

其实要使用编程的方式,方便和安全地实现字节码编辑和修改还有更好的办法,那就是使用 ASM 和 Javassist 之类的字节码操作工具,也可以在类加载器和 Agent 上面做文章,下一节课程会讨论 类加载器,其他主题则留待以后探讨。

4.8 对象初始化指令:new 指令, init 以及 clinit 简介

我们都知道 new是 Java 编程语言中的一个关键字, 但其实在字节码中,也有一个指令叫做 new。 当我们创建类的实例时, 编译器会生成类似下面这样的操作码:

0: new #2 // class demo/jvm0104/HelloByteCode 3: dup 4: invokespecial #3 // Method "<init>":()V

当你同时看到 new, dup 和 invokespecial 指令在一起时,那么一定是在创建类的实例对象!

为什么是三条指令而不是一条呢?这是因为:

new 指令只是创建对象,但没有调用构造函数。

invokespecial 指令用来调用某些特殊方法的, 当然这里调用的是构造函数。

dup 指令用于复制栈顶的值。

由于构造函数调用不会返回值,所以如果没有 dup 指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题, 接下来的代码就无法对其进行处理。

这就是为什么要事先复制引用的原因,为的是在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段。因此,接下来的那条指令一般是以下几种:

astore {N} or astore_{N} – 赋值给局部变量,其中 {N} 是局部变量表中的位置。

putfield – 将值赋给实例字段

putstatic – 将值赋给静态字段

在调用构造函数的时候,其实还会执行另一个类似的方法 <init> ,甚至在执行构造函数之前就执行了。

还有一个可能执行的方法是该类的静态初始化方法 <clinit>, 但 <clinit> 并不能被直接调用,而是由这些指令触发的: new, getstatic, putstatic or invokestatic。

也就是说,如果创建某个类的新实例, 访问静态字段或者调用静态方法,就会触发该类的静态初始化方法【如果尚未初始化】。

实际上,还有一些情况会触发静态初始化, 详情请参考 JVM 规范: [http://docs.oracle.com/javase/specs/jvms/se8/html/]

4.9 栈内存操作指令

有很多指令可以操作方法栈。 前面也提到过一些基本的栈操作指令: 他们将值压入栈,或者从栈中获取值。 除了这些基础操作之外也还有一些指令可以操作栈内存; 比如 swap 指令用来交换栈顶两个元素的值。下面是一些示例:

最基础的是 dup 和 pop 指令。

dup 指令复制栈顶元素的值。

pop 指令则从栈中删除最顶部的值。

还有复杂一点的指令:比如,swap, dup_x1 和 dup2_x1。

顾名思义,swap 指令可交换栈顶两个元素的值,例如A和B交换位置(图中示例4);

dup_x1 将复制栈顶元素的值,并在栈顶插入两次(图中示例5);

dup2_x1 则复制栈顶两个元素的值,并插入第三个值(图中示例6)。

dup_x1 和 dup2_x1 指令看起来稍微有点复杂。而且为什么要设置这种指令呢? 在栈中复制最顶部的值?

请看一个实际案例:怎样交换 2 个 double 类型的值?

需要注意的是,一个 double 值占两个槽位,也就是说如果栈中有两个 double 值,它们将占用 4 个槽位。

要执行交换,你可能想到了 swap 指令,但问题是 swap 只适用于单字(one-word, 单字一般指 32 位 4 个字节,64 位则是双字),所以不能处理 double 类型,但 Java 中又没有 swap2 指令。

怎么办呢? 解决方法就是使用 dup2_x2 指令,将操作数栈顶部的 double 值,复制到栈底 double 值的下方, 然后再使用 pop2 指令弹出栈顶的 double 值。结果就是交换了两个 double 值。 示意图如下图所示:

dup、dup_x1、dup2_x1指令补充说明

指令的详细说明可参考 JVM 规范:

dup 指令

官方说明是:复制栈顶的值,并将复制的值压入栈。

操作数栈的值变化情况(方括号标识新插入的值):

..., value →..., value [,value]

dup_x1 指令

官方说明是:复制栈顶的值,并将复制的值插入到最上面 2 个值的下方。

操作数栈的值变化情况(方括号标识新插入的值):

..., value2, value1 →..., [value1,] value2, value1

dup2_x1 指令

官方说明是:复制栈顶 1 个 64 位/或 2 个 32 位的值, 并将复制的值按照原始顺序,插入原始值下面一个 32 位值的下方。

操作数栈的值变化情况(方括号标识新插入的值):

# 情景 1: value1, value2, and value3 都是分组 1 的值(32 位元素)..., value3, value2, value1 →..., [value2, value1,] value3, value2, value1# 情景 2: value1 是分组 2 的值(64 位,long 或double), value2 是分组 1 的值(32 位元素)..., value2, value1 →..., [value1,] value2, value1

Table 2.11.1-B 实际类型与 JVM 计算类型映射和分组

实际类型

JVM 计算类型

类型分组

boolean

int

1

byte

int

1

char

int

1

short

int

1

int

int

1

float

float

1

reference

reference

1

returnAddress

returnAddress

1

long

long

2

double

double

2

4.10 局部变量表

stack 主要用于执行指令,而局部变量则用来保存中间结果,两者之间可以直接交互。

让我们编写一个复杂点的示例:

第一步,先编写一个计算移动平均数的类:

package demo.jvm0104;//移动平均数public class MovingAverage { private int count = 0; private double sum = 0.0D; public void submit(double value){ this.count ; this.sum = value; } public double getAvg(){ if(0 == this.count){ return sum;} return this.sum/this.count; }}

第二步,然后写一个类来调用:

package demo.jvm0104;public class LocalVariableTest { public static void main(String[] args) { MovingAverage ma = new MovingAverage(); int num1 = 1; int num2 = 2; ma.submit(num1); ma.submit(num2); double avg = ma.getAvg(); }}

其中 main 方法中向 MovingAverage 类的实例提交了两个数值,并要求其计算当前的平均值。

然后我们需要编译(还记得前面提到, 生成调试信息的 -g 参数吗)。

javac -g demo/jvm0104/*.java

然后使用 javap 反编译:

javap -c -verbose demo/jvm0104/LocalVariableTest

看 main 方法对应的字节码:

public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=6, args_size=1 0: new #2 // class demo/jvm0104/MovingAverage 3: dup 4: invokespecial #3 // Method demo/jvm0104/MovingAverage."<init>":()V 7: astore_1 8: iconst_1 9: istore_2 10: iconst_2 11: istore_3 12: aload_1 13: iload_2 14: i2d 15: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V 18: aload_1 19: iload_3 20: i2d 21: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V 24: aload_1 25: invokevirtual #5 // Method demo/jvm0104/MovingAverage.getAvg:()D 28: dstore 4 30: return LineNumberTable: line 5: 0 line 6: 8 line 7: 10 line 8: 12 line 9: 18 line 10: 24 line 11: 30 LocalVariableTable: Start Length Slot Name Signature 0 31 0 args [Ljava/lang/String; 8 23 1 ma Ldemo/jvm0104/MovingAverage; 10 21 2 num1 I 12 19 3 num2 I 30 1 4 avg D

编号 0 的字节码 new, 创建 MovingAverage 类的对象;

编号 3 的字节码 dup 复制栈顶引用值。

编号 4 的字节码 invokespecial 执行对象初始化。

编号 7 开始, 使用 astore_1 指令将引用地址值(addr.)存储(store)到编号为1的局部变量中: astore_1 中的 1 指代 LocalVariableTable 中ma对应的槽位编号,

编号8开始的指令: iconst_1 和 iconst_2 用来将常量值1和2加载到栈里面, 并分别由指令 istore_2 和 istore_3 将它们存储到在 LocalVariableTable 的槽位 2 和槽位 3 中。

8: iconst_1 9: istore_2 10: iconst_2 11: istore_3

请注意,store 之类的指令调用实际上从栈顶删除了一个值。 这就是为什么再次使用相同值时,必须再加载(load)一次的原因。

例如在上面的字节码中,调用 submit 方法之前, 必须再次将参数值加载到栈中:

12: aload_1 13: iload_2 14: i2d 15: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V

调用 getAvg() 方法后,返回的结果位于栈顶,然后使用 dstore 将 double 值保存到本地变量4号槽位,这里的d表示目标变量的类型为double。

24: aload_1 25: invokevirtual #5 // Method demo/jvm0104/MovingAverage.getAvg:()D 28: dstore 4

关于 LocalVariableTable 有个有意思的事情,就是最前面的槽位会被方法参数占用。

在这里,因为 main 是静态方法,所以槽位0中并没有设置为 this 引用的地址。 但是对于非静态方法来说, this 会将分配到第 0 号槽位中。

再次提醒: 有过反射编程经验的同学可能比较容易理解: Method#invoke(Object obj, Object... args); 有JavaScript编程经验的同学也可以类比: fn.apply(obj, args) && fn.call(obj, arg1, arg2);

理解这些字节码的诀窍在于:

给局部变量赋值时,需要使用相应的指令来进行 store,如 astore_1。store 类的指令都会删除栈顶值。 相应的 load 指令则会将值从局部变量表压入操作数栈,但并不会删除局部变量中的值。

4.11 流程控制指令

流程控制指令主要是分支和循环在用, 根据检查条件来控制程序的执行流程。

一般是 If-Then-Else 这种三元运算符(ternary operator), Java中的各种循环,甚至异常处的理操作码都可归属于 程序流程控制。

然后,我们再增加一个示例,用循环来提交给 MovingAverage 类一定数量的值:

package demo.jvm0104;public class ForLoopTest { private static int[] numbers = {1, 6, 8}; public static void main(String[] args) { MovingAverage ma = new MovingAverage(); for (int number : numbers) { ma.submit(number); } double avg = ma.getAvg(); }}

同样执行编译和反编译:

javac -g demo/jvm0104/*.javajavap -c -verbose demo/jvm0104/ForLoopTest

因为 numbers 是本类中的 static 属性, 所以对应的字节码如下所示:

0: new #2 // class demo/jvm0104/MovingAverage 3: dup 4: invokespecial #3 // Method demo/jvm0104/MovingAverage."<init>":()V 7: astore_1 8: getstatic #4 // Field numbers:[I 11: astore_2 12: aload_2 13: arraylength 14: istore_3 15: iconst_0 16: istore 4 18: iload 4 20: iload_3 21: if_icmpge 43 24: aload_2 25: iload 4 27: iaload 28: istore 5 30: aload_1 31: iload 5 33: i2d 34: invokevirtual #5 // Method demo/jvm0104/MovingAverage.submit:(D)V 37: iinc 4, 1 40: goto 18 43: aload_1 44: invokevirtual #6 // Method demo/jvm0104/MovingAverage.getAvg:()D 47: dstore_2 48: return LocalVariableTable: Start Length Slot Name Signature 30 7 5 number I 0 49 0 args [Ljava/lang/String; 8 41 1 ma Ldemo/jvm0104/MovingAverage; 48 1 2 avg D

位置 [8~16] 的指令用于循环控制。 我们从代码的声明从上往下看, 在最后面的LocalVariableTable 中:

0 号槽位被 main 方法的参数 args 占据了。

1 号槽位被 ma 占用了。

5 号槽位被 number 占用了。

2 号槽位是for循环之后才被 avg 占用的。

那么中间的 2,3,4 号槽位是谁霸占了呢? 通过分析字节码指令可以看出,在 2,3,4 槽位有 3 个匿名的局部变量(astore_2, istore_3, istore 4等指令)。

2号槽位的变量保存了 numbers 的引用值,占据了 2号槽位。

3号槽位的变量, 由 arraylength 指令使用, 得出循环的长度。

4号槽位的变量, 是循环计数器, 每次迭代后使用 iinc 指令来递增。

如果我们的 JDK 版本再老一点, 则会在 2,3,4 槽位发现三个源码中没有出现的变量: arr$, len$, i$, 也就是循环变量。

循环体中的第一条指令用于执行 循环计数器与数组长度 的比较:

18: iload 4 20: iload_3 21: if_icmpge 43

这段指令将局部变量表中 4号槽位 和 3号槽位的值加载到栈中,并调用 if_icmpge 指令来比较他们的值。

【if_icmpge 解读: if, integer, compare, great equal】, 如果一个数的值大于或等于另一个值,则程序执行流程跳转到pc=43的地方继续执行。

在这个例子中就是, 如果4号槽位的值 大于或等于 3号槽位的值, 循环就结束了,这里 43 位置对于的是循环后面的代码。如果条件不成立,则循环进行下一次迭代。

在循环体执行完,它的循环计数器加 1,然后循环跳回到起点以再次验证循环条件:

37: iinc 4, 1 // 4号槽位的值加1 40: goto 18 // 跳到循环开始的地方

4.12 算术运算指令与类型转换指令

Java 字节码中有许多指令可以执行算术运算。实际上,指令集中有很大一部分表示都是关于数学运算的。对于所有数值类型(int, long, double, float),都有加,减,乘,除,取反的指令。

那么 byte 和 char, boolean 呢? JVM 是当做 int 来处理的。另外还有部分指令用于数据类型之间的转换。

算术操作码和类型

当我们想将 int 类型的值赋值给 long 类型的变量时,就会发生类型转换。

类型转换操作码

在前面的示例中, 将 int 值作为参数传递给实际上接收 double 的 submit() 方法时,可以看到, 在实际调用该方法之前,使用了类型转换的操作码:

31: iload 5 33: i2d 34: invokevirtual #5 // Method demo/jvm0104/MovingAverage.submit:(D)V

也就是说, 将一个 int 类型局部变量的值, 作为整数加载到栈中,然后用 i2d 指令将其转换为 double 值,以便将其作为参数传给submit方法。

唯一不需要将数值load到操作数栈的指令是 iinc,它可以直接对 LocalVariableTable 中的值进行运算。 其他的所有操作均使用栈来执行。

4.13 方法调用指令和参数传递

前面部分稍微提了一下方法调用: 比如构造函数是通过 invokespecial 指令调用的。

这里列举了各种用于方法调用的指令:

invokestatic,顾名思义,这个指令用于调用某个类的静态方法,这也是方法调用指令中最快的一个。

invokespecial, 我们已经学过了, invokespecial 指令用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。

invokevirtual,如果是具体类型的目标对象,invokevirtual用于调用公共,受保护和打包私有方法。

invokeinterface,当要调用的方法属于某个接口时,将使用 invokeinterface 指令。

那么 invokevirtual 和 invokeinterface 有什么区别呢?这确实是个好问题。 为什么需要 invokevirtual 和 invokeinterface 这两种指令呢? 毕竟所有的接口方法都是公共方法, 直接使用 invokevirtual 不就可以了吗?

这么做是源于对方法调用的优化。JVM 必须先解析该方法,然后才能调用它。

使用 invokestatic 指令,JVM 就确切地知道要调用的是哪个方法:因为调用的是静态方法,只能属于一个类。

使用 invokespecial 时, 查找的数量也很少, 解析也更加容易, 那么运行时就能更快地找到所需的方法。

使用 invokevirtual 和 invokeinterface 的区别不是那么明显。想象一下,类定义中包含一个方法定义表, 所有方法都有位置编号。下面的示例中:A 类包含 method1 和 method2 方法; 子类B继承A,继承了 method1,覆写了 method2,并声明了方法 method3。

请注意,method1 和 method2 方法在类 A 和类 B 中处于相同的索引位置。

class A 1: method1 2: method2class B extends A 1: method1 2: method2 3: method3

那么,在运行时只要调用 method2,一定是在位置 2 处找到它。

现在我们来解释invokevirtual 和 invokeinterface 之间的本质区别。

假设有一个接口 X 声明了 methodX 方法, 让 B 类在上面的基础上实现接口 X:

class B extends A implements X 1: method1 2: method2 3: method3 4: methodX

新方法 methodX 位于索引 4 处,在这种情况下,它看起来与 method3 没什么不同。

但如果还有另一个类 C 也实现了 X 接口,但不继承 A,也不继承 B:

class C implements X 1: methodC 2: methodX

类 C 中的接口方法位置与类 B 的不同,这就是为什么运行时在 invokinterface 方面受到更多限制的原因。 与 invokinterface 相比, invokevirtual 针对具体的类型方法表是固定的,所以每次都可以精确查找,效率更高(具体的分析讨论可以参见参考材料的第一个链接)。

4.14 JDK7 新增的方法调用指令 invokedynamic

Java 虚拟机的字节码指令集在 JDK7 之前一直就只有前面提到的 4 种指令(invokestatic,invokespecial,invokevirtual,invokeinterface)。随着 JDK 7 的发布,字节码指令集新增了invokedynamic指令。这条新增加的指令是实现“动态类型语言”(Dynamically Typed Language)支持而进行的改进之一,同时也是 JDK 8 以后支持的 lambda 表达式的实现基础。

为什么要新增加一个指令呢?

我们知道在不改变字节码的情况下,我们在 Java 语言层面想调用一个类 A 的方法 m,只有两个办法:

使用A a=new A(); a.m(),拿到一个 A 类型的实例,然后直接调用方法;

通过反射,通过 A.class.getMethod 拿到一个 Method,然后再调用这个Method.invoke反射调用;

这两个方法都需要显式的把方法 m 和类型 A 直接关联起来,假设有一个类型 B,也有一个一模一样的方法签名的 m 方法,怎么来用这个方法在运行期指定调用 A 或者 B 的 m 方法呢?这个操作在 JavaScript 这种基于原型的语言里或者是 C# 这种有函数指针/方法委托的语言里非常常见,Java 里是没有直接办法的。Java 里我们一般建议使用一个 A 和 B 公有的接口 IC,然后 IC 里定义方法 m,A 和 B 都实现接口 IC,这样就可以在运行时把 A 和 B 都当做 IC 类型来操作,就同时有了方法 m,这样的“强约束”带来了很多额外的操作。

而新增的 invokedynamic 指令,配合新增的方法句柄(Method Handles,它可以用来描述一个跟类型 A 无关的方法 m 的签名,甚至不包括方法名称,这样就可以做到我们使用方法 m 的签名,但是直接执行的时候调用的是相同签名的另一个方法 b),可以在运行时再决定由哪个类来接收被调用的方法。在此之前,只能使用反射来实现类似的功能。该指令使得可以出现基于 JVM 的动态语言,让 jvm 更加强大。而且在 JVM 上实现动态调用机制,不会破坏原有的调用机制。这样既很好的支持了 Scala、Clojure 这些 JVM 上的动态语言,又可以支持代码里的动态 lambda 表达式。

RednaxelaFX 评论说:

简单来说就是以前设计某些功能的时候把做法写死在了字节码里,后来想改也改不了了。 所以这次给 lambda 语法设计翻译到字节码的策略是就用 invokedynamic 来作个弊,把实际的翻译策略隐藏在 JDK 的库的实现里(metafactory)可以随时改,而在外部的标准上大家只看到一个固定的 invokedynamic。

免责声明:本文由用户上传,如有侵权请联系删除!