从汇编代码的角度去理解C++多线程编程问题

news2024/9/30 15:19:16

目录

1、多线程问题实例

2、理解该多线程问题的预备知识

2.1、二进制机器码和汇编代码

2.2、多线程切换与CPU时间片

2.3、多线程创建与线程函数

3、从汇编代码的角度去理解多线程问题

4、问题解决办法

5、熟悉汇编代码有哪些用处?

5.1、在代码中插入汇编代码块,提升代码的执行效率

5.2、在分析C++软件异常时可能需要查看汇编代码

5.3、从汇编代码的角度可以理解很多高级语言没法理解的编程细节


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931       之前经常说,C++开发人员要了解汇编,了解汇编后多么多么好,今天正好得空写一篇汇编相关的文章。熟悉汇编,不仅可以辅助排查C++软件异常问题,还可以理解很多高级语言不好理解的编程细节(从汇编代码的角度可以看到代码的完整执行细节),特别是多线程编程中的问题。下面我们就来讲解一个典型的从汇编代码去理解多线程问题的实例,然后详细总结一下熟悉汇编代码的用处。

1、多线程问题实例

       《Windows核心编程》一书中,在第八章讲到多线程同步时,有个多线程操作一个全局变量的实例。这个实例是通过汇编代码去理解多线程执行细节的一个很典型的例子。

       该例子中定义了一个long型的全局变量,然后创建了两个线程,线程函数分别是ThreadFunc1和ThreadFunc2,这两个线程函数中均对g_x变量进行自加操作(在访问共享变量g_x时未加锁同步),相关代码如下:

// define a global variable
long g_x = 0;

DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
    g_x++;
    return 0;
}

DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
    g_x++;
    return 0;
}

这里有个问题,当这两个线程函数执行完后,全局变量g_x的值会是多少呢?一定会是2吗?

       实际上,在两个线程函数执行完后,g_x的值不一定为2。这个实例需要从汇编代码的角度去理解,从C++源码看则很难搞懂,这是一个从汇编代码角度去理解代码执行细节的典型实例。

2、理解该多线程问题的预备知识

       要理解上述多线程问题,需要了解一些基础知识,下面我们就来看看这些知识。

2.1、二进制机器码和汇编代码

       C++代码经过编译链接后生成的是二进制机器代码,存放在二进制文件中。在CPU中最终执行的是一条一条二进制机器码。汇编代码和二进制机器代码是等价的,汇编代码可读性比较强,所以我们一般去看汇编代码。一条汇编指令是CPU中执行的最小粒度。

2.2、多线程切换与CPU时间片

       线程是系统调度CPU时间片的最小单元,当线程失去CPU时间片时,会将当前线程的上下文信息保存到CONTEXT结构体中(这其中就包含各个寄存器的值),线程暂停执行。线程上下文结构体CONTEXT的定义类似如下:(该结构体定义位于winnt.h头文件中)

//
// Context Frame
//
//  This frame has a several purposes: 1) it is used as an argument to
//  NtContinue, 2) is is used to constuct a call frame for APC delivery,
//  and 3) it is used in the user level thread creation routines.
//
//  The layout of the record conforms to a standard call frame.
//

typedef struct _CONTEXT {

    //
    // The flags values within this flag control the contents of
    // a CONTEXT record.
    //
    // If the context record is used as an input parameter, then
    // for each portion of the context record controlled by a flag
    // whose value is set, it is assumed that that portion of the
    // context record contains valid context. If the context record
    // is being used to modify a threads context, then only that
    // portion of the threads context will be modified.
    //
    // If the context record is used as an IN OUT parameter to capture
    // the context of a thread, then only those portions of the thread's
    // context corresponding to set flags will be returned.
    //
    // The context record is never used as an OUT only parameter.
    //

    DWORD ContextFlags;

    //
    // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
    // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
    // included in CONTEXT_FULL.
    //

    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
    //

    FLOATING_SAVE_AREA FloatSave;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_SEGMENTS.
    //

    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_INTEGER.
    //

    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_CONTROL.
    //

    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;

    //
    // This section is specified/returned if the ContextFlags word
    // contains the flag CONTEXT_EXTENDED_REGISTERS.
    // The format and contexts are processor specific
    //

    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

上述结构体的部分字段就对应着各个寄存器。

       当该线程获取时间片时,会将之前保存的CONTEXT结构体中的线程上下文信息恢复到线程中(包括恢复各个寄存器的值),线程继续向下执行。

2.3、多线程创建与线程函数

       调用CreateThread或者__beginthreadex发起创建线程请求,线程创建起来后就会进入线程函数,但注意CreateThread或者__beginthreadex返回时不代表已经运行到线程函数中了(这个问题我们在排查C++软件异常崩溃时领教过)。线程函数执行完,线程函数退出后,线程也就结束了。

3、从汇编代码的角度去理解多线程问题

       对g_x变量的自加这句C++代码,对应的汇编代码如下:

MOV EAX, [g_x]  // 将g_x变量的值读到EAX寄存器中
INC EAX         // 将EAX中的值执行自加操作
MOV [g_x], EAX  // 然后将EAX中的值设置到g_x变量内存中

看C++代码:g_x++,有些人可能觉得就一个自加操作,应该执行很快的,中间似乎不会被打断。会不会被打断,其实要看汇编代码的,这行C++源码对应三行汇编代码,只能保证CPU执行某条汇编指令时不会被打断(汇编指令是CPU执行的最小粒度),但3条汇编指令,指令与指令之间是可能被打断的。

       为什么说两个线程执行完成后g_x变量的值是不确定的呢?比如可能存在两种场景:

1)场景1(最终结果g_x=2)

       假设线程1先快速执行了三行汇编指令,未被打断,g_x的值变成1。然后紧接着线程2执行,在g_x=1的基础上累加,最终两个线程执行完后,g_x等于2。

2)场景2(最终结果g_x=1)

       假设线程1先执行,当执行完前两条汇编指令后线程1失去时间片(线程上下文信息保存到CONTEXT结构体中):

即线程1前两条汇编指令执行完,第3条汇编指令还没来得及执行,就失去CPU时间片了!

        线程2执行,一次执行完三条指令,当前g_x=1。然后线程1获得CPU时间片,因为上次执行两条汇编指令后EAX寄存器中的值为1,因为线程1获取了时间片,保存线程上下文信息的CONTEXT恢复到线程1中,EAX=1,继续执行第3条指令,执行完后g_x还是1。

        所以,这个多线程问题,需要从汇编代码的角度去理解,从C++源码的角度很难想明白。

4、问题解决办法

        从本例可以看出,即使是简单的变量自加操作,多线程操作时也要做同步,可以加锁,可以使用系统的原子锁Interlocked系列函数,比如原子自加函数InterlockedIncrement和原子自减函数InterlockedDecrement:

LONG InterlockedIncrement(  
    LPLONG volatile lpAddend   // variable to increment
);

LONG InterlockedDecrement(  
    LPLONG volatile lpAddend   // variable address
);

这些原子函数能保证会被原子地被执行,中间不会被打断。 修改后的代码为:

// define a global variable
long g_x = 0;

DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
    InterlockedIncrement(&g_x);  // 调用原子锁函数InterlockedIncrement实现自加
    return 0;
}

DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
    InterlockedIncrement(&g_x);  // 调用原子锁函数InterlockedIncrement实现自加
    return 0;
}

5、熟悉汇编代码有哪些用处?

       汇编代码是底层语言,从事高级语言开发的开发人员在编码过程中没有接触过,但熟悉汇编代码有很多好处。

5.1、在代码中插入汇编代码块,提升代码的执行效率

       一般我们会在一些对执行效率要求比较高的代码中嵌入汇编代码,提高代码的执行效率,汇编代码的执行效率是最高的。比如我们在处理音视频编解码的算法代码中,时常会嵌入一些汇编代码,以提高代码的运行速度,比如音视频编解码模块负责色彩空间转换的接口都是汇编代码实现的(汇编代码实现的函数是开源的),如下:

       有人可能会问,经过IDE编译出来的二进制文件中也都是汇编指令,你人为的添加一段汇编代码,都是汇编代码,为啥会有执行速度上的差别呢?因为源代码经过编译器的处理生成的汇编代码在实现上可能不是最优的,这要依赖编译器,而我们人为地添加汇编,可以直接控制汇编代码,保证汇编代码是最优的,不再依赖编译器。

5.2、在分析C++软件异常时可能需要查看汇编代码

       一般程序是崩溃在某一条汇编指令上,汇编指令才能最直接反映为什么会发生崩溃。比如汇编指令中访问了一个不应该访问的地址,比如一个很小的内存地址(64K地址范围内的地址是小地址内存   区,禁止访问),如下:

或者一个很大的内核态地址(用户态的代码不能访问内核态的地址),如下:

都会引发崩溃。此外,有时分析崩溃时,需要查看汇编代码上下文去辅助分析。

       排查的C++软件异常与崩溃问题多了,就会知道汇编代码的好了,才会有深刻的理解和体会的!

5.3、从汇编代码的角度可以理解很多高级语言没法理解的编程细节

       比如函数调用时主调函数是如何将参数传递给被调用函数的。比如,有如下的实现两个整型变量相加的函数AddSum,在调用该函数时传入的参数代码如下:

int AddNum( int a, int b)
{
	int nSum = a + b;
	return nSum;
}

void CTestDlgDlg::OnBnClickedBtnTest()
{
	int a = 3;
	int b = 4;
	int nSum = AddNum( a, b );
}

我们在调用AddSum接口是通过栈将要传递的参数传给被调用函数AddSum的,我们通过汇编代码可以清楚看到这一过程:(可以直接在Visual Studio中看到上述C++代码的汇编代码)

从上述汇编代码上我们清楚地看到,在call AddSum函数之前,把要传递的参数a和b都压到了栈上!

       再比如,从汇编代码的角度可以很好地理解多线程编程中的细节问题,比如上面讲到的两个线程同时对一个long型变量进行自加操作的多线程问题。

       还比如,之前讲到Switch...case语句中case分支过多引发Stack Overflow线程栈溢出问题时,虽然变量定义在case分支中,虽然生命周期在case分支中,但变量的栈内存在所在函数的入口处就已经分配了,可以通过汇编代码看出来。比如如下的case分支代码:

void TestCaseFunc()
{
	int nType = 3;
	switch(nType)
	{
	case 0:
		{
			//STARTUPINFO startInfo;
			//memset(&startInfo, 0, sizeof(startInfo));
			// ...
			break;
		}

	case 1:
		{
			SHELLEXECUTEINFO shellexecuteInfo;
			memset(&shellexecuteInfo, 0, sizeof(shellexecuteInfo));
			// ...
			break;
		}

	case 2:
		{
			STARTUPINFO startInfo2;
			memset(&startInfo2, 0, sizeof(startInfo2));
			// ...
			break;
		}

	case 3:
		{
			SHELLEXECUTEINFO shellexecuteInfo2;
			memset(&shellexecuteInfo2, 0, sizeof(shellexecuteInfo2));
			// ...
			break;
		}

	case 4:
		{
			STARTUPINFO startInfo4;
			memset(&startInfo4, 0, sizeof(startInfo4));
			// ...
			break;
		}
	}
}

直接在Visual Studio中查看该函数的汇编代码:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/585603.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

信号处理与分析-傅里叶

目录 一、引言 二、傅里叶级数 1. 傅里叶级数的定义 2. 傅里叶级数的性质 三、傅里叶变换 1. 傅里叶变换的定义 2. 傅里叶变换的性质 四、离散傅里叶变换 1. 离散傅里叶变换的定义 2. 离散傅里叶变换的性质 五、应用实例 1. 信号处理 2. 图像处理 六、总结 一、引…

Revit中窗族的默认窗台高度与底高度是一样?

​  一、窗族的默认窗台高度与底高度是一样的吗? 窗族的系统设定中有一个自带的参数就是默认窗台高度,指的是窗户放置的时候窗户最底端离墙的最底端高度。 当我们创建一个建筑样板将我们创建好的窗族放置好的时候,这个参数就在窗的类型属性中&#xf…

2023年上半年 软件设计师答案解析

前言:2023年上半年软考已经落幕了,学长整理了一下软件设计师的题目以及个人理解的答案(仅供参考)希望能够帮助参加软考的各个小伙伴能够清晰的估分,希望大家都能通过考试~ 目录 2023年上半年 软件设计师 上午试卷 2023…

C Primer Plus第十二章编程练习答案

学完C语言之后,我就去阅读《C Primer Plus》这本经典的C语言书籍,对每一章的编程练习题都做了相关的解答,仅仅代表着我个人的解答思路,如有错误,请各位大佬帮忙点出! 1.不使用全局变量,重写程序…

网络连接中的舔狗协议

舔狗网络协议 (discard protocol) 最近互联网上,“舔狗” 这个词语很火,也衍生出来很多梗(快速说出互联网 4 大舔狗!!!)。然后今天偶然间看到了一个 RFC 文档, 发现了一…

用户需求分析工具:Y模型

用户需求分析工具:Y模型 《人人都是产品经理》作者苏杰提出 阿里巴巴产品经理多年 趣讲大白话:有个框框好同频 【趣讲信息科技180期】 **************************** 很多交流就是鸡同鸭讲 沟通的背景、动机、目的、方式、高度等严重不同 如果有一个模型…

服务器端安装jupyter notebook并在本地使用与环境配置一条龙服务【服务器上跑ipynb】

linux服务器端安装jupyter notebook并在本地使用 1.生成配置文件:2.配置Jupyter notebook密码3,修改配置文件~/.jupyter/jupyter_notebook_config.py4. 本地访问远端的服务器的jupyter1.首先在Linux服务器上启动Jupyter notebook2.然后在本地转发端口 为jupyter notebook配置co…

【云原生|探索 Kubernetes 系列 6】从 0 到 1,轻松搭建完整的 Kubernetes 集群

前言 大家好,我是秋意零。 前面一篇中,我们介绍了 kubeadm 的工作流程。那么今天我们就实际操作一下,探索如何快速、高效地从 0 开始搭建一个完整的 Kubernetes 集群,让你轻松驾驭容器化技术的力量!! &am…

json和pickle模块

目录 ❤ json和pickle模块 序列化 json pickle python从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129328397?spm1001.2014.3001.5502 ❤ json和pickle模块 序列化 把对象(变量)从内存中变成可存储或传输的过程称之为序列化&am…

3D EXPERIENCE“热知识” | 如何使用3D EXPERIENCE平台上的问题管理?

3D EXPERIENCE 平台上的问题管理对任何组织都是有用的工具,无论其规模大小。无论是使用它来标记和分发PDF还是在车间和工程部门之间分享想法,问题管理都可以简化日常活动。简而言之,它会根据权限列出现有问题,并让用户创建新问题&…

软件测试----软件测试四大测试过程

1、测试分析 (1)要点 1)软件需求分析 2)测试需求项的提取 3)用户使用场景分析 4)测试工具的调研和选取 5)测试缺陷分析 (2)分工 1)测试人员:提…

09:mysql---事务

目录 1:事务简介 2:事务操作 3:事务四大特性 4:并发事务问题 5:事务隔离级别 1:事务简介 事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功&…

周赛347(模拟、思维题、动态规划+优化)

文章目录 周赛347[2710. 移除字符串中的尾随零](https://leetcode.cn/problems/remove-trailing-zeros-from-a-string/)模拟 [2711. 对角线上不同值的数量差](https://leetcode.cn/problems/difference-of-number-of-distinct-values-on-diagonals/)模拟 [2712. 使所有字符相等…

索引下推(Index Condition Pushdown)

使用一张用户表t_user,表里创建联合索引(name, age)。 如果现在有一个需求:检索出表中名字第一个字是张,而且年龄是10岁的所有用户。那么,SQL语句是这么写的: 复制代码 select * from tuser w…

【教学类-35-01】带笔画步骤图的描字(姓氏)(A4整张)

作品展示: 1、图片一行(0-6):文字简单,写3*412个字 2、图片2行(6-12):笔画适中,写3*39个字 3、图片3行(12-18):笔画适中,…

LeetCode刷题(ACM模式)-03哈希表

参考引用:代码随想录 注:每道 LeetCode 题目都使用 ACM 代码模式,可直接在本地运行,蓝色字体为题目超链接 0. 哈希表理论基础 0.1 哈希表 哈希表(Hash table,也称散列表)是根据关键码的值而直…

Redis(六)主从模式与哨兵机制

文章目录 一、主从模式配置一主二从集群 二、哨兵机制哨兵模式演示:哨兵如何监控节点「主观下线」与[客观下线]哨兵如何选新主节点由哪个哨兵进行转移如何通知客户端新主节点的信息? 一、主从模式 配置一主二从集群 开启三个linux,并安装redis info …

【k8s】【Prometheus】【待写】

环境 k8s v1.18.0 192.168.79.31 master 192.168.79.32 node-1 192.168.79.33 node-2一、Prometheus 对 kubernetes 的监控 1.1 node-exporter 组件安装和配置 node-exporter 可以采集机器(物理机、虚拟机、云主机等)的监控指标数据,能够采…

C++11:可变参数模板、lambda表达式和包装器

目录 一. 可变参数模板 1.1 什么是可变模板参数 1.2 参数包具体值的获取 1.3 emplace/emplace_back接口函数 二. lambda表达式 2.1 lambda表达式的概念和定义方法 2.2 捕捉列表说明 2.3 lambda表达式的底层实现原理 三. 包装器 3.1 function包装 3.2 bind绑定 3.2.…

重估老板电器:加速增长飞轮,迸发品类红利

#王一博同款洗碗机,5月28日,这个话题登上微博热搜,并获得不小关注。数据显示,截至5月29日9:00,该话题一天内引发了166.1万人讨论,阅读量破2.7亿。同时,抖音上,官宣王一博为代言人的话…