从汇编层看多线程共享内存数据安全问题
前言
在前面文章《使用汇编写一个静态服务器》中我们提到过,当请求到来时,通过系统调用fork创建新的子进程去处理他,之所以没创建线程,是因为不会,因为Linux系统调用中没有直接提供创建线程的调用,所以就需要我们自己实现一个pthread_create函数,但因我不研究这方面,但又想体验lock指令的作用,所以不得不学习pthread_create函数的原理去创建新的线程。
简单说pthread_create底层是通过clone调用来完成的,而他的主要用途就是实现线程, 难在理解他们要共享哪些资源,clone调用需要传递要共享的特定资源相对应的标志,标准线程会具有以下标志。
CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_PARENT|CLONE_THREAD|CLONE_IO
具体标志作用可参考 https://man7.org/linux/man-pages/man2/clone.2.html
先看下整体代码
%include "/home/HouXinLin/project/nasm/include/io.inc"
bits 32
global CMAIN
%define CLONE_VM 0x00000100
%define CLONE_FS 0x00000200
%define CLONE_FILES 0x00000400
%define CLONE_SIGHAND 0x00000800
%define CLONE_PARENT 0x00008000
%define CLONE_THREAD 0x00010000
%define CLONE_IO 0x80000000
%define MAP_GROWSDOWN 0x0100
%define MAP_ANONYMOUS 0x0020
%define MAP_PRIVATE 0x0002
%define PROT_READ 0x1
%define PROT_WRITE 0x2
%define PROT_EXEC 0x4
%define THREAD_FLAGS
CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_PARENT|CLONE_THREAD|CLONE_IO
%define STACK_SIZE (4096 * 1024)
section .data
total: dd 0
timeval:
tv_sec dd 0
tv_usec dd 0
section .text
CMAIN:
mov ebp, esp
mov ebx, threadFunction
call createThread
mov ebx, threadFunction
call createThread
waitF:
mov dword [tv_sec], 1
mov dword [tv_usec], 0
mov eax, 162
mov ebx, timeval
mov ecx, 0
int 0x80
mov eax,[total]
mov ebx,0
mov ecx,10
mov edx,0
call printTotal
call exit
threadFunction:
mov ecx,10000
_nextAdd:
lock inc dword[total]
loop _nextAdd
call exit
createThread:
push ebx
call createStack
lea ecx, [eax + STACK_SIZE - 8]
pop dword [ecx]
mov ebx, THREAD_FLAGS
mov eax, 120
int 0x80
ret
createStack:
mov ebx, 0
mov ecx, STACK_SIZE
mov edx, PROT_WRITE | PROT_READ
mov esi, MAP_ANONYMOUS | MAP_PRIVATE | MAP_GROWSDOWN
mov eax, 192
int 0x80
ret
printTotal:
div ecx
push edx
inc ebx ;;存取结果是几位数
cmp eax,0 ;;商是否为0
jz printResult ;;是0退出
mov edx,0
mov ecx,10
jmp printTotal
printResult: ;;打印结果
nop
cmp ebx,0
jz exit
mov eax,[esp]
call printNumber
pop eax
dec ebx
jmp printResult
printNumber:
push ebx
add eax,48
push eax
mov edx, 1
mov ecx,esp
mov ebx, 1
mov eax, 4
int 80h
pop eax
pop ebx
ret
sum:
add ebx,4 ;;栈中偏移地址
add eax,dword[esp+ebx] ;;累加
loop sum ;;继续累加
ret
exit:
mov ebx,0
mov eax,1
int 80h
先不说创建线程的部分,主要看今天的问题,原因借用《x86从实模式到保护模式》中一句话,讲不清楚就不要讲
在测试中,先创建了两个线程,分别对内存中total变量进行自增1,每个线程增加10000次,最终得出应该是20000,而实际情况都知道,在少数情况下才会得到正确答案,但是当我们加上lock指令前缀的时候,就都变得正常。
lock inc dword[total]
因为inc指令并不是原子的,可以分为三步,内核先读取存储在内存位置的值,并计算出它的新值,然后将新值存储回去,但是在读取和存储之间存在延迟,其他线程在这个时候操作就可能会影响这个值,而当加入lock指令时,在多处理器环境中可以确保处理器独占使用这个共享内存。
而lock只支持add、adc、and、btc、btr、bts、cmpxchg、cmpxch8b、dec、inc、neg、not、or、sbb、sub、xor、xadd 和xchg。
java虚拟机中也会使用这几个指令,比如cas,他会用到lock+cmpxchg去实现。
mov eax,10
mov ebx,1000
cmpxchg [p],ebx
cmpxchg是比较并交换,当目标操作数和eax寄存器相等,那么就把源操作数加载到目标操作数中,如果不相等,就把目标操作数加载到eax中,比如在这里,当p变量和eax相等时,可以理解为还没有别人修改过内存,就把ebx(原操作数)加载到p变量中,如果在执行这句话前,有线程把p修改了,那么就把p变量加载到eax寄存器中,不会修改内存。
那如何判断结果到底加载到内存没?就是通过标志寄存器,当目标操作数和eax寄存器相等,指令执行后,最终zf是1。
但并发编程水太深,把握不住,我所知道的就这些。
原文始发于微信公众号(十四个字节):从汇编层看多线程共享内存数据安全问题