2023年10月

写在前面

今天笔者是来和大家讨论,对于“混乱”如何解决的问题,不知道大家工作中和学习中有没有这样的感受

【1】明明还有很多事情没做,但是却不知道从哪下得去手

【2】面对像山一样的工作,第一想法却是无能为力,不如摆烂

【3】工作的负荷和琐碎明显超出了自己的承受范围,感到力不从心

如此情况等等

笔者曾经就有过这样的情况,至少在考研复习的时候就是如此,笔者看着像山一样的资料,第一反应就是先睡一觉再起来处理吧,如此就是越睡时间越不够,越不够越想摆烂,如此恶性循环,那放到工作上也是一样的,我们作为码农,工作和学习确实没有显著的差别。

那么要怎么解决这些问题呢?

学会掌控一切

笔者也算一点历史爱好者,大家都知道清朝是一个权力高度集中的朝代,而且比起前朝的昏君,清十二帝更多的不是输在昏庸,而是输给了时代。现在中国的陆地面积是960万平方公里,实际上在割地赔款前的土地面积是1380万平方公里,那么笔者以前就好奇,一个人是如何掌控这么大一个国家政权的。

后来笔者翻阅历史材料才知道,原来清朝的皇帝有非常强大的情报系统,下面做了什么上面基本都知道,在雍正王朝里十三爷和太监李德全就有这么一段对话。

十三爷:我们在朝阳门码头的事情,皇阿玛知道了?

李德全:什么事能瞒得住咱皇上呢?

情报系统完善,皇帝清楚下面的动向,就能有应对的手段,皇权才能不被架空从而稳固,在封建主义的时代背景下,皇权稳固,国家政权才相对稳定。

那么政治问题在此不做过多的展开,但是我们从这就能看出,能够掌控工作恰恰就是应对自如的前提。

那么肯定有同学要问了,什么叫掌控工作,光掰扯理论我听不懂,很简单我给大家举一个例子,就你手上随便挑一份工作

你的领导,你的用户,你的同事针对你的工作问任何问题,你能否做到对答如流,而不是只是简单地说一句:我去看看。事实上,不知道同学们有没有发现,你在回答你去看看的时候,无形中不仅浪费了很多时间,有时候还会被领导和同事嘴碎几句,平白无故挨顿骂,这些都是会让同学们上班如上坟的因素。

就拿笔者之前开发的一个项目,就写过一份进度报告,事实上这个项目的用户不太给力,一会儿需求不提,一会儿进度延期,导致开发到现在还没完成。时隔数月一点进度都没有,后来有一天领导突然问起,为什么这个模块是空的,事实上时间久远,笔者如果没有记录的话早就忘了以为真没开发,但是笔者经常会翻进度报告,一下子就回答出来,这里根本还没提需求。从而避免了一场没有任何意义的误会和挨骂。

因此,同学们一定要学会列进度报告和工作清单,很多工作其实凭我们一己之力是没法完成的,经常需要用户,领导,同事配合,但是往往这些人会因为各种各样的原因无法提供帮助导致工作滞后,但是没有人会关心这些,他们只关心工作完没完成,工作搁置迁延日久,等到翻旧账的时候,只能重新花费时间成本去拾起来。所以同学们一定要记住,工作完不成不一定是你的问题,但是如果经常不清楚为什么完不成,那不是你的问题也是你的问题。

结论:工作中一定要对各个环节了如指掌,遇事能做到对答如流

手段解决问题


笔者在前面也提到了,能够掌控工作恰恰就是应对自如的前提,但是光有前提也不是个事,还要学会运用手段解决问题

还给大家举一个雍正王朝的案例:四爷,十三爷江南赈灾筹款

很多熟悉这部剧的同学应该了解,在第一集就给大家介绍了,黄河发大水,周边无粮可调,户部无款可拨,唯一的办法就是到富饶的江南筹款。但是筹款说起来简单,做起来谈何容易,江南可是四爷,十三爷的死对头大爷,八爷,九爷的地盘。势力盘根错节。两兄弟跑到江南,当地士绅各种拒不配合,胆子大的像江南巡盐道任伯安,甚至仗着后台,敢直接顶撞朝廷钦差。

事实上,四爷,十三爷很清楚当地的情况,这就已经做到了我们前面说的学会掌控一切。那么我们看看他们是怎么做的。

第一步:【缓解急症】打击敌对势力需要时间,但是灾民不等人,饿死的人多了,皇上就会责怪。因此选择“劫富济贫”,先强制富有人家保证灾民的吃喝问题。

第二步:【抓住把柄】巧在九爷这个时候雪中送炭,给任伯安寄了封信,正好被四爷十三爷截获【这里也显现出掌控工作的重要性,不然上哪截获去】,有了逼迫捐款的理由

第三步:【杀鸡儆猴】选择了大爷的门人,池州知府李淦,扣一顿罪名以后敲山震虎。告诉你任伯安,即便是大爷的门人,我照样不给面子,你还有把柄在我手里,你敢不给钱?真不给我把这事捅到皇上那里,你看九爷保不保你

同学们看到没有,这一套行云流水的操作,让一个烫手山芋,成功化险为安。

那么放回到我们的职场里,实际上也是一样的道理,同学们要达成目标就一定要学会运用手段,那肯定有同学要说了,我很多时候都是开发工作,大部分情况下一个人闷着头敲代码就行了,再不济我面向csdn编程,你扯这些人情世故的干啥。

那么笔者想回答同学们两件事

【1】首先,同学们有没有想过,你手里的技术其实也是手段。例如同样一个问题,用暴力算法和动态规划都可以解决,但是确实两种手段,你有没有想过最合适的是啥。笔者在前面的文章中就提过,笔者的工作风格是什么,就是以最小的代价换取最大限度的长治久安。我相信很多同学网上看过这样的奇葩案例,就是几百层的if条件嵌套,虽然这可能是杜撰出来的,但是这就是典型的昂贵的代价才换来短暂的平息的案例

【2】其次,笔者在前面也反复强调工程化思维,敲代码可能只占整个软件工程的两三成内容,甚至很多问题敲代码是一种解决方法,但是其实可能连代码都不用敲就可以解决,同学们不可能一辈子敲代码,早晚要成为项目经理,一人带一队人去工作的,到时候你可能会遇到产品经理,用户,领导,各种各样复杂的人,你确定没有任何人际关系嘛

结论:针对工作中的情况,要学会利用手段去解决

两者结合运用

那么说了这么半天,肯定很多同学要问具体怎么操作,笔者在这里给大家介绍一个自己总结的办法,当然只是参考,同学们可以根据自己的情况灵活变通

【1】整理好工作名称,列出工作目标,和对应辅助内容的清单以及内容。

这里等于是我们回到小学了,先说出我们要干什么,很多同学觉得很多余,甚至有些幼稚,一点都不幼稚,事实上很多同学工作了半天,有时候连怎么样算可以交差都不清楚,这点当然我们自己说了不算,不确定的要多和用户和领导确认。

例如你要做一个前端页面,那你可能的工作目标就是

  • 设计和开发用户界面,以提供良好的用户体验。
  • 与后端开发人员合作,实现前后端数据交互和功能集成。
  • 优化网站性能,提高页面加载速度和响应时间。
  • 跨浏览器和跨设备的兼容性测试和修复。

接下来你就要想办法,怎么把工作目标去实现,除了实打实的工作,你需要很多东西

像解决第一个问题,你可能就需要UI提供参考的设计稿,或者你自己得整理好理想中的设计稿

像解决第二个问题,
后端接口能够正确调用,你需要后端提供接口文档

有同学说,你这不废话吗,你不说我也知道,但是同学,请问每次我问你要什么文档你能第一时间找出来吗,还是要在电脑聊天记录里翻半天?

很多时候这些细节就会有很多问题

【2】整理进度报告,每一项工作都会有自己的进度报告,这个就没啥模板了,同学们自己根据自己的工作去制作

【3】根据需要制作辅助工具和辅助文件

这个就是手段,我相信大家平时在工作中多少也会有体会,为了工作会做一些文档来帮助整理思路

附注

总的来说,今天讲的内容事实上很多同学, 平时工作中或多或少都会接触到和涉及到,笔者只是帮助大家强化一下这个意识,告诉大家平时大家能够做到有条不紊的原因,那么今天就和大家聊到这里,希望笔者可以给大家带来一些帮助,笔者接下来会更加努力的工作,给大家带来更多的经验分享,希望同学们工作顺利,早日升职加薪、当上总经理、出任CEO、迎娶白富美、走上人生巅峰,想想是不是还有点小激动呢

1.简介

在实际自动化测试过程中,我们也避免不了会遇到下拉框选择的测试,因此宏哥在这里直接分享和介绍一下,希望小伙伴或者童鞋们在以后工作中遇到可以有所帮助。今天,我们讲下playwright的下拉框怎么处理,在使用selenium定位的过程中,我们可以选择使用selenium的Select类定位操作选择框(比较复杂),但是在playwright中真的炒鸡方便。

2.什么是下拉选择框

下拉框是一种常见的用户交互界面控件,一般用于向用户显示多项可选项,并从中让用户选择一个最佳答案。用户可以从下拉框内的给定列表中选择一项,从而输入对应内容,可以让Web设计师快速实现可空白集成以及简便操作,简化用户输入。

下拉框可以有不同的布局和表现形式。例如,普通的下拉框由复选框和滚动条组成,可以用来让用户在多个选择项中进行选择。也可以使用下拉框来处理大数据,使搜索变得更快。还有一种下拉框布局容纳输入框,提高用户输入效率。

下拉框有很多种优点。首先,它可以美化Web界面和节省空间,将多项选择以垂直形式呈现,节省空间。其次,它可以帮助保护用户免受错误输入,只能从列表内选择,从而避免用户输入错误的数据,如拼写错误的文本。此外,下拉框可以简化用户C(Control)操作,提高操作效率,更容易操作和反映用户意图。

更重要的是,下拉框可以帮助减少用户输入时间,并减少干扰,避免用户在全部文本选项中搜索。特别是在输入大量资料时,可以减少完成这项任务所需的时间,从而提高用户对网页的使用体验。

总之,下拉框在网页设计中经常使用,它具有很多优点,可以美化Web界面,提高用户的输入效率,减少用户的输入时间,帮助用户更好地控制后台系统,并降低错误录入的可能性。

3.Select用法

在Playwright中使用locator.select_option()选择元素中的一个或多个选项。我们可以指定选项value,或label选择并且可以选择多个选项。官方使用示例如下:

#Single selection matching the value
page.get_by_label('Choose a color').select_option('blue')#Single selection matching the label
page.get_by_label('Choose a color').select_option(label='Blue')#Multiple selected items
page.get_by_label('Choose multiple colors').select_option(['red', 'green', 'blue'])

3.1select元素示例

1.准备测试练习select.html,如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试Select</title>
    <style type="text/css">
        .button1 {
            background-color: #f44336; 
            border: none;
            color: white;
            padding: 15px 32px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 28px;
            margin-bottom: 100px;
            text-decoration:none;
            color: white;
        }
        #myAnchor
        {
          text-decoration:none;
          color: white;
        }
    </style>
</head>
<body>
 <button class="button1"><a id="myAnchor" href="https://www.cnblogs.com/du-hong/">北京-宏哥</a></button></br>
    快递邮寄地址:
    <select id="select_id" name="select_name" class ="select_cls">
        <option value="0">请选择</option>
        <option value="1">山西</option>
        <option value="2">陕西</option>
        <option value="3">山东</option>
        <option value="4">四川</option>
        <option value="5">河北</option>
    </select>省_XXX_市_ XXX_街道
</body>
</html>

2.页面效果,如下图所示:

3.2仿照官方示例

#single selection matching the value or label
element.select_option("1")#single selection matching the label
element.select_option(label="山东")#select_name selection for 0, 1 and second option
element.select_option(value=["0","1", "2", "3","4","5"])

3.3操作select选择框

3.3.1语法

第一种方法:通过page对象直接调用,如下:

page.select_option(selector,value)        #通过value选择
page.select_option(selector,index)        #通过index选择
page.select_option(selector,label)        #通过label选择

以上方法是:使用selector选择器,先定位元素

第一种通过value选择,顾名思义,可以通过我们的选择框的value元素进行选择
第二种通过index选择,意思是我们可以通过下标来选择
第三种通过label选择,意思是我们可以通过选项值来选择

第二种方法:先定位select元素,再定位选项,如下:

select = page.get_by_label("选择:")
select.select_option(label
="forth")

4.牛刀小试

4.1先定位select元素,再定位选项

首先宏哥准备一个测试demo的html,因为在线的不好找或者不满足要演示的要求。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试Select</title>
    <style type="text/css">.button1 {
background
-color: #f44336; border: none;
color: white;
padding: 15px 32px;
text
-align: center;
text
-decoration: none;
display: inline
-block;
font
-size: 28px;
margin
-bottom: 100px;
text
-decoration:none;
color: white;
}
#myAnchor {
text
-decoration:none;
color: white;
}
</style> </head> <body> <button class="button1"><a id="myAnchor" href="https://www.cnblogs.com/du-hong/">北京-宏哥</a></button></br> <label>快递邮寄地址:<select id="select_id" name="select_name" class ="select_cls"> <option value="0">请选择</option> <option value="1">山西</option> <option value="2">陕西</option> <option value="3">山东</option> <option value="4">四川</option> <option value="5">河北</option> </select>省_XXX_市_ XXX_街道</label> </body> </html>
4.1.1根据选项名称定位

1.参考代码


# coding=utf-8

Fiddler安装及汉化教程

一、下载安装

1.下载

官网链接:
https://www.telerik.com/download/fiddler

左侧填写用途,邮箱及城市,然后下载就可以

左侧下载即Download For Windows:Fiddler Classic(经典版),这个版本是免费的,不过只能在Windows上使用,但作为抓包工具完全够用

右侧下载: Fiddler Everywhere可以在所有平台使用,并且相当于Fiddler Classic+postman,但是是收费的。

2.安装

双击 FiddlerSetup.exe 文件进行安装,自定义路径,自动跳转该页面代表安装成功

或者在安装路径下,双击Fiddler.exe,能打开也代表安装成功

温馨提示:该软件不会创建快捷方式,需自己创建(。・ω・。)ノ♡

二、汉化教程

1.下载链接(任选其一)

夸克连接:Fiddler菜单汉化

百度网盘链接:
https://pan.baidu.com/s/1CbN41sLjr5RRRL5OYX2fFQ?pwd=frid

2.汉化完整过程

  1. 安装Fiddler英文原版(已安装的,不需再次安装)
  2. 比如,我将Fiddler安装在:D:\software\Fiddler
  3. 将【fiddler汉化】文件夹中的FiddlerTexts.txt复制到D:\software\Fiddler\
  4. 将【fiddler汉化】文件夹中的FdToChinese.dll复制到D:\software\Fiddler\Scripts\
  5. 重启Fiddler,菜单栏常用功能菜单已经被汉化了。需切换为英文则将上面两个文件移除即可。
  6. 打开【fiddler汉化】文件夹中的FiddlerTexts.txt文件,可以看到以&格式命名的,就是汉化前的选择项。有兴趣的小伙伴可以尝试一下其他未汉化的部分。

汉化文章来源:fiddler菜单汉化

三、抓包

1.基础操作

1.1 抓取请求

  • 界面左侧Web Sessions会话列表中的是HTTP数据包。(1)
  • 界面右侧Inspectors用于查看会话的内容,上边是Request请求信息,下边是Response响应信息。(2)
  • 左下角空白处点击变成Capturing会开始抓包 (3) (4)
    • ALL Processes抓取所有包
    • Web Browsers只抓取PC中浏览器的包
    • Non-Browser抓取非浏览器的包
    • Hide All隐藏所有代理
      • 代理手机时,Capturing无论是否点击,都会自动抓包,抓取想要的包后,可点此隐藏其他抓包
  • 字段说明
    • 名称 含义
      # 抓取HTTP Request的顺序,从1开始,以此递增
      Result HTTP状态码
      Protocol 请求使用的协议,如HTTP/HTTPS/FTP等
      Host 请求地址的主机名
      URL 请求资源的位置
      Body 该请求的大小
      Caching 请求的缓存过期时间或者缓存控制值
      Content-Type 请求响应的类型
      Process 发送此请求的进程:进程ID
      Comments 允许用户为此回话添加备注
      Custom 允许用户设置自定义值
      图标 含义
      01 请求已经发往服务器
      02 已从服务器下载响应结果
      03 请求从断点处暂停
      04 响应从断点处暂停
      05 请求使用 HTTP 的 HEAD 方法,即响应没有内容(Body)
      06 请求使用 HTTP 的 POST 方法
      07 请求使用 HTTP 的 CONNECT 方法,使用 HTTPS 协议建立连接隧道
      08 响应是 HTML 格式
      09 响应是一张图片
      10 响应是脚本格式
      11 响应是 CSS 格式
      12 响应是 XML 格式
      13 响应是 JSON 格式
      14 响应是一个音频文件
      15 响应是一个视频文件
      16 响应是一个 SilverLight
      17 响应是一个 FLASH
      18 响应是一个字体
      19 普通响应成功
      20 响应是 HTTP/300、301、302、303 或 307 重定向
      21 响应是 HTTP/304(无变更):使用缓存文件
      22 响应需要客户端证书验证
      23 服务端错误
      24 会话被客户端、Fiddler 或者服务端终止

1.2 删除全部请求

  • 方法1:点击工具栏中的×,删除请求。
  • 方法2:session列表下的黑框QuickExec中输入cls或clear删除请求。
  • 快捷键Ctrl+x

1.3 过滤请求

本人不是太理解这部分,使用中需自己摸索
  • 点击右侧选项卡中的Filters
  • 勾选User Filters
  • Host Filter选择Show only the following Hosts
  • 在下方框中填入想要过滤查看的主机地址,以“;”分隔。
  • 点击Actions,选择Run filterset now

1.4 抓取HTTPS ★★★★★

(重要部分,都标星了,你不看不怨我)

Fiddler 软件默认只抓取HTTP协议的网页,想抓取HTTPS则需要:

  • 打开Tools–Options–HTTPS选项卡

  • 勾选Capture HTTPS CONNECTs

  • 勾选Decrypt HTTPS traffic

  • 勾选Ignore server certificate errors(unsafe)忽略证书(如果不安装fiddler证书就如此)

点击OK保存。

弹出对话框“SCARY TEXT AHEAD:Read Carefully!”,点击YES。

弹出对话框“安全警告”,询问是否安装证书,点击是。

弹出对话框“Add certificate to the Machine Root List?”,点击YES。

弹出对话框“TrustCert Success”,点击确定。

再点击一下options中的ok,以防忘记保存配置。

  • Decrypt HTTPS traffic中的选项说明:
    from all processes :
    抓取所有的 https 程序, 包括电脑程序和手机APP。
    from browsers only :
    只抓取浏览器中的https请求。
    from non-browsers only :
    只抓取除了浏览器之外的所有https请求。
    from remote clients only:
    只抓取远程的客户端的https请求,就是只抓取手机APP上的https请求。

  • 注意事项:
    如果HTTPS请求出问题,例如,浏览器提示“您的链接不是私密链接”等,一般都是证书安装有问题,重新安装一遍证书,重复一遍HTTPS配置即可。

    Options——HTTPS——Actions——Trust Root Certificate。
    

2.移动端抓包

  • 手机与电脑必须在同一个局域网:
    1.手机和电脑连同一个WiFi
    2.手机连WiFi,电脑用网线连接开启这个WiFi的无线路由
    3.电脑开热点,手机连热点
  • 查看你的本机IP地址,在Fiddler的右上角有一个Online按钮,点击一下会显示你的IP信息
  • 配置连接信息:Tools > Options >Connections
    • 端口默认是8888,你可以进行修改。
    • 勾选Allow remote computers to connect选项,然后重启Fiddler,再次打开时会弹出一个信息,选择ok即可。

  • 在移动端连接wifi,长按选择修改网络,输入密码后往下拖动,然后勾选显示高级选项,然后在代理一栏选择手动,再将你先前查看的IP地址和端口号输入进去,然后保存。
  • 最后安装手机证书,在手机浏览器一栏输入电脑的IP地址和端口号
    这里我是192.168.1.157:8888
    进入一个网页,点击最下面那个FiddlerRoot certificate下载证书,下载成功后在设置里面安装
    • 安装步骤:打开高级设置->安全->从SD卡安装证书->找到证书文件->点击后为证书命名点击确定即可安装成功
    • (安装方式不同设备会有区别,可以自己试探或者上网找教程,如果不能安装显示不能读取证书可以试试去设置里搜索CA证书,验证密码后安装)

3.Fiddler内置命令与断点

FIddler断点功能就是将请求截获下来,但是不发送,此时可以做一些更改操作。

  • QuickExec命令
    命令 对应请求项 介绍 示例
    ? All 问号后边跟一个字符串,可以匹配出包含这个字符串的请求 ?google
    > Body 大于号后面跟一个数字,可以匹配出请求大小,大于这个数字请求 >1000
    < Body 小于号跟大于号相反,匹配出请求大小,小于这个数字的请求 <100
    = Result 等于号后面跟数字,可以匹配HTTP返回码 =200
    @ Host @后面跟Host,可以匹配域名 @www.baidu.com
    selec Content-Type select后面跟响应类型,可以匹配到相关的类型 select image
    cls All 清空当前所有请求 cls
    dump All 将所有请求打包成saz压缩包,保存到“我的文档\Fiddler2\Captures”目录下 dump
    start All 开始监听请求 start
    stop All 停止监听请求 stop
  • 断点命令
    bpafter All bpafter后边跟一个字符串,表示中断所有包含该字符串的请求 bpafter baidu(输入bpafter解除断点)
    bpu All 跟bpafter差不多,只不过这个是收到请求了,中断响应 bpu baidu(输入bpu解除断点)
    bps Result 后面跟状态吗,表示中断所有是这个状态码的请求 bps 200(输入bps解除断点)
    bpv / bpm HTTP方法 只中断HTTP方法的命令,HTTP方法如POST、GET bpv get(输入bpv解除断点)
    g / go All 放行所有中断下来的请求 g

四、问题解决

1.Fiddler启动后Chrome浏览器无法浏览网页?

打开Tools–Options–HTTPS选项卡;

点击Actions下拉选择Trust Root Certificate,弹框选yes,弹框选是(到这一步可能就解决了);

点击Actions下拉选择Export Root Certificate to Desktop,将fiddler证书导出到桌面;

打开谷歌浏览器,设置–高级–安全–管理证书;

导入证书

重启浏览器与fiddler。

2.如何屏蔽抓取特定端口?

在Fiddler中使用它,用Ctrl+R打开自定义规则,然后添加到OnBeforeRequest。

if (oSession.host=="localhost:9090"){
    oSession["ui-hide"] = "true";
}

一、简介
今天是 Net 高级调试的第二篇文章,第一篇文章记录了自己学习 Net 高级调试的第一步,认识一些调试工具,有了工具的倚仗,我们开始仗剑走天涯了,开始Net 高级调试正式的征程了。我先说一下,我的文章,【调试测试】这部分一般分为两个部分,第一部分是要用到的所有测试代码样例,也为大家提供方便,我第一次做测试还是走了不少弯路的。第二部分,就是使用 Windbg 调试器调试代码的部分,但是,需要说明一下,使用 Windbg还是有一些技巧的,或者说是方法的,如果大家不熟悉,建议提前熟悉一下,因为我的测试过程,不会把所有的过程都照搬下来,会省略一下不太重要的步骤,但是,如果是第一次使用这个软件的,调试的时候,得到的结果可能就和我的不一样,这也是我的一步一步的、痛苦的经验。
如果在没有说明的情况下,所有代码的测试环境都是 Net Framewok 4.8,但是,有时候为了查看源码,可能需要使用 Net Core 的项目,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。
调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10
调试工具:Windbg Preview(可以去Microsoft Store 去下载)
开发工具:Visual Studio 2022
Net 版本:Net Framework 4.8
CoreCLR源码:
源码下载

二、相关概念

1、Net 框架
Net 是一个虚拟的运行时环境,包含了一个虚拟的执行引擎(CLR)和一组相关的框架类库,如图:

1.1、宏观概念

a)、ECMA
C# 语言和公共语言基础结构 (CLI) 规范通过 Ecma International® 进行标准化。用通俗的话来说,ECMA是一个标准,它就是一个CLR的开发规范,或者说是一个设计文档。
https://learn.microsoft.com/zh-cn/dotnet/fundamentals/standards

b)、CLR
公共语言运行时。是我们 C#,VB.Net,F#的运行时环境,当然,这也是高级调试要关注的部分。
CLR 处理内存分配和管理。
CLR 也是一种虚拟机,不仅可执行应用,还可使用 JIT 编译器快速生成和编译代码。
最后,我们总结一下,CLR是针对 ECMA 标准的落地实现。

c)、NET 框架
NET框架有很多,比如:WPF,WinForm,WebForm,Mvc,WebAPI 等。

d)、Net应用程序
NET 应用程序,更多的指的是用户编写的应用程序,比如:基于 Winform 的ERP,基于 MVC、API 实现的网站系统。

1.2、Net程序的编译过程

Net程序的编译一般分为两个阶段,第一个阶段就是编译器编译,将C# 源码编译成为 IL 代码,第二个阶段就是 JIT 编译,将 IL 代码编译成为可以直接运行的机器代码。
a)、编译器编译
将我们的C#、VB.Net、F#等源码使用 Visual Studio,或者是 CSC 等类似的工具转换为 IL 代码。当然 IL 代码是不能直接运行的。
当然,IL 代码也是可以看到的,我们可以使用 ILSpy,或者DnSpy工具,加载相应的程序集,就可以查看了,很简单,就不细说了。

b)、JIT编译
CLR 运行时会将 IL 代码转换成 机器代码。

流程如下:【C# 源码】======》【编译器】=======》【Net 程序集(Exe或者Dll)】=====》【JIT即时编译(CLR)】=====》【机器代码】


2、PE头及Windows 加载器

2.1、什么是PE文件
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件,它的一个非常大的作用就是帮助 Windows 加载器 执行程序的入口。
对于 Net 的 PE 文件,有几点需要注意:
a)、AddressOfEntryPoint
程序的入口点相对偏移地址,即(exe+AddressOfEntryPoint)。


b)、DIRECTORY_ENTRY_COM_DESCRIPTOR
Net 程序独有的节点配置。


ILSpy 查看 Example_2_1_1.exe 的元数据。


c)、EntryPointToken
这个标签的地址,就是我们程序 Program.Main 方法的入口点地址。
IL 代码里面也是有标记的。


2.2、小知识
Windbg 有一个伪寄存器命令 ?  $exentry,可以直接告诉我们 exe 程序的入口点地址。

3、应用程序域



3.1、简介


对于 Windows 上的应用程序,大家都知道是按照【进程】进行隔离的

Net 将这种进程隔离缩小到了【应用程序域】层,即一个进程会有多个【应用程序域】,然后将应用程序部署在【应用程序域】上。


在 CLR 上,应用程序域分为三类,分别是:SystemDomain、SharedDomain、Domain1。当然,这是说的在 Net Framework 的情况下,在 Net Core 框架下,只有两个应用程序域,风别是:SystemDomain、Domain1,去掉了 SharedDomain 这个应用程序域。




3.2、应用程序域
a)、SystemDomain

系统及作用域,用于创建其他作用域。


将 mscorlib.dll 加载到 SharedDomain 共享及应用程序域。
记录字符串池中字符串常量。

初始化特定异常(OutOfMemoryException、StackOverflowException)。

b)、SharedDomain

加载 System 命名空间下的基本类型(String,Enum,ValueType)



c)、Domain1


用户的应用程序都是在这个域中运行。

三、调试测试
这个章节里,很简单,一共分为两个部分,第一部分是要用到的测试代码的样例,第二部分,就是具体的测试操作过程,说明一下,假设大家对 Windbg 有些熟悉。

1、测试代码

1.1、代码样例1

1 namespaceExample_2_1_12 {3     internal classProgram4 {5         static void Main(string[] args)6 {7             Console.WriteLine("Hello World");8 Console.ReadLine();9 }10 }11 }


2、调试过程
2.1、验证 CLR 和 JIT 的 存在。
验证代码:
Example_2_1_1
操作描述:编译
Example_2_1_1
项目,打开 Windbg,通过【launch executable】加载我们的程序集。当我们成功加载程序集,还必须通过【g】命令,或者【Go】按钮执行程序,这个时候,才能加载所有的东西。当我们运行完之后,就能看到运行界面,就可以看到和 CLR 和 JIT 有关的东西。红色字体表明加载了 CLR 和 JIT 两个组件。

1 0:000>g2 ModLoad: 74ec0000 74f39000   C:\Windows\SysWOW64\ADVAPI32.dll3 ModLoad: 771f0000 772af000   C:\Windows\SysWOW64\msvcrt.dll4 ModLoad: 757e0000 75855000C:\Windows\SysWOW64\sechost.dll5 ModLoad: 753c0000 7547a000   C:\Windows\SysWOW64\RPCRT4.dll6 ModLoad: 711c0000 7124d000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscoreei.dll7 ModLoad: 771a0000 771e5000   C:\Windows\SysWOW64\SHLWAPI.dll8 ModLoad: 757d0000 757df000   C:\Windows\SysWOW64\kernel.appcore.dll9 ModLoad: 74eb0000 74eb8000   C:\Windows\SysWOW64\VERSION.dll10 ModLoad: 70a10000 711c0000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll(CLR组件,加载的起始地址:70a1000011 ModLoad: 7587000075a04000   C:\Windows\SysWOW64\USER32.dll12 ModLoad: 77070000 77088000C:\Windows\SysWOW64\win32u.dll13 ModLoad: 70940000709eb000   C:\Windows\SysWOW64\ucrtbase_clr0400.dll14 ModLoad: 75fb0000 75fd3000   C:\Windows\SysWOW64\GDI32.dll15 ModLoad: 709f0000 70a04000   C:\Windows\SysWOW64\VCRUNTIME140_CLR0400.dll16 ModLoad: 750c0000 7519b000   C:\Windows\SysWOW64\gdi32full.dll17 ModLoad: 763900007640b000   C:\Windows\SysWOW64\msvcp_win.dll18 ModLoad: 76666660000 75670000C:\Windows\SysWOW64\ucrtbase.dll19 ModLoad: 74fa0000 74fc5000   C:\Windows\SysWOW64\IMM32.DLL20 ModLoad: 75ff0000 76270000C:\Windows\SysWOW64\combase.dll21 (7fc.b18): Unknown exception - code 04242420(first chance)22 ModLoad: 6f530000 7093e000   C:\Windows\assembly\NativeImages_v4.0.30319_32\mscorlib\218db16dceaef380c6daf35c6a48f313\mscorlib.ni.dll23 ModLoad: 76490000 76573000C:\Windows\SysWOW64\ole32.dll24 ModLoad: 75ff0000 76270000C:\Windows\SysWOW64\combase.dll25 ModLoad: 752a0000 752fc000   C:\Windows\SysWOW64\bcryptPrimitives.dll26 ModLoad: 6f4a0000 6f52a000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll(这个就是JIT编译器组件,在进程中的起始地址:6f4a000027 ModLoad: 751a0000 7523b000   C:\Windows\SysWOW64\OLEAUT32.dll


2.2、证明 Windows 加载器如何加载一个 Net 的程序集。
验证代码:
Example_2_1_1
PPEE是绿色软件,不用安装,直接下载就可以使用。打开 PPEE 软件,打开
Example_2_1_1
.exe,就可以看到 PE 文件。
当我们双加一个 Net 的Exe应用程序的时候,操作系统会做很多工作,比如:在内核态生成进行的地址空间,地址空间生成成功后,然后在生成一个Process 的进程,再给这个进程生成一个主线程,在内核态还要针对进程生成 EProcess 的数据结构,针对线程生成一个 ETHREAD 的数据结构。当所有的准备工作都完成后,要开始执行程序,从哪里开始执行程序呢?我们来证明一下,或者说熟悉一下,这个过程就和 PE 头文件有关了。


a、Windows 加载器会读取 PE 文件头里面的数据,来确定从哪里开始执行,第一步,我们通过 PPEE 查看
Example_2_1_1.exe
PE 文件,在 PE 头里依次点击【NT Header】-->【Optional Header】,在窗体的右侧就可以看到,有一项【AddressOfEntryPoint】,它的值是:00002782,这个地址是相对地址,是相对程序进程起始地址来说的。
如图:

我们有了入口程序的相对起始地址,我们找一下应用程序的进程起始地址,二者相加,就是 Windows 加载器要执行的地址。想要查看
Example_2_1_1.exe
进程地址,需要借助 Windbg,红色部分就是
Example_2_1_1.exe
进程的起始地址。

代码如下:

1 Executable search path is:2 ModLoad: 00ca0000 00ca8000   Example_2_1_1.exe3 ModLoad: 770d0000 77272000ntdll.dll4 ModLoad: 71050000710a2000   C:\Windows\SysWOW64\MSCOREE.DLL5 ModLoad: 74cc0000 74db0000   C:\Windows\SysWOW64\KERNEL32.dll6 ModLoad: 767a0000 769b3000   C:\Windows\SysWOW64\KERNELBASE.dll

二者相加就是WIndows 加载器的入口点地址,还不是我们的 Program.Main的地址,00ca0000(Example_2_1_1进程起始地址),
00002782 是 PE 头告诉的入口点地址,我们通过 U 命令,可以查看汇编代码
。通过代码我们可以看到,执行了 jmp 指令,跳转的地址是:
402000h

1 0:000> u 00ca0000+00002782
2 Example_2_1_1!COM+_Entry_Point <PERF> (Example_2_1_1+0x2782(PE 头里的值)):3 00ca2782 ff2500204000    jmp(执行跳转指令)     dword ptr ds:[402000h]4 Example_2_1_1!COM+_Entry_Point <PERF> (Example_2_1_1+0x2788):5 00ca2788 0000            add     byteptr [eax],al6 Example_2_1_1!COM+_Entry_Point <PERF> (Example_2_1_1+0x278a):7 00ca278a 0000            add     byteptr [eax],al8 Example_2_1_1!COM+_Entry_Point <PERF> (Example_2_1_1+0x278c):9 00ca278c 0000            add     byteptr [eax],al10 Example_2_1_1!COM+_Entry_Point <PERF> (Example_2_1_1+0x278e):11 00ca278e 0000            add     byteptr [eax],al12 Example_2_1_1!COM+_Entry_Point <PERF> (Example_2_1_1+0x2790):13 00ca2790 0000            add     byteptr [eax],al14 Example_2_1_1!COM+_Entry_Point <PERF> (Example_2_1_1+0x2792):15 00ca2792 0000            add     byteptr [eax],al16 Example_2_1_1!COM+_Entry_Point <PERF> (Example_2_1_1+0x2794):17 00ca2794 0000            add     byte ptr [eax],al

接下来,我们看看
402000h
这个地址有什么东西。

再次执行 Windbg,重新加载 Example_2_1_1.exe,通过【g】命令继续运行,暂停时,【break】开始调试状态,必须切换到主线程,也就是 0号线程。

1 //g 继续运行
2 0:000>g3 ModLoad: 76c30000 76ca9000   C:\Windows\SysWOW64\ADVAPI32.dll4 ModLoad: 765b0000 7666f000   C:\Windows\SysWOW64\msvcrt.dll5 ModLoad: 76e30000 76ea5000   C:\Windows\SysWOW64\sechost.dll6 ModLoad: 75c40000 75cfa000   C:\Windows\SysWOW64\RPCRT4.dll7 ModLoad: 70fc0000 7104d000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscoreei.dll8 ModLoad: 75d20000 75d65000   C:\Windows\SysWOW64\SHLWAPI.dll9 ModLoad: 755300007553f000   C:\Windows\SysWOW64\kernel.appcore.dll10 ModLoad: 74cb0000 74cb8000   C:\Windows\SysWOW64\VERSION.dll11 ModLoad: 70810000 70fc0000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll12 ModLoad: 76410000765a4000   C:\Windows\SysWOW64\USER32.dll13 ModLoad: 707f0000 70804000C:\Windows\SysWOW64\VCRUNTIME140_CLR0400.dll14 ModLoad: 76180000 76198000C:\Windows\SysWOW64\win32u.dll15 ModLoad: 70740000707eb000   C:\Windows\SysWOW64\ucrtbase_clr0400.dll16 ModLoad: 75b80000 75ba3000   C:\Windows\SysWOW64\GDI32.dll17 ModLoad: 766700007674b000   C:\Windows\SysWOW64\gdi32full.dll18 ModLoad: 76b30000 76bab000   C:\Windows\SysWOW64\msvcp_win.dll19 ModLoad: 76666660000 75670000C:\Windows\SysWOW64\ucrtbase.dll20 ModLoad: 754f0000 75515000C:\Windows\SysWOW64\IMM32.DLL21 Breakpoints 3 and 0match22 ModLoad: 75f00000 76180000C:\Windows\SysWOW64\combase.dll23 (3624.36d8): Unknown exception - code 04242420(first chance)24 ModLoad: 6f330000 7073e000   C:\Windows\assembly\NativeImages_v4.0.30319_32\mscorlib\218db16dceaef380c6daf35c6a48f313\mscorlib.ni.dll25 ModLoad: 76cb0000 76d93000   C:\Windows\SysWOW64\ole32.dll26 ModLoad: 75f00000 76180000C:\Windows\SysWOW64\combase.dll27 ModLoad: 76fa0000 76ffc000   C:\Windows\SysWOW64\bcryptPrimitives.dll28 ModLoad: 6f2a0000 6f32a000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll29 ModLoad: 76eb0000 76f4b000   C:\Windows\SysWOW64\OLEAUT32.dll30 (3624.20f0): Break instruction exception - code 80000003(first chance)31 eax=0106b000 ebx=00000000 ecx=7717cee0 edx=7717cee0 esi=7717cee0 edi=7717cee032 eip=77143410 esp=05a2fa9c ebp=05a2fac8 iopl=0nv up ei pl zr na pe nc33 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
34 ntdll!DbgBreakPoint:35 77143410 cc              int     3
36 
37 //切换到主线程
38 0:006> ~0s39 eax=00000000 ebx=0000009c ecx=00000000 edx=00000000 esi=0138ee3c edi=00000000
40 eip=771410fc esp=0138ed24 ebp=0138ed84 iopl=0nv up ei pl nz na pe nc41 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
42 ntdll!NtReadFile+0xc:43 771410fc c22400          ret     24h

我们通过【k】命令,查看显示调用栈

1 0:000>k2 # ChildEBP RetAddr3 00 0138ed84 768af25c     ntdll!NtReadFile+0xc
4 01 0138ed84 6f7e9b71     KERNELBASE!ReadFile+0xec
5 02 0138edf4 6ff1b275     mscorlib_ni+0x4b9b71
6 03 0138ee20 6ff1b17b     mscorlib_ni!System.IO.__ConsoleStream.ReadFileNative+0x89 [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 205]7 04 0138ee4c 6f7ce6a3     mscorlib_ni!System.IO.__ConsoleStream.Read+0x9f [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 134]8 05 0138ee64 6f7ceb5b     mscorlib_ni!System.IO.StreamReader.ReadBuffer+0x33 [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 595]9 06 0138ee80 70063786     mscorlib_ni!System.IO.StreamReader.ReadLine+0xe3 [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 748]10 07 0138ee90 6fec1845     mscorlib_ni!System.IO.TextReader.SyncTextReader.ReadLine+0x1a [f:\dd\ndp\clr\src\BCL\system\io\textreader.cs @ 363]11 08 0138ee98 03250876     mscorlib_ni!System.Console.ReadLine+0x15 [f:\dd\ndp\clr\src\BCL\system\console.cs @ 1984]12 WARNING: Frame IP not inany known module. Following frames may be wrong.13 09 0138eea8 7081f036     0x3250876
14 0a 0138eeb4 708222da     clr!CallDescrWorkerInternal+0x34
15 0b 0138ef08 7082859b     clr!CallDescrWorkerWithHandler+0x6b
16 0c 0138ef7c 709cb11b     clr!MethodDescCallSite::CallTargetWorker+0x16a
17 0d 0138f0a0 709cb7fa     clr!RunMain+0x1b3
18 0e 0138f30c 709cb727     clr!Assembly::ExecuteMainMethod+0xf7
19 0f 0138f7f0 709cb8a8     clr!SystemDomain::ExecuteMainMethod+0x5ef
20 10 0138f848 709cb9ce     clr!ExecuteEXE+0x4c
21 11 0138f888 709c7305     clr!_CorExeMainInternal+0xdc
22 12 0138f8c4 70fcfa84     clr!_CorExeMain+0x4d
23 13 0138f8fc 7105e81e     mscoreei!_CorExeMain+0xd6
24 14 0138f90c 71064338     MSCOREE!ShellShim__CorExeMain+0x9e
25 15 0138f924 74cdf989     MSCOREE!_CorExeMain_Exported+0x8
26 16 0138f924 77137084     KERNEL32!BaseThreadInitThunk+0x19
27 17 0138f980 77137054     ntdll!__RtlUserThreadStart+0x2f
28 18 0138f990 00000000     ntdll!_RtlUserThreadStart+0x1b

以上就是线程的调用栈,我们查找一下 _CorExeMain,这个方法,可以在 PPEE 软件里,在查看 Example_2_1_1.exe 的PE 头文件 DIRECTORY_ENTRY_IAT 里,效果如图:

接着上面的说,我们在【k】命令的结果中查找 _CorExeMain 方法。这里的执行结果是一部分,红色部分是重点,
MSCOREE 就是 mscoree.dll

1 0a 0138eeb4 708222da     clr!CallDescrWorkerInternal+0x34
2 0b 0138ef08 7082859b     clr!CallDescrWorkerWithHandler+0x6b
3 0c 0138ef7c 709cb11b     clr!MethodDescCallSite::CallTargetWorker+0x16a
4 0d 0138f0a0 709cb7fa     clr!RunMain+0x1b3(运行 Main 方法)
5 0e 0138f30c 709cb727     clr!Assembly::ExecuteMainMethod+0xf7(加载必须的 dll 程序集)
6 0f 0138f7f0 709cb8a8     clr!SystemDomain::ExecuteMainMethod+0x5ef(初始化系统程序域)
7 10 0138f848 709cb9ce     clr!ExecuteEXE+0x4c(开始执行 exe)
8 11 0138f888 709c7305     clr!_CorExeMainInternal+0xdc
9 12 0138f8c4 70fcfa84     clr!_CorExeMain+0x4d(从这里开始执行入口地址的方法)
10 13 0138f8fc 7105e81e     mscoreei!_CorExeMain+0xd6(加载 CLR)
11 14 0138f90c 71064338     MSCOREE!ShellShim__CorExeMain+0x9e
12 15 0138f924 74cdf989     MSCOREE!_CorExeMain_Exported+0x8(这里就是在 PPEE 看到的入口地址 AddressOfEntryPoint:00002782)
13 16 0138f924 77137084     KERNEL32!BaseThreadInitThunk+0x19
14 17 0138f980 77137054     ntdll!__RtlUserThreadStart+0x2f
15 18 0138f990 00000000     ntdll!_RtlUserThreadStart+0x1b(ntdll是windows 32位的API)

第12行代码就是 mscoree.dll 执行 _CorExeMain 方法,初始化环境,10 行代码加载 CLR,CLR 从第9 行执行入口函数,知道最后进入我们的托管层,我们可以使用 !clrstack 命令查看托管栈。

1 0:000> !clrstack2 OS Thread Id: 0x36d8 (0)3 Child SP       IP Call Site4 0138eda4 771410fc [InlinedCallFrame: 0138eda4]5 0138eda0 6f7e9b71 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)6 0138eda4 6ff1b275 [InlinedCallFrame: 0138eda4] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)7 0138ee08 6ff1b275 System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef) [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 205]8 0138ee3c 6ff1b17b System.IO.__ConsoleStream.Read(Byte[], Int32, Int32) [f:\dd\ndp\clr\src\BCL\system\io\__consolestream.cs @ 134]9 0138ee5c 6f7ce6a3 System.IO.StreamReader.ReadBuffer() [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 595]10 0138ee6c 6f7ceb5b System.IO.StreamReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\streamreader.cs @ 748]11 0138ee88 70063786 System.IO.TextReader+SyncTextReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\textreader.cs @ 363]12 0138ee98 6fec1845 System.Console.ReadLine() [f:\dd\ndp\clr\src\BCL\system\console.cs @ 1984]13 0138eea0 03250876 Example_2_1_1.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_2_1_1\Program.cs @ 10]14 0138f020 7081f036 [GCFrame: 0138f020] 

系统进入了托管栈,那它怎么知道要执行程序的 Main 方法呢?其实,在 PE 头文件里也有说明,在【DIRECTORY_ENTRY_COM_DESCRIPTOR】配置项里,我们点击该节点,在右侧显示详情,请注意【EntryPointToken】,值是:06000001,这个标记就是 Main方法。

效果如图:

我们为了证明 06000001就是 Main 方法,我们使用 ILSpy 或者 DnSpy 查看程序集的元数据,效果如图:

其实,我们在 Main 方法的 IL 代码里也有标记,红色部分就是,注意。

1 .method private hidebysig static 
2     voidMain (3         string[] args4 ) cil managed5 {6     //Method begins at RVA 0x20507     //Header size: 18     //Code size: 19 (0x13)
9     .maxstack 8
10 .entrypoint11 
12 IL_0000: nop13     IL_0001: ldstr "Hello World"
14     IL_0006: call void [mscorlib]System.Console::WriteLine(string)15 IL_000b: nop16     IL_000c: call string[mscorlib]System.Console::ReadLine()17 IL_0011: pop18 IL_0012: ret19 } //end of method Program::Main


2.3、查看应用程序域
验证代码:
Example_2_1_1
EECLASS 存放在
LowFrequencyHeap
,MethodTable 存放在
HighFrequencyHeap

1 0:000> !dumpdomain(执行的命令)2 --------------------------------------
3 System Domain:      7115caf8(系统级程序域)4 LowFrequencyHeap:   7115ce1c(低频堆)5 HighFrequencyHeap:  7115ce68(高频堆)6 StubHeap:           7115ceb4(桩堆)7 Stage:              OPEN8 Name:               None9 --------------------------------------
10 Shared Domain:      7115c7a8(共享程序域)11 LowFrequencyHeap:   7115ce1c(低频堆)12 HighFrequencyHeap:  7115ce68(高频堆)13 StubHeap:           7115ceb4(桩堆)14 Stage:              OPEN15 Name:               None16 Assembly:           00b4f3d8 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]17 ClassLoader:        00b4de0818 Module Name19 6f531000    C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll20 
21 --------------------------------------
22 Domain 1:           00b01518(应用程序域)23 LowFrequencyHeap:   00b01984(低频堆)24 HighFrequencyHeap:  00b019d0(高频堆)25 StubHeap:           00b01a1c(桩堆)26 Stage:              OPEN27 SecurityDescriptor: 00b02a5828 Name:               Example_2_1_1.exe29 Assembly:           00b4f3d8 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll]30 ClassLoader:        00b4de0831 SecurityDescriptor: 00b4f34032 Module Name33 6f531000    C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll34 
35 Assembly:           00b5b4d8 [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_2_1_1\bin\Debug\Example_2_1_1.exe]36 ClassLoader:        00b5afa837 SecurityDescriptor: 00b5aea038 Module Name39 00d44044    E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_2_1_1\bin\Debug\Example_2_1_1.exe


2.4、通过命令
? $exentry
查看入口点。

0:000> ?$exentry
Evaluate expression:
5711746 = 00572782


三、总结
今天的内容真不少,完全记录下来还是挺费劲的,俗话说,没有苦哪里来的甜呢,一天的辛苦还是值得的,自己的收获也不少。学习如逆水行舟,不进则退,但是也也有另外一句话,学的越多,好像懂得越少。不管如何,不忘初心,继续努力,老天不会辜负努力的人。


0. 前言

前面两篇文章
Kubernetes:kube-apiserver 之 scheme(一)

Kubernetes:kube-apiserver 之 scheme(二)
重点介绍了
kube-apiserver
中的资源注册表
scheme
。这里进入正题,开始介绍
kube-apiserver
的核心实现。

1. kube-apiserver 启动流程

kube-apiserver
使用
Cobra
作为
CLI
框架,其初始化示意图如下。

image

结合示意图和代码看初始化过程效果更佳。代码在
kubernetes/cmd/kube-apiserver/apiserver.go

# kubernetes/cmd/kube-apiserver/apiserver.go
package main

func main() {
	command := app.NewAPIServerCommand()
	code := cli.Run(command)
	os.Exit(code)
}

# kubernetes/cmd/kube-apiserver/app/server.go
func NewAPIServerCommand() *cobra.Command {
	s := options.NewServerRunOptions()
	cmd := &cobra.Command{
		Use: "kube-apiserver",
		...
		RunE: func(cmd *cobra.Command, args []string) error {
			// set default options
			completedOptions, err := s.Complete()
			if err != nil {
				return err
			}

			// validate options
			if errs := completedOptions.Validate(); len(errs) != 0 {
				return utilerrors.NewAggregate(errs)
			}

			return Run(completedOptions, genericapiserver.SetupSignalHandler())
		},
	}

    # parse flags to options
	fs := cmd.Flags()
	namedFlagSets := s.Flags()
	verflag.AddFlags(namedFlagSets.FlagSet("global"))

	return cmd
}

首先调用
options.NewServerRunOptions()
实例化
options
选项,接着
s.Complete()
补全默认
options
,将补全的
options
送入
Validate()
方法进行验证。验证通过后进入
Run(completedOptions, genericapiserver.SetupSignalHandler())

Run()
函数是不会退出的函数,在函数内运行
kube-apiserver

有一点要注意的是,
kube-apiserver

参数
通过
flag
解析赋给
options
,这是框架的用法,不多讲。

进入
Run()
函数内。

func Run(opts options.CompletedOptions, stopCh <-chan struct{}) error {
    // 实例化 kube-apiserver 配置 config
	config, err := NewConfig(opts)
	if err != nil {
		return err
	}

    // 补全默认配置
	completed, err := config.Complete()
	if err != nil {
		return err
	}

    // 创建服务链
	server, err := CreateServerChain(completed)
	if err != nil {
		return err
	}

	prepared, err := server.PrepareRun()
	if err != nil {
		return err
	}

	return prepared.Run(stopCh)
}

如注释所示,
Run()
函数内
kube-apiserver
的启动流程相当清晰。

下面分步看各个流程。

1.1 实例化配置

进入
NewConfig(opts)
看实例化
config
过程。

# kubernetes/cmd/kube-apiserver/app/config.go
func NewConfig(opts options.CompletedOptions) (*Config, error) {
    // 根据 options 实例化 Config
	c := &Config{
		Options: opts,
	}

    // 创建 controlPlane 配置文件
	controlPlane, serviceResolver, pluginInitializer, err := CreateKubeAPIServerConfig(opts)
	if err != nil {
		return nil, err
	}
	c.ControlPlane = controlPlane

    // 创建 apiExtensions 配置文件
	apiExtensions, err := apiserver.CreateAPIExtensionsConfig(*controlPlane.GenericConfig, controlPlane.ExtraConfig.VersionedInformers, pluginInitializer, opts.CompletedOptions, opts.MasterCount,
		serviceResolver, webhook.NewDefaultAuthenticationInfoResolverWrapper(controlPlane.ExtraConfig.ProxyTransport, controlPlane.GenericConfig.EgressSelector, controlPlane.GenericConfig.LoopbackClientConfig, controlPlane.GenericConfig.TracerProvider))
	if err != nil {
		return nil, err
	}
	c.ApiExtensions = apiExtensions

    // 创建 aggregator 配置文件
	aggregator, err := createAggregatorConfig(*controlPlane.GenericConfig, opts.CompletedOptions, controlPlane.ExtraConfig.VersionedInformers, serviceResolver, controlPlane.ExtraConfig.ProxyTransport, controlPlane.ExtraConfig.PeerProxy, pluginInitializer)
	if err != nil {
		return nil, err
	}
	c.Aggregator = aggregator

	return c, nil
}


kube-apiserver
的所有
REST
服务组合在一起是极为复杂的,这里
kube-apiserver
将服务拆分,解耦为三种
HTTP Server

KubeAPIServer

APIExtensionsServer

AggregatorServer

image

三种
HTTP Server
拥有各自的配置文件。这里以
APIExtensionsServer
为例,查看其启动流程,其它两种
HTTP Server
与此类似。

进入
CreateKubeAPIServerConfig(opts)

func CreateKubeAPIServerConfig(opts options.CompletedOptions) (
	*controlplane.Config,
	aggregatorapiserver.ServiceResolver,
	[]admission.PluginInitializer,
	error,
) {
    // 创建通用配置
    genericConfig, versionedInformers, storageFactory, err := controlplaneapiserver.BuildGenericConfig(
		opts.CompletedOptions,
		[]*runtime.Scheme{legacyscheme.Scheme, extensionsapiserver.Scheme, aggregatorscheme.Scheme},
		generatedopenapi.GetOpenAPIDefinitions,
	)

    config := &controlplane.Config{
		GenericConfig: genericConfig,
		ExtraConfig: controlplane.ExtraConfig{
			...
		},
	}

    // setup admission
	admissionConfig := &kubeapiserveradmission.Config{
		ExternalInformers:    versionedInformers,
		LoopbackClientConfig: genericConfig.LoopbackClientConfig,
		CloudConfigFile:      opts.CloudProvider.CloudConfigFile,
	}
    err = opts.Admission.ApplyTo(
		genericConfig,
		versionedInformers,
		clientgoExternalClient,
		dynamicExternalClient,
		utilfeature.DefaultFeatureGate,
		pluginInitializers...)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("failed to apply admission: %w", err)
	}

    ...
    return config, serviceResolver, pluginInitializers, nil
}

试想,三种
HTTP Server
肯定有通用的配置。
kube-apiserver
在函数
CreateKubeAPIServerConfig(opts)
内调用
BuildGenericConfig()
创建
HTTP Server
通用配置。

创建完通用配置后,实例化
KubeAPIServer
配置
config
。接着,实例化
admission
准入相关配置,通过
opts.Admission.ApplyTo()
将准入配置赋给
config

进入
BuildGenericConfig
看通用配置创建了什么。

func BuildGenericConfig(
	s controlplaneapiserver.CompletedOptions,
	schemes []*runtime.Scheme,
	getOpenAPIDefinitions func(ref openapicommon.ReferenceCallback) map[string]openapicommon.OpenAPIDefinition,
) (
	genericConfig *genericapiserver.Config,
	versionedInformers clientgoinformers.SharedInformerFactory,
	storageFactory *serverstorage.DefaultStorageFactory,

	lastErr error,
) {
    // NewConfig returns a Config struct with the default values
	genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs)
	genericConfig.MergedResourceConfig = controlplane.DefaultAPIResourceConfigSource()

    //  ApplyTo applies the run options to the method receiver and returns self
	if lastErr = s.GenericServerRunOptions.ApplyTo(genericConfig); lastErr != nil {
		return
	}

    // wrap the definitions to revert any changes from disabled features
	getOpenAPIDefinitions = openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(getOpenAPIDefinitions)
	namer := openapinamer.NewDefinitionNamer(schemes...)
	genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(getOpenAPIDefinitions, namer)
	genericConfig.OpenAPIConfig.Info.Title = "Kubernetes"
	genericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(getOpenAPIDefinitions, namer)
	genericConfig.OpenAPIV3Config.Info.Title = "Kubernetes"

    // New returns a new storage factory created from the completed storage factory configuration.
	storageFactoryConfig := kubeapiserver.NewStorageFactoryConfig()
	storageFactoryConfig.APIResourceConfig = genericConfig.MergedResourceConfig
	storageFactory, lastErr = storageFactoryConfig.Complete(s.Etcd).New()
	if lastErr != nil {
		return
	}

    // ApplyWithStorageFactoryTo mutates the provided server.Config.  It must never mutate the receiver (EtcdOptions).
	if lastErr = s.Etcd.ApplyWithStorageFactoryTo(storageFactory, genericConfig); lastErr != nil {
		return
	}

	// Authentication.ApplyTo requires already applied OpenAPIConfig and EgressSelector if present
	if lastErr = s.Authentication.ApplyTo(&genericConfig.Authentication, genericConfig.SecureServing, genericConfig.EgressSelector, genericConfig.OpenAPIConfig, genericConfig.OpenAPIV3Config, clientgoExternalClient, versionedInformers); lastErr != nil {
		return
	}

    // BuildAuthorizer constructs the authorizer
	genericConfig.Authorization.Authorizer, genericConfig.RuleResolver, err = BuildAuthorizer(s, genericConfig.EgressSelector, versionedInformers)
	if err != nil {
		lastErr = fmt.Errorf("invalid authorization config: %v", err)
		return
	}

    ...

	return
}

通用配置内创建了一系列配置,概括如下。

image

这里不继续往下探各个配置的详细信息,后续需要再回头看。配置文件创建好后,返回
Run()
函数看服务链的创建。

1.2 创建服务链

进入
CreateServerChain
查看服务链。

func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) {
	// New returns an HTTP handler that is meant to be executed at the end of the delegation chain.
	// It checks if the request have been made before the server has installed all known HTTP paths.
	// In that case it returns a 503 response otherwise it returns a 404.
	notFoundHandler := notfoundhandler.New(config.ControlPlane.GenericConfig.Serializer, genericapifilters.NoMuxAndDiscoveryIncompleteKey)

	// New returns a new instance of CustomResourceDefinitions from the given config.
	apiExtensionsServer, err := config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
	if err != nil {
		return nil, err
	}
	crdAPIEnabled := config.ApiExtensions.GenericConfig.MergedResourceConfig.ResourceEnabled(apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions"))

	kubeAPIServer, err := config.ControlPlane.New(apiExtensionsServer.GenericAPIServer)
	if err != nil {
		return nil, err
	}

	// aggregator comes last in the chain
	aggregatorServer, err := createAggregatorServer(config.Aggregator, kubeAPIServer.GenericAPIServer, apiExtensionsServer.Informers, crdAPIEnabled)
	if err != nil {
		// we don't need special handling for innerStopCh because the aggregator server doesn't create any go routines
		return nil, err
	}

	return aggregatorServer, nil
}

服务链中创建三种
HTTP Server
,这里还是介绍
apiExtensionsServer
服务。

首先将
notFoundHandler
handler 赋给
apiExtensionsServer
,当
REST
路由不到指定
API
时会路由到
notFoundHandler
处理请求。

进入
config.ApiExtensions.New

func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
	// 创建通用 APIServer
	genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
	if err != nil {
		return nil, err
	}

	// 实例化 APIExtensions server: CustomResourceDefinitions
	s := &CustomResourceDefinitions{
		GenericAPIServer: genericServer,
	}

	// 创建 apiGroupInfo,通过 apiGroupInfo 建立 REST API 到资源实例的路由
	apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
	storage := map[string]rest.Storage{}
	// customresourcedefinitions
	if resource := "customresourcedefinitions"; apiResourceConfig.ResourceEnabled(v1.SchemeGroupVersion.WithResource(resource)) {
		// 实例化 REST 资源:customResourceDefinitionStorage
		customResourceDefinitionStorage, err := customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
		if err != nil {
			return nil, err
		}
		storage[resource] = customResourceDefinitionStorage
		storage[resource+"/status"] = customresourcedefinition.NewStatusREST(Scheme, customResourceDefinitionStorage)
	}
	if len(storage) > 0 {
		// 建立 version 到 REST 资源的 mapping
		apiGroupInfo.VersionedResourcesStorageMap[v1.SchemeGroupVersion.Version] = storage
	}

	// 通过 apiGroupInfo 建立 REST API 到资源实例的路由
	if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
		return nil, err
	}

	...
}

config.ApiExtensions.New
中定义的流程如注释所示。下面逐层展开各个流程。

1.2.1 创建通用 APIServer

进入
c.GenericConfig.New
查看通用 APIServer 创建过程。

func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*GenericAPIServer, error) {
	// 创建通用 APIServer 的 handler
	apiServerHandler := NewAPIServerHandler(name, c.Serializer, handlerChainBuilder, delegationTarget.UnprotectedHandler())

	// 实例化通用 APIServer 并将前面创建的 apiServerHandler 赋给通用 APIServer
	s := &GenericAPIServer{
		Handler:                        apiServerHandler,
		listedPathProvider: apiServerHandler,
	}

	// 建立通用 REST 路由
	installAPI(s, c.Config)

	return s, nil
}

首先,进入
NewAPIServerHandler
查看通用
APIServer
handler
的创建。

func NewAPIServerHandler(name string, s runtime.NegotiatedSerializer, handlerChainBuilder HandlerChainBuilderFn, notFoundHandler http.Handler) *APIServerHandler {
	// 建立非 REST 资源的路由
	nonGoRestfulMux := mux.NewPathRecorderMux(name)
	if notFoundHandler != nil {
		nonGoRestfulMux.NotFoundHandler(notFoundHandler)
	}

	// 创建 go-restful container 处理 REST 资源的路由
	gorestfulContainer := restful.NewContainer()
	gorestfulContainer.ServeMux = http.NewServeMux()
	gorestfulContainer.Router(restful.CurlyRouter{}) // e.g. for proxy/{kind}/{name}/{*}
	gorestfulContainer.RecoverHandler(func(panicReason interface{}, httpWriter http.ResponseWriter) {
		logStackOnRecover(s, panicReason, httpWriter)
	})
	gorestfulContainer.ServiceErrorHandler(func(serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) {
		serviceErrorHandler(s, serviceErr, request, response)
	})

	// 将两种路由 REST 资源路由和 非 REST 资源路由赋给 director
	director := director{
		name:               name,
		goRestfulContainer: gorestfulContainer,
		nonGoRestfulMux:    nonGoRestfulMux,
	}

	// 返回 APIServerHandler
	return &APIServerHandler{
		FullHandlerChain:   handlerChainBuilder(director),
		GoRestfulContainer: gorestfulContainer,
		NonGoRestfulMux:    nonGoRestfulMux,
		Director:           director,
	}
}

kube-apiserver
基于
go-restful
框架建立
RESTful API
的路由。

创建好
apiServerHandler
后将该
handler
赋给通用 APIServer
GenericAPIServer
。接着进入
installAPI(s, c.Config)
看通用
REST
路由的创建过程。

func installAPI(s *GenericAPIServer, c *Config) {
	...
	routes.Version{Version: c.Version}.Install(s.Handler.GoRestfulContainer)
}

这里看一种
REST
路由
version
的建立过程,进入
Version.Install

// Install registers the APIServer's `/version` handler.
func (v Version) Install(c *restful.Container) {
	if v.Version == nil {
		return
	}

	// Set up a service to return the git code version.
	versionWS := new(restful.WebService)
	versionWS.Path("/version")
	versionWS.Doc("git code version from which this is built")
	versionWS.Route(
		versionWS.GET("/").To(v.handleVersion).
			Doc("get the code version").
			Operation("getCodeVersion").
			Produces(restful.MIME_JSON).
			Consumes(restful.MIME_JSON).
			Writes(version.Info{}))

	c.Add(versionWS)
}

// handleVersion writes the server's version information.
func (v Version) handleVersion(req *restful.Request, resp *restful.Response) {
	responsewriters.WriteRawJSON(http.StatusOK, *v.Version, resp.ResponseWriter)
}

可以看到,在
Install
内建立了
/version

v.handleVersion
的路由。

下一节,将继续介绍创建服务链的流程。未完待续...