Репликация `std :: thread :: spawn` приводит к переполнению стека - PullRequest
0 голосов
/ 16 января 2020

Описание

Я пытаюсь порождать потоки на моей машине Windows в ящике #[no_std], но у меня возникают проблемы с функцией __chkstk . Для начала я создал ящик с std и попытался найти места, которые отвечают за порождение потоков в libstd.

Код Libstd (сокращен, чтобы показать только соответствующие части)

// libstd/thread/mod.rs
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    Builder::new().spawn(f).expect("failed to spawn thread")
}

// libstd/thread/mod.rs
impl Builder {
    pub fn spawn<F, T>(self, f: F) -> io::Result<JoinHandle<T>>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,
    {
        unsafe { self.spawn_unchecked(f) }
    }

    pub unsafe fn spawn_unchecked<'a, F, T>(self, f: F) -> io::Result<JoinHandle<T>>
    where
        F: FnOnce() -> T,
        F: Send + 'a,
        T: Send + 'a,
    {
        // ...

        imp::Thread::new(
            mem::transmute::<Box<dyn FnOnce() + 'a>, Box<dyn FnOnce() + 'static>>(Box::new(
                main,
            )),
        )

        // ...
    }
}

// libstd/sys/windows/thread.rs
impl Thread {
    pub unsafe fn new(p: Box<dyn FnOnce()>) -> io::Result<Thread> {
        extern "system" fn thread_start(main: *mut c_void) -> c::DWORD {
            unsafe { start_thread(main as *mut u8); }
            0
        }

        let p = box p;

        let ret = c::CreateThread(
            // ...
            thread_start,
            &*p as *const _ as *mut _,
            // ...
        );

        if ret as usize == 0 {
            Err(io::Error::last_os_error())
        } else {
            mem::forget(p);
            Ok(Thread { handle: Handle::new(ret) })
        };
    }
}

// libstd/sys_common/thread.rs
pub unsafe fn start_thread(main: *mut u8) {
    // ...

    Box::from_raw(main as *mut Box<dyn FnOnce()>)()
}

// libstd/sys/windows/c.rs
extern "system" {
    // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread
    pub fn CreateThread(
        lpThreadAttributes: LPSECURITY_ATTRIBUTES,
        dwStackSize: SIZE_T,
        lpStartAddress: extern "system" fn(*mut c_void) -> DWORD,
        lpParameter: LPVOID,
        dwCreationFlags: DWORD,
        lpThreadId: LPDWORD,
    ) -> HANDLE;
}

Моя попытка репликации libstd

Я сократил код для этого примера, это короткий однопоточный способ, как я пытался реплицировать libstd:

#[repr(C)]
struct Handle(usize);

fn spawn_std_like<F, T>(f: F) -> Handle
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    try_spawn_std_like(f).expect("failed to spawn thread")
}

fn try_spawn_std_like<F, T>(f: F) -> Result<Handle, ()>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    extern "system" fn thread_start(main: *mut u8) -> u32 {
        unsafe { Box::from_raw(main as *mut Box<dyn FnOnce()>)(); }
        0
    }

    let p = Box::new(Box::new(f));

    let handle = CreateThread(
        thread_start,
        &*p as *const _ as *mut _
    );

    if handle.0 != 0 {
        core::mem::forget(p);
        Ok(handle)
    } else {
        Err(())
    }
}

// Minimal version of `kernel32.CreateThread`, with only the relevant parameters.
#[allow(non_snake_case)]
extern "system" fn CreateThread(
    start_address: extern "system" fn(*mut u8) -> u32,
    parameter: *mut u8
) -> Handle {
    start_address(parameter);
    // Emulate successful `CreateThread` call.
    Handle(4)
}

Вызов этого с spawn_std_like(|| println!("std_like!")); сбой процесса с переполнением стека в __chkstk, поскольку он использует некоторый адрес памяти вместо размера закрытия в качестве «счетчика» для доступа к страницам памяти: Exception thrown at 0x00007FF7E41FE948 in example.exe: 0xC00000FD: Stack overflow (parameters: 0x0000000000000001, 0x000000EFEDC06000).

Трассировка стека:

  • __chkstk() Строка 109 (d: \ agent_work \ 5 \ s \ src \ vctools \ crt \ vcstartup \ src \ misc \ amd64 \ chkstk.asm)
  • std::alloc::boxed::{{impl}}::call_once<(), FnOnce<()>>(core::ops::function::Box<FnOnce<()>> self) Строка 1015 (в штучной упаковке) .rs)
  • std::alloc::boxed::{{impl}}::call_once<(), alloc::boxed::Box<FnOnce<()>>>(core::ops::function::Box<FnOnce<()>> * self) Строка 1015 (в штучной упаковке)
  • try_spawn_std_like::thread_start(unsigned char * main) (main.rs)
  • try_spawn_std_like::<closure-0, ()>(main::closure-0) (main.rs)
  • spawn_std_like<closure-0, ()>(main::closeure-0 f) (main.rs)
  • main() (main.rs)

Другие варианты, которые я пробовал

// Explicitly typed out, `std` style.
fn spawn0<F, T>(f: F) -> Result<Handle, ()>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    extern "system" fn thread_start(f: *mut u8) -> u32 {
        let f = f as *mut Box<dyn FnOnce()>;
        let f: Box<Box<dyn FnOnce()>> = unsafe {
            Box::from_raw(f)
        };
        f();
        0
    }

    let p = Box::new(Box::new(f));

    let handle = CreateThread(
        thread_start,
        &*p as *const _ as *mut _
    );

    if handle.0 != 0 {
        core::mem::forget(p);
        Ok(handle)
    } else {
        Err(())
    }
}

// Explicitly typed out, with `into_raw`.
fn spawn1<F, T>(f: F) -> Result<Handle, ()>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    extern "system" fn thread_start(f: *mut u8) -> u32 {
        let f = f as *mut Box<dyn FnOnce()>;
        let f: Box<Box<dyn FnOnce()>> = unsafe {
            Box::from_raw(f)
        };
        f();
        0
    }

    let f: Box<Box<F>> = Box::new(Box::new(f));
    let f: *mut Box<F> = Box::into_raw(f);

    let handle = CreateThread(
        thread_start,
        f as *mut _
    );

    if handle.0 != 0 {
        Ok(handle)
    } else {
        unsafe { Box::from_raw(f); }
        Err(())
    }
}

// Implicitly typed `spawn1` variant.
fn spawn2<F, T>(f: F) -> Result<Handle, ()>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    extern "system" fn thread_start(f: *mut u8) -> u32 {
        unsafe {
            Box::from_raw(
                f as *mut Box<dyn FnOnce()>
            )();
        }
        0
    }

    let f = Box::into_raw(Box::new(Box::new(f)));

    let handle = CreateThread(
        thread_start,
        f as *mut _
    );

    if handle.0 != 0 {
        Ok(handle)
    } else {
        unsafe { Box::from_raw(f); }
        Err(())
    }
}

// Generic `thread_start` routine.
fn spawn3<F, T>(f: F) -> Result<Handle, ()>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    extern "system" fn thread_start<F, T>(f: *mut Box<F>) -> Handle
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static
    {
        unsafe { Box::from_raw(f)(); }

        Handle(1)
    }

    let f = Box::into_raw(Box::new(Box::new(f)));

    let handle = thread_start(f);

    if handle.0 != 0 {
        Ok(handle)
    } else {
        unsafe { Box::from_raw(f); }
        Err(())
    }
}

// More explicit type in type-cast in `thread_start`. Does not compile.
/*
fn spawn4<F, T>(f: F) -> Result<Handle, ()>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    extern "system" fn thread_start(f: *mut u8) -> u32 {
        unsafe {
            Box::from_raw(
                // f as *mut Box<dyn FnOnce() -> (dyn Send + 'static) + Send + 'static>
                // f as *mut Box<dyn FnOnce() -> (dyn Sized + Send + 'static) + Send + 'static>
            )();
        }
        0
    }

    let f = Box::into_raw(Box::new(Box::new(f)));

    let handle = CreateThread(
        thread_start,
        f as *mut _
    );

    if handle.0 != 0 {
        Ok(handle)
    } else {
        unsafe { Box::from_raw(f); }
        Err(())
    }
}
*/

// Like `spawn2`, but with `+ Send + 'static`.
fn spawn5<F, T>(f: F) -> Result<Handle, ()>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    // `kernel32.CreateThread` like start routine.
    extern "system" fn thread_start(f: *mut u8) -> u32 {
        unsafe {
            Box::from_raw(
                f as *mut Box<dyn FnOnce() + Send + 'static>
            )();
        }
        0
    }

    let f = Box::into_raw(Box::new(Box::new(f)));

    let handle = CreateThread(
        thread_start,
        f as *mut _
    );

    if handle.0 != 0 {
        Ok(handle)
    } else {
        unsafe { Box::from_raw(f); }
        Err(())
    }
}

Для всех версий кроме spawn3 Фактический код внутри замыкания удаляется компилятором из двоичного кода, поэтому он никогда не сможет работать. Я попытался сделать это в своем минимальном #[no_std] ящике, вызвав user32.MessageBox в закрытии, и он не отображается в списке импортируемых функций внутри двоичного файла. Это также дает сбой в моей реализации __chkstk. При отладке я вижу, что параметр, отправляемый в функцию в регистре rax (специальное соглашение о вызовах), содержит размер памяти вместо размера замыкания и уменьшает значение параметра в l oop на 0x1000 каждый раз и касается страницы стека, пока стек не переполнится.

Generi c kernel32.CreateThread

spawn3 - единственный вариант, который действительно работает. Но я не могу использовать это для реальных kernel32.CreateThread, потому что импортированные C функции и их параметры не могут быть обобщенными c в Rust (error[E0044]: foreign items may not have type parameters):

#[link(name = "kernel32", kind = "dylib")]
extern "system" {
    fn CreateThread<
        F: Send + 'static + FnOnce() -> T,
        T: Send + 'static
    >(
        security_attributes: *const u8,
        stack_size: usize,
        start_address: extern "system" fn(*mut Box<F>) -> u32,
        parameter: *mut Box<F>,
        attributes: u32,
        id: *mut u32
    ) -> usize;
}

Я думаю, это должно возможно, и я просто делаю что-то не так, так как это работает в libstd.

1 Ответ

2 голосов
/ 16 января 2020

В строке

let p = Box::new(Box::new(f));

вы создаете Box<Box<F>>. Два поля здесь - тонкие указатели, потому что F здесь Sized и потому что Box<T> всегда Sized.

На линии

unsafe { Box::from_raw(main as *mut Box<dyn FnOnce()>)(); }

вы пытаясь интерпретировать внутренний Box как Box<dyn FnOnce()>. Box<dyn FnOnce()> - толстый указатель: необработанный указатель в сочетании с некоторыми вспомогательными данными - в случае типов dyn Trait вспомогательные данные - это указатель на виртуальную таблицу.

Для того, чтобы ваш код работал, вам нужно создать Box<dyn FnOnce()>. Для этого вам нужно разыграть внутренний Box, например:

let p = Box::new(Box::new(f) as Box<dyn FnOnce()>);

Теперь этого недостаточно; приведение неверно, потому что F реализует FnOnce() -> T, а не FnOnce() (что является сокращением для FnOnce() -> ()). Изменение ограничения F: FnOnce() -> T на F: FnOnce() и удаление теперь лишних T на spawn_std_like и try_spawn_std_like решит эту проблему. Другой вариант - обернуть f в замыкание, которое возвращает ():

let p = Box::new(Box::new(|| { f(); }) as Box<dyn FnOnce()>);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...