Read about our upcoming Code of Conduct on this issue

Commit 4e6f70c0 authored by Armin Rigo's avatar Armin Rigo
Browse files

hg merge emit-call-x86: fix for multithreaded programs, particularly

those that run more threads than cores.  The issue was that a
call_release_gil instruction compiles to code that still accesses
ebp/rsp after it released the GIL.
import cffi, thread, time, sys
ffi = cffi.FFI()
ffi.cdef("""
long foobar(long a, long b, long c, long d, long e, long f,
long a2, long b2, long c2, long d2, long e2, long f2,
long a3, long b3, long c3, long d3, long e3, long f3,
long a4, long b4, long c4, long d4, long e4, long f4);
""")
lib = ffi.verify("""
long foobar(long a, long b, long c, long d, long e, long f,
long a2, long b2, long c2, long d2, long e2, long f2,
long a3, long b3, long c3, long d3, long e3, long f3,
long a4, long b4, long c4, long d4, long e4, long f4)
{
return a * 1 + b * 2 + c * 3 + d * 4 + e * 5 + f * 6 +
(a2 * 1 + b2 * 2 + c2 * 3 + d2 * 4 + e2 * 5 + f2 * 6) * (-3) +
(a3 * 1 + b3 * 2 + c3 * 3 + d3 * 4 + e3 * 5 + f3 * 6) * (-5) +
(a4 * 1 + b4 * 2 + c4 * 3 + d4 * 4 + e4 * 5 + f4 * 6) * (-7);
}
""")
def runme():
for j in range(10):
for i in range(10000):
args = [i-k for k in range(24)]
x = lib.foobar(*args)
(a,b,c,d,e,f,a2,b2,c2,d2,e2,f2,
a3,b3,c3,d3,e3,f3,a4,b4,c4,d4,e4,f4) = args
assert x == (
a * 1 + b * 2 + c * 3 + d * 4 + e * 5 + f * 6 +
(a2 * 1 + b2 * 2 + c2 * 3 + d2 * 4 + e2 * 5 + f2 * 6) * (-3) +
(a3 * 1 + b3 * 2 + c3 * 3 + d3 * 4 + e3 * 5 + f3 * 6) * (-5) +
(a4 * 1 + b4 * 2 + c4 * 3 + d4 * 4 + e4 * 5 + f4 * 6) * (-7))
done = []
def submain():
try:
runme()
err = None
except:
err = sys.exc_info()
done.append(err)
for i in range(2):
thread.start_new_thread(submain, ())
while len(done) < 2:
time.sleep(0.1)
for err in done:
if err is not None:
raise err[0], err[1], err[2]
import os, sys, py, subprocess
localdir = os.path.dirname(os.path.abspath(__file__))
def test_bug1():
if not sys.platform.startswith('linux'):
py.test.skip("linux-only test")
cmdline = ['taskset', '-c', '0',
sys.executable, os.path.join(localdir, 'bug1.py')]
popen = subprocess.Popen(cmdline)
err = popen.wait()
assert err == 0
......@@ -15,6 +15,9 @@ class TestLLTypeLLGraph(LLtypeBackendTest):
def test_memoryerror(self):
py.test.skip("does not make much sense on the llgraph backend")
def test_call_release_gil_variable_function_and_arguments(self):
py.test.skip("the arguments seem not correctly casted")
def test_cast_adr_to_int_and_back():
X = lltype.Struct('X', ('foo', lltype.Signed))
......
......@@ -108,8 +108,7 @@ class BaseAssembler(object):
self.malloc_slowpath_unicode = None
self._build_stack_check_slowpath()
if gc_ll_descr.gcrootmap:
self._build_release_gil(gc_ll_descr.gcrootmap)
self._build_release_gil(gc_ll_descr.gcrootmap)
if not self._debug:
# if self._debug is already set it means that someone called
# set_debug by hand before initializing the assembler. Leave it
......@@ -348,12 +347,19 @@ class BaseAssembler(object):
if after:
after()
@staticmethod
def _no_op():
pass
_NOARG_FUNC = lltype.Ptr(lltype.FuncType([], lltype.Void))
_CLOSESTACK_FUNC = lltype.Ptr(lltype.FuncType([rffi.LONGP],
lltype.Void))
def _build_release_gil(self, gcrootmap):
if gcrootmap.is_shadow_stack:
if gcrootmap is None:
releasegil_func = llhelper(self._NOARG_FUNC, self._no_op)
reacqgil_func = llhelper(self._NOARG_FUNC, self._no_op)
elif gcrootmap.is_shadow_stack:
releasegil_func = llhelper(self._NOARG_FUNC,
self._release_gil_shadowstack)
reacqgil_func = llhelper(self._NOARG_FUNC,
......
......@@ -2532,6 +2532,219 @@ class LLtypeBackendTest(BaseBackendTest):
assert rffi.charp2strn(buffer, buflen) == cwd
lltype.free(buffer, flavor='raw')
def test_call_release_gil_return_types(self):
from rpython.rlib.libffi import types
from rpython.rlib.rarithmetic import r_uint, r_longlong, r_ulonglong
from rpython.rlib.rarithmetic import r_singlefloat
cpu = self.cpu
for ffitype, result, TP in [
(types.ulong, r_uint(sys.maxint + 10), lltype.Unsigned),
(types.slong, -4321, lltype.Signed),
(types.uint8, 200, rffi.UCHAR),
(types.sint8, -42, rffi.SIGNEDCHAR),
(types.uint16, 50000, rffi.USHORT),
(types.sint16, -20000, rffi.SHORT),
(types.uint32, r_uint(3000000000), rffi.UINT),
(types.sint32, -2000000000, rffi.INT),
(types.uint64, r_ulonglong(9999999999999999999),
lltype.UnsignedLongLong),
(types.sint64, r_longlong(-999999999999999999),
lltype.SignedLongLong),
(types.double, 12.3475226, rffi.DOUBLE),
(types.float, r_singlefloat(-592.75), rffi.FLOAT),
]:
if sys.maxint < 2**32 and TP in (lltype.SignedLongLong,
lltype.UnsignedLongLong):
if not cpu.supports_longlong:
continue
if TP == rffi.DOUBLE:
if not cpu.supports_floats:
continue
if TP == rffi.FLOAT:
if not cpu.supports_singlefloats:
continue
#
result = rffi.cast(TP, result)
#
def pseudo_c_function():
return result
#
FPTR = self.Ptr(self.FuncType([], TP))
func_ptr = llhelper(FPTR, pseudo_c_function)
funcbox = self.get_funcbox(cpu, func_ptr)
calldescr = cpu._calldescr_dynamic_for_tests([], ffitype)
faildescr = BasicFailDescr(1)
kind = types.getkind(ffitype)
if kind in 'uis':
b3 = BoxInt()
elif kind in 'fUI':
b3 = BoxFloat()
else:
assert 0, kind
#
ops = [
ResOperation(rop.CALL_RELEASE_GIL, [funcbox], b3,
descr=calldescr),
ResOperation(rop.GUARD_NOT_FORCED, [], None, descr=faildescr),
ResOperation(rop.FINISH, [b3], None, descr=BasicFinalDescr(0))
]
ops[1].setfailargs([])
looptoken = JitCellToken()
self.cpu.compile_loop([], ops, looptoken)
deadframe = self.cpu.execute_token(looptoken)
fail = self.cpu.get_latest_descr(deadframe)
assert fail.identifier == 0
if isinstance(b3, BoxInt):
r = self.cpu.get_int_value(deadframe, 0)
if isinstance(result, r_singlefloat):
assert -sys.maxint-1 <= r <= 0xFFFFFFFF
r, = struct.unpack("f", struct.pack("I", r & 0xFFFFFFFF))
result = float(result)
else:
r = rffi.cast(TP, r)
assert r == result
elif isinstance(b3, BoxFloat):
r = self.cpu.get_float_value(deadframe, 0)
if isinstance(result, float):
r = longlong.getrealfloat(r)
else:
r = rffi.cast(TP, r)
assert r == result
def test_call_release_gil_variable_function_and_arguments(self):
from rpython.rlib.libffi import types
from rpython.rlib.rarithmetic import r_uint, r_longlong, r_ulonglong
from rpython.rlib.rarithmetic import r_singlefloat
cpu = self.cpu
rnd = random.Random(525)
ALL_TYPES = [
(types.ulong, lltype.Unsigned),
(types.slong, lltype.Signed),
(types.uint8, rffi.UCHAR),
(types.sint8, rffi.SIGNEDCHAR),
(types.uint16, rffi.USHORT),
(types.sint16, rffi.SHORT),
(types.uint32, rffi.UINT),
(types.sint32, rffi.INT),
]
if sys.maxint < 2**32 and cpu.supports_longlong:
ALL_TYPES += [
(types.uint64, lltype.UnsignedLongLong),
(types.sint64, lltype.SignedLongLong),
] * 2
if cpu.supports_floats:
ALL_TYPES += [
(types.double, rffi.DOUBLE),
] * 4
if cpu.supports_singlefloats:
ALL_TYPES += [
(types.float, rffi.FLOAT),
] * 4
for k in range(100):
POSSIBLE_TYPES = [rnd.choice(ALL_TYPES)
for i in range(random.randrange(2, 5))]
load_factor = rnd.random()
keepalive_factor = rnd.random()
#
def pseudo_c_function(*args):
seen.append(list(args))
#
ffitypes = []
ARGTYPES = []
for i in range(rnd.randrange(4, 20)):
ffitype, TP = rnd.choice(POSSIBLE_TYPES)
ffitypes.append(ffitype)
ARGTYPES.append(TP)
#
FPTR = self.Ptr(self.FuncType(ARGTYPES, lltype.Void))
func_ptr = llhelper(FPTR, pseudo_c_function)
funcbox = self.get_funcbox(cpu, func_ptr)
calldescr = cpu._calldescr_dynamic_for_tests(ffitypes, types.void)
faildescr = BasicFailDescr(1)
#
argboxes = [BoxInt()] # for the function to call
codes = ['X']
for ffitype in ffitypes:
kind = types.getkind(ffitype)
codes.append(kind)
if kind in 'uis':
b1 = BoxInt()
elif kind in 'fUI':
b1 = BoxFloat()
else:
assert 0, kind
argboxes.append(b1)
codes = ''.join(codes) # useful for pdb
print
print codes
#
argvalues = [funcbox.getint()]
for TP in ARGTYPES:
r = (rnd.random() - 0.5) * 999999999999.9
r = rffi.cast(TP, r)
argvalues.append(r)
#
argvalues_normal = argvalues[:1]
for ffitype, r in zip(ffitypes, argvalues[1:]):
kind = types.getkind(ffitype)
if kind in 'ui':
r = rffi.cast(lltype.Signed, r)
elif kind in 's':
r, = struct.unpack("i", struct.pack("f", float(r)))
elif kind in 'f':
r = longlong.getfloatstorage(r)
elif kind in 'UI': # 32-bit only
r = rffi.cast(lltype.SignedLongLong, r)
else:
assert 0
argvalues_normal.append(r)
#
ops = []
loadcodes = []
insideboxes = []
for b1 in argboxes:
load = rnd.random() < load_factor
loadcodes.append(' ^'[load])
if load:
b2 = b1.clonebox()
ops.insert(rnd.randrange(0, len(ops)+1),
ResOperation(rop.SAME_AS, [b1], b2))
b1 = b2
insideboxes.append(b1)
loadcodes = ''.join(loadcodes)
print loadcodes
ops += [
ResOperation(rop.CALL_RELEASE_GIL, insideboxes, None,
descr=calldescr),
ResOperation(rop.GUARD_NOT_FORCED, [], None, descr=faildescr),
ResOperation(rop.FINISH, [], None, descr=BasicFinalDescr(0))
]
ops[-2].setfailargs([])
# keep alive a random subset of the insideboxes
for b1 in insideboxes:
if rnd.random() < keepalive_factor:
ops.insert(-1, ResOperation(rop.SAME_AS, [b1],
b1.clonebox()))
looptoken = JitCellToken()
self.cpu.compile_loop(argboxes, ops, looptoken)
#
seen = []
deadframe = self.cpu.execute_token(looptoken, *argvalues_normal)
fail = self.cpu.get_latest_descr(deadframe)
assert fail.identifier == 0
expected = argvalues[1:]
[got] = seen
different_values = ['%r != %r' % (a, b)
for a, b in zip(got, expected)
if a != b]
assert got == expected, ', '.join(different_values)
def test_guard_not_invalidated(self):
cpu = self.cpu
i0 = BoxInt()
......
......@@ -40,4 +40,4 @@ else:
PASS_ON_MY_FRAME = 12
JITFRAME_FIXED_SIZE = 28 # 13 GPR + 15 XMM
assert PASS_ON_MY_FRAME >= 11 # asmgcc needs at least JIT_USE_WORDS + 2
assert PASS_ON_MY_FRAME >= 12 # asmgcc needs at least JIT_USE_WORDS + 3
......@@ -6,7 +6,7 @@ from rpython.jit.backend.llsupport.assembler import (GuardToken, BaseAssembler,
DEBUG_COUNTER, debug_bridge)
from rpython.jit.backend.llsupport.asmmemmgr import MachineDataBlockWrapper
from rpython.jit.backend.llsupport.gcmap import allocate_gcmap
from rpython.jit.metainterp.history import Const, Box
from rpython.jit.metainterp.history import Const, Box, VOID
from rpython.jit.metainterp.history import AbstractFailDescr, INT, REF, FLOAT
from rpython.rtyper.lltypesystem import lltype, rffi, rstr, llmemory
from rpython.rtyper.lltypesystem.lloperation import llop
......@@ -25,28 +25,17 @@ from rpython.jit.backend.x86.regloc import (eax, ecx, edx, ebx, esp, ebp, esi,
RegLoc, FrameLoc, ConstFloatLoc, ImmedLoc, AddressLoc, imm,
imm0, imm1, FloatImmedLoc, RawEbpLoc, RawEspLoc)
from rpython.rlib.objectmodel import we_are_translated
from rpython.jit.backend.x86 import rx86, codebuf
from rpython.jit.backend.x86 import rx86, codebuf, callbuilder
from rpython.jit.metainterp.resoperation import rop
from rpython.jit.backend.x86 import support
from rpython.rlib.debug import debug_print, debug_start, debug_stop
from rpython.rlib import rgc
from rpython.rlib.clibffi import FFI_DEFAULT_ABI
from rpython.jit.backend.x86.jump import remap_frame_layout
from rpython.jit.codewriter.effectinfo import EffectInfo
from rpython.jit.codewriter import longlong
from rpython.rlib.rarithmetic import intmask, r_uint
from rpython.rlib.objectmodel import compute_unique_id
# darwin requires the stack to be 16 bytes aligned on calls. Same for gcc 4.5.0,
# better safe than sorry
CALL_ALIGN = 16 // WORD
def align_stack_words(words):
return (words + CALL_ALIGN - 1) & ~(CALL_ALIGN-1)
class Assembler386(BaseAssembler):
_regalloc = None
_output_loop_log = None
......@@ -131,10 +120,10 @@ class Assembler386(BaseAssembler):
mc.MOV_rs(esi.value, WORD*2)
# push first arg
mc.MOV_rr(edi.value, ebp.value)
align = align_stack_words(1)
align = callbuilder.align_stack_words(1)
mc.SUB_ri(esp.value, (align - 1) * WORD)
else:
align = align_stack_words(3)
align = callbuilder.align_stack_words(3)
mc.MOV_rs(eax.value, WORD * 2)
mc.SUB_ri(esp.value, (align - 1) * WORD)
mc.MOV_sr(WORD, eax.value)
......@@ -1014,175 +1003,24 @@ class Assembler386(BaseAssembler):
gcrootmap = self.cpu.gc_ll_descr.gcrootmap
return bool(gcrootmap) and not gcrootmap.is_shadow_stack
def _emit_call(self, x, arglocs, start=0, tmp=eax,
argtypes=None, callconv=FFI_DEFAULT_ABI,
# whether to worry about a CALL that can collect; this
# is always true except in call_release_gil
can_collect=True,
# max number of arguments we can pass on esp; if more,
# we need to decrease esp temporarily
stack_max=PASS_ON_MY_FRAME):
#
if IS_X86_64:
return self._emit_call_64(x, arglocs, start, argtypes,
can_collect, stack_max)
stack_depth = 0
n = len(arglocs)
for i in range(start, n):
loc = arglocs[i]
stack_depth += loc.get_width() // WORD
if stack_depth > stack_max:
align = align_stack_words(stack_depth - stack_max)
self.mc.SUB_ri(esp.value, align * WORD)
if can_collect:
self.set_extra_stack_depth(self.mc, align * WORD)
def simple_call(self, fnloc, arglocs, result_loc=eax):
if result_loc is xmm0:
result_type = FLOAT
result_size = 8
elif result_loc is None:
result_type = VOID
result_size = 0
else:
align = 0
p = 0
for i in range(start, n):
loc = arglocs[i]
if isinstance(loc, RegLoc):
if loc.is_xmm:
self.mc.MOVSD_sx(p, loc.value)
else:
self.mc.MOV_sr(p, loc.value)
p += loc.get_width()
p = 0
for i in range(start, n):
loc = arglocs[i]
if not isinstance(loc, RegLoc):
if loc.get_width() == 8:
self.mc.MOVSD(xmm0, loc)
self.mc.MOVSD_sx(p, xmm0.value)
else:
self.mc.MOV(tmp, loc)
self.mc.MOV_sr(p, tmp.value)
p += loc.get_width()
# x is a location
if can_collect:
# we push *now* the gcmap, describing the status of GC registers
# after the rearrangements done just above, ignoring the return
# value eax, if necessary
noregs = self.cpu.gc_ll_descr.is_shadow_stack()
gcmap = self._regalloc.get_gcmap([eax], noregs=noregs)
self.push_gcmap(self.mc, gcmap, store=True)
#
self.mc.CALL(x)
if callconv != FFI_DEFAULT_ABI:
self._fix_stdcall(callconv, p - align * WORD)
elif align:
self.mc.ADD_ri(esp.value, align * WORD)
#
if can_collect:
self._reload_frame_if_necessary(self.mc)
if align:
self.set_extra_stack_depth(self.mc, 0)
self.pop_gcmap(self.mc)
def _fix_stdcall(self, callconv, p):
from rpython.rlib.clibffi import FFI_STDCALL
assert callconv == FFI_STDCALL
# it's a bit stupid, but we're just going to cancel the fact that
# the called function just added 'p' to ESP, by subtracting it again.
self.mc.SUB_ri(esp.value, p)
def _emit_call_64(self, x, arglocs, start, argtypes,
can_collect, stack_max):
src_locs = []
dst_locs = []
xmm_src_locs = []
xmm_dst_locs = []
singlefloats = None
# In reverse order for use with pop()
unused_gpr = [r9, r8, ecx, edx, esi, edi]
unused_xmm = [xmm7, xmm6, xmm5, xmm4, xmm3, xmm2, xmm1, xmm0]
on_stack = 0
# count the stack depth
floats = 0
for i in range(start, len(arglocs)):
arg = arglocs[i]
if arg.is_float() or argtypes and argtypes[i - start] == 'S':
floats += 1
all_args = len(arglocs) - start
stack_depth = (max(all_args - floats - len(unused_gpr), 0) +
max(floats - len(unused_xmm), 0))
align = 0
if stack_depth > stack_max:
align = align_stack_words(stack_depth - stack_max)
if can_collect:
self.set_extra_stack_depth(self.mc, align * WORD)
self.mc.SUB_ri(esp.value, align * WORD)
for i in range(start, len(arglocs)):
loc = arglocs[i]
if loc.is_float():
xmm_src_locs.append(loc)
if len(unused_xmm) > 0:
xmm_dst_locs.append(unused_xmm.pop())
else:
xmm_dst_locs.append(RawEspLoc(on_stack * WORD, FLOAT))
on_stack += 1
elif argtypes is not None and argtypes[i-start] == 'S':
# Singlefloat argument
if singlefloats is None:
singlefloats = []
if len(unused_xmm) > 0:
singlefloats.append((loc, unused_xmm.pop()))
else:
singlefloats.append((loc, RawEspLoc(on_stack * WORD, INT)))
on_stack += 1
else:
src_locs.append(loc)
if len(unused_gpr) > 0:
dst_locs.append(unused_gpr.pop())
else:
dst_locs.append(RawEspLoc(on_stack * WORD, INT))
on_stack += 1
# Handle register arguments: first remap the xmm arguments
remap_frame_layout(self, xmm_src_locs, xmm_dst_locs,
X86_64_XMM_SCRATCH_REG)
# Load the singlefloat arguments from main regs or stack to xmm regs
if singlefloats is not None:
for src, dst in singlefloats:
if isinstance(dst, RawEspLoc):
# XXX too much special logic
if isinstance(src, RawEbpLoc):
self.mc.MOV32(X86_64_SCRATCH_REG, src)
self.mc.MOV32(dst, X86_64_SCRATCH_REG)
else:
self.mc.MOV32(dst, src)
continue
if isinstance(src, ImmedLoc):
self.mc.MOV(X86_64_SCRATCH_REG, src)
src = X86_64_SCRATCH_REG
self.mc.MOVD(dst, src)
# Finally remap the arguments in the main regs
# If x is a register and is in dst_locs, then oups, it needs to
# be moved away:
if x in dst_locs:
src_locs.append(x)
dst_locs.append(r10)
x = r10
remap_frame_layout(self, src_locs, dst_locs, X86_64_SCRATCH_REG)
if can_collect:
# we push *now* the gcmap, describing the status of GC registers
# after the rearrangements done just above, ignoring the return
# value eax, if necessary
noregs = self.cpu.gc_ll_descr.is_shadow_stack()
gcmap = self._regalloc.get_gcmap([eax], noregs=noregs)
self.push_gcmap(self.mc, gcmap, store=True)
#
self.mc.CALL(x)
if align:
self.mc.ADD_ri(esp.value, align * WORD)
#
if can_collect:
self._reload_frame_if_necessary(self.mc)
if align:
self.set_extra_stack_depth(self.mc, 0)
self.pop_gcmap(self.mc)
result_type = INT
result_size = WORD
cb = callbuilder.CallBuilder(self, fnloc, arglocs,
result_loc, result_type,
result_size)
cb.emit()
def simple_call_no_coll