从零学习开源项目系列(四)LogServer源码探究

从零学习开源项目系列(四)LogServer源码探究 这是从零学习开源项目的第四篇,上一篇是《从零学习开源项目系列(三) CSBattleMgr服务源码研究》,这篇文章我们一起来学习LogServer,中文意思可能是“日志服务器”。那么这个日志服务器到底做了哪些工作呢? 我们在Visual Studio中将LogServer设置为启动项,然后按F5将LogServer启动起来,启动成功后显示如下图: 从上图中,我们可以到大致做了三件事: 1. 创建一个侦听端口(端口号1234) 2. 连接mysql数据库 3. 初始化日志处理程序 我们来验证一下这三件事的细节。我们再Visual Studio中将程序中断(【调试】菜单-【全部中断】,快捷键Ctrl + Alt + Break)。然后在线程窗口查看这个程序所有的线程,如下图所示: 所有用红色旗帜标记的线程都是用户线程,我们可以查看这些线程的调用堆栈。我们从最上面的主线程开始: 切换到main函数,我们可以看出这里是一个循环: int main() { auto res = CLogHandler::GetInstance().Init(); if (res) { while(true) { INetSessionMgr::GetInstance()->Update(); Sleep(1); } } return 0; } 这里一个是初始化动作,一个循环中Update动作,它们具体做了些什么,我们先不管,我们先看其他线程做了什么,再回过头来看这里的代码。 我们接着看下一个线程的内容: 从调用堆栈来看,这是一个使用boost::thread启动的线程,这个线程函数代码如下: void Active::Run() { if (m_BeginInThreadCallback){ m_BeginInThreadCallback(); } while (true){ Consume(); } } 我们先看下这个线程函数做了什么,主要是m_BeginInThreadCallback和**Consume()函数,看下Consume()**函数: void Active::Consume(){ boost::mutex::scoped_lock lock(m_IOMutex); while(m_Queue.empty()){ m_ConditionVar.wait(lock); } m_SwapQueue.swap(m_Queue); lock.unlock(); while(!m_SwapQueue.empty()){ Buffer* pBuffer = m_SwapQueue.front(); m_SwapQueue.pop(); m_Callback(pBuffer); --m_PendingWorkNum; if (pBuffer){ m_pBufferPool.ReleaseObejct(pBuffer); } } } 这段代码很好理解,先使用条件变量挂起当前线程,条件变量触发后,如果消费者和生产者共有队列m_Queue中有数据,将公用的队列m_Queue临时倒换到本地的一个局部队列m_SwapQueue中,然后挨个处理队列m_SwapQueue中的数据。 这个线程在哪里创建的呢?通过搜索线程函数,我们找到如下代码: void Active::Start(){ bool ifHvTimer = !m_ThreadTimer.IsEmpty(); if (ifHvTimer){ m_Thread = boost::thread(&Active::RunWithUpdate, this); } else{ m_Thread = boost::thread(&Active::Run, this); } m_ThreadID = get_native_thread_id(m_Thread); char sThreadName[30]; sprintf(sThreadName, "%s-%d", "Actor-Run", GetActorID()); _SetThreadName(m_ThreadID, sThreadName); } 在上面这个函数中添加断点,重启下程序,很快会触发断点,我们看下断点触发时的调用堆栈:...

January 11, 2021

从零实现一个http服务器

从零实现一个http服务器 我始终觉得,天生的出身很重要,但后天的努力更加重要,所以如今的很多“科班”往往不如后天努力的“非科班”。所以,我们需要重新给“专业”和“专家”下一个定义:所谓专业,就是别人不搞你搞,这就是你的“专业”;你和别人同时搞,你比别人搞的好,就是“专家”。 说到http协议和http请求,很多人都知道,但是他们真的“知道”吗?我面试过很多求职者,一说到http协议,他们能滔滔不绝,然后我问他http协议的具体格式是啥样子的?很多人不清楚,不清楚就不清楚吧,他甚至能将http协议的头扯到html文档头部。当我问http GET和POST请求的时候,GET请求是什么形式一般人都可以答出来,但是POST请求的数据放在哪里,服务器如何识别和解析这些POST数据,很多人又说不清道不明了。当说到http服务器时,很多人离开了apache、Nginx这样现成的http server之外,自己实现一个http服务器无从下手,如果实际应用场景有需要使用到一些简单http请求时,使用apache、Nginx这样重量级的http服务器程序实在劳师动众,你可以尝试自己实现一个简单的。 上面提到的问题,如果您不能清晰地回答出来,可以阅读一下这篇文章,这篇文章在不仅介绍http的格式,同时带领大家从零实现一个简单的http服务器程序。 一、项目背景 最近很多朋友希望我的flamingo服务器支持http协议,我自己也想做一个微信小程序,小程序通过http协议连接通过我的flamingo服务器进行聊天。flamingo是一个开源的即时通讯软件,目前除了服务器端,还有pc端、android端,后面会支持更多的终端。关于flamingo的介绍您可以参考这里: https://github.com/baloonwj/flamingo,更新日志:https://github.com/baloonwj/flamingo/issues/1。下面是flamingo的部分截图: 二、http协议介绍 1. http协议是应用层协议,一般建立在tcp协议的基础之上(当然你的实现非要基于udp也是可以的),也就是说http协议的数据收发是通过tcp协议的。 2. http协议也分为head和body两部分,但是我们一般说的html中的和标记不是http协议的头和身体,它们都是http协议的body部分。 那么http协议的头到底长啥样子呢?我们来介绍一下http协议吧。 http协议的格式如下: GET或POST 请求的url路径(一般是去掉域名的路径) HTTP协议版本号\r\n 字段1名: 字段1值\r\n 字段2名: 字段2值\r\n … 字段n名 : 字段n值\r\n \r\n http协议包体内容 也就是说http协议由两部分组成:包头和包体,包头与包体之间使用一个\r\n分割,由于http协议包头的每一行都是以**\r\n结束,所以http协议包头一般以\r\n\r\n**结束。 举个例子,比如我们在浏览器中请求http://www.hootina.org/index_2013.php这个网址,这是一个典型的GET方法,浏览器组装的http数据包格式如下: GET /index_2013.php HTTP/1.1\r\n Host: www.hootina.org\r\n Connection: keep-alive\r\n Upgrade-Insecure-Requests: 1\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n Accept-Encoding: gzip, deflate\r\n Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n \r\n 上面这个请求只有包头没有包体,http协议的包体不是必须的,也就是说GET请求一般没有包体。 如果GET请求带参数,那么一般是附加在请求的url后面,参数与参数之间使用&分割,例如请求http://www.hootina.org/index_2013.php?param1=value1&param2=value2&param3=value3,我们看下这个请求组装的的http协议包格式: GET /index_2013.php?param1=value1&param2=value2&param3=value3 HTTP/1.1\r\n Host: www.hootina.org\r\n Connection: keep-alive\r\n Upgrade-Insecure-Requests: 1\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n Accept-Encoding: gzip, deflate\r\n Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n \r\n 对比一下,你现在知道http协议的GET参数放在协议包的什么位置了吧。 那么POST的数据放在什么位置呢?我们再12306网站https://kyfw.12306.cn/otn/login/init中登陆输入用户名和密码: 然后发现浏览器以POST方式组装了http协议包发送了我们的用户名、密码和其他一些信息,组装的包格式如下: POST /passport/web/login HTTP/1.1\r\n Host: kyfw....

January 11, 2021

从零实现一个邮件收发客户端

从零实现一个邮件收发客户端 与邮件收发有关的协议有 POP3、SMPT 和 IMAP 等。 POP3 POP3全称是 Post Office Protocol 3 ,即邮局协议的第 3 个版本,它规定怎样将个人计算机连接到 Internet 的邮件服务器和下载电子邮件的电子协议,它是因特网电子邮件的第一个离线协议标准,POP3 允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时删除保存在邮件服务器上的邮件,而 POP3 服务器则是遵循 POP3 协议的接收邮件服务器,用来接收电子邮件的。 SMTP SMTP 的全称是 Simple Mail Transfer Protocol,即简单邮件传输协议。它是一组用于从源地址到目的地址传输邮件的规范,它帮助每台计算机在发送或中转邮件时找到下一个目的地。SMTP 服务器就是遵循 SMTP 协议的发送邮件服务器。SMTP 需要认证,简单地说就是要求必须在提供了账户名和密码之后才可以登录 SMTP 服务器,这就使得那些垃圾邮件的散播者无可乘之机,使用户避免受到垃圾邮件的侵扰。 IMAP IMAP全称是 Internet Mail Access Protocol,即交互式邮件存取协议,它是跟 POP3 类似邮件访问标准协议之一。不同的是,开启了 IMAP 后,在电子邮件客户端收取的邮件仍然保留在服务器上,同时在客户端上的操作都会反馈到服务器上,如:删除邮件,标记已读等,服务器上的邮件也会做相应的动作。所以无论从浏览器登录邮箱或者客户端软件登录邮箱,看到的邮件以及状态都是一致的。而 POP3 对邮件的操作只会在本地邮件客户端起作用。 读者如果需要自己编写相关的邮件收发客户端,需要登录对应的邮件服务器开启相应的 POP3/SMTP/IMAP 服务。以 163 邮箱为例: 请登录 163 邮箱(http://mail.163.com/),点击页面正上方的“设置”,再点击左侧上“POP3/SMTP/IMAP”,其中“开启 SMTP 服务”是系统默认勾选开启的。读者可勾选图中另两个选项,点击确定,即可开启成功。不勾选图中两个选项,点击确定,可关闭成功。 网易163免费邮箱相关服务器信息: 163免费邮客户端设置的POP3、SMTP、IMAP地址 POP3、SMTP、IMAP 协议就是我们前面介绍的以指定字符(串)为包的结束标志的协议典型例子。我们来以 SMTP 协议和 POP3 协议为例来讲解一下。 SMTP 协议 先来介绍 SMTP 协议吧,SMTP 全称是 Simple Mail Transfer Protocol,即简单邮件传输协议,该协议用于发送邮件。 SMTP 协议的格式: 关键字 自定义内容\r\n “自定义内容”根据“关键字”的类型是否设置,对于使用 SMTP 作为客户端的一方常用的“关键字“如下所示: //连接上邮件服务器之后登录服务器之前向服务器发送的问候信息 HELO 自定义问候语\r\n //请求登录邮件服务器 AUTH LOGIN\r\n base64形式的用户名\r\n base64形式的密码\r\n //设置发件人邮箱地址 MAIL FROM:发件人地址\r\n //设置收件人地址,每次发送可设置一个收件人地址,如果有多个收件地址,要分别设置对应次数 rcpt to:收件人地址\r\n //发送邮件正文开始标志 DATA\r\n //发送邮件正文,注意邮件正文以.\r\n结束 邮件正文\r\n.\r\n //登出服务器 QUIT\r\n 使用 SMTP 作为邮件服务器的一方常用的“关键字“是定义的各种应答码,应答码后面可以带上自己的信息,然后以\r\n作为结束,格式如下:...

January 11, 2021

从零实现一款12306刷票软件

从零实现一款12306刷票软件 写在前面的话 每年逢年过节,一票难求读者肯定不陌生。这篇文章,我们带领读者从零实现一款12306刷票软件,其核心原理是通过发送http请求模拟登录12306网站的购票的过程,最后买到票。 关于http请求的格式和如何组装http数据包给服务器发送请求,我们在上一篇文章《从零实现一个http服务器》中已经详细介绍过了,如果还不明白的朋友可以去那篇文章看下。 郑重申明一下:这里介绍的技术仅供用于学习,不可用于恶意攻击12306服务器,请勿滥用本文介绍的技术。对12306服务器造成的任何损失,后果自负。 当然,由于12306服务器用户量巨大,为了防止黄牛和其他一些非法攻击者,12306的很多url和在购票过程中各个步骤的协议细节经常发生变化。所以,本文中介绍的一些具体的url,可能在你看到本文时已经失效。但是这并没有关系,只要你掌握了本文中介绍的分析方法,您就可以灵活地修改您的代码,以适应最新的12306服务器的要求。 举个例子,如12306的查票接口目前的url是: https://kyfw.12306.cn/otn/leftTicket/query 可能过几天就变成了 https://kyfw.12306.cn/otn/leftTicket/queryX 再过几天又可能变成 https://kyfw.12306.cn/otn/leftTicket/queryY 然后一个星期后又可能变成 https://kyfw.12306.cn/otn/leftTicket/queryZ 这些笔者都见过。所以,重在原理的学习,掌握了原理,不管12306的相关url变成什么样,都可以以不变应万变。哎,12306在与黄牛的斗争中越走越远啊T_T 本文将使用以下工具来分析12306购票的过程,然后使用C++语言,模拟相关的过程,最终购票。 Chrome浏览器(其他的浏览器也可以,都有类似的界面,如Chrome,装了httpwatch的IE浏览器等) 一个可以登录12306网址并且可以购票的12306账号 Visual Studio(版本随意,我这里用的是VS 2013) 一、查票与站点信息接口 之所以先分析这个接口,是因为查票不需要用户登录的,相对来说最简单。我们在Chrome浏览器中打开12306余票查询页面,网址是:https://kyfw.12306.cn/otn/leftTicket/init,如下图所示: 然后在页面中右键菜单中选择【检查】菜单,打开后,选择【网络】选项卡。如下图所示: 打开后页面变成二分窗口了,左侧是正常的网页页面,右侧是浏览器自带的控制台,当我们在左侧页面中进行操作后,右侧会显示我们浏览器发送的各种http请求和应答。我们这里随便查一个票吧,如查2018年5月20日从上海到北京的票,点击12306网页中【查询】按钮后,我们发现右侧是这样的: 通过图中列表的type值是xhr,我们可以得出这是一个ajax请求(ajax是一种浏览器原生支持的异步技术,具体细节请读者自行搜索)。我们选择这个请求,你能看到这个请求的细节——请求和响应结果: 在reponse中,我们可以看到我们的这个http的去除http头的响应结果(包体,可能是解压缩或者解码后的): 这是一个json格式,我们找个json格式化工具,把这个json格式化后贴出来给大家看一下,其实您后面会发现12306的http请求结果中与购票相关的数据基本上都是json格式。这里的json如下: { "validateMessagesShowId": "_validatorMessage", "status": true, "httpstatus": 200, "data": { "result": ["null|23:00-06:00系统维护时间|5l0000G10270|G102|AOH|VNP|AOH|VNP|06:26|12:29|06:03|IS_TIME_NOT_BUY|RLVVIt093U2EZuy2NE+VQyRloXyqTzFp6YyNk6J52QcHEA01|20180520|3|HZ|01|11|1|0|||||||||||1|有|13||O090M0|O9M|0",(内容太长,这里省略) "], "flag": "1", "map": { "AOH": "上海虹桥", "BJP": "北京", "VNP": "北京南", "SHH": "上海" } }, "messages": [], "validateMessages": {} } 其中含有的余票信息在result节点中,这是一个数组。每个节点以|分割,我们可以格式化后显示在自己的界面上: 我这里做的界面比较简陋,读者如果有兴趣可以做更精美的界面。我们列下这个请求发送的http数据包和应答包: 请求包: GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-20&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1 Host: kyfw.12306.cn Connection: keep-alive Cache-Control: no-cache Accept: */* X-Requested-With: XMLHttpRequest If-Modified-Since: 0 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36 Referer: https://kyfw.12306.cn/otn/leftTicket/init Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_fromDate=2018-05-20; _jc_save_toDate=2018-05-19; _jc_save_wfdc_flag=dc 应答包:...

January 11, 2021

从零开发一个WebSocket服务器

从零开发一个WebSocket服务器 WebSocket 协议是为了解决 http 协议的无状态、短连接(通常是)和服务端无法主动给客户端推送数据等问题而开发的新型协议,其通信基础也是基于 TCP。由于较旧的浏览器可能不支持 WebSocket 协议,所以使用 WebSocket 协议的通信双方在进行 TCP 三次握手之后,还要再额外地进行一次握手,这一次的握手通信双方的报文格式是基于 HTTP 协议改造的。 WebSocket 握手过程 TCP 三次握手的过程我们就不在这里赘述了,任何一本网络通信书籍上都有详细的介绍。我们这里来介绍一下 WebSocket 通信最后一次的握手过程。 握手开始后,一方给另外一方发送一个 http 协议格式的报文,这个报文格式大致如下: GET /realtime HTTP/1.1\r\n Host: 127.0.0.1:9989\r\n Connection: Upgrade\r\n Pragma: no-cache\r\n Cache-Control: no-cache\r\n User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\r\n Upgrade: websocket\r\n Origin: http://xyz.com\r\n Sec-WebSocket-Version: 13\r\n Accept-Encoding: gzip, deflate, br\r\n Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n Sec-WebSocket-Key: IqcAWodjyPDJuhGgZwkpKg==\r\n Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n \r\n 对这个格式有如下要求: 握手必须是一个有效的 HTTP 请求; 请求的方法必须为 GET,且 HTTP 版本必须是 1.1; 请求必须包含 Host 字段信息; 请求必须包含 Upgrade字段信息,值必须为 websocket; 请求必须包含 Connection 字段信息,值必须为 Upgrade; 请求必须包含 Sec-WebSocket-Key 字段,该字段值是客户端的标识编码成 base64 格式; 请求必须包含 Sec-WebSocket-Version 字段信息,值必须为 13; 请求必须包含 Origin 字段; 请求可能包含 Sec-WebSocket-Protocol 字段,规定子协议; 请求可能包含 Sec-WebSocket-Extensions字段规定协议扩展; 请求可能包含其他字段,如 cookie 等。 对端收到该数据包后如果支持 WebSocket 协议,会回复一个 http 格式的应答,这个应答报文的格式大致如下:...

January 11, 2021

作者的故事

作者的故事 我的 2019 我是如何年薪五十万的

January 11, 2021

你一定要搞明白的C函数调用方式与栈原理

你一定要搞明白的C函数调用方式与栈原理 写在前面的话 这绝对不是标题党。而是C/C++开发中你必须要掌握的基础知识,也是高级技术岗位面试中高频题。我真的真的真的希望无论是学生还是广大C/C++开发者,都该掌握此文中介绍的知识。 正文 这篇blog试图讲明当一个c函数被调用时,一个**栈帧(stack frame)**是如何被建立,又如何被消除的。这些细节跟操作系统平台及编译器的实现有关,下面的描述是针对运行在Intel奔腾芯片上Linux的gcc编译器而言。c语言的标准并没有描述实现的方式,所以,不同的编译器,处理器,操作系统都可能有自己的建立栈帧的方式。 一个典型的栈帧 图1是一个典型的栈帧,图中,栈顶在上,地址空间往下增长。 这是如下一个函数调用时的栈的内容: int foo(int arg1, int arg2, int arg3); 并且,foo有两个局部的int变量(4个字节)。在这个简化的场景中,main调用foo,而程序的控制仍在foo中。这里,main是调用者(caller),foo是被调用者(callee)。 ESP被foo使用来指示栈顶。EBP相当于一个“基准指针”。从main传递到foo的参数以及foo本身的局部变量都可以通过这个基准指针为参考,加上偏移量找到。 由于被调用者允许使用EAX,ECX和EDX寄存器,所以如果调用者希望保存这些寄存器的值,就必须在调用子函数之前显式地把他们保存在栈中。另一方面,如果除了上面提到的几个寄存器,被调用者还想使用别的寄存器,比如EBX,ESI和EDI,那么,被调用者就必须在栈中保存这些被额外使用的寄存器,并在调用返回前回复他们。也就是说,如果被调用者只使用约定的EAX,ECX和EDX寄存器,他们由调用者负责保存并回复,但如果被调用这还额外使用了别的寄存器,则必须有他们自己保存并回复这些寄存器的值。 传递给foo的参数被压到栈中,最后一个参数先进栈,所以第一个参数是位于栈顶的。foo中声明的局部变量以及函数执行过程中需要用到的一些临时变量也都存在栈中。 小于等于4个字节的返回值会被保存到EAX中,如果大于4字节,小于8字节,那么EDX也会被用来保存返回值。如果返回值占用的空间还要大,那么调用者会向被调用者传递一个额外的参数,这个额外的参数指向将要保存返回值的地址。用C语言来说,就是函数调用: x = foo(a, b, c); 被转化为: foo(&x, a, b, c); 注意,这仅仅在返回值占用大于8个字节时才发生。有的编译器不用EDX保存返回值,所以当返回值大于4个字节时,就用这种转换。 当然,并不是所有函数调用都直接赋值给一个变量,还可能是直接参与到某个表达式的计算中,如: m = foo(a, b, c) + foo(d, e, f); 有或者作为另外的函数的参数, 如: fooo(foo(a, b, c), 3); 这些情况下,foo的返回值会被保存在一个临时变量中参加后续的运算,所以,foo(a, b, c)还是可以被转化成foo(&tmp, a, b, c)。 让我们一步步地看一下在c函数调用过程中,一个栈帧是如何建立及消除的。 函数调用前调用者的动作 在我们的例子中,调用者是main,它准备调用函数foo。在函数调用前,main正在用ESP和EBP寄存器指示它自己的栈帧。 首先,main把EAX,ECX和EDX压栈。这是一个可选的步骤,只在这三个寄存器内容需要保留的时候执行此步骤。 接着,main把传递给foo的参数一一进栈,最后的参数最先进栈。例如,我们的函数调用是: a = foo(12, 15, 18); 相应的汇编语言指令是: push dword 18 push dword 15 push dword 12 最后,main用call指令调用子函数: call foo 当call指令执行的时候,EIP指令指针寄存器的内容被压入栈中。因为EIP寄存器是指向main中的下一条指令,所以现在返回地址就在栈顶了。在call指令执行完之后,下一个执行周期将从名为foo的标记处开始。 图2展示了call指令完成后栈的内容。图2及后续图中的粗线指示了函数调用前栈顶的位置。我们将会看到,当整个函数调用过程结束后,栈顶又回到了这个位置。 被调用者在函数调用后的动作 当函数foo,也就是被调用者取得程序的控制权,它必须做3件事:建立它自己的栈帧,为局部变量分配空间,最后,如果需要,保存寄存器EBX,ESI和EDI的值。 首先foo必须建立它自己的栈帧。EBP寄存器现在正指向main的栈帧中的某个位置,这个值必须被保留,因此,EBP进栈。然后ESP的内容赋值给了EBP。这使得函数的参数可以通过对EBP附加一个偏移量得到,而栈寄存器ESP便可以空出来做其他事情。如此一来,几乎所有的c函数都由如下两个指令开始: push ebp mov ebp, esp 此时的栈入图3所示。在这个场景中,第一个参数的地址是EBP加8,因为main的EBP和返回地址各在栈中占了4个字节。 ​ 下一步,foo必须为它的局部变量分配空间,同时,也必须为它可能用到的一些临时变量分配空间。比如,foo中的一些C语句可能包括复杂的表达式,其子表达式的中间值就必须得有地方存放。这些存放中间值的地方同城被称为临时的,因为他们可以为下一个复杂表达式所复用。为说明方便,我们假设我们的foo中有两个int类型(每个4字节)的局部变量,需要额外的12字节的临时存储空间。简单地把栈指针减去20便为这20个字节分配了空间: sub esp, 20 现在,局部变量和临时存储都可以通过基准指针EBP加偏移量找到了。 最后,如果foo用到EBX,ESI和EDI寄存器,则它f必须在栈里保存它们。结果,现在的栈如图4所示。 ​ foo的函数体现在可以执行了。这其中也许有进栈、出栈的动作,栈指针ESP也会上下移动,但EBP是保持不变的。这意味着我们可以一直用[EBP+8]找到第一个参数,而不管在函数中有多少进出栈的动作。 函数foo的执行也许还会调用别的函数,甚至递归地调用foo本身。然而,只要EBP寄存器在这些子调用返回时被恢复,就可以继续用EBP加上偏移量的方式访问实际参数,局部变量和临时存储。 被调用者返回前的动作 在把程序控制权返还给调用者前,被调用者foo必须先把返回值保存在EAX寄存器中。我们前面已经讨论过,当返回值占用多于4个或8个字节时,接收返回值的变量地址会作为一个额外的指针参数被传到函数中,而函数本身就不需要返回值了。这种情况下,被调用者直接通过内存拷贝把返回值直接拷贝到接收地址,从而省去了一次通过栈的中转拷贝。 其次,foo必须恢复EBX,ESI和EDI寄存器的值。如果这些寄存器被修改,正如我们前面所说,我们会在foo执行开始时把它们的原始值压入栈中。如果ESP寄存器指向如图4所示的正确位置,寄存器的原始值就可以出栈并恢复。可见,在foo函数的执行过程中正确地跟踪ESP是多么的重要————也就是说,进栈和出栈操作的次数必须保持平衡。 这两步之后,我们不再需要foo的局部变量和临时存储了,我们可以通过下面的指令消除栈帧:...

January 11, 2021

做 Java 或者 C++ 开发都应该知道的 lsof 命令

做 Java 或者 C++ 开发都应该知道的 lsof 命令 lsof 命令是 Linux 系统的扩展工具,它的含义是 list opened filedesciptor (列出已经打开的文件描述符),在 Linux 系统中,所有的与资源句柄相关的东西都可以统一抽象成文件描述符(filedescriptor,简称 fd)。一个文件句柄是一个 fd,一个 socket 对象也可以称之为 fd 等等。 默认情况下,系统是不存在这个命令的,你需要安装一下,使用如下命令安装: yum install lsof 我们来看一下这个命令的使用效果: COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME systemd 1 root cwd DIR 202,1 4096 2 / nscd 453 469 nscd 8u netlink 0t0 11017 ROUTE nscd 453 470 nscd cwd DIR 202,1 4096 2 / nscd 453 470 nscd rtd DIR 202,1 4096 2 / nscd 453 470 nscd txt REG 202,1 180272 146455 /usr/sbin/nscd nscd 453 470 nscd mem REG 202,1 217032 401548 /var/db/nscd/hosts nscd 453 470 nscd mem REG 202,1 90664 132818 /usr/lib64/libz....

January 11, 2021

利用 cmake 工具生成 Visual Studio 工程文件

利用 cmake 工具生成 Visual Studio 工程文件 对于习惯了 Visual Studio 强大的管理项目、编码和调试功能的读者来说,在 Linux 下使用 gcc/g++ 编译、使用 gdb 调试是一件何其痛苦的事情,对于大多数的开源 C/C++ 项目,如果我们不在意 Windows 和 Linux 在一些底层 API 接口上的使用差别,想熟悉该项目的执行脉络和原理,在 Windows 上使用 Visual Studio 调试该项目也未尝不可。凡是可以使用 CMake 工具编译的 Linux 程序(即提供了 CMakeLists.txt 文件),我们同样也可以利用 CMake 工具生成 Windows 上的 Visual Studio 工程文件。 这里我们以著名的开源网络库 libuv 为例。 从 libuv 的官方地址提供的下载链接:https://dist.libuv.org/dist/ 下载最新的 libuv 的源码得到文件 libuv-v1.31.0.tar.gz(笔者写作此书时,libuv 最新版本是 1.31.0),解压该文件。作者的机器上我将代码解压至 *F:\mycode\libuv-v1.31.0* ,解压后的目录中确实存在一个 CMakeLists.txt 文件,如下图所示: 启动 Windows 上的 CMake 图形化工具(cmake-gui),按下图进行设置: 设置完成之后,点击界面上的Configure 按钮,会提示 vsprojects 目录不存在,提示是否创建,我们点击 Yes 进行创建。 如果您的机器上安装了多个版本的Visual Studio,接下来会弹窗对话框让我们选择要生成的工程文件对应的 Visual Studio 版本号。读者可以根据自己的实际情况按需选择。我这里选择 Visual Studio 2019。 点击 Finish 按钮后开始启动 CMake 的检测和配置工作。等待一会儿,CMake 底部的输出框中提示 “Configuring Done” 表示配置工作已经完成。 接下来点击 Generate 按钮即可生成所选版本的 Visual Studio 工程文件,生成的文件位于 vsprojects 目录。 我们可以在界面上点击按钮 Open Project 按钮直接打开工程文件,也可以找到对应目录下的 libuv.sln 打开。...

January 11, 2021

利用 telnet 命令发电子邮件

利用 telnet 命令发电子邮件 telnet 命令是我们最常用的网络调试命令之一。如果你的机器上还没有安装 telnet 命令,可以使用如下命令安装一下: yum install telnet 如果一个服务程序对外开启了侦听服务,我们都可以使用 telnet ip port 来连接上去,例如: [root@localhost ~]# telnet 120.55.94.78 8888 Trying 120.55.94.78... Connected to 120.55.94.78. Escape character is '^]'. 如果不指定端口号,telnet 会使用默认 23 号端口。 反过来说,可以通过 telnet 命令去检测指定 ip 地址和端口号的侦听服务是否存在。知道这点很重要,我们可以利用这个去检测一个服务是否可以正常连接。举个例子,比如某次从某处得到一个代码下载地址,这是一个 svn 地址:svn://120.55.94.78/mycode/mybook。为了检测这个 svn 服务是否还能正常对外服务,我们可以先用 ping 命令去检测一下到达这个 ip:120.55.94.78 的网络是否畅通: [root@localhost ~]# ping 120.55.94.78 PING 120.55.94.78 (120.55.94.78) 56(84) bytes of data. 64 bytes from 120.55.94.78: icmp_seq=1 ttl=128 time=15.3 ms 64 bytes from 120.55.94.78: icmp_seq=2 ttl=128 time=14.3 ms 64 bytes from 120.55.94.78: icmp_seq=3 ttl=128 time=16.4 ms 64 bytes from 120.55.94.78: icmp_seq=4 ttl=128 time=16.1 ms 64 bytes from 120.55.94.78: icmp_seq=5 ttl=128 time=15.5 ms ^C --- 120.55.94.78 ping statistics --- 5 packets transmitted, 5 received, 0% packet loss, time 4007ms rtt min/avg/max/mdev = 14....

January 11, 2021