文档库 最新最全的文档下载
当前位置:文档库 › 向其他进程注入代码的三种方法

向其他进程注入代码的三种方法

向其他进程注入代码的三种方法
向其他进程注入代码的三种方法

向其他进程注入代码的三种方法

向其他进程注入代码的三种方法

本文章翻译自Robet Kuster的Three Ways to Inject Your Code into Another Process 一文,原版地址见下面。本文章版权归原作者所有。

原版地址:https://www.wendangku.net/doc/4b9841943.html,/threa ... 152&msg=1025152

下载整个压缩包

下载WinSpy

作者:Robert Kuster

翻译:袁晓辉(https://www.wendangku.net/doc/4b9841943.html, hyzs@https://www.wendangku.net/doc/4b9841943.html,)

摘要:如何向其他线程的地址空间中注入代码并在这个线程的上下文中执行之。

目录:

●导言

●Windows 钩子(Hooks)

●CreateRemoteThread 和LoadLibrary 技术

○进程间通讯

●CreateRemoteThread 和WriteProcessmemory 技术

○如何使用该技术子类(SubClass)其他进程中的控件

○什么情况下适合使用该技术

●写在最后的话

●附录

●参考

●文章历史

导言:

我们在Code project(https://www.wendangku.net/doc/4b9841943.html,)上可以找到许多密码间谍程序(译者注:那些可以看到别的程序中密码框内容的软件),他们都依赖于Windows钩子技术。要实现这个还有其他的方法吗?有!但是,首先,让我们简单回顾一下我们要实现的目标,以便你能弄清楚我在说什么。

要读取一个控件的内容,不管它是否属于你自己的程序,一般来说需要发送

WM_GETTEXT 消息到那个控件。这对edit控件也有效,但是有一种情况例外。如果这个edit控件属于其他进程并且具有ES_PASSWORD 风格的话,这种方法就不会成功。只有“拥有(OWNS)”这个密码控件的进程才可以用WM_GETTEXT 取得它的内容。所以,我们的问题就是:如何让下面这句代码在其他进程的地址空间中运行起来:::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );

一般来说,这个问题有三种可能的解决方案:

1. 把你的代码放到一个DLL中;然后用windows 钩子把它映射到远程进程。

2. 把你的代码放到一个DLL中;然后用CreateRemoteThread 和LoadLibrary 把它映射到远程进程。

3. 不用DLL,直接复制你的代码到远程进程(使用WriteProcessMemory)并且用CreateRemoteThread执行之。在这里有详细的说明:

Ⅰ. Windows 钩子

示例程序:HookSpy 和HookInjEx

Windows钩子的主要作用就是监视某个线程的消息流动。一般可分为:

1.局部钩子,只监视你自己进程中某个线程的消息流动。

2.远程钩子,又可以分为:

a.特定线程的,监视别的进程中某个线程的消息;

b.系统级的,监视整个系统中正在运行的所有线程的消息。

如果被挂钩(监视)的线程属于别的进程(情况2a和2b),你的钩子过程(hook procedure)必须放在一个动态连接库(DLL)中。系统把这包含了钩子过程的DLL映射到被挂钩的线程的地址空间。Windows会映射整个DLL而不仅仅是你的钩子过程。这就是为什么windows钩子可以用来向其他线程的地址空间注入代码的原因了。

在这里我不想深入讨论钩子的问题(请看MSDN中对SetWindowsHookEx的说明),让我再告诉你两个文档中找不到的诀窍,可能会有用:

1.当SetWindowHookEx调用成功后,系统会自动映射这个DLL到被挂钩的线程,但并不是立即映射。因为所有的Windows钩子都是基于消息的,直到一个适当的事件发生后这个DLL才被映射。比如:

如果你安装了一个监视所有未排队的(nonqueued)的消息的钩子

(WH_CALLWNDPROC),只有一个消息发送到被挂钩线程(的某个窗口)后这个DLL才被映射。也就是说,如果在消息发送到被挂钩线程之前调用了UnhookWindowsHookEx那么这个DLL就永远不会被映射到该线程(虽然SetWindowsHookEx调用成功了)。为了强制映射,可以在调用SetWindowsHookEx后立即发送一个适当的消息到那个线程。

同理,调用UnhookWindowsHookEx之后,只有特定的事件发生后DLL才真正地从被挂钩线程卸载。

2.当你安装了钩子后,系统的性能会受到影响(特别是系统级的钩子)。然而如果你只是使用的特定线程的钩子来映射DLL而且不截获如何消息的话,这个缺陷也可以轻易地避免。看一下下面的代码片段:

BOOL APIENTRY DllMain( HANDLE hModule,

DWORD ul_reason_for_call,

LPVOID lpReserved )

{

if( ul_reason_for_call == DLL_PROCESS_ATTACH )

{

//用LoadLibrary增加引用次数

char lib_name[MAX_PATH];

::GetModuleFileName( hModule, lib_name, MAX_PATH );

::LoadLibrary( lib_name );

// 安全卸载钩子

::UnhookWindowsHookEx( g_hHook );

}

return TRUE;

}

我们来看一下。首先,我们用钩子映射这个DLL到远程线程,然后,在DLL被真正映射进去后,我们立即卸载挂钩(unhook)。一般来说当第一个消息到达被挂钩线程后,这DLL会被卸载,然而我们通过LoadLibrary来增加这个DLL的引用次数,避免了DLL被卸载。

剩下的问题是:使用完毕后如何卸载这个DLL?UnhookWindowsHookEx不行了,因为我们已经对那个线程取消挂钩(unhook)了。你可以这么做:

○在你想要卸载这个DLL之前再安装一个钩子;

○发送一个“特殊”的消息到远程线程;

○在你的新钩子的钩子过程(hook procedure)中截获该消息,调用FreeLibrary 和(译者注:对新钩子调用)UnhookwindowsHookEx。

现在,钩子只在映射DLL到远程进程和从远程进程卸载DLL时使用,对被挂钩线程的

性能没有影响。也就是说,我们找到了一种(相比第二部分讨论的LoadLibrary技术)WinNT 和Win9x下都可以使用的,不影响目的进程性能的DLL映射机制。

但是,我们应该在何种情况下使用该技巧呢?通常是在DLL需要在远程进程中驻留较长时间(比如你要子类[subclass]另一个进程中的控件)并且你不想过于干涉目的进程时比较适合使用这种技巧。我在HookSpy中并没有使用它,因为那个DLL只是短暂地注入一段时间――只要能取得密码就足够了。我在另一个例子HookInjEx中演示了这种方法。HookInjEx把一个DLL映射进“explorer.exe”(当然,最后又从其中卸载),子类了其中的开始按钮,更确切地说我是把开始按钮的鼠标左右键点击事件颠倒了一下。

你可以在本文章的开头部分找到HookSpy和HookInjEx及其源代码的下载包链接。

Ⅱ. CreateRemoteThread 和LoadLibrary 技术

示例程序:LibSpy

通常,任何进程都可以通过LoadLibrary动态地加载DLL,但是我们如何强制一个外部进程调用该函数呢?答案是CreateRemoteThread。

让我们先来看看LoadLibrary和FreeLibrary的函数声明:

HINSTANCE LoadLibrary(

LPCTSTR lpLibFileName // address of filename of library module

);

BOOL FreeLibrary(

HMODULE hLibModule // handle to loaded library module

);

再和CreateRemoteThread的线程过程(thread procedure)ThreadProc比较一下:DWORD WINAPI ThreadProc(

LPVOID lpParameter // thread data

);

你会发现所有的函数都有同样的调用约定(calling convention)、都接受一个32位的参数并且返回值类型的大小也一样。也就是说,我们可以把LoadLibrary/FreeLibrary的指针作为参数传递给CrateRemoteThread。

然而,还有两个问题(参考下面对CreateRemoteThread的说明)

1.传递给ThreadProc的lpStartAddress 参数必须为远程进程中的线程过程的起始地址。

2.如果把ThreadProc的lpParameter参数当做一个普通的32位整数(FreeLibrary 把它当做HMODULE)那么没有如何问题,但是如果把它当做一个指针(LoadLibrary把它当做一个char*),它就必须指向远程进程中的内存数据。

第一个问题其实已经迎刃而解了,因为LoadLibrary和FreeLibrary都是存在于kernel32.dll中的函数,而kernel32可以保证任何“正常”进程中都存在,且其加载地址都是一样的。(参看附录A)于是LoadLibrary/FreeLibrary在任何进程中的地址都是一样的,这就保证了传递给远程进程的指针是个有效的指针。

第二个问题也很简单:把DLL的文件名(LodLibrary的参数)用WriteProcessMemory复制到远程进程。

所以,使用CreateRemoteThread和LoadLibrary技术的步骤如下:

1.得到远程进程的HANDLE(使用OpenProcess)。

2.在远程进程中为DLL文件名分配内存(VirtualAllocEx)。

3.把DLL的文件名(全路径)写到分配的内存中(WriteProcessMemory)

4.使用CreateRemoteThread和LoadLibrary把你的DLL映射近远程进程。

5.等待远程线程结束(WaitForSingleObject),即等待LoadLibrary返回。也就是说当我们的DllMain(是以DLL_PROCESS_ATTACH为参数调用的)返回时远程线程也

就立即结束了。

6.取回远程线程的结束码(GetExitCodeThtread),即LoadLibrary的返回值――我们DLL加载后的基地址(HMODULE)。

7.释放第2步分配的内存(VirtualFreeEx)。

8.用CreateRemoteThread和FreeLibrary把DLL从远程进程中卸载。调用时传递第6步取得的HMODULE给FreeLibrary(通过CreateRemoteThread的lpParameter 参数)。

9.等待线程的结束(WaitSingleObject)。

同时,别忘了在最后关闭所有的句柄:第4、8步得到的线程句柄,第1步得到的远程进程句柄。

现在我们看看LibSpy的部分代码,分析一下以上的步骤是任何实现的。为了简单起见,没有包含错误处理和支持Unicode的代码。

HANDLE hThread;

char szLibPath[_MAX_PATH]; // "LibSpy.dll"的文件名

// (包含全路径!);

void* pLibRemote; // szLibPath 将要复制到地址

DWORD hLibModule; //已加载的DLL的基地址(HMODULE);

HMODULE hKernel32 = ::GetModuleHandle("Kernel32");

//初始化szLibPath

//...

// 1. 在远程进程中为szLibPath 分配内存

// 2. 写szLibPath到分配的内存

pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),

MEM_COMMIT, PAGE_READWRITE );

::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,

sizeof(szLibPath), NULL );

// 加载"LibSpy.dll" 到远程进程

// (通过CreateRemoteThread & LoadLibrary)

hThread = ::CreateRemoteThread( hProcess, NULL, 0,

(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,

"LoadLibraryA" ),

pLibRemote, 0, NULL );

::WaitForSingleObject( hThread, INFINITE );

//取得DLL的基地址

::GetExitCodeThread( hThread, &hLibModule );

//扫尾工作

::CloseHandle( hThread );

::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );

我们放在DllMain中的真正要注入的代码(比如为SendMessage)现在已经被执行了(由于DLL_PROCESS_ATTACH),所以现在可以把DLL从目的进程中卸载了。

// 从目标进程卸载LibSpu.dll

// (通过CreateRemoteThread & FreeLibrary)

hThread = ::CreateRemoteThread( hProcess, NULL, 0,

(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,

"FreeLibrary" ),

(void*)hLibModule, 0, NULL );

::WaitForSingleObject( hThread, INFINITE );

// 扫尾工作

::CloseHandle( hThread );

进程间通讯

到目前为止,我们仅仅讨论了任何向远程进程注入DLL,然而,在多数情况下被注入的DLL需要和你的程序以某种方式通讯(记住,那个DLL是被映射到远程进程中的,而不是在你的本地程序中!)。以密码间谍为例:那个DLL需要知道包含了密码的的控件的句柄。很明显,这个句柄是不能在编译期间硬编码(hardcoded)进去的。同样,当DLL

得到密码后,它也需要把密码发回我们的程序。

幸运的是,这个问题有很多种解决方案:文件映射(Mapping),WM_COPYDATA,剪贴板等。还有一种非常便利的方法#pragma data_seg。这里我不想深入讨论因为它们在MSDN(看一下Interprocess Communications部分)或其他资料中都有很好的说明。我在LibSpy中使用的是#pragma data_seg。

你可以在本文章的开头找到LibSpy及源代码的下载链接。

Ⅲ.CreateRemoteThread和WriteProcessMemory技术

示例程序:WinSpy

另一种注入代码到其他进程地址空间的方法是使用WriteProcessMemory API。这次你不用编写一个独立的DLL而是直接复制你的代码到远程进程(WriteProcessMemory)并用CreateRemoteThread执行之。

让我们看一下CreateRemoteThread的声明:

HANDLE CreateRemoteThread(

HANDLE hProcess, // handle to process to create thread in

LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security

// attributes

DWORD dwStackSize, // initial thread stack size, in bytes

LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread

// function

LPVOID lpParameter, // argument for new thread

DWORD dwCreationFlags, // creation flags

LPDWORD lpThreadId // pointer to returned thread identifier

);

和CreateThread相比,有一下不同:

●增加了hProcess参数。这是要在其中创建线程的进程的句柄。

●CreateRemoteThread的lpStartAddress参数必须指向远程进程的地址空间中的函数。这个函数必须存在于远程进程中,所以我们不能简单地传递一个本地ThreadFucn的地址,我们必须把代码复制到远程进程。

●同样,lpParameter参数指向的数据也必须存在于远程进程中,我们也必须复制它。

现在,我们总结一下使用该技术的步骤:

1.得到远程进程的HANDLE(OpenProcess)。

2.在远程进程中为要注入的数据分配内存(VirtualAllocEx)、

3.把初始化后的INJDATA结构复制到分配的内存中(WriteProcessMemory)。

4.在远程进程中为要注入的数据分配内存(VirtualAllocEx)。

5.把ThreadFunc复制到分配的内存中(WriteProcessMemory)。

6.用CreateRemoteThread启动远程的ThreadFunc。

7.等待远程线程的结束(WaitForSingleObject)。

8.从远程进程取回指执行结果(ReadProcessMemory 或GetExitCodeThread)。

9.释放第2、4步分配的内存(VirtualFreeEx)。

10.关闭第6、1步打开打开的句柄。

另外,编写ThreadFunc时必须遵守以下规则:

1.ThreadFunc不能调用除kernel32.dll和user32.dll之外动态库中的API函数。只有kernel32.dll和user32.dll(如果被加载)可以保证在本地和目的进程中的加载地址是一样的。(注意:user32并不一定被所有的Win32进程加载!)参考附录A。如果你需要调用其他库中的函数,在注入的代码中使用LoadLibrary和GetProcessAddress强制加载。如果由于某种原因,你需要的动态库已经被映射进了目的进程,你也可以使用GetMoudleHandle代替LoadLibrary。同样,如果你想在ThreadFunc中调用你自己的函数,那么就分别复制这些函数到远程进程并通过INJDATA把地址提供给ThreadFunc。

2.不要使用static字符串。把所有的字符串提供INJDATA传递。为什么?编译器会把所有的静态字符串放在可执行文件的“.data”段,而仅仅在代码中保留它们的引用(即指针)。这样,远程进程中的ThreadFunc就会执行不存在的内存数据(至少没有在它自己的内存空间中)。

3.去掉编译器的/GZ编译选项。这个选项是默认的(看附录B)。

4.要么把ThreadFunc和AfterThreadFunc声明为static,要么关闭编译器的“增量连接(incremental linking)”(看附录C)。

5.ThreadFunc中的局部变量总大小必须小于4k字节(看附录D)。注意,当degug 编译时,这4k中大约有10个字节会被事显患用。

6.如果有多于3个switch分支的case语句,必须像下面这样分割开,或用if-else if代替:

switch( expression ) {

case constant1: statement1; goto END;

case constant2: statement2; goto END;

case constant3: statement2; goto END;

}

switch( expression ) {

case constant4: statement4; goto END;

case constant5: statement5; goto END;

case constant6: statement6; goto END;

}

END:

(参考附录E)

如果你不按照这些游戏规则玩的话,你注定会使目的进程挂掉!记住,不要妄想远程进程中的任何数据会和你本地进程中的数据存放在相同内存地址!(参看附录F)(原话如此:You will almost certainly crash the target process if you don't play by those rules. Just remember: Don't assume anything in the target process is at the same address as it is in your process.)

GetWindowTextRemote(A/W)

所有取得远程edit中文本的工作都被封装进这个函数:GetWindowTextRemote(A/W):

int GetWindowT extRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );

int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );

参数:

hProcess

目的edit所在的进程句柄

hWnd

目的edit的句柄

lpString

接收字符串的缓冲

返回值:

成功复制的字符数。

让我们看以下它的部分代码,特别是注入的数据和代码。为了简单起见,没有包含支持Unicode的代码。

INJDATA

typedef LRESULT (WINAPI

*SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);

typedef struct {

HWND hwnd; // handle to edit control

SENDMESSAGE fnSendMessage; // pointer to user32!SendMessageA

char psText[128]; // buffer that is to receive the password

} INJDATA;

INJDATA是要注入远程进程的数据。在把它的地址传递给SendMessageA之前,我们要先对它进行初始化。幸运的是unse32.dll在所有的进程中(如果被映射)总是被映射到相同的地址,所以SendMessageA的地址也总是相同的,这也保证了传递给远程进程的地址是有效的。

ThreadFunc

static DWORD WINAPI ThreadFunc (INJDATA *pData)

{

pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // 得到密码

sizeof(pData->psText),

(LPARAM)pData->psT ext );

return 0;

}

// This function marks the memory address after ThreadFunc.

// int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.

static void AfterThreadFunc (void)

{

}

ThreadFunc是远程线程实际执行的代码。

●注意AfterThreadFunc是如何计算ThreadFunc的代码大小的。一般地,这不是最好的办法,因为编译器会改变你的函数中代码的顺序(比如它会把ThreadFunc放在AfterThreadFunc之后)。然而,你至少可以确定在同一个工程中,比如在我们的WinSpy 工程中,你函数的顺序是固定的。如果有必要,你可以使用/ORDER连接选项,或者,用反汇编工具确定ThreadFunc的大小,这个也许会更好。

如何用该技术子类(subclass)一个远程控件

示例程序:InjectEx

让我们来讨论一个更复杂的问题:如何子类属于其他进程的一个控件?

首先,要完成这个任务,你必须复制两个函数到远程进程:

1.ThreadFunc,这个函数通过调用SetWindowLong API来子类远程进程中的控件,

2.NewProc,那个控件的新窗口过程(Window Procedure)。

然而,最主要的问题是如何传递数据到远程的NewProc。因为NewProc是一个回调(callback)函数,它必须符合特定的要求(译者注:这里指的主要是参数个数和类型),我们不能再简单地传递一个INJDATA的指针作为它的参数。幸运的我已经找到解决这个问题的方法,而且是两个,但是都要借助于汇编语言。我一直都努力避免使用汇编,但是这一次,我们逃不掉了,没有汇编不行的。

解决方案1

看下面的图片:

不知道你是否注意到了,INJDATA紧挨着NewProc放在NewProc的前面?这样的话在编译期间NewProc就可以知道INJDATA的内存地址。更精确地说,它知道INJDATA相对于它自身地址的相对偏移,但是这并不是我们真正想要的。现在,NewProc看起来是这个样子:

static LRESULT CALLBACK NewProc(

HWND hwnd, // handle to window

UINT uMsg, // message identifier

WPARAM wParam, // first message parameter

LPARAM lParam ) // second message parameter

{

INJDATA* pData = (INJDATA*) NewProc; // pData 指向

// NewProc;

pData--; // 现在pData指向INJDATA;

// 记住,INJDATA 在远程进程中刚好位于

// NewProc的紧前面;

//-----------------------------

// 子类代码

// ........

//-----------------------------

//调用用来的的窗口过程;

// fnOldProc (由SetWindowLong返回) 是被ThreadFunc(远程进程中的)初始化

// 并且存储在远程进程中的INJDATA里的;

return pData->fnCallWindowProc( pData->fnOldProc,

hwnd,uMsg,wParam,lParam );

}

然而,还有一个问题,看第一行:

INJDATA* pData = (INJDATA*) NewProc;

pData被硬编码为我们进程中NewProc的地址,但这是不对的。因为NewProc 会被复制到远程进程,那样的话,这个地址就错了。

用C/C++没有办法解决这个问题,可以用内联的汇编来解决。看修改后的

NewProc:

static LRESULT CALLBACK NewProc(

HWND hwnd, // handle to window

UINT uMsg, // message identifier

WPARAM wParam, // first message parameter

LPARAM lParam ) // second message parameter

{

// 计算INJDATA 的地址;

// 在远程进程中,INJDATA刚好在

//NewProc的前面;

INJDATA* pData;

_asm {

call dummy

dummy:

pop ecx // <- ECX 中存放当前的EIP

sub ecx, 9 // <- ECX 中存放NewProc的地址

mov pData, ecx

}

pData--;

//-----------------------------

// 子类代码

// ........

//-----------------------------

// 调用原来的窗口过程

return pData->fnCallWindowProc( pData->fnOldProc,

hwnd,uMsg,wParam,lParam );

}

这是什么意思?每个进程都有一个特殊的寄存器,这个寄存器指向下一条要执行的指令的内存地址,即32位Intel和AMD处理器上所谓的EIP寄存器。因为EIP是个特殊的寄存器,所以你不能像访问通用寄存器(EAX,EBX等)那样来访问它。换句话说,你找不到一个可以用来寻址EIP并且对它进行读写的操作码(OpCode)。然而,EIP同样可以被JMP,CALL,RET等指令隐含地改变(事实上它一直都在改变)。让我们举例说明32位的Intel和AMD处理器上CALL/RET是如何工作的吧:

当我们用CALL调用一个子程序时,这个子程序的地址被加载进EIP。同时,在EIP被改变之前,它以前的值会被自动压栈(在后来被用作返回指令指针[return instruction-pointer])。在子程序的最后RET指令自动把这个值从栈中弹出到EIP。

现在我们知道了如何通过CALL和RET来修改EIP的值了,但是如何得到他的当前值?

还记得CALL把EIP的值压栈了吗?所以为了得到EIP的值我们调用了一个“假(dummy)函数”然后弹出栈顶值。看一下编译过的NewProc:

Address OpCode/Params Decoded instruction

--------------------------------------------------

:00401000 55 push ebp ; entry point of

; NewProc

:00401001 8BEC mov ebp, esp

:00401003 51 push ecx

:00401004 E800000000 call 00401009 ; *a* call dummy

:00401009 59 pop ecx ; *b*

:0040100A 83E909 sub ecx, 00000009 ; *c*

:0040100D 894DFC mov [ebp-04], ecx ; mov pData, ECX

:00401010 8B45FC mov eax, [ebp-04]

:00401013 83E814 sub eax, 00000014 ; pData--;

.....

.....

:0040102D 8BE5 mov esp, ebp

:0040102F 5D pop ebp

:00401030 C21000 ret 0010

a.一个假的函数调用;仅仅跳到下一条指令并且(译者注:更重要的是)把EIP 压栈。

b.弹出栈顶值到ECX。ECX就保存的EIP的值;这也就是那条“pop ECX”指令的地址。

c.注意从NewProc的入口点到“pop ECX”指令的“距离”为9字节;因此把ECX 减去9就得到的NewProc的地址了。

这样一来,不管被复制到什么地方,NewProc总能正确计算自身的地址了!然而,要注意从NewProc的入口点到“pop ECX”的距离可能会因为你的编译器/链接选项的不同而不同,而且在Release和Degub版本中也是不一样的。但是,不管怎样,你仍然可以在编译期知道这个距离的具体值。

1.首先,编译你的函数。

2.在反汇编器(disassembler)中查出正确的距离值。

3.最后,使用正确的距离值重新编译你的程序。

这也是InjectEx中使用的解决方案。InjectEx和HookInjEx类似,交换开始按钮上的鼠标左右键点击事件。

解决方案2

在远程进程中把INJDATA放在NewProc的前面并不是唯一的解决方案。看一下下面的NewProc:

static LRESULT CALLBACK NewProc(

HWND hwnd, // handle to window

UINT uMsg, // message identifier

WPARAM wParam, // first message parameter

LPARAM lParam ) // second message parameter

{

INJDATA* pData = 0xA0B0C0D0; // 一个假值

//-----------------------------

// 子类代码

// ........

//-----------------------------

// 调用以前的窗口过程

return pData->fnCallWindowProc( pData->fnOldProc,

hwnd,uMsg,wParam,lParam );

}

C程序代码大全

//根据半径计算圆的周长和面积#include const float PI=3.1416; //声明常量(只读变量)PI为3.1416 float fCir_L(float); //声明自定义函数fCir_L()的原型 float fCir_S(float); //声明自定义函数fCir_S()的原型 //以下是main()函数 main() { float r,l,s; //声明3个变量 cout<<"r="; //显示字符串 cin>>r; //键盘输入 l=fCir_L(r); //计算圆的周长,赋值给变量l s=fCir_S(r); //计算圆的面积,赋值给变量s cout<<"l="<=70) cout<<"Your grade is a C."<=60) cout<<"Your grade is a D."< main() { int n; cout<<"n="; cin>>n; if (n>=0 && n<=100 &&n%2==0) cout<<"n="< main() { int a,b,Max; .10 for(int i=1;i<=10;i++) cout<=1;j--) cout<

进程调度算法实验报告

作业调度 一、实验名称 作业调度算法 二、实验目标 在单道环境下编写作业调度的模拟程序,以加深对作业调度的理解。单道环境的特点使被调度的作业占有所有的资源。实现的算法有先来先服务,最短作业优先,最高响应比三种作业调度算法。 三、实验环境要求: 1.PC机。 2.Windows; 3.CodeBlocks 四、实验基本原理 1.本实验设计一个可指定作业个数的作业调度系统。可以输出先来先服务,最短作业优先,最高响应比三种作业调度算法的结果。 2.先来先服务就是按照各个作业进入系统的自然次序进行调度。最短作业优先就是优先调度并且处理短作业。最高响应比优先就是根据在程序运行过程中的最高响应比对应的作业先进行调度处理。 3.在设计程序过程中,将time相关的内容封装到类中,重载了加减乘除和输入输出以及比较运算符,方便12:00这种形式的数据的加减乘除运算和比较运算, 五、数据结构设计 1.时间类

class time { public: time(int x = 0, int y = 0) { time::hour = x; time::minute = y; } time& operator = (const time &t1) { this->hour=t1.hour; this->minute=t1.minute; return *this; } time operator + (time t2) { intminutes,hours; minutes = (minute + t2.minute) % 60; hours=hour+t2.hour+ (minute + t2.minute) /60; return time(hours,minutes); } time operator -(time t2) { intminutes,hours; minutes =minute - t2.minute; if (minute<0) { minutes += 60; hour--; }

PE格式基础及程序的装入

DOS MZ header部分是DOS时代遗留的产物,是PE文件的一个遗传基因,一个Win32程序如果在DOS下也是可以执行,只是提示:“This program cannot be run in DOS mode.”然后就结束执行,提示执行者,这个程序要在Win32系统下执行。 DOS stub 部分是DOS插桩代码,是DOS下的16位程序代码,只是为了显示上面的提示数据。这段代码是编译器在程序编译过程中自动添加的。 PE header 是真正的Win32程序的格式头部,其中包括了PE格式的各种信息,指导系统如何装载和执行此程序代码。 Section table部分是PE代码和数据的结构数据,指示装载系统代码段在哪里,数据段在哪里等。对于不同的PE文件,设计者可能要求该文件包括不同的数据的Section。所以有一个Section Table 作为索引。Section多少可以根据实际情况而不同。但至少要有一个Section。如果一个程序连代码都没有,那么他也不能称为可执行代码。在Section Table后,Section数目的多少是不定的。 二、程序的装入 当我们在explorer.exe(资源管理器)中双击某文件,执行一个可执行程序,系统会根据文件扩展名启动一个程序装载器,称之为Loader。Loader会首先检查DOS MZ Header,如果存在,就继续寻找PE header,如果这两项都不存在,就认为是DOS 16位代码,如果只存在DOS MZ Header,而其中又指示了而其中又指示了PE Header 的位置,那么Loader 就判定此文件不一个有效的PE文件,拒绝执行。 如果DOS Header 和PE Header都正常有效,那么Loader就会根据PE Header 及Section Table的指示,将相应的代码和数据映射到内存中,然后根据不同的Section进行数据的初始化,最后开始执行程序段代码。 三、PE格式高级分析 下面我们以一个真实的程序为例详细分析PE格式,分析PE格式最好有PE分析器,常用的软件是Lord PE,也有其它的分析工具和软件如PE Editor 、Stud PE等。 先分析一下磁盘文件的内容,这里我们使用UltraEdit32(UE)工具,这是一个实用的文件编辑器,可以编辑文本和二进制文件。

数控编程G、M、T、S代码大全(精选.)

数控机床标准G、M代码 一.准备功能字G 准备功能字是使数控机床建立起某种加工方式的指令,如插补、刀具补偿、固定循环等。G功能字由地址符G和其后的两位数字组成,从G00—G99共100种功能。JB3208-83标准中规定如下表: 代码功能 作用范 围 功能 代码 功能作用范围功能 G00 点定位 G50 * 刀具偏置0/- G01 直线插补 G51 * 刀具偏置+/0 G02 顺时针圆弧插补 G52 * 刀具偏置-/0 G03 逆时针圆弧插补 G53 直线偏移注销 G04 * 暂停 G54 直线偏移 X G05 * 不指定 G55 直线偏移Y G06 抛物线插补 G56 直线偏移Z G07 * 不指 定 G57 直线偏移XY G08 * 加速 G58 直线偏移XZ G09 * 减速 G59 直线偏移YZ G10-G16 * 不指定 G60 准确定位(精) G17 XY平面选 择 G61 准确定位(中) G18 ZX平面选择 G62 准确定位(粗) G19 YZ平面选择 G63 * 该丝 G20-G32 * 不指定 G64-G67 * 不指定 G33 螺纹切削,等螺距 G68 * 刀具偏置,内角 G34 螺纹切削,增螺距 G69 * 刀具偏置,外角 G35 螺纹切削,减螺距 G70-G79 * 不指定 G36-G39 * 不指定 G80 固定循环注销 G40 刀具补偿/刀具偏置 注销 G81-G89 固定循环 G41 刀具补偿--左 G90 绝对尺寸 G42 刀具补偿-- 右 G91 增量尺寸 G43 * 刀具偏置--正 G92 * 预置寄存 G44 * 刀具偏置--右 G93 进给率,时间倒数 G45 * 刀具偏置+/+ G94 每分钟进给 G46 * 刀具偏置+/- G95 主轴每转进给 G47 * 刀具偏置-/- G96 恒线速 度 G48 * 刀具偏置-/+ G97 每分钟转数(主轴) G49 * 刀具偏置0/+ G98-G99 * 不指定 注:*表示如作特殊用途,必须在程序格式中说明二.辅助功能字M

进程调度算法实验报告

操作系统实验报告(二) 实验题目:进程调度算法 实验环境:C++ 实验目的:编程模拟实现几种常见的进程调度算法,通过对几组进程分别使用不同的调度算法,计算进程的平均周转时间和平均带权周转时间,比较 各种算法的性能优劣。 实验内容:编程实现如下算法: 1.先来先服务算法; 2.短进程优先算法; 3.时间片轮转调度算法。 设计分析: 程序流程图: 1.先来先服务算法 开始 初始化PCB,输入进程信息 各进程按先来先到的顺序进入就绪队列 结束 就绪队列? 运行 运行进程所需CPU时间 取消该进程 2.短进程优先算法

3.时间片轮转调度算法 实验代码: 1.先来先服务算法 #include #define n 20 typedef struct { int id; //进程名

int atime; //进程到达时间 int runtime; //进程运行时间 }fcs; void main() { int amount,i,j,diao,huan; fcs f[n]; cout<<"请输入进程个数:"<>amount; for(i=0;i>f[i].id; cin>>f[i].atime; cin>>f[i].runtime; } for(i=0;if[j+1].atime) {diao=f[j].atime; f[j].atime=f[j+1].atime; f[j+1].atime=diao; huan=f[j].id; f[j].id=f[j+1].id; f[j+1].id=huan; } } } for(i=0;i #define n 5 #define num 5 #define max 65535 typedef struct pro { int PRO_ID; int arrive_time;

C语言代码大全

------------------------------------------------------------------------摘自宋鲁生程序设计大赛 乘法口诀表 #include #include void main(void) { int i,j,x,y; clrscr(); printf("\n\n * * * 乘法口诀表* * * \n\n"); x=9; y=5; for(i=1;i<=9;i++) { gotoxy(x,y); printf("%2d ",i); x+=3; } x=7; y=6; for(i=1;i<=9;i++) { gotoxy(x,y); printf("%2d ",i); y++; } x=9; y= 6; for(i=1;i<=9;i++) { for(j=1;j<=9;j++) { gotoxy(x,y); printf("%2d ",i*j); y++; } y-=9; x+=3; } printf("\n\n"); }

用一维数组统计学生成绩 #include void main() { char SelectKey,CreditMoney,DebitMoney; while(1) { do{ clrscr(); puts("========================="); puts("| Please select key: |"); puts("| 1. Quary |"); puts("| 2. Credit |"); puts("| 3. Debit |"); puts("| 4. Return |"); puts("========================="); SelectKey = getch(); }while( SelectKey!='1' && SelectKey!='2' && SelectKey!='3' && SelectKey!='4' ); switch(SelectKey) { case '1': clrscr(); puts("================================"); puts("| Your balance is $1000. |"); puts("| Press any key to return... |"); puts("================================"); getch(); break; case '2': do{ clrscr(); puts("=================================="); puts("| Please select Credit money: |"); puts("| 1. $50 |"); puts("| 2. $100 |"); puts("| 3. Return |"); puts("=================================="); CreditMoney = getch(); }while( CreditMoney!='1' && CreditMoney!='2' && CreditMoney!='3' ); switch(CreditMoney)

进程模拟调度算法课程设计

一.课程概述 1.1.设计构想 程序能够完成以下操作:创建进程:先输入进程的数目,再一次输入每个进程的进程名、运行总时间和优先级,先到达的先输入;进程调度:进程创建完成后就选择进程调度算法,并单步执行,每次执行的结果都从屏幕上输出来。 1.2.需求分析 在多道程序环境下,主存中有着多个进程,其数目往往多于处理机数目,要使这多个进程能够并发地执行,这就要求系统能按某种算法,动态地把处理机分配给就绪队列中的一个进程,使之执行。分配处理机的任务是由处理机调度程序完成的。由于处理机是最重要的计算机资源,提高处理机的利用率及改善系统必(吞吐量、响应时间),在很大程度上取决于处理机调度性能的好坏,因而,处理机调度便成为操作系统设计的中心问题之一。本次实验在VC++6.0环境下实现先来先服务调度算法,短作业优先调度算法,高优先权调度算法,时间片轮转调度算法和多级反馈队列调度算法。 1.3.理论依据 为了描述和管制进程的运行,系统为每个进程定义了一个数据结构——进程控制块PCB(Process Control Block),PCB中记录了操作系统所需的、用于描述进程的当前情况以及控制进程运行的全部信息,系统总是通过PCB对进程进行控制,亦即,系统是根据进程的PCB 而不是任何别的什么而感知进程的存在的,PCB是进程存在的惟一标志。本次课程设计用结构体Process代替PCB的功能。 1.4.课程任务 一、用C语言(或C++)编程实现操作模拟操作系统进程调度子系统的基本功能;运用多 种算法实现对进程的模拟调度。 二、通过编写程序实现进程或作业先来先服务、高优先权、按时间片轮转、短作业优先、多 级反馈队列调度算法,使学生进一步掌握进程调度的概念和算法,加深对处理机分配的理解。 三、实现用户界面的开发

PE文件头解析大全

PE可选头部 PE可执行文件中接下来的224个字节组成了PE可选头部。虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。OPTHDROFFSET宏可以获得指向可选头部的指针: PEFILE.H #define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + \ ((PIMAGE_DOS_HEADER)a)->e_lfanew + \ SIZE_OF_NT_SIGNATURE + \ sizeof(IMAGE_FILE_HEADER))) 可选头部包含了很多关于可执行映像的重要信息,例如初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等等。IMAGE_OPTIONAL_HEADER结构如下: WINNT.H typedef struct _IMAGE_OPTIONAL_HEADER { // // 标准域 // USHORT Magic; UCHAR MajorLinkerVersion; UCHAR MinorLinkerVersion; ULONG SizeOfCode; ULONG SizeOfInitializedData; ULONG SizeOfUninitializedData; ULONG AddressOfEntryPoint; ULONG BaseOfCode; ULONG BaseOfData; // // NT附加域 // ULONG ImageBase; ULONG SectionAlignment;

ULONG FileAlignment; USHORT MajorOperatingSystemVersion; USHORT MinorOperatingSystemVersion; USHORT MajorImageVersion; USHORT MinorImageVersion; USHORT MajorSubsystemVersion; USHORT MinorSubsystemVersion; ULONG Reserved1; ULONG SizeOfImage; ULONG SizeOfHeaders; ULONG CheckSum; USHORT Subsystem; USHORT DllCharacteristics; ULONG SizeOfStackReserve; ULONG SizeOfStackCommit; ULONG SizeOfHeapReserve; ULONG SizeOfHeapCommit; ULONG LoaderFlags; ULONG NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER; 如你所见,这个结构中所列出的域实在是冗长得过分。为了不让你对所有这些域感到厌烦,我会仅仅讨论有用的——就是说,对于探究PE文件格式而言有用的。 标准域 首先,请注意这个结构被划分为“标准域”和“NT附加域”。所谓标准域,就是和UNIX可执行文件的COFF 格式所公共的部分。虽然标准域保留了COFF中定义的名字,但是Windows NT仍然将它们用作了不同的目的——尽管换个名字更好一些。 ·Magic。我不知道这个域是干什么的,对于示例程序EXEVIEW.EXE示例程序而言,这个值是0x010B

C 经典程序代码大全

C 经典程序代码大全 #include const float PI= 3.1416; //声明常量(只读变量)PI为 3.1416 float fCir_L(float); //声明自定义函数fCir_L()的原型 float fCir_S(float); //声明自定义函数fCir_S()的原型 //以下是main()函数 main() { float r,l,s; //声明3个变量 cout>r; //键盘输入 l=fCir_L(r); //计算圆的周长,赋值给变量l s=fCir_S(r); //计算圆的面积,赋值给变量s cout=0.0) //如果参数大于0,则计算圆的周长 z=2*PI*x; return(z); //返回函数值 } //定义计算圆的面积的函数fCir_S() float fCir_S(float x) { float z=- 1.0; //声明局部变量 if (x>=0.0) //如果参数大于0,则计算圆的面积 z=PI*x*x; return(z); //返回函数值 } /* Program: P1- 2.CPP Written by: Hap Date written: 02:11:10 */ #include void main(void) { double s1,s2,s3; s1= 1.5; /* 对变量s1赋值*/ cout main() { double r=

1.0; cout>r; //键盘输入 l=2* 3.1416*r; //计算圆的周长,赋值给变量l cout //包含iostream.h头文件 void main() { //输出字符常量.变量和字符串 char c1= A ; cout //包含iostream.h头文件 main() { //输入输出字符 char c; cin>>c; cout>n; cout>x; cout>n; cout>c>>n>>x; cout //包含iostream.h头文件 main() { //声明整型变量 int a,b; //从键盘上为整型变量赋值cout>a; cout>b; //整型数的算术运算 cout //包含iostream.h 头文件 main() { //声明变量,并初始化 int a=010,b=10,c=0X10; //以进制形式显示数据 cout>a; cout>b; cout>c; cout //包含iostream.h头文件 #include // iomanip.h头文件包含setprecision()的定义 main() { //float型变量的声明.输入.计算和输出 float fx,fy; cout>fx; cout>fy; cout>dx; cout>dy; cout //包含iostream.h 头文件 main() { //字符类型变量的声明 char c1= A ; char c2; //字符数据的运算及输出 c2=c1+32; cout>c1>>c2; cout //包含iostream.h头文件 main() { char c1= \a ,TAB= \t ; //阵铃一声 cout //包含iostream.h头文件 main()

进程调度算法1

进程调度算法(附录)#include #include #include #include #include #include #define P_NUM 5 #define P_TIME 50 enum state { ready, execute, block, finish }; struct pcb { char name[4]; int priority; int cputime; int needtime; int count; int round; state process; pcb * next; }; pcb * get_process(); pcb * get_process() { pcb *q; pcb *t; pcb *p; int i=0; cout<<"input name and time"<>q->name; cin>>q->needtime; q->cputime=0; q->priority=P_TIME-q->needtime; q->process=ready; q->next=NULL; if (i==0){ p=q; t=q;} else{t->next=q;t=q; } i++; } //while return p; }

void display(pcb *p) { cout<<"name"<<" "<<"cputime"<<" "<<"needtime"<<" "<<"priority"<<" "<<"state"<name; cout<<" "; cout<cputime; cout<<" "; cout<needtime; cout<<" "; cout<priority; cout<<" "; switch(p->process) { case ready:cout<<"ready"<next; } } int process_finish(pcb *q) { int bl=1; while(bl&&q){ bl=bl&&q->needtime==0; q=q->next; } return bl; } void cpuexe(pcb *q) { pcb *t=q; int tp=0; while(q){ if (q->process!=finish) { q->process=ready; if(q->needtime==0){ q->process=finish; } } if(tppriority&&q->process!=finish) { tp=q->priority; t=q;

CNC加工中心程序代码大全

1. 数控程序中字母的含义 O:程序号,设定程序号 N:程序段号,设定程序顺序号 G:准备功能 X/Y/Z :尺寸字符,轴移动指令 A/B/C/U/V/W:附加轴移动指令 R:圆弧半径 I/J/K:圆弧中心坐标(矢量) F:进给,设定进给量 S:主轴转速,设定主轴转速 T:刀具功能,设定刀具号 M:辅助功能,开/关控制功能 H/D:刀具偏置号,设定刀具偏置号 P/X:延时,设定延时时间 P:程序号指令,设定子程序号(如子程序调用:M98P1000) L:重复,设定子程序或固定循环重复次数(如:M98 P1000 L2,省略L代表L1)P/W/R/Q:参数,固定循环使用的参数(如:攻牙G98/(G99)G84 X_ Y_ R_ Z_ P_ F_) 2. 常用G代码解释 G00:定位或快速移动 G01:直线插补 G02:圆弧插补/螺旋线插补CW G03:圆弧插补/螺旋线插补CCW G04:停留时间或延时时间 如:G04 X1000(或G04 X1.0) G04 P1000表示停留1秒钟 G09:准确停止或精确停止检查(检查是否在目标范围内) G10:可编程数据输入 G17:选择XPYP 平面 XP:X 轴或其平行轴 G18:选择ZPXP 平面 YP:Y 轴或其平行轴 G19:选择YPZP 平面 ZP:Z 轴或其平行轴 G20:英寸输入 G21:毫米输入 G28:返回参考点检测 格式:G91/(G90) G28 X__ Y__ Z__ 经过中间点X__ Y__ Z__返回参考点(绝对值/增量值指令) G29:从参考点返回 G91/(G90) G29 X__ Y__ Z__ 从起始点经过参考点返回到目标点X__ Y__ Z__的指令(绝对值/增量值指令) G30 返回第2,3,4 参考点 G91/(G90) G30 P2 X__ Y__ Z__;返回第2 参考点(P2 可以省略。) G91/(G90) G30 P3 X__ Y__ Z__;返回第3 参考点

PE文件结构

检验PE文件的有效性 <1>首先检验文件头部第一个字的值是否等于IMAGE_DOS_SIGNATURE,是则表示DOS MZ header有效 <2>一旦证明文件的Dos header 有效后,就可用e_lfanew来定位PE header <3>比较PE header 的第一个字的值是否等于IMAGE_NT_HEADER,如果前后两个值都匹配. PS.WinHex使用方法 1.Alt+G跳到指定位置 2.Ctrl+Shift+N放入新文件 3.大文件扩容,新建一个扩容大小+1的文件,把这个文件的数据复制后写入整个文件的尾地址. 4.文本搜索ctrl+F 5.十六进制搜索ctrl+alt+x 6.文本显示F7 7.打开内存alt+F9 8.进制转换器F8 9.分析选块F2 10.计算HASH ctrl+F2 11.收集文本信息ctrl+F10 12.编辑模式F6 一.IMAGE_DOS_HEADER <1>位置00H,WORD(2个字节)的e_magic为4D5A,即MZ <2>位置3CH,60,LONG(4个节节)的e_lfanew为64+112=176即B0H, 二.IMAGE_NT_HEADERS <1>位置B0H,DWORD(4个字节),PE开始标记,写入50450000,即PE <2>位置B4H,WORD,PE所要求的CPU,对于Intel平台,为4C01 <2>位置B6,WORD,PE中段总数,计划有3个段,.text代码段,.rdata只读数据段,.data全局变量数据段,所以值为0300, <3>位置C4,WORD,表示后面的PE文件可选头的占空间大小,即224字节(E0),值为E000 <4>位置C6,WORD,表示文件是EXE还是DLL,如果是可执行文件写0200,如果是dll,写0020, <5>位置C8,WORD,表示文件格式,如果是0B01表示.exe,如果是0701表示ROM映像

操作系统模拟进程调度算法

操作系统 ——项目文档报告 进程调度算法 专业: 班级: 指导教师: 姓名: 学号:

一、核心算法思想 1.先来先服务调度算法 先来先服务调度算法是一种最简单的调度算法,该算法既可以用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将他们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。 2.短作业(进程)优先调度算法 短作业(进程)优先调度算法SJ(P)F,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机再重新调度。SJ(P)F调度算法能有效地降低作业(进程)的平均等待时间,提高系统吞吐量。该算法对长作业不利,完全未考虑作业的紧迫程度。 3.高响应比优先调度算法 在批处理系统中,短作业优先算法是一种比较好的算法,其主要不足之处是长作业的运行得不到保证。如果我们能为每个作业引人动态优先权,并使作业的优先级随着等待时间的增加而以速率a提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为: 优先权=(等待时间+要求服务时间)/要求服务时间 即优先权=响应时间/要求服务时间 如果作业的等待时间相同,则要求服务的时间越短,其优先权越高,因而该算法有利于短作业。 当要球服务的时间相同时,作业的优先权决定于其等待时间,等待时间越长,优先权越高,因而它实现的是先来先服务 对于长作业,作业的优先级可以随着等待时间的增加而提高,当其等待时间足够长时,其优先级便可以升到很高,从而也可获得处理机。 4.时间片轮转算法 在时间片轮转算法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。当执行的时间片用完时,由一个计数器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。 二、核心算法流程图

相关文档
相关文档 最新文档