IDC语言是一种跟C语言比较类似的脚本语言。IDC和C语言具有非常类似的语言标记:字符集,常量,标识符,关键词,等。然而,由于IDC是一种脚本语言,就无法包含指针这样的高级特性了,但是所有的变量类型,脚本解释器都是可以支持的。IDC中的变量是弱类型的,一个变量可以保存任何类型的数据,因此变量声明的时候是不需要指定变量类型的,比如你可以这样进行变量声明: auto myvar;
一个IDC脚本是由很多的函数组成的。默认情况下,脚本从 main 函数开始执行。
下面我按照主题的方式进行IDC编程脚本的介绍:
变量
函数
语句
表达式
预定义符号
切片
异常
函数列表
调试器相关函数列表
在IDC脚本语言中,存在两种变量:
局部变量:这类变量在函数开头的时候声明创建,在函数退出的时候销毁
全局变量:这类变量在脚本编译期创建,在IDA分析数据库关闭的时候销毁(译者注:全局变量的创建和销毁原文描述的其实不是太清晰,经过我的测试,全局变量是在脚本加载进来的时候就被创建,然后当你关闭当前IDA分析的文件的时候,IDA也会清理你加载过的IDC脚本,其实可以理解为IDC脚本清理的时候销毁)
变量可以保存哪些数据类型:
LONG:32位有符号整形(如果是64位的IDA, 则表示64位有符号整形)
INT64:64位有符号整形
STR:字符串
FLOAT:浮点数据(高精度数据,最高支持25位小数)
OBJECT:一种很类似C++对象的数据类型,具有 方法 和 属性的集合
REF:其他变量的引用
FUNC:函数引用
局部变量怎么声明
下面我给出声明一个局部变量的例子,大概是这样的:
auto var1;
auto var2 = expr;
全局变量怎么声明
同样的,我也给出一个全局变量的例子,大概是这样的:
extern var;
需要特别说明的是,全局变量可以声明很多次。但是IDA只保留第一次声明的那个,后续的所有同名的全局变量都自动忽略。在IDC脚本中,全局变量和C语言中的使用还有一个很大的不同点,那就是不能在声明的时候进行初始化赋值。
在给变量命名的时候要注意,所有的C C++的关键词和保留字都不能作为变量的名称,其他就没有什么特别的限制了
虽然IDC脚本允许你在函数体内的任意一个地方声明一个变量,但是所有你声明的函数内临时变量都只会在函数的开始位置初始化一次,然后在函数退出的时候销毁。举个例子,如果你在一个循环体内声明了一个变量,这个变量也只会初始化一次,除非你在某个位置对这个变量进行赋值,不然这个变量的值就一直保持那个最初的初始化值。
如果你在IDC脚本内声明的变量或者函数IDA无法找到相应的声明,那么IDA会尝试从当前打开的分析文件中的符号进行匹配,如果能够匹配成功,这个无法识别的变量或者函数就使用这个匹配出来的符号来代替,还是举个具体的例子来说明:
上面这个例子中,Message调用将会输出 413060, 也就是_errtabled的地址,如果IDA匹配出来的符号是一个结构体,还可以进一步访问结构体的成员变量,比如上面的 errTable就是一个结构体,可以进一步这样使用结构体的成员:Message("address is: %x\n", _errtable.errnocode); 这句调用将打印 413064 这个结果。从上面的这两个例子可以看出来,IDA对符号的处理过程中,只会返回结构体的地址而不是值,如果你想要获取具体的值可以使用 get_field_ea 这个函数来进行。
注意:
如果当前IDA的调试器处于激活状态,也就是说你正在使用IDA调试一个程序,那么你就可以在IDC脚本中使用处理器的寄存器了,你可以使用寄存器名称直接进行寄存器读写。还有一个必须明确,寄存器的读写动作必须要在被调试进程处于暂停状态下进行
你可以使用array函数来枚举获取全局空间的变量或者用array函数创建全局的常驻内存的数组
IDC脚本中的函数必须要有返回值。在IDC脚本中,支持两类函数:
内建函数
用户自定义函数
用户自定义函数一般是像下面这样的方式写的:
需要注意的一点是,声明函数参数的时候,没必要指定函数参数的类型了,因为IDC会根据你传入的参数自动进行参数类型转换的。
默认情况下,函数调用的时候参数传递是按值传递的,但是以下三种情况例外(引用传递):
对象类型的参数总是使用引用方式传参
函数类型的参数总是使用应用方式传参
还可以强制使用 & 符号来让参数使用引用方式传参
如果IDC脚本中调用的函数不存在,IDA就会尝试解析并使用当前被调试的进程中的符号或者标签,如果解析成功,那么就会执行一次 AppCall(AppCall稍后会进一步解释)
首先需要明确一点,AppCall是IDC脚本中的一个内建函数
先来看函数原型
功能:调用被调试进程的函数
参数:ea - 调用的函数地址
参数:type - 调用的函数的类型或者说方式,支持三种调用形式
字符串形式,比如:“int func(void);”
类型对象形式,比如:GetTinfo(ea)
零:相当于让IDA自行决定,这种情况下,类型一般是从idb中进行获取的
参数:..., 这个是可变参数,用来传递你要调用的函数的参数
返回值:被调用函数的返回值
Remark:如果函数调用失败,并且失败的原因是内存访问异常或者其他异常,脚本会抛出一个runtime错误信息,可以在脚本中使用 try/catch 进行异常捕获。在实际的使用过程中,很少使用AppCall这个函数调用,只是说IDA拥有这种在IDC脚本中存在未知函数的时候尝试在被调试进程中匹配符号的能力,举个例子:_printf("hello\n") 这条语句会调用被分析程序的 _printf 函数
AppCall函数有2个选项可以使用,我们可以在IDC脚本中使用 SetAppcallOptions宏进行设置:
#define APPCALL_MANUAL 0x0001
只设置AppCall, 不执行,执行完毕之后,需要调用一次 CleanupAppcall
#define APPCALL_DEBEV 0x0002
返回调试详细信息, 如果设置了这个标志位,当AppCall执行过程中发生异常的时候,会生成一个包含详细异常信息的异常对象
#define APPCALL_TIMEOUT 0x0004
AppCall调用的超时时间, 超时时间是以毫秒为单位的,并且值是放在option的高2位字节中,如果AppCall调用超时,错误信息会放到 errbuf中,并且值是字符串 timeout
#define SET_APPCALL_TIMEOUT(x) ((x<<16)|0x0004)
指定AppCall超时时间的时候,需要拼装option的值,这个就是一个辅助宏,用来生成option的值
使用AppCall这个功能,可以很随意的调用被调试进程内的函数而不需要进行任何的dll注入或者修改被调试进程的内存,如果被调用的函数名称存在的话,AppCall还可以简化成 func(args)的形式,前提是func这个符号是存在的,举个例子:
上面这段代码将会在被调试进程内创建一个OSVERSIONINFOA结构体,并且将结构体地址传递给GetVersionExA调用,调用完毕之后,verinfo 对象就被转换成了IDC对象,内存结构如下:
其中_at_属性表示这个结构体的内存地址,在这个例子中verinfo是一个临时变量,所以_at_的值意义不大,大家在使用使用的过程中可能会遇到这个值很有用的情况。
AppCall会自动在IDC对象和C对象之间进行转换,转换的时候依据IDA中拥有的类型信息来进行,但是还要遵循以下几个转换规则:
基本数据类型:
如果目标数据类型也是一个基本数据类型(非指针),只需要执行简单的转换即可(附带符号处理或者截断处理),比如: IDC中的值-1转换成 _int32(0xFFFFFFFF),IDC中的0x555转换成 _int8(0x55)
指针:
如果目标类型是一个指针,并且IDC的值是一个字符串,这个字符串就转换成一个指针对象,字符串的内容直接拷贝到被调试进程,后面追加一个结束符 \0
如果对应的IDC的值是一个数字,转换之后的结果是指针所指向的内存的值是这个数值,如果你想得到一个数值的地址,可以直接使用 &符号
如果对应的IDC值不是字符串,那这个值将会转换成一个对象,指针指向的内存使用这个对象进行初始化
结构体
如果目标类型是一个结构体,IDA会通过对应的属性来尝试一个一个的初始化结构体的成员。比如:在上面的例子中只有dwOSVersionInfoSize 属性存在,那么这个结构体字段就使用这个属性类初始化,对于不存在的字段直接初始化成0
数组
数组的每个元素都是单独初始化的,当然如果对应的IDC值是字符串除外,因为字符串本身就是可以当成一个完整的数组来使用的。
下面针对上述的几种情况,我们来举一些例子:
调用printf
调用sscanf
结构体使用
另外,_userCall 这个内建调用也是支持这些自动转换的
对于会发生异常的调用,配合APPCALL_MANUAL 标志位,可以实现单步的效果,具体方式还需要大家仔细去研究一下.
在IDC脚本中,允许你使用如下几种类型的语句:
普通语句和语句块
if语句 和 if else语句
for语句
while语句
do while语句
循环控制语句
返回语句
异常捕获语句
主动抛出异常
空语句
值得注意的一点是, C语言的中的switch语句是不支持的,切记
在IDC脚本中,除了 += 这个表达式之外,其他所有的C语言的表达式都可以直接使用
常量的定义跟C语言很类似,但是也有一点点小区别,比如IDC支持以下四种数据转换操作:
long(expr) 转换过程中,浮点会被截断,转换成long型
char(expr)
float(expr)
_int64(expr)
在大部分情况下,在IDC脚本中不需要进行显式的类型转换操作,IDC脚本内部会进行自动的转换,规则如下:
加 操作:
如果2个操作数都是字符串,直接进行字符串拼接
如果2个操作数都是对象,直接进行对象的组合(返回一个新对象)
如果其中一个操作数是浮点,则两个操作数全部转换成浮点来计算
对于其他的情况,两个操作数全部转换成long型再计算
减/乘/除:
如果存在浮点操作数,则2个操作数全部转换成浮点再计算
如果两个操作数都是对象并且操作是减,直接执行减法操作(返回一个新对象)
对于其他的情况,两个操作数全部转换成long型再计算
比较操作(==, !=, etc):
如果两个操作数都是字符串,直接进行字符串比较操作
如果存在浮点操作数,两个操作数都转换成浮点再比较
其他的情况,都转换成数字再比较
其他操作符
一律转换成long型进行计算
如果其中一个long型数据是64位的,那么其他的操作数也会转换成64位的
类型转换这个事情还有一个例外的情况:如果一个操作数是字符串,另外一个是0, 那么最后会执行字符串操作,0会被转换成一个空字符串
&符号可以用来取变量的引用, 你可以像使用指针那样来使用引用,通过引用来修改原始对象的值。需要注意的是引用变量一旦创建就不能修改了,不像C语言的指针那样,可以指来指去。还是举个例子吧:
引用的引用还是直接指向原始的对象,例子如下:
在给函数传参的时候,默认情况下非对象类型都是值传递的,有的时候,非对象类型通过引用传参是一个不错的选择。
下面的几个符号是内建预定义的:
NT :表示IDA当前运行在Windows平台上
LINUX :表示IDA当前运行在Linux平台上
MAC :表示IDA当前运行在Mac平台上
UNIX :表示IDA当前运行在Unix 平台上(包括linux mac)
EA64 :表示当前的IDA是64位版本的
QT :表示IDA的界面库版本是QT的
_GUI:表示IDA的GUI版本
TXT :表示IDA的Text版本
IDA_VERSION :表示当前IDA的版本号
在C头文件中,你也可以使用这些预定义符号,比如在编写IDA插件的时候
函数列表巨长,不能在帖子里面全部贴完了(函数名没什么好翻译的),大家可以去官方文档直接查,我博客上也保留了一份完整的列表, 我后续会更新函数文档翻译
.data:00413060 errtable dd 1 ; oscode
.data:00413060 dd 16h ; errnocodeMessage("address is: %x\n", _errtable);
static func(arg1,arg2,arg3)
{
statements ...
}
anyvalue Appcall(ea, type, ...); verinfo = object();
verinfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);GetVersionExA(&verinfo);
object
__at__: 18FEB0h
dwBuildNumber: 7600.
dwMajorVersion: 6.
dwMinorVersion: 1.
dwOSVersionInfoSize:
dwPlatformId: 2.
szCSDVersion: "\x00\x00\x00\x00..."
auto n = 5;
auto s = "short";
_printf("Hello world, number is %d, string is %s\n", n, s); auto x;
auto nsuccess = _sscanf(s, "%d", &x); verinfo = object(); GetVersionExA(verinfo); expression;
或者
{ statements... } if (expression) statement
或者if (expression) statement else statement for ( expr1; expr2; expr3 ) statement while (expression) statement do statement while (expression); break;
contiue; return <expr>;
return; the same as 'return 0;' try statement catch ( var ) statement throw <expr>; ; auto x, r;
r = &x;
r = 1; // x is equal to 1 now
auto x, r1, r2;
r1 = &x;
r2 = &r1; // r2 points to x
IDC语言是一种跟C语言比较类似的脚本语言。IDC和C语言具有非常类似的语言标记:字符集,常量,标识符,关键词,等。然而,由于IDC是一种脚本语言,就无法包含指针这样的高级特性了,但是所有的变量类型,脚本解释器都是可以支持的。IDC中的变量是弱类型的,一个变量可以保存任何类型的数据,因此变量声明的时候是不需要指定变量类型的,比如你可以这样进行变量声明: auto myvar;
一个IDC脚本是由很多的函数组成的。默认情况下,脚本从 main 函数开始执行。
下面我按照主题的方式进行IDC编程脚本的介绍:
变量
函数
语句
表达式
预定义符号
切片
异常
函数列表
调试器相关函数列表
在IDC脚本语言中,存在两种变量:
局部变量:这类变量在函数开头的时候声明创建,在函数退出的时候销毁
全局变量:这类变量在脚本编译期创建,在IDA分析数据库关闭的时候销毁(译者注:全局变量的创建和销毁原文描述的其实不是太清晰,经过我的测试,全局变量是在脚本加载进来的时候就被创建,然后当你关闭当前IDA分析的文件的时候,IDA也会清理你加载过的IDC脚本,其实可以理解为IDC脚本清理的时候销毁)
变量可以保存哪些数据类型:
LONG:32位有符号整形(如果是64位的IDA, 则表示64位有符号整形)
INT64:64位有符号整形
STR:字符串
FLOAT:浮点数据(高精度数据,最高支持25位小数)
OBJECT:一种很类似C++对象的数据类型,具有 方法 和 属性的集合
REF:其他变量的引用
FUNC:函数引用
局部变量怎么声明
下面我给出声明一个局部变量的例子,大概是这样的:
auto var1;
auto var2 = expr;
全局变量怎么声明
需要特别说明的是,全局变量可以声明很多次。但是IDA只保留第一次声明的那个,后续的所有同名的全局变量都自动忽略。在IDC脚本中,全局变量和C语言中的使用还有一个很大的不同点,那就是不能在声明的时候进行初始化赋值。
在给变量命名的时候要注意,所有的C C++的关键词和保留字都不能作为变量的名称,其他就没有什么特别的限制了
虽然IDC脚本允许你在函数体内的任意一个地方声明一个变量,但是所有你声明的函数内临时变量都只会在函数的开始位置初始化一次,然后在函数退出的时候销毁。举个例子,如果你在一个循环体内声明了一个变量,这个变量也只会初始化一次,除非你在某个位置对这个变量进行赋值,不然这个变量的值就一直保持那个最初的初始化值。
如果你在IDC脚本内声明的变量或者函数IDA无法找到相应的声明,那么IDA会尝试从当前打开的分析文件中的符号进行匹配,如果能够匹配成功,这个无法识别的变量或者函数就使用这个匹配出来的符号来代替,还是举个具体的例子来说明:
.data:00413060 errtable dd 1 ; oscode
.data:00413060 dd 16h ; errnocodeMessage("address is: %x\n", _errtable);
上面这个例子中,Message调用将会输出 413060, 也就是_errtabled的地址,如果IDA匹配出来的符号是一个结构体,还可以进一步访问结构体的成员变量,比如上面的 errTable就是一个结构体,可以进一步这样使用结构体的成员:Message("address is: %x\n", _errtable.errnocode); 这句调用将打印 413064 这个结果。从上面的这两个例子可以看出来,IDA对符号的处理过程中,只会返回结构体的地址而不是值,如果你想要获取具体的值可以使用 get_field_ea 这个函数来进行。
注意:
下面我给出声明一个局部变量的例子,大概是这样的:
auto var1;
auto var2 = expr;
IDC脚本中的函数必须要有返回值。在IDC脚本中,支持两类函数:
内建函数
用户自定义函数
用户自定义函数一般是像下面这样的方式写的:
static func(arg1,arg2,arg3)
{
statements ...
}
需要注意的一点是,声明函数参数的时候,没必要指定函数参数的类型了,因为IDC会根据你传入的参数自动进行参数类型转换的。
默认情况下,函数调用的时候参数传递是按值传递的,但是以下三种情况例外(引用传递):
对象类型的参数总是使用引用方式传参
函数类型的参数总是使用应用方式传参
还可以强制使用 & 符号来让参数使用引用方式传参
如果IDC脚本中调用的函数不存在,IDA就会尝试解析并使用当前被调试的进程中的符号或者标签,如果解析成功,那么就会执行一次 AppCall(AppCall稍后会进一步解释)
首先需要明确一点,AppCall是IDC脚本中的一个内建函数
先来看函数原型
anyvalue Appcall(ea, type, ...);
功能:调用被调试进程的函数
参数:ea - 调用的函数地址
参数:type - 调用的函数的类型或者说方式,支持三种调用形式
字符串形式,比如:“int func(void);”
类型对象形式,比如:GetTinfo(ea)
零:相当于让IDA自行决定,这种情况下,类型一般是从idb中进行获取的
参数:..., 这个是可变参数,用来传递你要调用的函数的参数
返回值:被调用函数的返回值
Remark:如果函数调用失败,并且失败的原因是内存访问异常或者其他异常,脚本会抛出一个runtime错误信息,可以在脚本中使用 try/catch 进行异常捕获。在实际的使用过程中,很少使用AppCall这个函数调用,只是说IDA拥有这种在IDC脚本中存在未知函数的时候尝试在被调试进程中匹配符号的能力,举个例子:_printf("hello\n") 这条语句会调用被分析程序的 _printf 函数
AppCall函数有2个选项可以使用,我们可以在IDC脚本中使用 SetAppcallOptions宏进行设置:
#define APPCALL_MANUAL 0x0001
#define APPCALL_DEBEV 0x0002
#define APPCALL_TIMEOUT 0x0004
#define SET_APPCALL_TIMEOUT(x) ((x<<16)|0x0004)
使用AppCall这个功能,可以很随意的调用被调试进程内的函数而不需要进行任何的dll注入或者修改被调试进程的内存,如果被调用的函数名称存在的话,AppCall还可以简化成 func(args)的形式,前提是func这个符号是存在的,举个例子:
verinfo = object();
verinfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);GetVersionExA(&verinfo);
上面这段代码将会在被调试进程内创建一个OSVERSIONINFOA结构体,并且将结构体地址传递给GetVersionExA调用,调用完毕之后,verinfo 对象就被转换成了IDC对象,内存结构如下:
object
__at__: 18FEB0h
dwBuildNumber: 7600.
dwMajorVersion: 6.
dwMinorVersion: 1.
dwOSVersionInfoSize:
dwPlatformId: 2.
szCSDVersion: "\x00\x00\x00\x00..."
其中_at_属性表示这个结构体的内存地址,在这个例子中verinfo是一个临时变量,所以_at_的值意义不大,大家在使用使用的过程中可能会遇到这个值很有用的情况。
AppCall会自动在IDC对象和C对象之间进行转换,转换的时候依据IDA中拥有的类型信息来进行,但是还要遵循以下几个转换规则:
基本数据类型:
指针:
如果目标类型是一个指针,并且IDC的值是一个字符串,这个字符串就转换成一个指针对象,字符串的内容直接拷贝到被调试进程,后面追加一个结束符 \0
如果对应的IDC的值是一个数字,转换之后的结果是指针所指向的内存的值是这个数值,如果你想得到一个数值的地址,可以直接使用 &符号
如果对应的IDC值不是字符串,那这个值将会转换成一个对象,指针指向的内存使用这个对象进行初始化
结构体
数组
下面针对上述的几种情况,我们来举一些例子:
调用printf
auto n = 5;
auto s = "short";
_printf("Hello world, number is %d, string is %s\n", n, s);
调用sscanf
auto x;
auto nsuccess = _sscanf(s, "%d", &x);
结构体使用
verinfo = object(); GetVersionExA(verinfo);
另外,_userCall 这个内建调用也是支持这些自动转换的
对于会发生异常的调用,配合APPCALL_MANUAL 标志位,可以实现单步的效果,具体方式还需要大家仔细去研究一下.
功能:调用被调试进程的函数
参数:ea - 调用的函数地址
参数:type - 调用的函数的类型或者说方式,支持三种调用形式
字符串形式,比如:“int func(void);”
类型对象形式,比如:GetTinfo(ea)
零:相当于让IDA自行决定,这种情况下,类型一般是从idb中进行获取的
参数:..., 这个是可变参数,用来传递你要调用的函数的参数
返回值:被调用函数的返回值
Remark:如果函数调用失败,并且失败的原因是内存访问异常或者其他异常,脚本会抛出一个runtime错误信息,可以在脚本中使用 try/catch 进行异常捕获。在实际的使用过程中,很少使用AppCall这个函数调用,只是说IDA拥有这种在IDC脚本中存在未知函数的时候尝试在被调试进程中匹配符号的能力,举个例子:_printf("hello\n") 这条语句会调用被分析程序的 _printf 函数
如果目标类型是一个指针,并且IDC的值是一个字符串,这个字符串就转换成一个指针对象,字符串的内容直接拷贝到被调试进程,后面追加一个结束符 \0
如果对应的IDC的值是一个数字,转换之后的结果是指针所指向的内存的值是这个数值,如果你想得到一个数值的地址,可以直接使用 &符号
如果对应的IDC值不是字符串,那这个值将会转换成一个对象,指针指向的内存使用这个对象进行初始化
在IDC脚本中,除了 += 这个表达式之外,其他所有的C语言的表达式都可以直接使用
常量的定义跟C语言很类似,但是也有一点点小区别,比如IDC支持以下四种数据转换操作:
long(expr) 转换过程中,浮点会被截断,转换成long型
char(expr)
float(expr)
_int64(expr)
在大部分情况下,在IDC脚本中不需要进行显式的类型转换操作,IDC脚本内部会进行自动的转换,规则如下:
加 操作:
如果2个操作数都是字符串,直接进行字符串拼接
如果2个操作数都是对象,直接进行对象的组合(返回一个新对象)
如果其中一个操作数是浮点,则两个操作数全部转换成浮点来计算
对于其他的情况,两个操作数全部转换成long型再计算
减/乘/除:
如果存在浮点操作数,则2个操作数全部转换成浮点再计算
如果两个操作数都是对象并且操作是减,直接执行减法操作(返回一个新对象)
对于其他的情况,两个操作数全部转换成long型再计算
比较操作(==, !=, etc):
如果两个操作数都是字符串,直接进行字符串比较操作
如果存在浮点操作数,两个操作数都转换成浮点再比较
其他的情况,都转换成数字再比较
其他操作符
一律转换成long型进行计算
如果其中一个long型数据是64位的,那么其他的操作数也会转换成64位的
类型转换这个事情还有一个例外的情况:如果一个操作数是字符串,另外一个是0, 那么最后会执行字符串操作,0会被转换成一个空字符串
&符号可以用来取变量的引用, 你可以像使用指针那样来使用引用,通过引用来修改原始对象的值。需要注意的是引用变量一旦创建就不能修改了,不像C语言的指针那样,可以指来指去。还是举个例子吧:
auto x, r;
r = &x;
r = 1; // x is equal to 1 now
引用的引用还是直接指向原始的对象,例子如下:
auto x, r1, r2;
r1 = &x;
r2 = &r1; // r2 points to x
在给函数传参的时候,默认情况下非对象类型都是值传递的,有的时候,非对象类型通过引用传参是一个不错的选择。
加 操作:
如果2个操作数都是字符串,直接进行字符串拼接
如果2个操作数都是对象,直接进行对象的组合(返回一个新对象)
如果其中一个操作数是浮点,则两个操作数全部转换成浮点来计算
对于其他的情况,两个操作数全部转换成long型再计算
减/乘/除:
如果存在浮点操作数,则2个操作数全部转换成浮点再计算
如果两个操作数都是对象并且操作是减,直接执行减法操作(返回一个新对象)
对于其他的情况,两个操作数全部转换成long型再计算
比较操作(==, !=, etc):
如果两个操作数都是字符串,直接进行字符串比较操作
如果存在浮点操作数,两个操作数都转换成浮点再比较
其他的情况,都转换成数字再比较
其他操作符
一律转换成long型进行计算
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)