【总目录/源代码】:https://blog.youkuaiyun.com/qq_40636117/article/details/94383044
【下一篇】:https://blog.youkuaiyun.com/qq_40636117/article/details/94415960
我们知道,哪怕运行像输出“Hello World”这样简单的程序,java虚拟机都要先加载相关的类。加载类文件需要通过类路径(classpath)来寻找。当然根据搜索类路径先后顺序又分成三种。我们不管,先简单粗暴一点,完成基本功能,后面再慢慢优化。
所以classLoader文件基本功能如下:
1、寻找类文件:通过遍历classpath下的所有文件查找该类文件,并返回该类文件的绝对路径(找不到该类,返回classpath)。
2、加载类文件字节码:通过该类文件的绝对路径,读取该类字节码信息。
3、存放信息:存放这些类的字节码,并提供读取该类字节码信息的方法。
一、寻找类文件
搜索文件的方法就不用多说了,就是遍历多叉树嘛,其中非文件夹的结点为叶子节点。但是也有不少问题需要解决的,比如:
(1)有的.class文件存放在.jar或.zip里面,这怎么整?比如java标准库大部分在rt.jar里面。Go语言有archive/zip包,直接搜索。而C++好像没有提供这种包。本来想着直接解压.jar及.zip的,但解压要耗费不少时间,效率感人。本人姿势水平不够,先留着,以后再说吧。
(2)重名怎么办?两个类文件都叫Object.class但路径不同的情况也是有的。这个问题好解决,可以根据类文件大小,创建日期,最后修改日期等选择一个出来加载嘛,小问题小问题。
实现这个功能的方法就命名为findFile好了。首先根路径(即classpath)和要加载的类文件名是必须的。由于想弄全局变量,我把findpath也传了进来。findpath=classpath,说明没找到该文件;反之则是找到了。
void findFile(string classpath,string fname,string &findpath){//文件句柄
long hFile = 0;
struct _finddata_t fInfo;//文件信息
string p;
if ((hFile = _findfirst(p.assign(classpath).append("\\*").c_str(), &fInfo)) != -1){
do{
if ((fInfo.attrib & _A_SUBDIR)){
if (strcmp(fInfo.name, ".") != 0 && strcmp(fInfo.name, "..") != 0){
findFile(p.assign(classpath).append("\\\\").append(fInfo.name),fname,findpath);
}
}else{
if(fname==fInfo.name){
findpath=classpath.append("\\\\").append(fInfo.name);
}
}
} while (_findnext(hFile, &fInfo) == 0);
_findclose(hFile);
}
}
二、读取类文件字节码信息
首先.class文件存放的是字节码,所以数据类型是byte就跑不掉了。C++没有这种类型,没关系,char不也是8位吗?那就拿char当byte用就行了。通过观察《自己动手写java虚拟机》作者贴出的字节码,我发现没有负数,那就是无符号(unsigned)了。
先开一个unsigned char的数组用于存放信息。本来不知道数组开多大还挺头疼的,后来查了一下发现居然可以读取文件的大小,简直了!于是就顺利读取出来了。
一开始对比读取的数据,发现有的地方不一样,吓了一跳,后来运行了作者提供的go语言版虚拟机才明白过来,原来是版本问题,好险。
//读取文件内容
fileData *readFile(string absPath){//绝对路径
const char *path=absPath.data();
FILE *fp = fopen(path, "rb");
if(fp==NULL){
return NULL;
}
unsigned char *fdata;//字节数组
fseek(fp, 0, SEEK_END);
int size = ftell(fp);//文件大小
fseek(fp, 0, SEEK_SET);
if(size > 0){
fdata = (unsigned char *)calloc(size+1,sizeof(unsigned char));
}
/*读文件内容存入内存*/
fread(fdata, size, 1, fp);
fclose(fp);
fdata[size] = '\0';
fileData *data=new fileData(fdata,size);
return data;
}
三、存放信息
读取完文件数据后,我们还要定义一个类,专门存放这些信息。为了以后读取方便,还要提供方法读取里面的内容。
查阅资料我发现解析字节码的时候不一定8位8位地读取信息,有可能16位(2字节),32位(4字节)甚至64位(4字节)地读取。而且不一定读取一个8位数据,也有可能读取若干个8位数据。为了方便,当然要做出相应的方法啦。
u1(8位数据)——>C++的unsingned char类型
u2(16位数据)——>C++的unsigned int类型
u4(32位数据)——>C++的unsigned long类型
u8(64位数据)——>C++的unsigned _int64数据
类:
/*
fileData类:存放从.class文件里读取的字节数据,
并提供若干方法供外部读取数据(8位,16位,32位,64位)以便解码
*/
class fileData{
private:
unsigned char *data;//存放数据(8位)
int start;//开始读取数据的索引,用于数据处理
int size;//大小
//[0,start)为已经读取过的数据
//[start,size)为未读取数据
public:
fileData(unsigned char *d,int s){
data=d;size=s;
start=0;
}
unsigned char* getData(){
return data;
}
int getSize(){
return size;
}
//读取1个8位数据:(unsigned char)1字节
unsigned char readU1(){
if(size-start>0){
return data[start++];
}
return 0;
}
//读取1个16位数据:(unsigned int)2字节
unsigned int readU2(){
unsigned int read=0;
int usize;
size-start>2 ? usize=2 : usize=size-start;
for(int i=usize-1;i>=0;--i){
read+=data[start++]<<(8*i);
}
return read;
}
//读取1个32位数据:(unsigned long)4字节
unsigned long readU4(){
unsigned int read=0;
int usize;
size-start>4 ? usize=4 : usize=size-start;
for(int i=usize-1;i>=0;--i){
read+=data[start++]<<(8*i);
}
return read;
}
//读取1个64位数据:(unsigned __int64)8字节
unsigned _int64 readU8(){
unsigned _int64 read=0;
int usize;
size-start>8 ? usize=8 : usize=size-start;
for(int i=usize-1;i>=0;--i){
read+=data[start++]<<(8*i);
}
return read;
}
//读取若干位8位数据
unsigned char* readBytes(unsigned int n){
unsigned char* s=(unsigned char*)calloc(n+1,sizeof(unsigned char));
for(int i=0;i<n;++i){
s[i]=readU1();
}
s[n]='\0';//一定要加上这玩意,代表结束!!不然会一直找下去,找到'\0'为止
return s;
}
//读取若干16位数据
unsigned int* readU2s(int n){
unsigned int* s=(unsigned int*)calloc(n,sizeof(unsigned int));
for(int i=0;i<n;++i){
s[i]=readU2();
}
return s;
}
};
四、测试:读取数据
先测试一下读取Object.class文件里面的内容
解析倒是解析出来了,但我里当场去世就差那么一点点。
于是我也学着书本一样,把这些数据做一些处理:
首先由于是8位2进制数,所以数值在00~FF之间,所以转成16进制后,就能确保数值在1~2位;再经过特殊的处理,就能保证数值都在2位,工工整整,方便输出!
处理后的数据:
嗯,干净多了,后面就是解码这些字节码的工作了