首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
139 阅读
2
nvim番外之将配置的插件管理器更新为lazy
98 阅读
3
2018总结与2019规划
69 阅读
4
从零开始配置 vim(15)——状态栏配置
58 阅读
5
PDF标准详解(五)——图形状态
46 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
linux
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
elisp
文本编辑器
Java
读书笔记
反汇编
OLEDB
数据库编程
数据结构
Masimaro
累计撰写
332
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
linux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
332
篇与
的结果
2019-01-19
算法与数据结构(二):链表
上一篇简单的开了一个头,简单介绍了一下所谓的时间复杂度与空间复杂度,从这篇开始将陆陆续续写一下常用的数据结构:链表、队列、栈、树等等。链表当初是我在学校时唯一死磕过的数据结构,那个时候自己还算是一个好学生,虽然上课没怎么听懂,但是课后还是根据仔细调试过老师给的代码,硬是自己给弄懂了,它是我离校时唯一能够写出实现的数据结构,现在回想起来应该是它比较简单,算法也比较直来直去吧。虽然它比较简单,很多朋友也都会链表。但是作为一个系列,如果仅仅因为它比较简单而不去理会,总觉得少了点什么,所以再这仍然将其列举出来。单向链表单向链表是链表中的一种,它的特点是只有一个指向下一个节点的指针域,对单向链表的访问需要从头部开始,根据指针域依次访问下一个节点,单向链表的结构如下图所示单向链表的创建单向链表的结构只需要一个数据域与指针域,这个数据域可以是一个结构体,也可以是多个基本数据类型;指针域是一个指向节点类型的指针,简单的定义如下:typedef struct _LIST_NODE { int nVal; struct _LIST_NODE *pNext; }LIST_NODE, *LPLIST_NODE;创建链表可以采用头插法或者尾插法来初始化一个有多个节点的链表头插法的示意图如下:它的过程就像示意图中展现的,首先使用新节点p的next指针指向当前的头节点把新节点加入到链表头,然后变更链表头指针,这样就在头部插入了一个节点,用代码来展示就是p->next = head; head = p;我们使用一个函数来封装就是LPLIST_NODE CreateListHead() { LPLIST_NODE pHead = NULL; while (TRUE) { LPLIST_NODE p = (LPLIST_NODE)malloc(sizeof(LIST_NODE)); if (NULL == p) { break; } memset(p, 0x00, sizeof(LIST_NODE)); printf("请输入节点值(为0时将退出创建节点):"); scanf_s("%d", &p->nVal); //这里不需要对链表为空单独讨论 //当链表为空时pHead 的值为NULL, 这两句代码就变为 //p->pNext = NULL; //pHead = p; p->pNext = pHead; pHead = p; if (p->nVal == 0) { break; } } return pHead; }采用尾插法的话,首先得获得链表的尾部 pTail, 然后使尾节点的next指针指向新节点,然后更新尾节点,用代码来表示就是pTail->next = p; pTail = p;下面的函数是采用尾插法来构建链表的例子//这个函数多定义了一个变量用来保存 // 可以不需要这个变量,这样在插入之前需要遍历一遍链表,以便找到尾节点 // 但是每次插入之前都需要遍历一遍,没有定义一个变量保存尾节点这种方式来的高效 LPLIST_NODE CreateListTail() { LPLIST_NODE pHead = NULL; LPLIST_NODE pTail = pHead; while (NULL != pTail && NULL != pTail->pNext) { pTail = pTail->pNext; } while (TRUE) { LPLIST_NODE p = (LPLIST_NODE)malloc(sizeof(LIST_NODE)); if (NULL == p) { break; } memset(p, 0x00, sizeof(LIST_NODE)); printf("请输入节点值(为0时将退出创建节点):"); scanf_s("%d", &p->nVal); //由于这种方法需要对尾节点的next域赋值,所以需要考虑链表为空的情况 if (NULL == pTail) { pHead = p; pTail = pHead; }else { pTail->pNext = p; pTail = p; } if (p->nVal == 0) { break; } } return pHead; }链表的遍历链表的每个节点在内存中不是连续的,所以它不能像数组那样根据下标来访问(当然可以利用C++中的运算符重载来实现使用下标访问),链表中的每一个节点都保存了下一个节点的地址,所以我们根据每个节点指向的下一个节点来依次访问每个节点,访问的代码如下:void TraverseList(LPLIST_NODE pHead) { while (NULL != pHead) { printf("%d\n", pHead->nVal); pHead = pHead->pNext; } }链表的删除链表的每个节点都是在堆上分配的,在不再使用的时候需要手工清除每个节点。清除时需要使用遍历的方法,一个个的删除,只是需要在遍历的指针移动到下一个节点前保存当前节点,以便能够删除当前节点,删除的函数如下void DestroyList(LPLIST_NODE pHead) { LPLIST_NODE pTmp = pHead; while (NULL != pTmp) { pTmp = pHead->pNext; delete pHead; pHead = pTmp; } }删除单个节点如上图所示,假设我们要删除q节点,那么首先需要遍历找到q的上一个节点p,将p的next指针指向q的下一个节点,也就是赋值为q的next指针的值,用代码表示就是p->next = q->next;删除节点的函数如下:void DeleteNode(LPLIST_NODE* ppHead, int nValue) { if (NULL == ppHead || NULL == *ppHead) { return; } LPLIST_NODE p, q; p = *ppHead; while (NULL != p) { if (nValue == p->nVal) { if (*ppHead == p) { *ppHead = p->pNext; free(p); }else { q->pNext = p->pNext; free(p); } p = NULL; q = NULL; break; } q = p; p = p->pNext; } }在上述代码中首先来遍历链表,找到要删除的节点p和它的上一个节点q,由于头节点没有上一个节点,所以需要特别判断一下需要删除的是否为头节点,如果为头结点,则直接将头指针指向它的下一个节点,然后删除头结点即可,如果不是则采用之前的方法来删除。任意位置插入节点如上图所示,如果需要在q节点之后插入p节点的话,需要两步,将q的next节点指向q,然后将q指向之前p的下一个节点,这个时候需要注意一下顺序,如果我们先执行q->next = p 的话,那么之前q的下一个节点的地址就被覆盖掉了,这个时候后面的节点都丢掉了,所以这里我们要先执行p->next = q->next 这条语句,然后在执行q->next = p下面是一个创建有序链表的例子,这个例子演示了在任意位置插入节点LPLIST_NODE CreateSortedList() { LPLIST_NODE pHead = NULL; while (TRUE) { LPLIST_NODE p = (LPLIST_NODE)malloc(sizeof(LIST_NODE)); if (NULL == p) { break; } memset(p, 0x00, sizeof(LIST_NODE)); printf("请输入节点值(为0时将退出创建节点):"); scanf_s("%d", &p->nVal); if (NULL == pHead) { pHead = p; }else { if (pHead->nVal > p->nVal) { p->pNext = pHead; pHead = p; }else { LPLIST_NODE q = pHead; LPLIST_NODE r = q; q = q->pNext; while (NULL != q && q->nVal < p->nVal) { r = q; q = q->pNext; } p->pNext = r->pNext; r->pNext = p; } } if (p->nVal == 0) { break; } } return pHead; }当确定新节点的值之后,首先遍历链表,直到找到比新节点中数值大的节点,那么这个新节点就是需要插入到该节点之前。在遍历的时候使用r来保存之前的节点。这里需要注意这些情况:链表为空:这种情况下,直接让头指针指向当前节点如果头节点本身就是大于新节点的值,这种情况下采用头插法,将新节点插入到头部如果链表中未找到比新节点的值更大的值,这种情况下直接采用尾插发在链表中找到比新节点值更大的节点,这种情况下,在链表中插入但是在代码中并没有考虑到尾部插入的情况,由于在尾部插入时,r等于尾节点,r->pNext 的值为NULL, 所以 p->pNext = r->pNext;r->pNext = p; 可以看成 p->pNext = NULL; r->pNext = p; 也就是将p的next指针指向空,让其作为尾节点,将之前的尾节点的next指针指向新节点。循环链表循环链表是建立在单向链表的基础之上的,循环链表的尾节点并不指向空,而是指向其他的节点,可以是头结点,可以是自身,也可以是链表中的其他节点,为了方便操作,一般将循环链表的尾节点的next指针指向头节点,它的操作与单链表的操作类似,只需要将之前判断尾节点的条件变为 pTail->pNext == pHead 即可。这里就不再详细分析每种操作了,直接给出代码LPLIST_NODE CreateLoopList() { LPLIST_NODE pHead = NULL; LPLIST_NODE pTail = pHead; while(1) { LPLIST_NODE p = (LPLIST_NODE)malloc(sizeof(LIST_NODE)); if (NULL == p) { break; } memset(p, 0x00, sizeof(LIST_NODE)); printf("请输入一个值:"); scanf_s("%d", &p->nVal); if (NULL == pHead) { pHead = p; p->pNext = pHead; pTail = pHead; }else { pTail->pNext = p; p->pNext = pHead; pTail = p; } if (0 == p->nVal) { break; } } return pHead; } void TraverseLoopList(LPLIST_NODE pHead) { LPLIST_NODE pTmp = pHead; if (NULL == pTmp) { return; } do { printf("%d, ", pTmp->nVal); pTmp = pTmp->pNext; } while (pTmp != pHead); } void DestroyLoopList(LPLIST_NODE pHead) { LPLIST_NODE pTmp = pHead; LPLIST_NODE pDestroy = pTmp; if (NULL == pTmp) { return; } do { pTmp = pDestroy->pNext; free(pDestroy); pDestroy = pTmp; }while (pHead != pTmp); }判断链表是否为循环链表在上面说过,循环链表的尾指针不一定指向头节点,它可以指向任何节点,那么该怎么判断一个节点是否为循环链表呢?既然它可以指向任意的节点,那么肯定是找不到尾节点的,而且堆内存的分配是随机的,我们也不可能按照指针变量的大小来判断哪个节点在前哪个在后。回想一下在学校跑一千米的时候是不是回出现这样的情况,跑的块的会领先跑的慢的一周?根据这种情形我们可以考虑使用这样一种办法:定义两个指针,一个一次走两步也是就是p = p->next->next, 一个慢指针一次走一步,也就是q = q->next,如果是循环链表,那么快指针在某个时候一定会领先慢指针一周,也就是达到 p == q 这个条件,否则就是非循环链表。根据这个思路,可以考虑写下如下代码:bool IsLoopList(LPLIST_NODE pHead) { if (NULL == pHead) { return false; } LPLIST_NODE p = pHead; LPLIST_NODE q = pHead->pNext; while (NULL != p && NULL != q && NULL != q->pNext && p != q) { p = p->pNext; q = q->pNext->pNext; } if (q == NULL || NULL == p || NULL == q->pNext) { return false; } return true; }双向链表之前在插入或者删除的时候,需要定义两个指针变量,让其中一个一直更在另一个的后面,单向链表有一个很大的问题,不能很方便的找到它的上一个节点,为了解决这一个问题,提出了双向链表,双向链表与单向相比,多了一个指针域,用来指向它的上一个节点,也就是如下图所示:双向链表的操作与单向链表的类似,只是多了一个指向前一个节点的指针域,它要考虑的情况与单向链表相似删除节点删除节点的示意图如下:假设删除的节点p,那么首先根据p的pre指针域,找到它的上一个节点q,采用与单向链表类似的操作:q->next = p->next; p->next->pre = q;下面是删除节点的例子:void DeleteDNode(LPDLIST_NODE* ppHead, int nValue) { if (NULL == ppHead || NULL == *ppHead) { return; } LPDLIST_NODE p = *ppHead; while (NULL != p && p->nVal != nValue) { p = p->pNext; } if (NULL == p) { return; } if (*ppHead == p) { *ppHead = (*ppHead)->pNext; p->pPre = NULL; free(p); } else if (p->pNext == NULL) { p->pPre->pNext = NULL; free(p); }else { p->pPre->pNext = p->pNext; p->pNext->pPre = p->pPre; } }插入节点插入节点的示意图如下:假设新节点为p,插入的位置为q,则插入操作可以进行如下操作p->next = q->next; p->pre = q; q->next->pre = p; q->next = p;也是一样要考虑不能覆盖q的next指针域否则可能存在找不到原来链表中q的下一个节点的情况。所以这里先对p的next指针域进行操作下面也是采用创建有序列表的例子LPDLIST_NODE CreateSortedDList() { LPDLIST_NODE pHead = NULL; while (1) { LPDLIST_NODE pNode = (LPDLIST_NODE)malloc(sizeof(DLIST_NODE)); if (NULL == pNode) { return pHead; } memset(pNode, 0x00, sizeof(DLIST_NODE)); printf("请输入一个整数:"); scanf_s("%d", &pNode->nVal); if(NULL == pHead) { pHead = pNode; }else { LPDLIST_NODE q = pHead; LPDLIST_NODE r = q; while (NULL != q && q->nVal < pNode->nVal) { r = q; q = q->pNext; } if (q == pHead) { pNode->pNext = pHead; pHead->pPre = pNode; pHead = pNode; }else if (NULL == q) { r->pNext = pNode; pNode->pPre = r; }else { pNode->pPre = r; pNode->pNext = q; r->pNext = pNode; q->pPre = pNode; } } LPDLIST_NODE q = pHead; LPDLIST_NODE r = q; if (0 == pNode->nVal) { break; } } return pHead; }链表还有一种是双向循环链表,对于这种链表主要是在双向链表的基础上,将头结点的pre指针指向某个节点,将尾节点的next节点指向某个节点,而且这两个指针可以指向同一个节点也可以指向不同的节点,一般在使用中都是head的pre节点指向尾节点,而tail的next节点指向头节点。这里就不再详细说明,这些链表只要掌握其中一种,剩下的很好掌握的。
2019年01月19日
5 阅读
0 评论
0 点赞
2019-01-12
算法与数据结构(一):时间复杂度与空间复杂度
最近突然萌生了一个想法,好好系统的学习一下算法与数据结构然后产生一系列的文章来回顾与总结学到的东西,这部分我想从最简单的部分一一介绍总结,包括一些很基础的内容为什么要学习数据结构与算法以前在学校的时候就知道 程序 = 算法 + 数据结构,程序的作用是用来处理与解决现实问题,而在描述与解决现实问题时不可避免的会涉及到数据,如何将这些数据有效的组织起来并利用一定的方法来运算与处理应该算是程序的核心问题。当然如果仅仅将编程作为谋生的手段,确实不用太关心这部分,现实中很多语言和库都封装了这些东西,需要的时候直接用即可,不懂算法与数据结构并不会对编程产生什么影响,在实际工作中可能并没有机会自己实现一个链表、队列等等。但是如果真正热爱这一行,希望能更上一层楼的,算法与数据结构必定是绕不开的一环。学习数据结构并不是为了要在工作中自己实现它,而是:了解使用算法解决问题的一些思想,能够让你知道如何更好地优化自己的代码了解更多的数据结构与算法的知识,能在编程中更加游刃有余,能更好的解决实际问题程序的性能瓶颈往往都跟算法和数据结构有关系, 好的算法能更多的提升程序的性能当我们面对一个完全未知的问题时,了解更多的算法知识能够多出一些尝试为了能够在面试中脱颖而出当时在学校学习的时候我是被各种系统程序以及各种漂亮的Web程序给吸引了,认为算法这种东西永远都在处理平时根本碰不上的问题,有时间浪费在这些虚无缥缈的东西上还不如学学怎么做一个应用,写一个网站出来。那个时候基本放弃了对这方面的学习。后来在工作中经常出现网上对你所面临的问题没有明确的结局方案,需要在现有的方案上做修改,这个时候就束手无策了。使用了某种算法解决了问题,但是效率不高,遇到大规模访问时容易出错崩溃,这个时候还是没辙。还有就是在网上看别人的开源代码时需要花额外的精力来研究老外的某个写法,其实如果懂点算法的知识,可能并不需要这些额外的时间开销。由于有了这些精力,我想在新年开始的这段时间里研究一下数据结构与算法的相关内容,提升一下自己的基本功。时间复杂度有了前面说的一些经历,下面就进入正题了:算法的时间复杂度与空间复杂度;时间复杂度与空间复杂度是评价一个算法好话的一个常用标准。时间复杂度是以程序代码中基本语句执行次数作为衡量标准。换句话说时间复杂度是用这个算法需要执行代码量来评价的。假设一个问题的规模为n,常见的比如说有n个数据需要进行处理,如果算法是类似这样的:for(int i = 0; i < n; i++) { //do some thing setup1(); for(int j = 0; j < n; j++) { //do something setup2(); } }假设setup1和setup2 函数执行了j、k次运算 那么我们来计算一下这个算法总共执行了多少次:首先在内层循环中循环了n次,那么setup2函数执行了nci,这个时候有 k * n, 外层循环也是执行了n次,所以这个算法总共执行了 n * (j + n * k) = n^2 + n(k + j), 这样得到事件复杂度为 T(n) = n^2 + n(k + j) 由于k j都是常数,所以计算这个时间复杂度又可以写作 T(n) = n^2 + nt, 对于这个表达式取自高次幂 得到这个算法的时间复杂度为 T(n) = n^2从上面的计算来看,计算事件复杂度就是计算它需要执行基本代码执行多少次,然后将得到的表达式取最高次幂并去掉系数。常见的时间复杂度有O(1)、O(logn)、O(n)、O(nlogn)、O(n^2)、O(n^k)、O(n!)、O(2^n)从效率上看,它们是依次降低的。一般来说算法最多达到O(n^2), 如果比O(n^2)高,这个时候就需要好好考虑一下优化算法了。一般常见的算法时间复杂度如下:每层循环,时间复杂度都需要在原来的基础之上乘以n没有循环的时间复杂度一般为常数二分法时间复杂度为logn空间复杂度空间复杂度是指算法占用内存空间的值,需要注意的是,这个内存占用主要是在算法内部动态分配了内存,算法函数中的临时变量储存在栈空间,算法执行完成后会回收,一般不考虑局部变量占用的内存。同时静态变量,全局变量都不考虑进来在算法中值考虑在函数中分配的内存以及递归调用时栈空间的占用内存。计算算法的空间复杂度并不复杂,因此这里就不给例子了。算法的空间复杂度应该控制在O(1),也就是尽量不要在算法内部分配内存,少用递归。
2019年01月12日
4 阅读
0 评论
0 点赞
2019-01-02
2018总结与2019规划
时间也是过得很快,不知不觉又过了一年。这一年发生了很多事,但是好像又过的很平淡。回想起来自己好像做过好多事,但好像又没做过什么事,在这里我再次回顾一下去年的一些状态、然后展望一下未来,接着立一下对应的flag。去年的目标总结:我去年好像说过要好好锻炼的,这个基本放弃了,或者说从来没有开始过,但是体重好像也算是控制助理,没有想象中涨的那么快,去年120,今年130。当时给自己定下的是140后开始锻炼。这个算是不了了之了。之前好像说过要尝试着自己做饭,但是后来找到理由说服自己了:买菜10分钟、洗菜10分钟、做饭可能20分钟、饭后洗碗10分钟,吃饭10分钟,这么算下来好像做饭很亏的样子,所以这个也就不了了之了。这么算下来自己当初定下的一些小目标好像都没有实现过。而关于读书这个我统计了一下,包括现在正在看的一本,好像总共17本,未达到当时定下的20本的目标。在对照着之前写的2017的总结那篇文章上的目标好像自己完成的不多,但是我感觉这年在手机的使用时间上却是是降下来了,每天大概在1小时左右。这个降下来还主要是由于加班太多了。从6月份开始好像就很少能在10点之前到家的。工作总结今年我正式接手了公司主要项目——Web扫描器的维护。在刚接手这个时候我也是被它里面有如此多的烂代码所震惊:2万行代码的函数、大量重复的代码、大量的宏定义(包括许多无用的宏)、大量的全局变量、与界面绑死的界面、大量不知所云的局部变量。项目经历过不同的维护人员、不同的维护人员不同的代码风格全在里面,而且没有文档(不是没有详细的文档、而是压根没有文档)。就靠面对面的口述来进行交接。这是面临的主要问题,当时我想过进行重构,但是项目代码实在太多了,代码里面的很多逻辑我还没搞明白,而且只有我一个人,重构肯定是不现实的。后来我自己采取折中的措施,将我自己能看懂的部分进行重构,但是很多地方关联的太紧密,经常就是改了这块测的没有问题,结果临近发布新版本了,发现另外一个原先没有的问题,每次大改必定会带来新的bug,这样搞了几次我实在是身心俱疲,放弃了。转而向之前的维护人员那样,慢慢加功能就好了,其他的不管了。这样做之后,好像一切都正常了,再也不挨批了,偶尔还能得到办事能力强,能迅速完成老板要求的这么一个好评。既然不能重构、那么写写文档吧,把之前没有的文档都补起来,这个想法是我在6月份想起来的,但是后来经历了一系列的事,一直没有时间实践。我在自己的另一份年中总结上写过,公司很多老员工都走了,我也从小X转变为了X哥了,慢慢的手下也有几个人,开始带一些人接手新项目。在10月份我开始带着几个新人开始新的项目。开始时我想按照软件工程上的方式,从需求到分析、到设计、再到编码实现与测试、当时也强调过要手下的人学会写单元测试,这是我带队的第一个项目,自然希望将它做好,但是我发现时间是真的不允许,项目总工的时间是1个半月,我发现从我开始调研需求到形成原型图、开会讨论需求、到最后生成需求文档这一系列就用了两周,还有一个月还没开始编码。这个时候我有点慌了,将最重要的设计工作的时间压缩到一周,白天维护扫描器,晚上加班加点进行对应的设计工作。一周结束之后我发现我完成了对应的架构设计,知道系统应该分为几个模块,每个模块该实现什么功能,至于如何实现具体功能、如何进行模块间通信与管理,这些根本没有时间,只有让手下几个人仓皇上阵。最后的结果可想而知,很多早期设想由于手下的人没有时间做最后砍掉了,最后一遍遍精简,形成了一个最简单的系统。由于编码时间有限我后续没有要求进行单元测试,只进行了最后的内部统一测试,测试时问题百出,有少数bug在短时间内无法解决,最后在不影响系统功能的情况下作了相应的精简。而且项目不得不延期。总体来说,我第一次带的这个项目是失败的,虽然我早期对它的设想很明确,先需求分析、再概要设计、然后详细设计、编码的同时进行单元测试、每个功能模块完成后有对应的功能测试与代码的review、并在最终完成之后进行对应的统一测试。并最终形成对应的需求分析文档、概要设计文、详细设计文档、数据库文档、测试文档、验收报告等等。并制定了相应的编码规范,前期甚至计划每天按照规范review他们提交的代码。但是最终并没有按照这条路走。针对这个项目我总结出来大概有这么几个原因:自己的维护工作与带队工作没有规划好,经常就是忙于处理扫描器bug、而无法兼顾这个新项目,这个问题公司中有人已经警告过我,让我盯紧、但是被我以维护工作忙等原因给忽略了自己水平问题,我不知道一般专业的项目经理或者团队的架构师在做需求和设计大概需要多久,我总体进行需求分析与概要设计大概花了有3周时间,从项目的时间周期来看我感觉这个时间偏长自己管理问题,前期虽然指定了一些列的编码规范、搭建了gitlab作为项目管理的工具,但是后期我基本没看过他们提交的代码,也没有做到每天查看进度,甚至在后期编码的时候已经没有进度计划了。我发现我自己在给自己制定计划的时候很从容,而且后续也基本能够按照进度走,而为团队制定计划的时候,我总会考虑团队成员的水平,总担心他们水平不行,能不能在工作时间内做完,如果逼的太紧会不会影响他们的正常作息,一直没法给出一个合适的计划表。当然这也跟后续详细设计没好好做有关,当时设计上有4个模块,按照每周一个的进度简单的定了一个计划,但是后续并没有严格执行,没周最后我询问进度时,下面总反应有难度,然后就延期。当然也有未延期的,但是我没有时刻紧盯进度,所以具体啥时候完成了模块我也不太清楚。手下水平问题,这次项目中我感觉明显有部分人是在拖后腿的,由于是实习生,我本来没对他们报太大的希望,只希望他们能完成打杂的工作就好,写写前端页面、帮忙弄弄数据库、搭测试环境啥的。但是我发现有的连这些都完成不了,还得团队其他成员帮忙完成这些。有的实习生好像是抱着来学习的态度在做事,有问题了直接问,自己从来不搜索,不尝试自己解决问题。当时招进来的时候确实也感觉到能力不怎么样,但是看着还未毕业,想着可以来慢慢培养。通过这次我发现,招实习生也得招那些能做事的,培不培养另说,至少要能做事。总说理想很丰满,现实很骨感。项目刚刚接过来我跟领导信誓旦旦的保证完成,但是后续在实施过程中遇到许多困难。从这次项目中我学到了许多、知道程序员没有想象中那么轻松,那些管理岗位并不是只要发号施令就OK、还得要合理的进行相应的规划、合理的发号施令。而且还要盯紧下面的人,有的人只有盯紧了才能发挥全部能力,否则总会缺斤少两,总想偷懒。自己需要摆脱老好人的思维方式,多为项目考虑,而不要过分考虑团队中其他人的感受。适当基于压力不一定是坏事。当然在工作中最成功的还是自己独立写出来一个facebook爬虫,项目的细节我已经在我另外一篇博客中详细的写了出来。这个项目中使用了新的JS解析工具、并且翻译了它的中文文档。在这个项目中,被许多人叫做大神,甚至有人给我打赏,请求帮忙解决一些问题。这些都让我的虚荣心得到满足。而且也拿到了项目奖金。或许这个项目是今年最成功的项目。学习总结在学习上好像之前也立过不少flag,但是执行的都不怎么样。当时总是信誓旦旦的说要学习网络原理,要看完TCP/IP协议这本书。但是后来慢慢的就将它抛之脑后。后续脑袋里面冒出过很多想法,有很多要学的东西,但是很多都做到一半就结束了。这年的状态经常是这样的:这个技术好,我要好好学学,用它写一个XX程序出来然后是找视频或者看书前面的好像很简单,不用细看了,快速阅读吧基本语法我都会了,开始写项目吧这个东西好像没有什么好的界面库,还是用B/S架构把前端技术好像不怎么会,学学这个吧HTML 标签我都知道,直接学CSS吧CSS 这些都很简单,看看JS吧JS的语法跟C很像,不看了,用的时候再查吧网上找一个前端界面,自己从头开始写这个JS代码我看不懂,还是转回去学学JS吧最近看了一下这个前端框架,先用上吧这个框架好像要求懂HTML + CSS + JS,还是好好学一下这些吧从头折回去学那些东西最后正式开始写的时候又发现,好像用另一门技术或者语音更容易写类似的程序,先学一下新技术吧这一年似乎都是这么一个死循环,结果专业术语了解不少,但是代码明显写的少,很多书买了一堆、各种在线教育平台的课程买了一堆,后续因为看上了另外的技术而放弃了前面的内容。结果时间花出去了,钱花出去了。但是仍然一事无成。看似很努力,但是没有什么结果。最近看到一篇微博上写的大意是这样的:在学习上有真正使你进步的,还有就是让你以为你进步了的。我感觉我这一年应该属于第二类。感觉很努力应该比那些天天刷抖音、快手的强。但是仔细想想可能还不如这些人,毕竟我时间也花出去了,结果与这些人水平无异。在学习上我完成的只有之前定下的,VC的高级编程与Windows驱动编程的内容,我想我能完成这些在于这些是当时刚立下flag的时候进行的,那个时候还是很有毅力的,还有就是这些我手上的资料比较少,只能看那个。而且没有什么要完成项目的想法,仅仅只是学习防止日后有用。我发现对我自己而言很多别人很好的建议在我面前都没有什么很好的效果,比如说很多人建议的,在学习过程中以结果为驱动,以完成某个项目作为驱动,但是在实践中我发现,我自己在写项目的时候容易发现自己的不足,转而去学习另外的东西,结果导致什么都没学会,项目也没有完成。还比如很多人建议的,广泛阅读资料,在这条我会发现自己很多不会,转而又去关注不会的东西,而把原来的任务抛之脑后。针对这些问题,我想今后的解决方案是这样的:在写项目的过程中,只关注那些与具体技术相关的内容,而像界面这些东西,能直接拿来用就行,不用太关注。在学习新技术的时候,不要看某些东西简单就挑过,也不要因为某些东西看不懂就转而去研究这些看不懂的,而是在所有内容看完后在回过头来,关注那些不懂的。有时候不懂的那些是因为另外的技术不懂,这时候可以把不懂的新技术作为下一阶段的学习目标总结与flag不管怎么说2018已经过去了,在怎么追悔都无济于事,我想做总结的目的不在于一件件的数那些成绩,然后沾沾自喜,也不在于一遍遍数落自己的缺点大骂自己没用。总结的意义在于发现自己的好,来年继续坚持。发现自己的不足,来年争取改正。在这里我给自己再立一下flag:读书(20本)写博客(一周一篇)学习计算机的基础内容(算法、数据结构、编译原理、网络协议)学习新语言(GO、JavaScript、PHP)学习Web安全的基础内容(XSS、SQL注入等等)这次学聪明了点,flag不能立太多,我觉得能把这些完成就算不虚度年华了。最后祝所有朋友在新的一年越来越好、单身的早日脱单。。。。。
2019年01月02日
69 阅读
6 评论
0 点赞
2018-11-24
VC++ IPv6的支持
最近根据项目需要,要在产品中添加对IpV6的支持,因此研究了一下IPV6的相关内容,Ipv6 与原来最直观的改变就是地址结构的改变,IP地址由原来的32位扩展为128,这样原来的地址结构肯定就不够用了,根据微软的官方文档,只需要对原来的代码做稍许改变就可以适应ipv6。修改地址结构Windows Socket2 针对Ipv6的官方描述根据微软官方的说法,要做到支持Ipv6首先要做的就是将原来的SOCKADDR_IN等地址结构替换为SOCKADDR_STORAGE 该结构的定义如下:typedef struct sockaddr_storage { short ss_family; char __ss_pad1[_SS_PAD1SIZE]; __int64 __ss_align; char __ss_pad2[_SS_PAD2SIZE]; } SOCKADDR_STORAGE, *PSOCKADDR_STORAGE;ss_family:代表的是地址家族,IP协议一般是AF_INET, 但是如果是IPV6的地址这个参数需要设置为 AF_INET6。后面的成员都是作为保留字段,或者说作为填充结构大小的字段,这个结构兼容了IPV6与IPV4的地址结构,跟以前的SOCKADDR_IN结构不同,我们现在不能直接从SOCKADDR_STORAGE结构中获取IP地址了。也没有办法直接往结构中填写IP地址。使用兼容函数除了地址结构的改变,还需要改变某些函数,有的函数是只支持Ipv4的,我们需要将这些函数改为即兼容的函数,根据官方的介绍,这些兼容函数主要是下面几个:WSAConnectByName : 可以直接通过主机名建立一个连接WSAConnectByList: 从一组主机名中建立一个连接getaddrinfo: 类似于gethostbyname, 但是gethostbyname只支持IPV4所以一般用这个函数来代替GetAdaptersAddresses: 这个函数用来代替原来的GetAdaptersInfoWSAConnectByName函数:函数原型如下:BOOL PASCAL WSAConnectByName( __in SOCKET s, __in LPSTR nodename, __in LPSTR servicename, __inout LPDWORD LocalAddressLength, __out LPSOCKADDR LocalAddress, __inout LPDWORD RemoteAddressLength, __out LPSOCKADDR RemoteAddress, __in const struct timeval* timeout, LPWSAOVERLAPPED Reserved ); s: 该参数为一个新创建的未绑定,未与其他主机建立连接的SOCKET,后续会采用这个socket来进行收发包的操作nodename: 主机名,或者主机的IP地址的字符串servicename: 服务名称,也可以是对应的端口号的字符串,传入服务名时需要传入那些知名的服务,比如HTTP、FTP等等, 其实这个字段本身就是需要传入端口的,传入服务名,最后函数会根据服务名称转化为这些服务的默认端口LocalAddressLength, LocalAddress, 返回当前地址结构,与长度RemoteAddressLength, RemoteAddress,返回远程主机的地址结构,与长度timeout: 超时值Reserved: 重叠IO结构为了使函数能够支持Ipv6,需要在调用前使用setsockopt函数对socket做相关设置,设置的代码如下:iResult = setsockopt(ConnSocket, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&ipv6only, sizeof(ipv6only) );调用函数的例子如下(该实例为微软官方的例子):SOCKET OpenAndConnect(LPWSTR NodeName, LPWSTR PortName) { SOCKET ConnSocket; DWORD ipv6only = 0; int iResult; BOOL bSuccess; SOCKADDR_STORAGE LocalAddr = {0}; SOCKADDR_STORAGE RemoteAddr = {0}; DWORD dwLocalAddr = sizeof(LocalAddr); DWORD dwRemoteAddr = sizeof(RemoteAddr); ConnSocket = socket(AF_INET6, SOCK_STREAM, 0); if (ConnSocket == INVALID_SOCKET){ return INVALID_SOCKET; } iResult = setsockopt(ConnSocket, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&ipv6only, sizeof(ipv6only) ); if (iResult == SOCKET_ERROR){ closesocket(ConnSocket); return INVALID_SOCKET; } bSuccess = WSAConnectByName(ConnSocket, NodeName, PortName, &dwLocalAddr, (SOCKADDR*)&LocalAddr, &dwRemoteAddr, (SOCKADDR*)&RemoteAddr, NULL, NULL); if (bSuccess){ return ConnSocket; } else { return INVALID_SOCKET; } }WSAConnectByList该函数从传入的一组hostname中选取一个建立连接,函数内部会调用WSAConnectByName,它的原型,使用方式与WSAConnectByName类似,这里就不再给出具体的原型以及调用方法了。getaddrinfo该函数的作用与gethostbyname类似,但是它可以同时支持获取V4、V6的地址结构,函数原型如下:int getaddrinfo( const char FAR* nodename, const char FAR* servname, const struct addrinfo FAR* hints, struct addrinfo FAR* FAR* res );nodename: 主机名或者IP地址的字符串servname: 知名服务的名称或者端口的字符串hints:一个地址结构,该结构规定了应该如何进行地址转化。res:与gethostbyname类似,它也是返回一个地址结构的链表。后续只需要遍历这个链表即可。使用的实例如下:char szServer[] = "www.baidu.com"; char szPort[] = "80"; addrinfo hints = {0}; struct addrinfo* ai = NULL; getaddrinfo(szServer, szPort, NULL, &ai); while (NULL != ai) { SOCKET sConnect = socket(ai->ai_family, SOCK_STREAM, ai->ai_protocol); connect(sConnect, ai->ai_addr, ai->ai_addrlen); shutdown(sConnect, SD_BOTH); closesocket(sConnect); ai = ai->ai_next; } freeaddrinfo(ai); //最后别忘了释放链表针对硬编码的情况针对这种情况一般是修改硬编码,如果希望你的应用程序即支持IPV6也支持IPV4,那么就需要去掉这些硬编码的部分。微软提供了一个工具叫"Checkv4.exe" 这个工具一般是放到VS的安装目录中,作为工具一起安装到本机了,如果没有可以去官网下载。工具的使用也非常简单checkv4.exe 对应的.h或者.cpp 文件这样它会给出哪些代码需要进行修改,甚至会给出修改意见,我们只要根据它的提示修改代码即可。几个例子因为IPV6 不能再像V4那样直接往地址结构中填写IP了,因此在IPV6的场合需要大量使用getaddrinfo函数,来根据具体的IP字符串或者根据主机名来自动获取地址信息,然后根据地址信息直接调用connect即可,下面是微软的例子int ResolveName(char *Server, char *PortName, int Family, int SocketType) { int iResult = 0; ADDRINFO *AddrInfo = NULL; ADDRINFO *AI = NULL; ADDRINFO Hints; memset(&Hints, 0, sizeof(Hints)); Hints.ai_family = Family; Hints.ai_socktype = SocketType; iResult = getaddrinfo(Server, PortName, &Hints, &AddrInfo); if (iResult != 0) { printf("Cannot resolve address [%s] and port [%s], error %d: %s\n", Server, PortName, WSAGetLastError(), gai_strerror(iResult)); return SOCKET_ERROR; } if(NULL != AddrInfo) { SOCKET sConnect = socket(AddrInfo->ai_family, SOCK_STREAM, AddrInfo->ai_protocol); connect(sConnect, AddrInfo->ai_addr, AddrInfo->ai_addrlen); shutdown(sConnect, SD_BOTH); closesocket(sConnect); } freeaddrinfo(AddrInfo); return 0; }这个例子需要传入额外的family参数来规定它使用何种地址结构,但是如果我只有一个主机名,而且事先并不知道需要使用何种IP协议来进行通信,这种情况下又该如何呢?针对服务端,不存在这个问题,服务端是我们自己的代码,具体使用IPV6还是IPV4这个实现是可以确定的,因此可以采用跟上面类似的写法:BOOL Create(int af_family) { //这里不建议使用IPPROTO_IP 或者IPPROTO_IPV6,使用TCP或者UDP可能会好点,因为它们是建立在IP协议之上的 //当然,具体情况具体分析 s = socket(af_family, SOCK_STREAM, IPPROTO_TCP); } BOOL Bind(int family, UINT nPort) { addrinfo hins = {0}; hins.ai_family = family; hins.ai_flags = AI_PASSIVE; /* For wildcard IP address */ hins.ai_protocol = IPPROTO_TCP; hins.ai_socktype = SOCK_STREAM; addrinfo *lpAddr = NULL; CString csPort = ""; csPort.Format("%u", nPort); if (0 != getaddrinfo(NULL, csPort, &hins, &lpAddr)) { closesocket(s); return FALSE; } int nRes = bind(s, lpAddr->ai_addr, lpAddr->ai_addrlen); freeaddrinfo(lpAddr); if(nRes == 0) return TRUE; return FALSE; } //监听,以及后面的收发包并没有区分V4和V6,因此这里不再给出跟他们相关的代码针对服务端,我们自然没办法事先知道它使用的IP协议的版本,因此传入af_family参数在这里不再适用,我们可以利用getaddrinfo函数根据服务端的主机名或者端口号来提前获取它的地址信息,这里我们可以封装一个函数int GetAF_FamilyByHost(LPCTSTR lpHost, int nPort, int SocketType) { addrinfo hins = {0}; addrinfo *lpAddr = NULL; hins.ai_family = AF_UNSPEC; hins.ai_socktype = SOCK_STREAM; hins.ai_protocol = IPPROTO_TCP; CString csPort = ""; csPort.Format("%u", nPort); int af = AF_UNSPEC; char host[MAX_HOSTNAME_LEN] = ""; if (lpHost == NULL) { gethostname(host, MAX_HOSTNAME_LEN);// 如果为NULL 则获取本机的IP地址信息 }else { strcpy_s(host, MAX_HOSTNAME_LEN, lpHost); } if(0 != getaddrinfo(host, csPort, &hins, &lpAddr)) { return af; } af = lpAddr->ai_family; freeaddrinfo(lpAddr); return af; }有了地址家族信息,后面的代码即可以根据地址家族信息来分别处理IP协议的不同版本,也可以使用上述服务端的思路,直接使用getaddrinfo函数得到的addrinfo结构中地址信息,下面给出第二种思路的部分代码:if(0 != getaddrinfo(host, csPort, &hins, &lpAddr)) { connect(s, lpAddr->ai_addr, lpAddr->ai_addrlen); }当然,也可以使用前面提到的 WSAConnectByName 函数,不过它需要针对IPV6来进行特殊的处理,需要事先知道服务端的IP协议的版本。VC中各种地址结构在学习网络编程中,一个重要的概念就是IP地址,而巴克利套接字中提供了好几种结构体来表示地址结构,微软针对WinSock2 又提供了一些新的结构体,有的时候众多的结构体让人眼花缭乱,在这我根据自己的理解简单的回顾一下这些常见的结构SOCKADD_IN 与sockaddr_in结构在Winsock2 中这二者是等价的, 它们的定义如下:struct sockaddr_in{ short sin_family; unsigned short sin_port; struct in_addr sin_addr; char sin_zero[8]; };sin_family: 地址协议家族sin_port:端口号sin_addr: 表示ip地址的结构sin_zero: 用于与sockaddr 结构体的大小对齐,这个数组里面为全0in_addr 结构如下:struct in_addr { union { struct{ unsigned char s_b1, s_b2, s_b3, s_b4; } S_un_b; struct { unsigned short s_w1, s_w2; } S_un_w; unsigned long S_addr; } S_un; }; 这个结构是一个公用体,占4个字节,从本质上将IP地址仅仅是一个占4个字节的无符号整型数据,为了方便读写才会采用点分十进制的方式。仔细观察这个结构会发现,它其实定义了IP地址的几种表现形式,我们可以将IP地址以一个字节一个字节的方式拆开来看,也可以以两个字型数据的形式拆开,也可以简单的看做一个无符号长整型。当然在写入的时候按照这几种方式写入,为了方便写入IP地址,微软定义了一个宏:#define s_addr S_un.S_addr因此在填入IP地址的时候可以简单的使用这个宏来给S_addr这个共用体成员赋值一般像bind、connect等函数需要传入地址结构,它们需要的结构为sockaddr,但是为了方便都会传入SOCKADDR_IN结构sockaddr SOCKADDR结构这两个结构也是等价的,它们的定义如下struct sockaddr { unsigned short sa_family; char sa_data[14];};从结构上看它占16个字节与 SOCKADDR_IN大小相同,而且第一个成员都是地址家族的相关信息,后面都是存储的具体的IPV4的地址,因此它们是可以转化的,为了方便一般是使用SOCKADDR_IN来保存IP地址,然后在需要填入SOCKADDR的时候强制转化即可。sockaddr_in6该结构类似于sockaddr_in,只不过它表示的是IPV6的地址信息,在使用上,由于IPV6是128的地址占16个字节,而sockaddr_in 中表示地址的部分只有4个字节,所以它与之前的两个是不能转化的,在使用IPV6的时候需要特殊的处理,一般不直接填写IP而是直接根据IP的字符串或者主机名来连接。sockaddr_storage这是一个通用的地址结构,既可以用来存储IPV4地址也可以存储IPV6的地址,这个地址结构在前面已经说过了,这里就不再详细解释了。各种地址之间的转化一般我们只使用从SOCKADDR_IN到sockaddr结构的转化,而且仔细观察socket函数族发现只需要从其他结构中得到sockaddr结构,而并不需要从sockaddr转化为其他结构,因此这里重点放在如何转化为sockaddr结构从SOCKADDR_IN到sockaddr只需要强制类型转化即可从addrinfo结构中只需要调用其成员即可从SOCKADDR_STORAGE结构到sockaddr只需要强制转化即可。其实在使用上更常用的是将字符串的IP转化为对应的数值,针对IPV4有我们常见的inet_addr、inet_ntoa 函数,它们都是在ipv4中使用的,针对v6一般使用inet_pton,inet_ntop来转化,它们两个分别对应于inet_addr、inet_ntoa。但是在WinSock中更常用的是WSAAddressToString 与 WSAStringToAddressINT WSAAddressToString( LPSOCKADDR lpsaAddress, DWORD dwAddressLength, LPWSAPROTOCOL_INFO lpProtocolInfo, OUT LPTSTR lpszAddressString, IN OUT LPDWORD lpdwAddressStringLength );lpsaAddress: ip地址dwAddressLength: 地址结构的长度lpProtocolInfo: 协议信息的结构体,这个结构一般给NULLlpszAddressString:目标字符串的缓冲lpdwAddressStringLength:字符串的长度而WSAStringToAddress定义与使用与它类似,这里就不再说明了。
2018年11月24日
4 阅读
0 评论
0 点赞
2018-11-10
从项目中学习HTML+CSS
最近由于工作原因以及自己的懈怠,已经很久都没有更新过博客了。通过这段时间,我发现坚持一件事情是真的很难,都说万事开头难,但是在放弃这件事上好像开头了后面就顺理成章的继续下去了。中间即使不怎么情愿也在努力的每周更新博客,但是自从9月份以来,第一次因为工作需要加班而断更之后,后面好像很容易找到理由断更。从这件事上我学到了一点:在坚持一件事的时候千万要坚持,只要中间放弃一次,后续就可以心安理得的将其抛之脑后。这次在这里也是希望自己能够再次坚持之前的每周至少一更。即使没有内容。。。。感想就这么多,现在进入真正的主题——HTML+CSS相关内容的整理,因为网上针对HTML+CSS的相关知识已经很多了,而且都是很零碎的点,大多是对应的代码,也可以说是应用性极强的,我本人是不太喜欢大段大段的帖代码的。学习的过程中我喜欢从理论或者从实践开始,根据需求或者理论来写代码,需求清楚了,流程出来了,代码就是水到渠成的事。所以这次就根据具体的一个网页项目来梳理一下我这段时间学习这些东西的成果。最终的效果图如下:我希望自己通过对Web开发的学习能够自己独立的开发一套博客系统,因此我在选择练手项目的时候主要找的是博客的相关页面。这是从站长之家上找的一个博客网站模板的首页,它相对其他的模板来说显的比较中规中矩,而且对初学者来说实现起来更加简单。基本布局从大体上看,它可以分为几个部分:大体上分为3个部分,头部、内容部分,以及下方的页脚部分。头部可以分为上面的标题以及下方的导航部分,内容部分又可以分为左边和右边两个部分。然后根据区域的划分,可以写下大体的代码:<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>CSS + HTML项目博客首页</title> <meta charset="utf-8"> <link rel="stylesheet" type="text/css" href="css/style.css"> </head> <body> <!--顶部--> <div class = "header"> <div class = "title"> </div> <div class = "nav"> </div> </div> <div class = "container"> <div class = "left"> </div> <div class = "right"> </div> </div> <!--底部--> <div class = "footer"> </div> </body> </html>然后再使用CSS的样式规定具体的布局颜色:*{ margin:auto; /*只有设置了对应的宽度,才会默认居中*/ padding:0px; font-family: "Microsoft YaHei","微软雅黑","Lantinghei SC","Open Sans",Arial,"Hiragino Sans GB","STHeiti","WenQuanYi Micro Hei",SimSun,sans-serif; } .header{ margin-top:15px; } .title{ height: 20px; line-height: 15px; width:1200px; color:#999; } .nav{ width:1200px; margin-top:8px; } .container{ width:1200px; margin-top:15px; } .left{ float:left; width:820px; } .right{ float:left; margin-left:20px;; } .footer{ height:60px; width:100%; background-color:#fff; text-align: center; padding-top:24px; font-size:12px; color:#999; }这里有一个问题,我当时一直以为margin:auto;这个会直接将对应的元素居中,但是我在实践中发现它好像并没有,原来当时我忘记了设置元素的宽度,而元素默认的宽度是与父元素相同的,这样就导致margin:auto这个属性认为不需要给外边距,所以也就没有居中,只有给了宽度,它才会将元素相对于父元素居中。导航栏的实现这里导航栏使用无序列表 + a链接来实现,我们先写上对应的HTML代码<ul> <li><a href="index.html">首页</a></li> <li><a href="index.html">列表页</a></li> <li><a href="index.html">详细页</a></li> <li><a href="index.html">404</a></li> <li><a href="index.html">MZ-NetBlog主题</a></li> <li><a href="index.html">IT技术笔记</a></li> <li><a href="index.html">源码分享</a></li> <li><a href="index.html">靠谱网赚</a></li> <li style = "margin-right: 0px;"><a href="index.html">资讯分享</a></li> </ul>然后通过CSS样式来调整/*先去掉列表前的小圆点*/ .nav ul { list-style-type: none; } /*让列表项左浮动,以便导航项可以横向排列,同时设置右外边距,让各项可以分割开来*/ .nav ul li{ float:left; margin-right:34px; } /*上述内容已经有了导航栏的雏形,剩下的就是设置导航项的字体、颜色、以及点击的相关属性*/ .nav ul li a{ text-decoration:none; color:#999; font-size:18px; } .nav ul li a:hover{ color:lightskyblue; } .nav ul li a:active{ color:lightskyblue; }通过上述的简单的CSS就可以制作对应的导航栏了左上角标签页的制作从原始的网页效果图来看,标签页可以看成上下两个部分,上方是一个导航栏,而下方则是一个div,这个div根据点击导航上的具体项来显示不同的内容。因此它的大致内容结构可以用下面的HTML来定义<div class = "about"> <!--上方是一个导航栏--> <div class = "tab-header"> <ul> <li><a href = "#" style = "color:lightskyblue;background-color:#fff;">统计信息</a></li> <li><a href = "#">联系站长</a></li> </ul> </div> <!--下方是用来显示具体的内容--> <div class = "info"> <p>日志总数:888篇</p> <p>网站运行:88天</p> </div> </div>上方的导航可以沿用之前的导航栏的CSS代码,而下方只需要设置对应的北京颜色即可,这里就不再贴出了文章列表文章列表采用的仍然是列表的方式,我们可以针对列表的每个项设置对应的边框,以及长度和宽度即可。下面只贴出对应的CSS代码/**列表本身属性**/ .article-list{ width:820px; height:960px; background-color:#fff; margin-top:15px; } /**列表项属性**/ .article{ width:820px; height: 192px; border-top: solid 1px rgb(234,234,234); }文章项的制作文章列表中有具体的文章项,这个文章项可以简单的分为几个部分:图片、标题、文章属性等等内容、文章的摘要;在这里我将它们都作为同级元素,然后调整浮动以及大小,它自然就会按照这样的布局进行排列。<div class ="article"> <a class = "article-img"><img src = "images/article.jpg"></a> <div class = "label"> <div class = "rect"><span>MZ-NetBlog主题</span></div> <div class = "arrow"></div> <a href = "#">用DTcms做一个独立博客网站(响应式模板)</a> </div> <div class = "time-watch-comment"> <span class = "time"><img src = "images/clock.jpg"><span>2018-11-06</span></span> <span class = "watch"><img src = "images/eye.jpg"><span>666</span></span> <span class = "comment"><img src = "images/comment.jpg"><span>4</span></span> </div> <p>用DTcms做一个独立博客网站(响应式模板),采用DTcms V4.0正式版(MSSQL)。开发环境:SQL2008R2+VS2010。DTcms V4.0正式版功能修复和优化:1、favicon.ico图标后台上传。(解决要换图标时要连FTP或者开服务器的麻烦)</p> </div>这个部分我感觉最需要提出来的是对标签的制作,这里的标签是文章标题前面的那个蓝色背景,白色字体的矩形后带有箭头的东西,这个的制作我采用的是前方一个标签,而后方利用另一个div 来制作的小箭头。想要制作小箭头首先需要回归一下CSS中讲到的border属性,我们知道border表示的是边框,我们可以通过设置border的值来规定边框的大小颜色等等属性,那么当我们在四个边上都规定边框的时候,边框是如何来显示的呢,我们写下如下的实例<div class = "div1"></div>.div1{ width:100px; height:100px; background: orange; float: left; border-top:10px solid black; border-bottom:12px solid green; border-left:15px solid red; border-right:20px solid blue; border-style: solid; }刷新浏览器,我们发现它产生的是这样的一个效果之前在学习的时候我一直实验的是border为1个像素,但是没想到给边框加粗后能产生这样的效果,它能够产生这样一种像话框的效果,随着边框的加粗,中间的内容越小,而这个画框的边框就越大。这个时候很容易就产生一种想法,随着边框的加粗,最终上下或者左右边框完全占据元素的所有空间,而另一侧为空,那么就可以产生一个类似于箭头的效果,根据这个想法,我们再修改一下上面的CSS代码.div1{ width:0px; height:0px; border-top:50px solid black; border-bottom:50px solid green; border-left:15px solid red; border-style: solid; }这个时候它的效果如下:这样我们把上下两个边框的眼色设置为父元素的背景色,左边框设置为需要的颜色,就可以做一个小的箭头了。而要调整它的宽度、角度等等只需要调整上下边框的宽度即可。下面是箭头最终的CSS代码/*方向向右的小箭头*/ .arrow{ float:left; background-color:#fff; width:0; height:0; border-top:5px solid transparent; border-bottom: 5px solid transparent; border-left: 6px solid #3399CC; margin-top: 31px; }搜索框的实现这个搜索框我简单的使用了一个带边框的文本输入框加一个按钮。它的HTML代码如下:<input class = "search-box" type = "text" value = "请输入关键字"/> <input class = "search-submit" type = "submit" value = "搜索"/>对应的CSS代码如下:.search-box{ width:258px; height:34px; border:solid 1px rgb(51, 153, 204); margin-top:7px; margin-left:22px; margin-right:0px; color:#999; padding-left:9px; } .search-submit{ width:52px; height:36px; background-color:rgb(51, 153, 204); border-style:none; margin-left:-4px; color:#fff; }项目后记这个页面虽然说完成了,但是也是有一些不足的地方:页面中几乎每一个元素写了它的属性,而且有的属性是几乎类似的,代码只是简单的完成了页面没有考虑到重用页面是静态的,简单的利用HTML+CSS来做展示,没有交互的东西,而原始的模板是有的,交互这个的部分我想学习了JavaScript 和 JQuery之后再来加虽然我主要用C/C++ 与Python做过一些服务程序和其他的Web程序,但是对于前端的相关内容也仅仅是会用HTML,关于布局和CSS的东西几乎不懂,而这次我想抽点时间学习一下这方面的内容。为什么会想要学习前端呢?之前不知道在哪看到这么一句话: "黑客一定是程序员,而程序员不一定是黑客", 作为一个初步迈入Web安全大门的我来说,想要深入Web安全就必须学会Web开发,而Web开发是绕不开前端的。虽然不要求有很高的前端水平,但是基本的布局、css、JavaScript、jQuery还是得会的,所以我想先抽点时间好好补一下这方面的内容。
2018年11月10日
4 阅读
0 评论
0 点赞
2018-10-14
xampp 中 mysql的相关配置
最近开始接触PHP,而一般搭建PHP环境使用的都是xampp 这个集成环境,由于之前我的系统中已经安装了mysql服务,所以在启动mysql的时候出现一些列错误,我通过查询各种资料解决了这个问题,现在记录一下,方便日后遇到同样的问题时能够快速解决,也为遇到同样问题的朋友提供一种解决的思路。启动刚开始时我在点击启动mysql的时候发现它一直卡在尝试启动mysql这个位置,xampp提示内容如下:Attempting to start MySQL service...它启动不成功但是也不提示出错,而且查询日志发现没有错误的日志,这个时候我想到应该是我本地之前安装了mysql,导致失败。而且我还将mysql安装成为了服务,后来查询相关资料,有网友说需要将mysql服务的地址改为xampp下mysql所在地址,具体怎么改我就不写了,一般都可以找到,但是我想说的是,这个方式好像在我这边不起作用。那么就干脆一点直接删除服务就好了。sc delete mysql上述命令直接删除mysql这个服务。然后重启xampp,再次启动mysql,它终于报错了。只要报错就好说了,现在来查询日志,发现日志如下:2018-10-13 22:52:19 37d0 InnoDB: Warning: Using innodb_additional_mem_pool_size is DEPRECATED. This option may be removed in future releases, together with the option innodb_use_sys_malloc and with the InnoDB's internal memory allocator. 2018-10-13 22:52:19 14288 [Note] InnoDB: innodb_empty_free_list_algorithm has been changed to legacy because of small buffer pool size. In order to use backoff, increase buffer pool at least up to 20MB. 2018-10-13 22:52:19 14288 [Note] InnoDB: Using mutexes to ref count buffer pool pages 2018-10-13 22:52:19 14288 [Note] InnoDB: The InnoDB memory heap is disabled 2018-10-13 22:52:19 14288 [Note] InnoDB: Mutexes and rw_locks use Windows interlocked functions 2018-10-13 22:52:19 14288 [Note] InnoDB: _mm_lfence() and _mm_sfence() are used for memory barrier 2018-10-13 22:52:19 14288 [Note] InnoDB: Compressed tables use zlib 1.2.3 2018-10-13 22:52:19 14288 [Note] InnoDB: Using generic crc32 instructions 2018-10-13 22:52:19 14288 [Note] InnoDB: Initializing buffer pool, size = 16.0M 2018-10-13 22:52:19 14288 [Note] InnoDB: Completed initialization of buffer pool 2018-10-13 22:52:19 14288 [Note] InnoDB: Highest supported file format is Barracuda. 2018-10-13 22:52:19 14288 [Warning] InnoDB: Resizing redo log from 2*3072 to 2*320 pages, LSN=1600607 2018-10-13 22:52:19 14288 [Warning] InnoDB: Starting to delete and rewrite log files. 2018-10-13 22:52:19 14288 [Note] InnoDB: Setting log file C:\xampp\mysql\data\ib_logfile101 size to 5 MB 2018-10-13 22:52:19 14288 [Note] InnoDB: Setting log file C:\xampp\mysql\data\ib_logfile1 size to 5 MB 2018-10-13 22:52:19 14288 [Note] InnoDB: Renaming log file C:\xampp\mysql\data\ib_logfile101 to C:\xampp\mysql\data\ib_logfile0 2018-10-13 22:52:19 14288 [Warning] InnoDB: New log files created, LSN=1601036 2018-10-13 22:52:19 14288 [Note] InnoDB: 128 rollback segment(s) are active. 2018-10-13 22:52:19 14288 [Note] InnoDB: Waiting for purge to start 2018-10-13 22:52:19 14288 [Note] InnoDB: Percona XtraDB (http://www.percona.com) 5.6.39-83.1 started; log sequence number 1600607 2018-10-13 22:52:20 3508 [Note] InnoDB: Dumping buffer pool(s) not yet started 2018-10-13 22:52:20 14288 [Note] Plugin 'FEEDBACK' is disabled. 2018-10-13 22:52:20 14288 [ERROR] Could not open mysql.plugin table. Some plugins may be not loaded 2018-10-13 22:52:20 14288 [ERROR] Can't open and lock privilege tables: Table 'mysql.servers' doesn't exist 2018-10-13 22:52:20 14288 [Note] Server socket created on IP: '::'. 2018-10-13 22:52:20 14288 [ERROR] Fatal error: Can't open and lock privilege tables: Table 'mysql.user' doesn't exist 2018-10-13 22:55:18 3024 InnoDB: Warning: Using innodb_additional_mem_pool_size is DEPRECATED. This option may be removed in future releases, together with the option innodb_use_sys_malloc and with the InnoDB's internal memory allocator. 2018-10-13 22:55:18 12324 [Note] InnoDB: innodb_empty_free_list_algorithm has been changed to legacy because of small buffer pool size. In order to use backoff, increase buffer pool at least up to 20MB.找到其中的ERROR项,发现它提示mysql.user这个表不存在,这个表保存的是mysql的账号信息,如果没有这个,它无法知道哪些是合法用户,合法用户又有哪些权限,因此这里就需要创建这个表。通过查询资料发现这是由于未进行mysql数据初始化的缘故,这个错误经常见于通过源码包在编译安装的时候。这个时候需要使用命令 mysql_install_db 来初始化数据库表mysql_install_db --user=mysql -d C:\xampp\mysql\data\-d 后面跟上mysql表数据所在路径执行之后发现程序又报错了,这次提示mysql的版本不对Can't find messagefile "D:\mysql-8.0.11-winx64\share\errmsg.sys". Probably from another version of MariaDB这个时候就很奇怪了,我启动的是xampp中的mysql,为何它给我定位的是之前安装的MySQL所在路径呢?出现这种现象肯定是系统中的相关配置的路径不对,之前已经删掉了mysql服务,那么应该不可能会是服务配置导致的,剩下的应该就是环境变量了,通过一个个的查看环境变量,终于发现了 MYSQL_HOME这个变量给的是 D:\mysql-8.0.11-winx64 这个路径,我们将这个环境变量的值修改为xampp中mysql的路径然后再执行命令初始化mysql表数据,这个时候成功了。完成了这些操作,我这边就可以通过xampp面板启动mysql了。数据库配置刚开始时使用root账户登录是不需要密码的,这样是很危险的操作,容易发生数据泄露,为了安全起见,首先给root账户输入一个复杂的密码mysqladmin -uroot -p password回车之后它会让你输入新的密码,如果是修改密码可以使用下面的命令mysqladmin -uroot -p"test" password其中test为原始密码在回车之后它会让你输入新的密码我们为root设置了一个相对复杂的密码,但是与Linux系统相似,为了安全一般不能随便给出root账户,这个时候就需要一个非root账户并为它设置相关权限,我们可以在进入mysql后,使用grant 命令来创建账户以及分配权限grant all privileges on *.* to masimaro@localhost identified by "masimarotest"; flush privileges;它的语法格式为: grant 权限 on 数据库.表 to 用户名@主机 identified by "密码" 权限,all privileges 表示所有权限,如果不想分配所有权限,可以考虑使用 select,insert,update,delete,create,drop,index,alter,grant,references,reload,shutdown,process,file 权限中的任意一个或者多个。数据库,表:我们可以指定具体的用户对具体的数据库表有何种权限主机:主机可以是localhost,%(任意主机),或者具体的主机名、ip等等,表示这个账户只能通过对应的主机来登录分配完成之后通过 flush privileges; 语句来保存我们分配的账户和权限为了方便操作,还可以对phpmyadmin进行配置,以便能够使用phpmyadmin来连接并操作mysql数据库。可以在phpmyadmin目录中找到 config.inc.php 文件,找到这么几行$cfg['Servers'][$i]['user'] = ''; //连接数据库的用户 $cfg['Servers'][$i]['password'] = ''; //连接数据库的用户密码 $cfg['Servers'][$i]['host'] = '127.0.0.1'; //数据库所在主机 $cfg['Servers'][$i]['controluser'] = 'root'; //phpmyadmin 所使用的配置账户 $cfg['Servers'][$i]['controlpass'] = ''; //配置账户的密码根据具体情况配置这些信息之后,就可以直接连上PHPmyadmin了,然后根据它的提示来初始化相关数据库和表即可
2018年10月14日
4 阅读
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-09
Windows下的代码注入
木马和病毒的好坏很大程度上取决于它的隐蔽性,木马和病毒本质上也是在执行程序代码,如果采用独立进程的方式需要考虑隐藏进程否则很容易被发现,在编写这类程序的时候可以考虑将代码注入到其他进程中,借用其他进程的环境和资源来执行代码。远程注入技术不仅被木马和病毒广泛使用,防病毒软件和软件调试中也有很大的用途,最近也简单的研究过这些东西,在这里将它发布出来。想要将代码注入到其他进程并能成功执行需要解决两个问题:第一个问题是如何让远程进程执行注入的代码。原始进程有它自己的执行逻辑,想要破坏原来的执行流程,使EIP寄存器跳转到注入的代码位置基本是不可能的第二个问题是每个进程中地址空间是独立的,比如在调用某个句柄时,即使是同一个内核对象,在不同进程中对应的句柄也是不同的,这就需要进行地址转化。要进行远程代码注入的要点和难点主要就是这两个问题,下面给出两种不同的注入方式来说明如何解决这两个问题DLL注入DLL注入很好的解决了第二个问题,DLL被加载到目标进程之后,它里面的代码中的地址就会自动被转化为对应进程中的地址,这个特性是由于DLL加载的过程决定的,它会自己使用它所在进程中的资源和地址空间,所以只要DLL中不存在硬编码的地址,基本不用担心里面会出现函数或者句柄需要进行地址转化的问题。那么第一个问题改怎么解决呢?要执行用户代码,在Windows中最常见的就是使用回调的方式,Windows采用的是事件驱动的方式,只要发生了某些事件就会调用回调,在众多使用回调的场景中,线程的回调是最简单的,它不会干扰到目标进程的正常执行,也就不用考虑最后还原EIP的问题,因此DLL注入采用的最常见的就是创建一个远程线程,让线程加载DLL代码。DLL注入中一般的思路是:使用CreateRemoteThread来在目标进程中创建一个远程的线程,这个线程主要是加载DLL到目标进程中,由于DLL在入口函数(DLLMain)中会处理进程加载Dll的事件,所以将注入代码写到这个事件中,这样就能执行注入的代码了。那么如何在远程进程中执行DLL的加载操作呢?我们知道加载DLL主要使用的是函数LoadLibrary,仔细分析线程的回调函数和LoadLibrary函数的原型,会发现,它们同样都是传入一个参数,而CreateRemoteThread函数正好需要一个函数的地址作为回调,并且传入一个参数作为回调函数的参数。这样就有思路了,我们让LoadLibrary作为线程的回调函数,将对应dll的文件名和路径作为参数传入,这样就可以在对应进程中加载dll了,进一步也就可以执行dllmain中的对应代码了。还有一个很重要的问题,我们知道不同进程中,地址空间是隔离的,那么我在注入的进程中传入LoadLibrary函数的地址,这算是一个硬编码的地址,它在目标进程中是否是一样的呢?答案是,二者的地址是一样的,这是由于kernel32.dll在32位程序中加载的基地址是一样的,而LoadLibrary在kernel32.dll中的偏移是一定的(只要不同的进程加载的是同一份kernel32.dll)那么不同进程中的LoadLibrary函数的地址是一样的。其实不光是LoadLibrary函数,只要是kernel32.dll中导出的函数,在不同进程中的地址都是一样的。注意这里只是32位,如果想要使用32位程序往64位目标程序中注入,可能需要考虑地址转换的问题,只要知道kernel32.dll在64位中的偏移,就可以计算出对应函数的地址了。LoabLibrary函数传入的代表路径的字符串的首地址在不同进程中同样是不同的,而且也没办法利用偏移来计算,这个时候解决的办法就是在远程进程中申请一块虚拟地址空间,并将目标字符串写入对应的地址中,然后将对应的首地址作为参数传入即可。最后总结一下DLL注入的步骤:获取LoadLibrary函数的地址调用VirtualAllocEx 函数在远程进程中申请一段虚拟内存调用WriteProcessMemory 函数将参数写入对应的虚拟内存调用CreateRemoteThread 函数创建远程线程,线程的回调函数为LoadLibrary,参数为对应的字符串的地址按照这个思路可以编写如下的代码:typedef HMODULE(WINAPI *pfnLoadLibrary)(LPCWSTR); if (!DebugPrivilege()) //提权代码,在Windows Vista 及以上的版本需要将进程的权限提升,否则打开进程会失败 { return FALSE; } //打开目标进程 HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); //dwPid是对应的进程ID if (NULL == hRemoteProcess) { AfxMessageBox(_T("OpenProcess Error")); } //查找LoadLibrary函数地址 pfnLoadLibrary lpLoadLibrary = (pfnLoadLibrary)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "LoadLibraryW"); //在远程进程中申请一块内存用于保存对应线程的参数 PVOID pBuffer = VirtualAllocEx(hRemoteProcess, NULL, MAX_PATH, MEM_COMMIT, PAGE_READWRITE); //在对应内存位置处写入参数值 DWORD dwWritten = 0; WriteProcessMemory(hRemoteProcess, pBuffer, m_csDLLName.GetString(), (m_csDLLName.GetLength() + 1) * sizeof(TCHAR), &dwWritten); //创建远程线程并传入对应参数 HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpLoadLibrary, pBuffer, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); VirtualFreeEx(hRemoteProcess, pBuffer, 0, MEM_RELEASE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess);卸载远程DLL上面进行了代码的注入,作为一个文明的程序,自然得考虑卸载dll,毕竟现在提倡环保,谁使用,谁治理。这里既然注入了,自然得考虑卸载。卸载的思路与注入的类似,只是函数变为了FreeLibrary,传入的参数变成了对应的dll的句柄了。如何获取这个模块的句柄呢?我们可以枚举进程中的模块,根据模块的名称来找到对应的模块并获取它的句柄。枚举的方式一般是使用toolhelp32中对应的函数,下面是卸载的例子代码HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, m_dwPid); if (INVALID_HANDLE_VALUE == hSnapshot) { AfxMessageBox(_T("CreateToolhelp32Snapshot Error")); return; } MODULEENTRY32 me = {0}; me.dwSize = sizeof(MODULEENTRY32); BOOL bRet = Module32First(hSnapshot, &me); while (bRet) { CString csModuleFile = _tcsupr(me.szExePath); if (csModuleFile == _tcsupr((LPTSTR)m_csDLLName.GetString()) != -1) { break; } ZeroMemory(&me, sizeof(me)); me.dwSize = sizeof(PROCESSENTRY32); bRet = Module32Next(hSnapshot, &me); } CloseHandle(hSnapshot); typedef BOOL (*pfnFreeLibrary)(HMODULE); pfnFreeLibrary FreeLibrary = (pfnFreeLibrary)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "FreeLibrary"); HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_dwPid); if (hRemoteProcess == NULL) { AfxMessageBox(_T("OpenProcess Error")); return; } HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)FreeLibrary, me.modBaseAddr, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess);无DLL的注入注入不一定需要使用DLL,虽然使用DLL比较简单一点,无DLL注入在解决上述两个问题的第一个思路是一样的,也是使用CreateRemoteThread来创建一个远程线程来执行目标代码。无dll的注入主要麻烦是在进行地址转化上,在调用API的时候,如果无法保证对应的dll的基地址不变的话,就得在目标进程中自行调用LoadLibrary来动态获取函数地址,并调用。在动态获取API函数的地址的时候,主要使用的函数是LoadLibrary、GetModuleHandle、GetProcAddress这三个函数,而线程的回调函数只能传入一个参数,所以我们需要将对应的需要传入的参数组成一个结构体,并将结构体对应的数据写入到目标进程的内存中,特别要注意的是,里面不要使用指针或者句柄这种与地址有关的东西。例如我们想在目标进程中注入一段代码,让它弹出一个对话框,以便测试是否注入成功。这种情况除了要传入上述三个函数的地址外,还需要MesageBox,而MessageBox是在user32.dll中,user32.dll在每个进程中的基地址并不相同,因此在注入的代码中需要动态加载,因此可以定义下面一个结构typedef struct REMOTE_DATA { DWORD dwLoadLibrary; DWORD dwGetProcAddress; DWORD dwGetModuleHandle; DWORD dwGetModuelFileName; //辅助函数 char szUser32dll[MAX_PATH]; //存储user32dll的路径,以便调用LoadLibrary加载 char szMessageBox[128]; //存储字符串MessageBoxA 这个字符串,以便使用GetProcAddress加载MesageBox函数 char szMessage[512]; //弹出对话框上显示的字符 }不使用DLL注入与使用DLL注入的另一个区别是,不使用DLL注入的时候需要自己加载目标代码到对应的进程中,这个操作可以借由WriteProcessMemory 将函数代码写到对应的虚拟内存中。最后注入的代码主要如下:DWORD WINAPI RemoteThreadProc(LPVOID lpParam) { LPREMOTE_DATA lpData = (LPREMOTE_DATA)lpParam; typedef HMODULE (WINAPI *pfnLoadLibrary)(LPCSTR); typedef FARPROC (WINAPI *pfnGetProcAddress)(HMODULE, LPCSTR); typedef HMODULE (*pfnGetModuleHandle)(LPCSTR); typedef DWORD (WINAPI *pfnGetModuleFileName)( HMODULE,LPSTR, DWORD); pfnGetModuleHandle MyGetModuleHandle = (pfnGetModuleHandle)lpData->dwGetModuleHandle; pfnGetModuleFileName MyGetModuleFileName = (pfnGetModuleFileName)lpData->dwGetModuleFileName; pfnGetProcAddress MyGetProcAddress = (pfnGetProcAddress)lpData->dwGetProcAddress; pfnLoadLibrary MyLoadLibrary = (pfnLoadLibrary)lpData->dwGetProcAddress; typedef int (WINAPI *pfnMessageBox)(HWND, LPCSTR, LPCSTR, UINT); //加载User32.dll HMODULE hUser32Dll = MyLoadLibrary(lpData->szUerDll); //加载MessageBox函数 pfnMessageBox MyMessageBox = (pfnMessageBox)MyGetProcAddress(hUser32Dll, lpData->szMessageBox); char szTitlte[MAX_PATH] = ""; MyGetModuleFileName(NULL, szTitlte, MAX_PATH); MyMessageBox(NULL, lpData->szMessage, szTitlte, MB_OK); return 0; } m_dwPid = GetPid(); //获取目标进程ID DebugPrivilege(); //进程提权 HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_dwPid); if (NULL == hRemoteProcess) { AfxMessageBox(_T("OpenProcess Error")); return; } LPREMOTE_DATA lpData = new REMOTE_DATA; ZeroMemory(lpData, sizeof(REMOTE_DATA)); //获取对应函数的地址 lpData->dwGetModuleFileName = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetModuleFileNameA"); lpData->dwGetModuleHandle = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetModuleHandleA"); lpData->dwGetProcAddress = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "GetProcAddress"); lpData->dwLoadLibrary = (DWORD)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "LoadLibraryA"); // 拷贝对应的字符串 StringCchCopyA(lpData->szMessage, MAX_STRING_LENGTH, "Inject Success!!!"); StringCchCopyA(lpData->szUerDll, MAX_PATH, "user32.dll"); StringCchCopyA(lpData->szMessageBox, MAX_PROC_NAME_LENGTH, "MessageBoxA"); //在远程空间中申请对应的内存,写入参数和函数的代码 LPVOID lpRemoteBuf = VirtualAllocEx(hRemoteProcess, NULL, sizeof(REMOTE_DATA), MEM_COMMIT, PAGE_READWRITE); // 存储data结构的数据 LPVOID lpRemoteProc = VirtualAllocEx(hRemoteProcess, NULL, 0x4000, MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 存储函数的代码 DWORD dwWrittenSize = 0; WriteProcessMemory(hRemoteProcess, lpRemoteProc, &RemoteThreadProc, 0x4000, &dwWrittenSize); WriteProcessMemory(hRemoteProcess, lpRemoteBuf, lpData, sizeof(REMOTE_DATA), &dwWrittenSize); HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpRemoteProc, lpRemoteBuf, 0, NULL); WaitForSingleObject(hRemoteThread, INFINITE); VirtualFreeEx(hRemoteProcess, lpRemoteBuf, 0, MEM_RELEASE); VirtualFreeEx(hRemoteProcess, lpRemoteProc, 0, MEM_RELEASE); CloseHandle(hRemoteThread); CloseHandle(hRemoteProcess); delete[] lpData;
2018年09月09日
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日
4 阅读
0 评论
0 点赞
2018-08-28
VC++ 崩溃处理以及打印调用堆栈
我们在程序发布后总会面临崩溃的情况,这个时候一般很难重现或者很难定位到程序崩溃的位置,之前有方法在程序崩溃的时候记录dump文件然后通过windbg来分析。那种方法对开发人员的要求较高,它需要程序员理解内存、寄存器等等一系列概念还需要手动加载对应的符号表。Java、Python等等语言在崩溃的时候都会打印一条异常的堆栈信息并告诉用户那块出错了,根据这个信息程序员可以很容易找到对应的代码位置并进行处理,而C/C++则会弹出一个框告诉用户程序崩溃了,二者对比来看,C++似乎对用户太不友好了,而且根据它的弹框很难找到对应的问题,那么有没有可能使c++像Java那样打印异常的堆栈呢?这个自然是可能的,本文就是要讨论如何在Windows上实现类似的功能异常处理一般当程序发生异常时,用户代码停止执行,并将CPU的控制权转交给操作系统,操作系统接到控制权后,将当前线程的环境保存到结构体CONTEXT中,然后查找针对此异常的处理函数。系统利用结构EXCEPTION_RECORD保存了异常描述信息,它与CONTEXT一同构成了结构体EXCEPTION_POINTERS,一般在异常处理中经常使用这个结构体。异常信息EXCEPTION_RECORD的定义如下:typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; //异常码 DWORD ExceptionFlags; //标志异常是否继续,标志异常处理完成后是否接着之前有问题的代码 struct _EXCEPTION_RECORD* ExceptionRecord; //指向下一个异常节点的指针,这是一个链表结构 PVOID ExceptionAddress; //异常发生的地址 DWORD NumberParameters; //异常附加信息 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常的字符串 } EXCEPTION_RECORD, *PEXCEPTION_RECORD;Windows平台提供的这一套异常处理的机制,我们叫它结构化异常处理(SEH),它的处理过程一般如下:如果程序是被调试运行的(比如我们在VS编译器中调试运行程序),当异常发生时,系统首先将异常信息交给调试程序,如果调试程序处理了那么程序继续运行,否则系统便在发生异常的线程栈中查找可能的处理代码。若找到则处理异常,并继续运行程序如果在线程栈中没有找到,则再次通知调试程序,如果这个时候仍然不能处理这个异常,那么操作系统会对异常进程默认处理,这个时候一般都是直接弹出一个错误的对话框然后终止程序。系统在每个线程的堆栈环境中都维护了一个SEH表,表中是用户注册的异常类型以及它对应的处理函数,每当用户在函数中注册新的异常处理函数,那么这个信息会被保存在链表的头部,也就是说它是采用头插法来插入新的处理函数,从这个角度上来说,我们可以很容易理解为什么在一般的高级语言中一般会先找与try块最近的catch块,然后在找它的上层catch,由里到外依次查找。与try块最近的catch是最后注册的,由于采用的是头插法,自然它会被首先处理。在Windows中针对异常处理,扩展了__try 和 __except 两个操作符,这两个操作符与c++中的try和catch非常相似,作用也基本类似,它的一般的语法结构如下:__try { //do something } __except(filter) { //handle }使用 __try 和 __except 的时候它主要分为3个部分,分别为:保护代码体、过滤表达式、异常处理块保护代码体一般是try中的语句,它值被保护的代码,也就是说我们希望处理那个代码块产生的异常过滤表达式是 except后面扩号中的值,它只能是3个值中的一个,EXCEPTION_CONTINUE_SEARCH继续向下查找异常处理,也就是说这里的异常处理块不处理这种异常,EXCEPTION_CONTINUE_EXECUTION表示异常已被处理,这个时候可以继续执行直线产生异常的代码,EXCEPTION_EXECUTE_HANDLER表示异常已被处理,此时直接跳转到except里面的代码块中,这种方式下它的执行流程与一般的异常处理的流程类似.异常处理块,指的是except下面的扩号中的代码块.注意:我们说过滤表达式只能是这三个值中的一个,但是没有说这里一定得填这三个值,它还支持函数或者其他的表达式类型,只要函数或者表达式的返回值是这三个值中的一个即可。上述的方式也有他的局限性,也就是说它只能保护我们指定的代码,如果是在 __try 块之外的代码发生了崩溃,可能还是会造成程序被kill掉,而且每个位置都需要写上这么些代码实在是太麻烦了。其实处理异常还有一种方式,那就是采用 SetUnhandledExceptionFilter来注册一个全局的异常处理函数来处理所有未被处理的异常,其实它的主要工作原理就是往异常处理的链表头上添加一个处理函数,函数的原型如下:LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(__in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);它需要传入一个函数,以便发生异常的时候调用这个函数,这个回调函数的原型如下:LONG WINAPI UnhandledExceptionFilter( __in struct _EXCEPTION_POINTERS* ExceptionInfo );回调函数会传入一个表示当前堆栈和异常信息的结构体的指针,结构的具体信息请参考MSDN, 函数会返回一个long型的数值,这个数值为上述3个值中的一个,表示当系统调用了这个异常处理函数处理异常之后该如何继续执行用户代码。SetUnhandledExceptionFilter 函数返回一个函数指针,这个指针指向链表的头部,如果插入处理函数失败那么它将指向原来的链表头,否则指向新的链表头(也就是注册的这个回调函数的地址)而这次要实现这么一个能打印异常信息和调用堆栈的功能就是要使用这个方法。打印函数调用堆栈关于打印堆栈的内容,这里不再多说了,请参考本人之前写的博客windows平台调用函数堆栈的追踪方法这里的主要思路是使用StackWalker来根据当前的堆栈环境来获取对应的函数信息,这个信息需要根据符号表来生成,因此我们需要首先加载符号表,而获取当前线程的环境,我们可以像我博客中写的那样使用GetThreadContext来获取,但是在异常中就简单的多了,还记得异常处理函数的原型吗?异常处理函数本身会带入一个EXCEPTION_POINTERS结构的指针,而这个结构中就包含了异常堆栈的信息。还有一些需要注意的问题,我把它放到实现那块了,请小心的往下看^_^实现实现部分的源码我放到了github上,地址这个项目中主要分为两个类CBaseException,主要是对异常的一个简单的封装,提供了我们需要的一些功能,比如获取加载的模块的信息,获取调用的堆栈,以及解析发生异常时的相关信息。而这些的基础都在CStackWalker中。使用上,我把CBaseException中的大部分函数都定义成了virtual 允许进行重写。因为具体我还没想好这块后续会需要进行哪些扩展。但是里面最主要的功能是OutputString函数,这个函数是用来进行信息输出的,默认CBaseException是将信息输出到控制台上,后续可以重载这个函数把数据输出到日志中。CBaseException 类CBaseException 主要是用来处理异常,在代码里面我提供了两种方式来进行异常处理,第一种是通过 SetUnhandledExceptionFilter 来注册一个全局的处理函数,这个函数是类中的静态函数UnhandledExceptionFilter,在这个函数中我主要根据异常的堆栈环境来初始化了一个CBaseException类,然后简单的调用类的方法显示异常与堆栈的相关信息。第二种是通过 _set_se_translator 来注册一个将SEH转化为C++异常的方法,在对应的回调中我简单的抛出了一个CBaseException的异常,在具体的代码中只要简单的用c++的异常处理捕获这么一个异常即可CBaseException 类中主要用来解析异常的信息,里面提供这样功能的函数主要有3个ShowExceptionResoult: 这个函数主要是根据异常码来获取到异常的具体字符串信息,比如非法内存访问、除0异常等等GetLogicalAddress:根据发生异常的代码的地址来获取对应的模块信息,比如它在PE文件中属于第几个节,节的地址范围等等,它在实现上首先使用 VirtualQuery来获取对应的虚拟内存信息,主要是这个模块的首地址信息,然后解析PE文件获取节表的信息,我们循环节表中的每一项,根据节表中的地址范围来判断它属于第几个节,注意这里我们根据它在内存中的偏移计算了它在PE文件中的偏移,具体的计算方式请参考PE文件的相关内容.3.ShowRegistorInformation:获取各个寄存器的值,这个值保存在CONTEXT结构中,我们只需要简单打印它就好CStackWalker类这个类主要实现一些基础的功能,它主要提供了初始化符号表环境、获取对应的调用堆栈信息、获取加载的模块信息在初始化符号表的时候尽可以多的遍历了常见的几种符号表的位置并将这些位置中的符号表加载进来,以便能更好的获取到堆栈调用的情况。在获取到对应的符号表位置后有这样的代码if (NULL != m_lpszSymbolPath) { m_bSymbolLoaded = SymInitialize(m_hProcess, T2A(m_lpszSymbolPath), TRUE); //这里设置为TRUE,让它在初始化符号表的同时加载符号表 } DWORD symOptions = SymGetOptions(); symOptions |= SYMOPT_LOAD_LINES; symOptions |= SYMOPT_FAIL_CRITICAL_ERRORS; symOptions |= SYMOPT_DEBUG; SymSetOptions(symOptions); return m_bSymbolLoaded;这里将 SymInitialize的最后一个函数置为TRUE,这个参数的意思是是否枚举加载的模块并加载对应的符号表,直接在开始的时候加载上可能会比较浪费内存,这个时候我们可以采用动态加载的方式,在初始化的时候先填入FALSE,然后在需要的时候自己枚举所有的模块,然后手动加载所有模块的符号表,手动加载需要调用SymLoadModuleEx。这里需要提醒各位的是,这里如果填的是FALSE的话,后续一定得自己加载模块的符号表,否则在后续调用SymGetSymFromAddr64的时候会得到一堆的487错误(也就是地址无效)我之前就是这个问题困扰了我很久的时间。在获取模块的信息时主要提供了两种方式,一种是使用CreateToolhelp32Snapshot 函数来获取进程中模块信息的快照然后调用Module32Next 和 Module32First来枚举模块信息,还有一种是使用EnumProcessModules来获取所有模块的句柄,然后根据句柄来获取模块的信息,当然还有另外的方式,其他的方式可以参考我的这篇博客 枚举进程中的模块在枚举加载的模块的同时还针对每个模块调用了 GetModuleInformation 函数,这个函数主要有两个功能,获取模块文件的版本号和获取加载的符号表信息。接下来就是重头戏了——获取调用堆栈。获取调用堆栈首先得获取当前的环境,在代码中进行了相应的判断,如果当前传入的CONTEXT为NULL,则函数自己获取当前的堆栈信息。在获取堆栈信息的时候首先判断是否为当前线程,如果不是那么为了结果准确,需要先停止目标线程,然后获取,否则直接使用宏来获取,对应的宏定义如下:#define GET_CURRENT_THREAD_CONTEXT(c, contextFlags) \ do\ {\ memset(&c, 0, sizeof(CONTEXT));\ c.ContextFlags = contextFlags;\ __asm call $+5\ __asm pop eax\ __asm mov c.Eip, eax\ __asm mov c.Ebp, ebp\ __asm mov c.Esp, esp\ } while (0)在调用StackWalker时只需要关注esp ebp eip的信息,所以这里我们也只简单的获取这些寄存器的环境,而其他的就不管了。这样有一个问题,就是我们是在CStackWalker类中的函数中获取的这个线程环境,那么这个环境里面会包含CStackWalker::StackWalker,结果自然与我们想要的不太一样(我们想要的是隐藏这个库中的相关信息,而只保留调用者的相关堆栈信息)。这个问题我还没有什么好的解决方案。在获取到线程环境后就是简单的调用StackWalker以及那堆Sym开头的函数来获取各种信息了,这里就不再详细说明了。至此这个功能已经实现的差不多了。库的具体使用请参考main.cpp这个文件,相信有这篇博文以及源码各位应该很容易就能够使用它。据说这些函数不是多线程安全的,我自己没有在多线程环境下进行测试,所以具体它在多线程环境下表现如何还是个未知数,如果后续我有兴趣继续完善它的话,可能会加入多线程的支持。
2018年08月28日
13 阅读
0 评论
0 点赞
1
...
19
20
21
...
34