65.9K
CodeProject 正在变化。 阅读更多。
Home

迁移到 Windows Vista x64

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (128投票s)

2007年1月21日

CPOL

60分钟阅读

viewsIcon

362160

downloadIcon

5197

一篇关于 x64 和 Windows Vista 的文章

目录

Windows Vista x64 Logo

引言

这是对 Windows Vista 和 x64 架构的介绍。写这样的文章总是不容易,因为有很多可以谈论的内容,但另一方面,它是一篇文章,而不是一本书。我试图专注于一些重要的方面,但不用说,我不得不删掉很多内容(例如,用户模式驱动框架,我对此感到非常抱歉)。这只是对某些主题的概述,如果您想了解更多,那么您应该真正考虑转向特定的指南。此外,我不会谈论一些关于 x64 架构的显而易见的问题,比如应用程序现在可以访问更大的内存范围等。本文应被视为 x86/XP 开发人员的快速升级指南。

在我写这篇文章的时候,我已经使用 Windows Vista 一个月了,它的正式发布日期定于 1 月 30 日(也就是再过一个月)。几个月前,我随 XP 迁移到了 x64,当时我很惊讶地发现我所有设备的驱动程序都找到了。但是,正如我们所知,Windows Vista 要求驱动程序经过认证,而为了获得认证,公司必须提供驱动程序的 x64 版本。仅有 x86 版本的驱动程序将无法获得认证。然而,在我写这篇文章的时候,很多应用程序,比如虚拟驱动器加密器,都没有为 Vista 提供驱动程序(因为 x64 版本还没有获得证书)。如果您不知道认证的事,别担心,我稍后会谈到,您会看到没有它仍然可以运行驱动程序。我只是想说,硬件兼容性不再像一年前那样是个问题,切换到 Windows Vista x64 并不会冒太大的风险。

我试图将本文组织成两个部分,一部分关于 x64 带来的变化,另一部分关于 Vista 带来的变化。我尽力将这两件事分开,因为 x64 技术在 Windows XP 下已经存在,所以对我来说,让读者清楚地区分那些只影响 Vista 的事物和那些同时影响两个主题的事物非常重要。

x64 部分

x64 汇编

在本段中,我将尝试解释 x64 汇编的基础知识。我假设读者已经熟悉 x86 汇编,否则他将无法理解本段的内容。此外,由于这只是一个非常(但非常)简短的指南,您将需要查阅 AMD64 文档以了解更高级的内容。有些东西我甚至不会提及,您会自己发现某些指令已不再使用:例如,lea 指令已经完全取代了 mov offset。

您会立刻注意到 x64 语法中有更多寄存器

  • 8 个新的通用寄存器(GPRs)。
  • 8 个新的 128 位 XMM 寄存器。

当然,所有通用寄存器都是 64 位宽的。我们已经知道的旧寄存器在其 64 位形式中很容易识别:rax、rbx、rcx、rdx、rsi、rdi、rbp、rsp(如果我们想算上指令指针,还有 rip)。这些旧寄存器仍然可以在其较小的位范围内访问,例如:rax、eax、ax、ah、al。新寄存器从 r8 到 r15,可以在它们的各种位范围内访问,如下所示:r8 (qword)、r8d (dword)、r8w (word)、r8b (低字节)。

这是摘自 AMD 文档的一张图

应用程序仍然可以使用段寄存器作为寻址的基础,但 64 位模式只识别三个旧的段寄存器(并且只有两个可以用于基地址计算)。这是另一张图

现在,是最重要的事情。调用约定和堆栈。x64 汇编使用 FASTCALLs 作为调用约定,这意味着它使用寄存器传递前 4 个参数(然后是堆栈)。因此,堆栈帧由以下部分组成:堆栈参数、寄存器参数、返回地址(我提醒您这是一个 qword)和局部变量。第一个参数是 rcx 寄存器,第二个是 rdx,第三个是 r8,第四个是 r9。说参数寄存器是堆栈帧的一部分,也清楚地表明任何调用另一个子函数的函数都必须初始化堆栈,为这四个寄存器提供空间,即使传递给子函数的参数少于四个。堆栈指针的初始化只在函数的序言中进行,它必须足够大以容纳传递给子函数的所有参数,并且清理堆栈始终是调用者的责任。现在,要理解堆栈帧中如何提供空间,最重要的一点是堆栈必须是 16 字节对齐的。实际上,返回地址必须对齐到 16 字节。所以,堆栈空间总是像 16n + 8 这样的形式,其中 n 取决于参数的数量。这是一个堆栈帧的小图

如果您还没有完全弄清楚它是如何工作的,请不要担心:现在我们将看到一些代码示例,在我看来,这些示例总是让理论更容易理解。让我们以一个 hello-world 应用程序为例,比如

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                     LPSTR szCmdLine, int iCmdShow)
{
    MessageBox(NULL, _T("Hello World!"), _T("My First x64 Application"), 0);
    return 0;
}

这段代码反汇编后看起来像

.text:0000000000401220 sub_401220 proc near       ; CODE XREF: start+10E p

.text:0000000000401220
.text:0000000000401220 arg_0= qword ptr 8
.text:0000000000401220 arg_8= qword ptr 10h
.text:0000000000401220 arg_10= qword ptr 18h
.text:0000000000401220 arg_18= dword ptr 20h
.text:0000000000401220
.text:0000000000401220    mov [rsp+arg_18], r9d
.text:0000000000401225    mov [rsp+arg_10], r8
.text:000000000040122A    mov [rsp+arg_8], rdx
.text:000000000040122F    mov [rsp+arg_0], rcx
.text:0000000000401234    sub rsp, 28h
.text:0000000000401238    xor r9d, r9d            ; uType

.text:000000000040123B    lea r8, Caption         ; "My First x64 Application"

.text:0000000000401242    lea rdx, Text           ; "Hello World!"

.text:0000000000401249    xor ecx, ecx            ; hWnd

.text:000000000040124B    call cs:MessageBoxA
.text:0000000000401251    xor eax, eax
.text:0000000000401253    add rsp, 28h
.text:0000000000401257    retn
.text:0000000000401257 sub_401220 endp

堆栈指针的初始化完全是关于我前面说过的那些事情。因为我们正在调用一个带参数的子函数,我们需要为所有四个参数寄存器(0x20,这个值已经对齐到 16 字节)和返回地址(0x08)提供空间。因此,我们将得到 0x28。请记住,如果堆栈值太小或未对齐,您的代码将立即崩溃。另外,不要奇怪为什么这个函数中没有 `ExitProcess`:用 Visual C++ 编译上面的代码总是会添加一个存根(`WinMainCRTStartup`),然后调用我们的 `WinMain`。所以,`ExitProcess` 在存根代码中。但是,如果 `MessageBox` 之前的代码调用了一个需要七个参数而不是四个参数的函数,会发生什么呢?

.text:0000000000401180 sub_401180 proc near        ; CODE XREF: sub_4011F0+4 p

.text:0000000000401180                             ; sub_4011F0+11 p

.text:0000000000401180
.text:0000000000401180 var_28= qword ptr -28h
.text:0000000000401180 var_20= qword ptr -20h
.text:0000000000401180 var_18= qword ptr -18h
.text:0000000000401180
.text:0000000000401180    sub rsp, 48h
.text:0000000000401184    lea rax, unk_402040
.text:000000000040118B    mov [rsp+48h+var_18], rax
.text:0000000000401190    lea rax, unk_402044
.text:0000000000401197    mov [rsp+48h+var_20], rax
.text:000000000040119C    lea rax, unk_402048
.text:00000000004011A3    mov [rsp+48h+var_28], rax
.text:00000000004011A8    lea r9, qword_40204C   ; __int64

.text:00000000004011AF    lea r8, qword_40204C+4 ; __int64

.text:00000000004011B6    lea rdx, unk_402054    ; __int64

.text:00000000004011BD    lea rcx, aAa           ; "ptr"

.text:00000000004011C4    call TakeSevenParameters
.text:00000000004011C9    xor r9d, r9d               ; uType

.text:00000000004011CC    lea r8, Caption        ; "My First x64 Application"

.text:00000000004011D3    lea rdx, Text          ; "Hello World!"

.text:00000000004011DA    xor ecx, ecx           ; hWnd

.text:00000000004011DC    call cs:MessageBoxA
.text:00000000004011E2    add rsp, 48h
.text:00000000004011E6    retn
.text:00000000004011E6 sub_401180 endp

如前所述,子函数接受 7 个参数,因此需要在堆栈上为 3 个额外的参数提供空间。所以,7 * 8 = 0x38,对齐到 16 字节是 0x40。然后,为返回地址提供空间使其变为 0x48,这正是我们的值。我想你现在已经理解了堆栈帧的逻辑,它实际上很容易理解,但需要一点时间从旧的 x86/stdcall 逻辑转换过来。但现在这些就够了,既然我们已经看到了 x64 代码是如何工作的,我们将尝试自己编译一个汇编源文件。

在我们开始之前,我必须说明一点。网上有一些汇编器可以让工作变得更容易,主要是因为它们自己初始化堆栈,或者它们创建的代码很容易从 x86 转换或转换到 x86。但我认为这不是本文的重点。事实上,我将使用微软的汇编器(ml64.exe),它要求你把所有东西都写下来,就像在反汇编中一样。另一种选择是使用另一个汇编器编译,然后用 ml64 链接。我认为读者应该自己做这些决定。就我而言,我不认为应该用汇编编写太多代码,并且在可能的情况下应该避免。这项新的 x64 技术是重新思考这些问题的好机会。在过去的几年里,我一直用 C/C++ 编写 64 位兼容的代码(当然,我指的是非托管代码),当我需要为 x64 重新编译一个 7 万行代码的项目时,我一行代码都不需要改(我稍后会谈到 C/C++ 编程)。尽管汇编器提供了各种宏,但我严重怀疑那些用汇编编写整个代码的人能够如此轻松地切换到 x64(记住,有一天甚至 IA64 语法也可能被采用)。我认为在大多数情况下,显而易见的选择将是,不转换到新技术,坚持使用 x86,但这并非总是可能的,这取决于软件类别。

微软汇编器包含在 SDK 和 DDK(Vista 的 WDK)中。现在,我正在使用 Vista 的 WDK,我是从 MSDN 免费下载的。我要展示的第一个代码示例是一个简单的 Hello-World 消息框应用程序。

extrn MessageBoxA : proc
extrn ExitProcess : proc

.data
body db 'Hello World!', 0
capt db 'My First x64 Application', 0

.code
Main proc
sub rsp, 28h
xor r9d, r9d        ; uType = 0
lea r8, capt        ; lpCaption
lea rdx, body       ; lpText
xor rcx, rcx        ; hWnd = NULL
call MessageBoxA
xorecx, ecx        ; exit code = 0
call ExitProcess
Main endp

end

正如您所看到的,我没有费心去展开堆栈,因为我调用了 `ExitProcess`。语法与旧的 MASM 非常相似,尽管有一些不同之处。ml64 控制台输出应该像这样

编译的命令行是

ml64 C:\...\test.asm /link /subsystem:windows
    /defaultlib:C:\WinDDK\6000\lib\wnet\amd64\kernel32.lib
    /defaultlib:C:\WinDDK\6000\lib\wnet\amd64\user32.lib /entry:Main

如果库文件不在 ml64.exe 所在的目录,您需要像我一样提供路径。入口点必须提供,否则您将不得不使用 `WinMainCRTStartup` 作为主入口点。

我要展示的下一个代码示例是通过调用 CreateWindowEx 来显示一个窗口。通过这段代码,您将学习到结构对齐以及如何将资源集成到您的项目中。就像我之前说的,我不想鼓励您用汇编来编写窗口,但我相信这类代码有助于学习。现在是代码,之后是解释。

extrn GetModuleHandleA : proc
extrn MessageBoxA : proc
extrn RegisterClassExA : proc
extrn CreateWindowExA : proc
extrn DefWindowProcA : proc
extrn ShowWindow : proc
extrn GetMessageA : proc
extrn TranslateMessage : proc
extrn DispatchMessageA : proc
extrn PostQuitMessage : proc
extrn DestroyWindow : proc
extrn ExitProcess : proc

WNDCLASSEX struct
  cbSize            dd      ?
  style             dd      ?
  lpfnWndProc       dq      ?
  cbClsExtra        dd      ?
  cbWndExtra        dd      ?
  hInstance         dq      ?
  hIcon             dq      ?
  hCursor           dq      ?
  hbrBackground     dq      ?
  lpszMenuName      dq      ?
  lpszClassName     dq      ?
  hIconSm           dq      ?
WNDCLASSEX ends

POINT struct
  x                 dd      ?
  y                 dd      ?
POINT ends

MSG struct
  hwnd              dq      ?
  message           dd      ?
  padding1          dd      ?      ; padding

  wParam            dq      ?
  lParam            dq      ?
  time              dd      ?
  pt                POINT   <>
  padding2          dd      ?      ; padding

MSG ends

.const
NULL equ 0
CS_VREDRAW equ 1
CS_HREDRAW equ 2
COLOR_WINDOW equ 5
; WS_OVERLAPPEDWINDOW = (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |

; WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)

WS_OVERLAPPEDWINDOW equ 0CF0000h
CW_USEDEFAULT equ 80000000h
SW_SHOW equ 5
WM_DESTROY equ 2
WM_COMMAND equ 111h
IDC_MENU equ 109
IDM_ABOUT equ 104
IDM_EXIT equ 105

.data
szWindowClass db 'FirstApp', 0
szTitle db 'My First x64 Windows', 0
szHelpTitle db 'Help', 0
szHelpText db 'This will be a big help...', 0

.data?
hInstance qword ?
hWnd qword ?
wndclass WNDCLASSEX <>
wmsg MSG <>

.code

WndProc: ; proc hWnd : qword, uMsg : dword, wParam : qword, lParam : qword

  mov [rsp+8], rcx        ; hWnd (save parameters as locals)

  mov [rsp+10h], edx      ; Msg

  mov [rsp+18h], r8       ; wParam

  mov [rsp+20h], r9       ; lParam

  sub rsp, 38h
  cmp edx, WM_DESTROY
  jnz @next1

  xor ecx, ecx          ; exit code

  call PostQuitMessage
  xor rax, rax
  add rsp, 38h
  ret

@next1:
  cmp edx, WM_COMMAND
  jnz @default

  mov rbx, rsp
  add rbx, 38h
  mov r10, [rbx+18h]     ; wParam

  cmp r10w, IDM_ABOUT
  jz @about
  cmp r10w, IDM_EXIT
  jz @exit
  jmp @default

@about:
  xor r9d, r9d
  lea r8, szHelpTitle
  lea rdx, szHelpText
  xor ecx, ecx
  call MessageBoxA
  jmp @default

@exit:
  mov rbx, rsp
  add rbx, 38h
  mov rcx, [rbx+8h]       ; hWnd

  call DestroyWindow
  xor rax, rax
  add rsp, 38h
  ret

@default:
  mov rbx, rsp
  add rbx, 38h
  mov r9, [rbx+20h]       ; lParam

  mov r8, [rbx+18h]       ; wParam

  mov edx, [rbx+10h]      ; Msg

  mov rcx, [rbx+8]        ; hWnd

  call DefWindowProcA
  add rsp, 38h
  ret

MyRegisterClass:  ; proc hInst : qword

  sub rsp, 28h
  mov wndclass.cbSize, sizeof WNDCLASSEX
  mov eax, CS_VREDRAW
  or eax, CS_HREDRAW
  mov wndclass.style, eax
  lea rax, WndProc
  mov wndclass.lpfnWndProc, rax
  mov wndclass.cbClsExtra, 0
  mov wndclass.cbWndExtra, 0
  mov wndclass.hInstance, rcx
  mov wndclass.hIcon, NULL
  mov wndclass.hCursor, NULL
  mov wndclass.hbrBackground, COLOR_WINDOW
  mov wndclass.lpszMenuName, IDC_MENU
  lea rax, szWindowClass
  mov wndclass.lpszClassName, rax
  mov wndclass.hIconSm, NULL
  lea rcx, wndclass
  call RegisterClassExA
  add rsp, 28h
  ret


InitInstance: ; proc hInst : qword

  sub rsp, 78h
  mov rax, CW_USEDEFAULT
  xor rbx, rbx
  mov [rsp+58h], rbx            ; lpParam

  mov [rsp+50h], rcx            ; hInstance

  mov [rsp+48h], rbx            ; hMenu = NULL

  mov [rsp+40h], rbx            ; hWndParent = NULL

  mov [rsp+38h], rbx            ; Height

  mov [rsp+30h], rax            ; Width

  mov [rsp+28h], rbx            ; Y

  mov [rsp+20h], rax            ; X

  mov r9d, WS_OVERLAPPEDWINDOW  ; dwStyle

  lea r8, szTitle               ; lpWindowName

  lea rdx, szWindowClass        ; lpClassName

  xor ecx, ecx                  ; dwExStyle

  call CreateWindowExA
  mov hWnd, rax
  mov edx, SW_SHOW
  mov rcx, hWnd
  call ShowWindow
  mov rax, hWnd                 ; set return value

  add rsp,78h
  ret


Main proc
  sub rsp, 28h
  xor rcx, rcx
  call GetModuleHandleA
  mov hInstance, rax
  mov rcx, rax
  call MyRegisterClass
  test rax, rax
  jz @close              ; if the RegisterClassEx fails, exit


  mov rcx, hInstance
  call InitInstance
  test rax, rax
  jz @close              ; if the InitInstance fails, exit


@handlemsgs:             ; message processing routine

  xor r9d, r9d
  xor r8d, r8d
  xor edx, edx
  lea rcx, wmsg
  call GetMessageA
  test eax, eax
  jz @close
  lea rcx, wmsg
  call TranslateMessage
  lea rcx, wmsg
  call DispatchMessageA
  jmp @handlemsgs

@close:
  xor ecx, ecx
  call ExitProcess
Main endp

end

如您所见,我试图尽可能地保持低级。我避免在主函数以外的其他函数中使用 proc 宏的原因是,ml64 会自行添加一个我不需要的序言和尾声。避免使用宏使得我可以在不受编译器任何干扰的情况下定义自己的堆栈帧。滚动这段代码时首先要注意的是这个结构

MSG struct
  hwnd              dq      ?
  message           dd      ?
  padding1          dd      ?      ; padding

  wParam            dq      ?
  lParam            dq      ?
  time              dd      ?
  pt                POINT   <>
  padding2          dd      ?      ; padding

MSG ends

它需要两个填充,而同样结构的 x86 声明则不需要。简而言之,原因是 qword 成员应该对齐到 qword 边界(这是第一个填充)。结构末尾的额外填充遵循以下规则:每个结构都应该对其齐到其最大成员。因此,由于其最大成员是 `qword`,该结构应该对齐到 8 字节边界。

要编译这个示例,命令行是

ml64 c:\myapp\test.asm /link /subsystem:windows
    /defaultlib:C:\WinDDK\6000\lib\wnet\amd64\kernel32.lib
    /defaultlib:C:\WinDDK\6000\lib\wnet\amd64\user32.lib
    /entry:Main c:\myapp\test.res

test.res 是我从一个 VC++ 向导项目拿来的文件,我懒得自己做一个。无论如何,用 VC++ 制作资源文件非常容易,但没人禁止你用记事本,只是会花更多时间。要编译资源文件,你只需要使用命令行:“rc test.rc”。

我认为代码的其余部分很容易理解。我没有用这一段涵盖所有内容,但现在您应该对 x64 汇编有了一个相当好的了解。让我们继续。

C/C++ 编程

用 C/C++ 编写 x64 兼容代码非常容易。所需要的只是遵循一些基本规则。使 99% 的旧 32 位源代码不兼容的最常见错误是错误的类型转换。例如

ptr1 = (DWORD) (sizoef (x) + ptr2);  // <-- WRONG!

这行代码假设指针是 32 位长的,但在 x64 上指针是 64 位长的,上面这行代码基本上截断了指针,使其无效。所以,总是像这样进行类型转换

ptr1 = (ULONG_PTR) (sizoef (x) + ptr2);  <-- RIGHT!

无论您使用 `ULONG_PTR`、`LONG_PTR`、`DWORD_PTR` 还是其他什么都不重要。重要的是您使用这些宏定义之一(或直接使用指针类型:(`void *`))。

请记住,所有句柄和句柄派生类型都是 qwords。`HANDLE`、`HKEY`、`HICON`、`HBITMAP`、`HINSTANCE`、`HMODULE`、`HWND` 等。这些都是 64 位长,即使它们并非都是相同的句柄(例如,`HINSTANCE` 只是一个指针,而不是一个真正的句柄)。甚至 `WPARAM` 和 `LPARAM` 现在也是 64 位长。没有规则可循,只是不要假设这些类型是 32 位或 64 位长:编写与两种情况都兼容的代码。

HWND *hWndArray = (HWND *) malloc(sizeof (DWORD) * n);  <-- WRONG!

而是写成

HWND *hWndArray = (HWND *) malloc(sizeof (HWND) * n);  <-- RIGHT!

如您所见,这不是一条规则,只是常识。

用于编写与体系结构相关的代码的宏定义是

_M_IX86 仅 x86 代码。
_M_AMD64 仅 x64 代码。
_M_IA64 仅 Itanium 代码。
_WIN32 32 位代码(x86,可能用于 WINCE 的 ARM)。
_WIN64 64 位代码(x64,Itanium)。

例如,如果你想只为 x86 编写一段代码,你可以写成

#ifdef _M_IX86
    // x86 only code

#endif

现在您知道了所有规则,您只需要为 x64 编译您的项目。请记住,VC++ 中的每个项目(现在)都以 x86 配置开始:您的工作是向项目添加一个项目配置,但别担心,这非常简单。您所要做的就是打开配置管理器(Build -> Configuration Manager),然后在“Active solution platform”下点击 New,就像这样

一个对话框会弹出,您可以在其中选择要为其创建新项目配置的新平台。除了构建之外,没有其他事情要做。

内联汇编

坏消息!微软完全移除了在 C/C++ 中对内联汇编的支持,无论是用户模式还是内核模式。如果你尝试在 x64/Itanium 上编译像这样的代码示例

#include "stdafx.h"

#include <Windows.h>


int _tmain(int argc, _TCHAR* argv[])
{
    __asm int 3;
    return 0;
}

它会给你不止一个错误。由于 __asm 关键字不再被支持,__naked declspec 也被移除了(因为它在没有内联汇编的情况下没有意义)。

现在,准备好迎接好消息吧。在你开始考虑使用外部汇编文件之类的事情之前,你应该知道 VC++ 提供了一些非常强大的汇编内联函数(intrinsics)。要使用这些内联函数,需要包含的头文件是 "intrin.h"。让我们以 `_ReturnAddress()` 和 `_AddressOfReturnAddress()` 这两个内联函数为例。第一个给我们当前函数的返回地址,第二个给我们返回地址本身的地址。让我们分析一下我从 MSDN 拿来的这个小代码示例

int _tmain(int argc, _TCHAR* argv[])
{
    void* pvAddressOfReturnAddress = _AddressOfReturnAddress();
    printf_s("%p\n", pvAddressOfReturnAddress);
    printf_s("%p\n", *((void**) pvAddressOfReturnAddress));
    printf_s("%p\n", _ReturnAddress());

    return 0;
}

第二个和第三个 `printf_s` 会显示相同的输出,因为它们都显示当前函数的返回地址。这些内联函数非常强大,没有什么能阻止我们做一些我们过去用内联汇编做的旧技巧。例如,有了返回地址的地址,我就可以改变它,让函数返回到别处。让我们试试看

ULONG_PTR OldAddress = 0;

void f1()
{
    printf_s("Hello there!\n");

    ULONG_PTR *pAddressOfReturnAddress = (ULONG_PTR *)
        _AddressOfReturnAddress();

    if (OldAddress == 0)
    {
        OldAddress = *pAddressOfReturnAddress;
        *pAddressOfReturnAddress =  (ULONG_PTR) &f1;
    }
    else
    {
        *pAddressOfReturnAddress = OldAddress;
    }

}

这个函数的输出是

Hello there!
Hello there!

那是因为,正如您从代码中看到的,我更改了当前函数的返回地址,使其再次执行。我设置了一个条件,使其只再次执行一次,否则会导致无限循环。一个重要的事情是,这个示例只在发布模式下禁用代码优化时才有效,否则 VC++ 会移除设置新返回地址的那行代码。我确定有办法欺骗 VC++ 不这么做,但问题是,如果函数只被一个像这样的调用者调用,VC++ 会将函数的代码直接放入调用者的代码中,所以在这种情况下设置一个新的返回地址有点冒险。我认为,禁用优化是最安全的方式。

这些琐事说够了。这是从 MSDN 摘录的 x64 内联函数列表(其中许多在 x86 上也受支持)

_AddressOfReturnAddress 提供保存当前函数返回地址的内存位置的地址。此地址不能用于访问其他内存位置(例如,函数的参数)。
__addgsbyte, __addgsword, __addgsdword, __addgsqword 将一个值添加到由相对于 GS 段起始位置的偏移量指定的内存位置。
__assume 向优化器传递一个提示。
_BitScanForward, _BitScanForward64 从最低有效位(LSB)到最高有效位(MSB)搜索掩码数据,以查找一个置位(1)。
_BitScanReverse, _BitScanReverse64 从最高有效位(MSB)到最低有效位(LSB)搜索掩码数据,以查找一个置位(1)。
_bittest, _bittest64 生成 bt 指令,该指令检查地址 a 的位置 b 上的位,并返回该位的值。
_bittestandcomplement, _bittestandcomplement64 生成 btc 指令,该指令检查地址 a 的位 b,返回其当前值,并将该位设置为其补码。
_bittestandreset, _bittestandreset64 生成 btr 指令,该指令检查地址 a 的位 b,返回其当前值,并将该位重置为 0。
_bittestandset, _bittestandset64 生成 bts 指令,该指令检查地址 a 的位 b,返回其当前值,并将该位设置为 1。
__debugbreak 在您的代码中引发一个断点,此时会提示用户运行调试器。
_disable 禁用中断。
__emul, __emulu 执行会溢出 32 位整数所能容纳范围的乘法运算。
_enable 启用中断。
__faststorefence 保证在任何后续存储操作之前,所有之前的存储操作都全局可见。
__getcallerseflags 从调用者的上下文中返回 EFLAGS 值。
__inbyte 生成 in 指令,返回从 Port 指定的端口读取的一个字节。
__inbytestring 使用 rep insb 指令从指定端口读取数据。
__incgsbyte, __incgsword, __incgsdword, __incgsqword 将由相对于 GS 段起始位置的偏移量指定的内存位置的值加一。
__indword 使用 in 指令从指定端口读取一个双字数据。
__indwordstring 使用 rep insd 指令从指定端口读取数据。
__int2c 生成 int 2c 指令,该指令触发 2c 中断。
_InterlockedAnd, _InterlockedAnd64 用于对多线程共享的变量执行原子与(AND)操作。
_interlockedbittestandreset, _interlockedbittestandreset64 生成 lock_btr 指令,该指令检查地址 a 的位 b 并返回其当前值。
_interlockedbittestandset, _interlockedbittestandset64 生成 lock_bts 指令,该指令检查地址 a 的位 b 并返回其当前值。
_InterlockedCompareExchange, _InterlockedCompareExchange64, _InterlockedCompare64Exchange128, _InterlockedCompare64Exchange128_acq, _InterlockedCompare64Exchange128_rel 为 Win32 平台 SDK 的 InterlockedCompareExchange 函数提供编译器内联支持。
_InterlockedCompareExchangePointer 执行原子交换操作,将作为第二个参数传入的地址复制到第一个参数,并返回第一个参数的原始地址。
_InterlockedDecrement, _InterlockedDecrement64 为 Win32 平台 SDK 的 InterlockedDecrement 函数提供编译器内联支持。
_InterlockedExchange, _InterlockedExchange64 为 Win32 平台 SDK 的 InterlockedExchange 函数提供编译器内联支持。
_InterlockedExchangeAdd, _InterlockedExchangeAdd64 为 Win32 平台 SDK 的 _InterlockedExchangeAdd 内联函数提供编译器内联支持。
_InterlockedExchangePointer 执行原子交换操作,将作为第二个参数传入的地址复制到第一个参数,并返回第一个参数的原始地址。
_InterlockedIncrement, _InterlockedIncrement64 为 Win32 平台 SDK 的 InterlockedIncrement 函数提供编译器内联支持。
_InterlockedOr, _InterlockedOr64 对多线程共享的变量执行原子操作(在此情况下为 OR 操作)。
_InterlockedXor, _InterlockedXor64 用于对多线程共享的变量执行原子操作(在这种情况下是异或 XOR 操作)。
__invlpg 生成 x86 的 invlpg 指令,该指令使与 Address 指向的内存相关的页面的转换后援缓冲(TLB)无效。
__inword 使用 in 指令从指定端口读取数据。
__inwordstring 使用 rep insw 指令从指定端口读取数据。
__ll_lshift 将第一个参数指定的 64 位值向左移动第二个参数指定的位数。
__ll_rshift 将第一个参数指定的 64 位值向右移动第二个参数指定的位数。
__load128, __load128_acq 原子性地加载一个 128 位的值。
_mm_cvtsd_si64x 生成将标量双精度浮点值转换为 64 位整数(cvtsd2si)指令的 x64 扩展形式,它取值中的第一个元素(双精度浮点数)并将其转换为一个 64 位整数。
_mm_cvtsi128_si64x 生成 movd 指令的 x64 扩展形式,该指令从 __m128i 结构中提取低 64 位整数。
_mm_cvtsi64x_sd 生成将双字整数转换为标量双精度浮点值(cvtsi2sd)的指令。
_mm_cvtsi64x_si128 生成 movd 指令的 x64 扩展形式,该指令将一个 64 位值复制到一个表示 XMM 寄存器的 __m128i 结构中。
_mm_cvtsi64x_ss 生成将 64 位整数转换为标量单精度浮点值(cvtsi2ss)指令的 x64 扩展版本。
_mm_cvtss_si64x 生成将标量单精度浮点数转换为 64 位整数(cvtss2si)指令的 x64 扩展版本。
_mm_cvttsd_si64x 生成将带截断的标量双精度浮点值转换为 64 位整数(cvttsd2si)指令的 x64 扩展版本,该指令获取输入打包双精度浮点数结构中的第一个双精度浮点数,将其转换为 64 位整数,并返回结果。
_mm_cvttss_si64x 发出将带截断的单精度浮点数转换为 64 位整数(cvttss2si)指令的 x64 扩展版本。
_mm_set_epi64x 返回一个 __m128i 结构,其两个 64 位整数值被初始化为传入的两个 64 位整数的值。
_mm_set1_epi64x 提供了一种用两个相同的整数初始化 __m128i 结构的两个 64 位元素的方法。
_mm_setl_epi64 在结果的低 64 位中返回源参数的低 64 位。
_mm_stream_si64x 将源中的数据写入由 Dest 指定的内存位置,而不污染缓存。
__movsb 生成一个移动字符串(rep movsb)指令。
__movsd 生成一个移动字符串(rep movsd)指令。
__movsq 生成一个重复移动字符串(rep movsq)指令。
__movsw 生成一个移动字符串(rep movsw)指令。
__mul128 将作为前两个参数传入的两个 64 位整数相乘,并将乘积的高 64 位放入由 HighProduct 指向的 64 位整数中,并返回乘积的低 64 位。
__mulh 返回两个 64 位有符号整数乘积的高 64 位。
__outbyte 生成 out 指令,该指令将由 Data 指定的 1 个字节发送到由 Port 指定的 I/O 端口。
__outbytestring 生成 rep outsb 指令,该指令将 Buffer 指向的数据的前 Count 个字节发送到 Port 指定的端口。
__outdword 生成 out 指令,将一个双字 Data 发送到端口 Port。
__outdwordstring 生成 rep outsd 指令,该指令将从 Buffer 开始的 Count 个双字发送到由 Port 指定的 I/O 端口。
__rdtsc 生成 rdtsc 指令,该指令返回处理器时间戳。处理器时间戳记录自上次重置以来的时钟周期数。
_ReadBarrier 强制内存读取完成。
__readcr0, __readcr2, __readcr3, __readcr4, __readcr8 读取控制寄存器。这些内联函数仅在内核模式下可用。
__readfsbyte, __readfsdword, __readfsqword, __readfsword 从相对于 FS 段起始位置的偏移量指定的位置读取内存。这些内联函数仅在内核模式下可用。
__readgsbyte, __readgsdword, __readgsqword, __readgsword 从相对于 GS 段起始位置的偏移量指定的位置读取内存。这些内联函数仅在内核模式下可用。
__readmsr 生成 rdmsr 指令,该指令读取由寄存器指定的特定于模型的寄存器并返回其值。此函数只能在内核模式下使用。
__readpmc 生成 rdpmc 指令,该指令读取由 counter 指定的性能监控计数器。
_ReadWriteBarrier 有效阻止对全局内存的读写优化。
_ReturnAddress _ReturnAddress 内联函数提供调用函数中,在控制权返回给调用者后将要执行的指令的地址。
__shiftleft128 将一个表示为两个 64 位量 LowPart 和 HighPart 的 128 位量向左移动由 Shift 指定的位数,并返回结果的高 64 位。
__shiftright128 将一个表示为两个 64 位量 LowPart 和 HighPart 的 128 位量向右移动由 Shift 指定的位数,并返回结果的低 64 位。
__store128, __store128_rel 原子性地存储一个 128 位的值。
__stosb 生成存储字符串指令(rep stosb)。
__stosd 生成存储字符串指令(rep stosd)。
__stosq 生成存储字符串指令(rep stosq)。
__stosw 生成存储字符串指令(rep stosw)。
__ull_rshift 在 x64 上,将第一个参数指定的 64 位值向右移动第二个参数指定的位数。
_umul128 将作为前两个参数传入的两个 64 位无符号整数相乘,并将乘积的高 64 位放入由 HighProduct 指向的 64 位无符号整数中,并返回乘积的低 64 位。
__umulh 返回两个 64 位无符号整数乘积的高 64 位。
__wbinvd 生成写回并使缓存失效(wbinvd)指令。
_WriteBarrier 强制内存写入完成,并根据调用点的程序逻辑保持正确。
__writecr0, __writecr3, __writecr4, __writecr8 写入控制寄存器。这些内联函数仅在内核模式下可用。
__writefsbyte, __writefsdword, __writefsqword, __writefsword 将内存写入由相对于 FS 段起始位置的偏移量指定的位置。这些内联函数仅在内核模式下可用。
__writegsbyte, __writegsdword, __writegsqword, __writegsword 将内存写入由相对于 GS 段起始位置的偏移量指定的位置。这些内联函数仅在内核模式下可用。
__writemsr 生成写入模型特定寄存器(wrmsr)的指令。此函数只能在内核模式下使用。

还有一些 3D 内联函数(称为 `3DNow`),对游戏/3D 编程人员会很有用。我没有把这些内联函数放在列表中,因为它们太多了,而且你需要包含另一个头文件来使用它们:"mm3dnow.h"。

如果这些内联函数还不够,您可能需要使用外部汇编文件。另一方面,如果您真的很懒,只需要一些临时性的东西,有一种快速的方法可以在您的 C/C++ 文件中嵌入汇编代码。

#include "stdafx.h"

#include <Windows.h>

unsigned char BitSwapAsm[7] =
{
    0x48, 0x8B, 0xC1,        // mov rax, rcx

    0x48, 0x0F, 0xC8,        // bswap rax

    0xC3            // retn

};
__int64 (*BitSwap)(__int64 Value) =  (__int64 (*)(__int64))
                                                     (ULONG_PTR) BitSwapAsm;

int _tmain(int argc, _TCHAR* argv[])
{
    //

    // I have to change the page protection, otherwise the code would crash

    //

    DWORD dwOldProtect;
    VirtualProtect(BitSwap, sizeof (BitSwapAsm), PAGE_EXECUTE_READWRITE,
                   &dwOldProtect);

    printf_s("%p\n", BitSwap(0xDDCCBBAA));
    getchar();
}

这段代码依赖于函数指针,为了让它执行,我必须更改页面保护标志。这确实是个笨方法,但在某些情况下可以节省时间。

Windows On Windows

当然,必须在 x64(以及 Itanium)上提供对 32 位应用程序的兼容性,这就是 WOW64(Windows on Windows 64)的全部意义所在。当我们用 32 位版本的 Task Explorer 查看 32 位应用程序加载的模块时,我们看到这个

看起来很正常,当然,除了系统文件路径,在我们的例子中是 `syswow64` 而不是旧的通用 `System32`。很容易理解为什么会这样:`System32` 文件夹现在保留给 64 位环境,而 32 位文件必须放在别处。但看看当我用 x64 版本的 Task Explorer 打开同一个进程时会发生什么

突然之间,所有 32 位模块都消失了,剩下的是 WOW64 仿真模块。这是 MSDN 对这些模块的描述

WOW64 仿真器在用户模式下运行,提供了 32 位版本的 Ntdll.dll 与处理器内核之间的接口,并且它会拦截内核调用。该仿真器由以下 DLL 组成
  • Wow64.dll 提供了核心仿真基础设施和 Ntoskrnl.exe 入口点函数的 thunk。
  • Wow64Win.dll 提供了 Win32k.sys 入口点函数的 thunk。
  • Wow64Cpu.dll 在 Itanium 处理器上提供 x86 指令仿真。它在处理器上执行模式切换指令。对于 x64 处理器,此 DLL 不是必需的,因为它们可以全时钟速度执行 x86-32 指令。

除了 64 位版本的 Ntdll.dll,这些是唯一可以加载到 32 位进程中的 64 位二进制文件。在启动时,Wow64.dll 加载 x86 版本的 Ntdll.dll 并运行其初始化代码,该代码会加载所有必需的 32 位 DLL。几乎所有 32 位 DLL 都是 32 位 Windows 二进制文件的未修改副本。但是,其中一些 DLL 被编写为在 WOW64 上的行为与在 32 位 Windows 上的行为不同 [...]。

32 位二进制文件不再使用 x86 系统服务调用序列,而是被重建以使用自定义的调用序列。这个新序列对于 WOW64 来说拦截成本很低,因为它完全保留在用户模式中。当检测到新的调用序列时,WOW64 CPU 会转换回原生 64 位模式并调用 Wow64.dll。转换(Thunking)在用户模式下完成,以减少对 64 位内核的影响,并降低因转换中的错误导致内核模式崩溃、数据损坏或安全漏洞的风险。这些转换器从 32 位堆栈中提取参数,将它们扩展到 64 位,然后进行原生系统调用。

32 位应用程序的最大空间为 2GB(如果明确要求,则为 4GB),其余空间由系统处理。当然,这没有太大变化,因为在 x86 上,用户模式应用程序在 4GB 的虚拟内存空间中拥有 2GB(另外 2GB 保留给内核模式)。在 x64 上,这另外的 2GB 现在可以被 32 位应用程序访问。为了实现这一点,必须在文件头的 Characteristics 字段中设置 `IMAGE_FILE_LARGE_ADDRESS_AWARE` 标志。您可以通过编程方式或使用像 CFF Explorer 这样的普通 PE 编辑器手动完成,就像这样

我见过 3D 游戏玩家为了提高性能而这样做。当然,这只对内存消耗非常大的应用程序有用。

一个非常有用的函数,用于确定一个进程是否在 WOW64 下运行,是

BOOL IsWow64Process(
  HANDLE hProcess,   // [in] Handle to a process.

  PBOOL Wow64Process // [out] Pointer to a value that is set to TRUE if the

                     // process is running under WOW64. Otherwise, the value

                     // is set to FALSE.

);

Wow64Cpu.dll 在 x64 上的工作为零,因为 x64 原生支持 x86。我最初曾想研究一下调用序列是如何工作的,以便自己实现一个,并提供一种在同一地址空间中从 x64 使用 x86 组件的方法,但转念一想,即使可以实现,它也无法在 Itanium 上工作。这就引出了我们接下来的一个段落,因为在正常情况下,32 位应用程序无法加载 64 位 DLL,而 64 位应用程序也无法加载 32 位 DLL。因此,进程间通信在 64 位系统上成为一个重要方面。无论如何,在此之前,我必须谈谈文件系统和注册表重定向,因为它们与 WOW64 密切相关,但由于其重要性,值得单独一个段落来讨论。

文件系统和注册表重定向

由于 `System32` 路径是为 64 位文件保留的,所以每当 32 位应用程序尝试访问此目录时,它都会被重定向到 `SysWow64` 目录。然而,System32 的一些子目录在 32 位和 64 位应用程序之间共享,因此不需要重定向。这些子目录是

  • %windir%\system32\catroot
  • %windir%\system32\catroot2
  • %windir%\system32\drivers\etc
  • %windir%\system32\logfiles
  • %windir%\system32\spool

此外,还有一些与 WOW64 文件系统重定向相关的函数

GetSystemWow64Directory 检索 WOW64 使用的系统目录的路径。此目录在 32 位 Windows 上不存在。
Wow64DisableWow64FsRedirection 为调用线程禁用文件系统重定向。文件系统重定向默认是启用的。
Wow64EnableWow64FsRedirection 为调用线程启用或禁用文件系统重定向。当存在嵌套调用时,此函数可能无法可靠工作。因此,此函数已被 Wow64DisableWow64FsRedirection 和 Wow64RevertWow64FsRedirection 函数取代。
Wow64RevertWow64FsRedirection 为调用线程恢复文件系统重定向。

我想很容易理解如何使用这些函数。不过,我还是添加了一个小的代码示例(你可以在 MSDN 上找到几乎相同的示例)

int _tmain(int argc, _TCHAR* argv[])
{
    BOOL bIsWOW64Enabled;

    if (IsWow64Process(GetCurrentProcess(), &bIsWOW64Enabled))
    {
        if (bIsWOW64Enabled == TRUE) // we run under WOW64

        {
            PVOID pOldValue;
            DWORD FileSize;

            HANDLE hFile = CreateFile(_T("c:\\windows\\system32\\notepad.exe"),
                GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

            FileSize = GetFileSize(hFile, NULL);

            CloseHandle(hFile);

            _tprintf(_T("File Size: %d Bytes\n"), FileSize);

            Wow64DisableWow64FsRedirection(&pOldValue); // disable redirection


            hFile = CreateFile(_T("c:\\windows\\system32\\notepad.exe"),
                               GENERIC_READ, FILE_SHARE_READ, NULL,
                               OPEN_EXISTING, 0, NULL);

            FileSize = GetFileSize(hFile, NULL);

            CloseHandle(hFile);

            _tprintf(_T("File Size: %d Bytes\n"), FileSize);

            Wow64RevertWow64FsRedirection(pOldValue);  // restore redirection


            getchar();
        }
    }

    return 0;
}

该程序的输出是

File Size: 151040 Bytes
File Size: 169472 Bytes

文件大小发生变化,因为程序一次打开 32 位的记事本,一次打开 64 位的记事本。当然,请记住,当您使用这些函数时,始终将它们与 GetProcAddress 一起使用,否则您的代码将无法在不提供这些函数的旧系统上工作。

让我们继续讨论注册表。与文件系统一样,注册表也被重定向,或者更确切地说,是它的一些键。这些键是

  • HKEY_LOCAL_MACHINE\Software
  • HKEY_USERS\*\Software\Classes
  • HKEY_USERS\*_Classes

您可以找到这些键中每一个为 32 位应用程序复制的键都在它们的 WOW 节点中:这些键中的任何一个都有一个名为 `Wow6432Node` 的子键,其中包含父键的副本。例如

这些 WOW64 重定向的键中,有一些子键是“反射”的。在这种情况下,反射意味着当我在 32 位节点中更改一个反射的键时,这个更改也会被“反射”到 64 位的键上,反之亦然。这是必要的,因为一些键需要保持同步。这与在 64 位和 32 位模式之间共享键有很大不同,因为反射可以被过滤,也可以被禁用。这些是反射的键

  • HKEY_LOCAL_MACHINE\Software\Classes
  • HKEY_LOCAL_MACHINE\Software\Microsoft\COM3
  • HKEY_LOCAL_MACHINE\Software\Microsoft\EventSystem
  • HKEY_LOCAL_MACHINE\Software\Microsoft\Ole
  • HKEY_LOCAL_MACHINE\Software\Microsoft\Rpc
  • HKEY_USERS\*\Software\Classes
  • HKEY_USERS\*_Classes

处理反射的函数是

RegQueryReflectionKey 确定指定键的反射是否已被禁用或启用。
RegDisableReflectionKey 禁用指定键的注册表反射。禁用一个键的反射不会影响任何子键的反射。
RegEnableReflectionKey 为指定的已禁用键恢复注册表反射。为一个键恢复反射不会影响任何子键的反射。

它们的工作方式就像 WOW64 文件系统函数一样,所以我不认为需要代码示例。在 64 位和 32 位应用程序之间也有一些共享的键

  • HKEY_LOCAL_MACHINE\SOFTWARE\Classes\HCP
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Calais\Current
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Calais\Readers
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Services
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\SystemShared
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\TIP
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\DFS
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Driver Signing
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\EnterpriseCertificates
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSMQ
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Non-Driver Signing
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\RAS
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Software\Microsoft\Shared Tools\MSInfo
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SystemCertificates
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\TermServLicensing
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Transaction Server
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDpi
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontMapper
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\
    CurrentVersion\FontSubstitutes
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkCards
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Ports
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Control Panel\Cursors\Schemes
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\
    CurrentVersion\Setup\OC Manager
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\
    CurrentVersion\Telephony\Locations
  • HKEY_LOCAL_MACHINE\SOFTWARE\Policies

如前所述,这些键是共享的,因此对它们所做的任何更改都会影响 32 位和 64 位应用程序,并且无法像反射键那样避免这种情况。

但是,如果一个 32 位应用程序想要访问 64 位注册表,或者反之亦然,该怎么办呢?别担心!当我处理同样的问题时发现,微软提供了一种非常简单的方法来完成这项工作。标志 `KEY_WOW64_64KEY` 和 `KEY_WOW64_32KEY` 可以与这些函数一起使用:RegCreateKeyEx、RegDeleteKeyEx 和 RegOpenKeyEx。

KEY_WOW64_64KEY 从 32 位或 64 位应用程序访问 64 位密钥。
KEY_WOW64_32KEY 从 32 位或 64 位应用程序访问 32 位密钥。

我需要做的是从一个 32 位应用程序访问一个 64 位键的子键,转换成代码就是

RegOpenKeyEx(HKEY_LOCAL_MACHINE, MyKey, 0, KEY_READ | KEY_WOW64_64KEY, &hKey);

很简单,不是吗?

总而言之,微软提供的关于文件系统和注册表重定向的文档非常好,我只是报告了我最初在 MSDN 上找到的内容。我认为这些重定向不会给程序员带来太多问题。

进程间通信

如“Windows On Windows”段落所述,进程间通信在 x64 上成为一个重要方面,因为 64 位应用程序可能需要使用 32 位组件,反之亦然。MSDN 建议进程之间通过以下方式进行通信

  • 命名对象(如互斥锁、信号量和文件句柄)的句柄都可以共享。
  • 窗口句柄(HWND)可以共享。
  • RPC.
  • COM 本地服务器。
  • 如果共享内存的内容不依赖于指针,则可以使用共享内存。
  • `CreateProcess` 和 `ShellExecute` 函数可以从 32 位或 64 位进程启动 32 位和 64 位进程。
  • `CreateRemoteThread` 函数针对特定函数有特殊处理,允许 64 位调试器进入 32 位进程。

使用 CreateProcess 或 ShellExecute 意味着您可以通过参数和读取输出来进行通信。如果您需要更复杂(且专业)的东西,您别无选择,只能使用 RPCs(远程过程调用)或 COM 对象。对于 RPCs,您需要学习一些关于 MIDL(微软接口定义语言)的知识,但最终我尝试的每个代码示例都无法在 Vista x64 上工作,所以我放弃了 RPCs。我建议您使用 COM,用 MFC 编写它们非常容易(与不用 MFC 编写相比,我是说)。CodeProject 上有一系列非常好的关于编写 ActiveXs 的文章。实际上,该指南是关于如何用纯 C 编写 ActiveXs 的(我必须减小我的 ActiveX 的大小,所以我不能使用 MFC),但理论是相同的,而且这些文章写得很好,可以省去您阅读一本书的精力。如果您以前从未编写过 COM 对象,您最终会发现它可能很烦人。

共享内存并不是一个真正的选择。如果您正在寻找 `CreateProcess` 和 COM 对象之间的解决方案,您可以使用管道或类似的东西。实际上,您可以通过共享内存和互斥锁来实现自己的管道。这就是我在一些项目中所做的

进程名旁边的“*32”是任务管理器告诉我们哪些是 32 位进程的方式。如您所见,服务器是 64 位进程,客户端是 32 位进程。这两个进程之间通信没有问题。然而,别太激动,有一些问题,我稍后会解释它们是什么。现在,让我们看一个代码示例(文章顶部可下载 communication.zip)。

这是客户端代码

#include <Windows.h>

#include <tchar.h>

#define BUF_SIZE  256 * sizeof (TCHAR)

TCHAR MyEvent[] = _T("Global\\SharedMemoryEvent");

TCHAR szName[]= _T("Global\\MyFileMappingObject");

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR szCmdLine, int iCmdShow)
{
    //
    // Create the event to communicate between server and client
    //

    HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, MyEvent);

    //
    // Start server process
    //

    PROCESS_INFORMATION pi = { 0 };
    STARTUPINFO si = { 0 };

    if (!CreateProcess(_T("Server.exe"), NULL, NULL, NULL, FALSE, 0, NULL,
                       NULL, &si, &pi))
        return 1;

    //
    // Wait for the server to complete the job
    //

    WaitForSingleObject(hEvent, INFINITE);

    //
    // Access shared memory object
    //

    HANDLE hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,   // read/write access

        FALSE,                 // do not inherit the name

        szName);               // name of mapping object


    if (hMapFile == NULL) return 1;

    LPCTSTR pBuf = (LPTSTR) MapViewOfFile(
        hMapFile,              // handle to map object

        FILE_MAP_ALL_ACCESS,   // read/write permission

        0,
        0,
        BUF_SIZE);

    if (pBuf == NULL) return 1;

    //
    // Shows Server Output
    //

    MessageBox(NULL, pBuf, _T("Server Output"), MB_OK);

    UnmapViewOfFile(pBuf);

    CloseHandle(hMapFile);

    //
    // Tell the server that the object isn't used any longer
    //

    SetEvent(hEvent);

    return 0;
}

这是服务器代码

#include <Windows.h>

#include <tchar.h>

#define BUF_SIZE  256 * sizeof (TCHAR)

TCHAR MyEvent[] = _T("Global\\SharedMemoryEvent");

TCHAR szName[] = _T("Global\\MyFileMappingObject");
TCHAR szMsg[] = _T("Message from server process");

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                     LPSTR szCmdLine, int iCmdShow)
{
    //
    // Create the memory shared object
    //

    HANDLE hMapFile;
    LPCTSTR pBuf;

    hMapFile = CreateFileMapping(
        INVALID_HANDLE_VALUE,    // use paging file

        NULL,                    // default security

        PAGE_READWRITE,          // read/write access

        0,                       // max. object size

        BUF_SIZE,                // buffer size

        szName);                 // name of mapping object

    if (hMapFile == NULL) return 1;

    pBuf = (LPTSTR) MapViewOfFile(hMapFile,   // handle to map object

        FILE_MAP_ALL_ACCESS,             // read/write permission

        0,
        0,
        BUF_SIZE);

    if (pBuf == NULL) return 1;

    CopyMemory((PVOID) pBuf, szMsg, (_tcslen(szMsg) + 1) * sizeof (TCHAR));

    //
    // Wait for event before closing file object
    //

    HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, MyEvent);

    SetEvent(hEvent);

    WaitForSingleObject(hEvent, INFINITE);

    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);

    return 0;
}

这些应用程序所做的是

  • 客户端创建一个通信事件。

  • 客户端启动服务器并等待通信事件被设置。

  • 服务器创建一个共享内存对象并用输出填充它。

  • 服务器设置通信事件,以告知客户端处理输出。

  • 服务器等待客户端清除共享内存。

  • 客户端处理服务器的输出。

  • 客户端告诉服务器它现在可以清除共享内存了。

我相信理解代码本身比这个列表更容易。我前面提到的问题是,为了在进程间共享内存对象(或事件),我必须在 "Global\\*" 部分创建它。在 Vista 中,只有具有管理员权限的应用程序才能使用 `CreateFileMapping` 访问此部分(尽管互斥锁或事件没有问题),而且由于应用程序通常在 Vista 中以用户权限运行,你必须明确告诉 Vista 以管理员权限运行客户端应用程序,这不是很专业。这个问题的解决方案可能是通过临时文件甚至注册表(对于小数据)来共享内存。

便携式可执行文件

如果您的软件与可移植可执行文件(PE)有任何关系,那么迁移到 x64 并不会太难(如果您还没有这样做的话)。基本上,PE64 中改变的是虚拟地址(VAs)的大小,现在是 64 位宽。请记住,并非所有描述为虚拟地址的字段都是真正的虚拟地址,大多数时候它们只是相对虚拟地址(RVAs),就像在 PE32 中一样,是 32 位宽。简而言之,改变的是可选头(Optional Header,它有一些 64 位宽的字段,如 ImageBase)、导入目录 thunks(两个 thunk 数组,OFTs 和 FTs,现在是 64 位宽,因为 thunks 被设计用来包含虚拟地址以及其他东西)、加载配置目录(Load Config Directory)和 TLS 目录。

让我们以旧的 PE32 可选头为例

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

以及 PE64 的

typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   ImageBase;
    DWORD       SectionAlignment;
    DWORD       FileAlignment;
    WORD        MajorOperatingSystemVersion;
    WORD        MinorOperatingSystemVersion;
    WORD        MajorImageVersion;
    WORD        MinorImageVersion;
    WORD        MajorSubsystemVersion;
    WORD        MinorSubsystemVersion;
    DWORD       Win32VersionValue;
    DWORD       SizeOfImage;
    DWORD       SizeOfHeaders;
    DWORD       CheckSum;
    WORD        Subsystem;
    WORD        DllCharacteristics;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

当然,`ULONGLONG` 是 64 位宽的字段。如您所见,`AddressOfEntryPoint` 像所有 RVA 一样,仍然是一个 dword。相反,`ImageBase` 作为一个虚拟地址,变成了一个 qword。

区分 PE32 和 PE64 应该通过检查可选头中的 Magic 字段来完成。该字段可以是以下值之一

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC       0x107

您可以选择每次都重复编写代码来处理 PE32/64,或者编写一个类来自动处理它们。

异常处理

还记得您在代码中设置 SEH 的旧日子吗?好吧,在 x64/Itanium 上,它们已经一去不复返了。异常处理程序现在以结构化的形式存储在 PE64 的异常目录中。基本结构是这样的

typedef struct _RUNTIME_FUNCTION {
    DWORD BeginAddress;
    DWORD EndAddress;
    DWORD UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

所有三个字段都是 RVA(否则它们就不会是 dwords)。

BeginAddress 指向相关代码部分的起始地址。
EndAddress 指向同一代码部分的结束地址。
UnwindData 指向一个 `UNWIND_INFO` 结构。

`UNWIND_INFO` 结构说明了如何处理该部分代码。这是我在 MSDN 上找到的声明

typedef union _UNWIND_CODE {
    struct {
        UBYTE CodeOffset;
        UBYTE UnwindOp : 4;
        UBYTE OpInfo   : 4;
    };
    USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

typedef struct _UNWIND_INFO {
    UBYTE Version       : 3;
    UBYTE Flags         : 5;
    UBYTE SizeOfProlog;
    UBYTE CountOfCodes;
    UBYTE FrameRegister : 4;
    UBYTE FrameOffset   : 4;
    UNWIND_CODE UnwindCode[1];
/*  UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
*   union {
*       OPTIONAL ULONG ExceptionHandler;
*       OPTIONAL ULONG FunctionEntry;
*   };
*   OPTIONAL ULONG ExceptionData[]; */
} UNWIND_INFO, *PUNWIND_INFO;

以下是直接从 MSDN 摘录的 `UNWIND_INFO` 结构成员的描述

版本 展开数据的版本号,当前为 1。
标志 目前定义了三个标志

`UNW_FLAG_EHANDLER` 该函数有一个异常处理程序,在查找需要检查异常的函数时应调用该处理程序。

`UNW_FLAG_UHANDLER` 该函数有一个终止处理程序,在展开异常时应调用该处理程序。

`UNW_FLAG_CHAININFO` 此展开信息结构不是该过程的主要结构。相反,链式展开信息条目是前一个 `RUNTIME_FUNCTION` 条目的内容。有关链式展开信息结构的解释,请参阅以下文本。如果设置了此标志,则必须清除 `UNW_FLAG_EHANDLER` 和 `UNW_FLAG_UHANDLER` 标志。此外,帧寄存器和固定堆栈分配字段必须与主要展开信息中的值相同。

SizeOfProlog 函数序言的长度(以字节为单位)。
CountOfCodes 这是展开码数组中的槽位数。请注意,某些展开码(例如,`UWOP_SAVE_NONVOL`)需要在数组中占用一个以上的槽位。
FrameRegister 如果非零,则函数使用帧指针,并且该字段是用作帧指针的非易失性寄存器的编号,使用与 `UNWIND_CODE` 节点的操作信息字段相同的编码。
FrameOffset 如果帧寄存器字段非零,则这是从 RSP 应用到 FP 寄存器建立时的缩放偏移量。实际的 FP 寄存器设置为 RSP + 16 * 这个数字,允许从 0 到 240 的偏移量。这允许将 FP 寄存器指向动态堆栈帧的局部堆栈分配的中间,通过更短的指令(更多指令可以使用 8 位有符号偏移形式)来提高代码密度。
UnwindCode 这是一个项目数组,解释了序言对非易失性寄存器和 RSP 的影响。有关各个项目的含义,请参见 `UNWIND_CODE` 部分。为了对齐,此数组将始终具有偶数个条目,最后一个条目可能未使用(在这种情况下,数组将比展开码计数字段指示的长度长一个)。
ExceptionHandler 这是一个相对于镜像的指针,指向函数的特定于语言的异常/终止处理程序(如果标志 `UNW_FLAG_CHAININFO` 被清除,并且标志 `UNW_FLAG_EHANDLER` 或 `UNW_FLAG_UHANDLER` 中的一个被设置)。
特定语言的处理程序数据 (ExceptionData) 这是函数的特定于语言的异常处理程序数据。此数据的格式未指定,完全由使用的特定异常处理程序决定。
链式展开信息 (ExceptionData) 如果设置了 `UNW_FLAG_CHAININFO` 标志,那么 `UNWIND_INFO` 结构以三个 `UWORD` 结尾。这些 `UWORD` 表示链式展开函数的 `RUNTIME_FUNCTION` 信息。

Flags 字段的可能值是

#define UNW_FLAG_EHANDLER  0x01
#define UNW_FLAG_UHANDLER  0x02
#define UNW_FLAG_CHAININFO 0x04

让我们以这段代码为例

#include <Windows.h>
#include <tchar.h>
#include <intrin.h>

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                  LPTSTR lpCmdLine, int nCmdShow)
{
   __try
   {
      __debugbreak();
   }
   __except (EXCEPTION_EXECUTE_HANDLER)
   {
      MessageBox(0, _T("Hello!"), _T("SEH"), 0);
   }

   return 0;
}

反汇编代码将是

.text:0000000000401000 wWinMain proc near  ; CODE XREF: __tmainCRTStartup+18C p

.text:0000000000401000    sub rsp, 28h     ; BeginAddress

.text:0000000000401004    int 3            ; Trap to Debugger

.text:0000000000401005    jmp short loc_401021
.text:0000000000401007 ; ---------------------------------------------------

.text:0000000000401007    xor r9d, r9d     ; ExceptionHandler

.text:000000000040100A    lea r8, Caption
.text:0000000000401011    lea rdx, Text
.text:0000000000401018    xor ecx, ecx
.text:000000000040101A    call cs:__imp_MessageBoxW
.text:0000000000401020    nop
.text:0000000000401021
.text:0000000000401021 loc_401021:         ; CODE XREF: wWinMain+5 j

.text:0000000000401021    xor eax, eax
.text:0000000000401023    add rsp, 28h
.text:0000000000401027    retn
.text:0000000000401027 wWinMain endp       ; EndAddress (+ alignment)

如果您需要动态生成代码并为其设置异常处理程序,您可以使用函数 `RtlAddFunctionTable`,它接受一个 `RUNTIME_FUNCTION` 结构数组作为参数。这当然意味着,您必须自己填写一个或多个 `UNWIND_INFO` 结构。这肯定比在 x86 上复杂一些,但我猜很多软件保护措施会使用这种方法。

.NET Framework

x86 和 x64 的 .NET Framework 都可以和平地共存于 x64 Windows 上。如果您同时安装了两者,每样东西都会有两个:.NET 目录中有两个目录,全局缓存中有两个目录,还有两个主要的注册表项。既然 .NET 程序集不包含本机代码,那么在同一台计算机上同时拥有这两个框架有什么用呢?

.NET 程序集可以通过 System.Runtime.InteropServices 命名空间调用本机代码。当然,一个在 x64 .NET 框架上运行的 .NET 程序集是一个 64 位进程,因此它不能调用 x86 组件的函数。反之,在 x86 上执行的程序集不能使用 x64 组件。您可以通过进入 项目 -> 属性 -> 生成 来明确告诉 Visual Studio 为特定平台(x86、x64、Itanium)编译您的程序集。

64 位 PE 总是为特定平台构建,只有 PE32 程序集被允许在每个框架上运行(x86 系统无法执行 PE64)。无论如何,通过在 .NET 目录(`IMAGE_COR20_DIRECTORY`)中设置一个标志,就可以让 PE32 程序集只在 x86 框架上运行。该标志是 `COMIMAGE_FLAGS_32BITREQUIRED`。通过设置此标志,您将强制给定的程序集即使在 64 位平台上也作为 32 位进程执行。

32 位和 64 位 .NET 框架之间也存在一些差异。我注意到 64 位框架对程序集中的对齐和完整性检查非常严格,而新的 3.0 框架甚至有更多的检查。

Vista 部分

版本

Windows Vista 有多个版本,在安装新系统之前,您应该了解某些版本中缺少的功能。这是微软给出的官方功能表

特点
家庭普通版

家庭高级版
商用版 Ultimate
有史以来最安全的 Windows
配备 Windows Defender 和 Windows 防火墙
Home Basic Home Premium Business Ultimate
快速找到您需要的东西
通过即时搜索和 Windows Internet
浏览器 7
Home Basic Home Premium Business Ultimate
优雅的 Windows Aero 桌面体验
支持 Windows Flip 3D 导航
Home Premium Business Ultimate
笔记本电脑的最佳选择
拥有增强的 Windows 移动中心和
平板电脑支持
Home Premium Business Ultimate
协作和共享文档
使用 Windows 会议空间
Home Premium Business Ultimate
体验照片和娱乐
在您的客厅中使用 Windows Media Center
Home Premium Ultimate
享受 Windows Media Center
通过 Xbox 360™ 和其他设备在您家中的电视上观看
Home Premium Ultimate
帮助防范硬件故障
凭借先进的商业备份功能
Business Ultimate
商业网络和远程桌面
为了更轻松的连接
Business Ultimate
更好地保护您的数据
使用 Windows 防止数据丢失或被盗
BitLocker™ 驱动器加密
Ultimate

但还有其他一些普通用户可能不会注意到的事情。例如,商用版和旗舰版允许虚拟化(将系统用作虚拟机)。Vista 家庭普通版和高级版的最终用户许可协议写道

4. 与虚拟化技术一起使用。 您不得在虚拟(或其他仿真)硬件系统中使用安装在许可设备上的软件。

显然,微软在家庭版中加入了特殊的检查,以防止它们在仿真环境中工作。我读到 VMWare 对微软采取的这一政策感到非常失望。这场争议可能会改变事情,但我认为许多程序员和公司在购买 Vista 的某个版本之前应该知道这一点。

Microsoft Visual Studio

我选择将此作为 Windows Vista 部分的第二段,因为每个程序员在拥有一个全新的系统时首先要做的事情就是安装和设置他的编译器。问题是,由于 Vista 改变了很多东西,唯一兼容的 Visual Studio 平台是 2005 版。不再提供对 Visual Studio 6 和 Visual Studio .NET 2003 的兼容性(尽管 Visual Basic 6 似乎受支持)。不仅如此,为了让 VS.NET 2005 正常工作,您还需要下载并安装其 Service Pack 1。

对于像我这样的许多程序员来说,这没什么大不了的,因为保持代码更新非常重要,但任何与小公司合作过的人都知道,他们中的许多人没有兴趣做同样的事情,结果就是,例如,许多解决方案仍然是为 .NET framework 1.0 开发的。这些解决方案当然仍然可以在 Vista 上运行,但将没有任何工具来编译它们。我不认为微软会解决这个问题。因此,对于许多公司来说,切换到 Vista 需要一些时间。

附言:Microsoft Visual Studio Service Pack 1 的安装将花费很长时间。别担心,这是正常的。

用户帐户控制

在 Windows Vista 上,阻碍大多数应用程序正常工作的是用户帐户控制(UAC),也称为有限用户访问(LUA)。正如我们在“进程间通信”段落中看到的,创建共享内存对象需要管理员权限,但 Windows Vista 以用户权限运行每个进程(系统进程除外)。不兼容性,大多数时候,是由程序员错误地假设他们的代码将在管理员级别运行而产生的。在 Windows NT 4-5.1 上以用户权限运行没有问题的程序,在 Vista 上正常运行也不会有任何问题。然而,常见的错误(或坏习惯)如

  • 在自己的 Program Files 目录中修改文件。
  • 写入 HKEY_LOCAL_MACHINE 来存储设置。

这些不再是问题了,因为在 Vista 上有一个叫做虚拟化的东西。基本上,在像 Program Files 这样的系统目录中修改的每个文件,实际上都存储在一个叫做 Virtual Store 的目录中。我的这个目录路径是 C:\Users\Daniel\AppData\Local\VirtualStore。Windows 资源管理器会向您显示系统目录中的那些文件,但实际上它们都在 VirtualStore 中,而且不用说,每个用户都是唯一的。文件在 Virtual Store 中就是这样存储的

虚拟存储中的目录层次结构与没有虚拟化时完全相同。我想你已经弄清楚它是如何工作的了。

就像文件系统一样,注册表也有虚拟化。所有写入 `HKEY_LOCAL_MACHINE\Software` 键的内容实际上都会被存储在 `HKEY_CLASSES_ROOT\VirtualStore\Software` 下。

您应该记住的是,在您的软件的标准执行中,任何可能影响其他用户的事情都不再可能。这意味着,除了上面所说的事情,您将无法加载驱动程序,修改某些文件(甚至可能无法读取它们),修改或读取特定的注册表键/值,访问某些全局对象,枚举或修改比您权限更高的进程的内存等。您甚至无法枚举或向由这些进程创建的窗口发送消息。这可以防止我们过去已经见过的漏洞利用。有一篇非常好的 MSDN 文章(最低权限环境下的应用程序开发者最佳实践和指南)是关于 UAC 及其对开发人员的影响的,它详细解释了我在这里试图简短说明的一切。

兼容性验证

有一个工具可以验证您的应用程序是否与 UAC 兼容。这个工具叫做 Microsoft Application Verifier,它也可以检测其他问题,但现在我们对那些不感兴趣。使用它非常简单,您只需要将一个应用程序添加到列表中,并选择验证器应该进行哪种检查。在我们的例子中,我们将选择 LUA(即 UAC)兼容性检查。

下一步是运行您添加到列表中的应用程序。完成后,您保存日志,然后,正如您从上面的截图中看到的,您用 Microsoft Standard User Analyzer 打开它。不要想用其他应用程序打开它,根据您的软件大小,验证器将生成一个巨大的 XML 日志,这会使任何其他程序消耗您的大部分内存。对于这个测试,我使用了 Task Explorer,它当然会尝试调整其令牌以获得 SeDebugPrivilege,以便列出甚至系统进程。事实上

由于此应用程序没有以管理员权限运行,它将无法获取调试权限。我不建议您总是使用这个工具,因为遵循良好的设计规则应该就足够了。然而,它可能很有用。

获取管理员权限

当然,仍然可以用管理员权限运行应用程序。你可以手动操作,也可以通过代码。要手动操作,你可以右键单击应用程序,然后点击“以管理员身份运行”,或者在属性 -> 兼容性下勾选“以管理员身份运行此程序”复选框。然而,程序员不能指望用户自己进行这些操作,所以他们需要另一种方法。

一个非常简单的方法是通过清单文件告诉系统请求的执行级别。我假设读者知道什么是清单文件以及如何将其集成到应用程序中。关于这个主题有很多指南,我认为不应该在这里重新讨论。

这样一个清单文件的模式应该是这样的(这个实际上是针对 x86 的,正如你在 `processorArchitecture` 下看到的)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity version="1.0.0.0"
    processorArchitecture="X86"
    name="YourAppName"
    type="win32"/>

 <description>Description of your application</description>
 <!-- Identify the application security requirements. -->
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel
          level="requireAdministrator"
          uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

level 的可用值为

asInvoker 该应用程序以与父进程相同的令牌运行。
highestAvailable 应用程序以当前用户可以获得的最高权限运行。
requireAdministrator 该应用程序仅对管理员运行,并要求该应用程序以管理员的完整令牌启动。

以及 uiAccess

false 该应用程序不需要向桌面上的另一个窗口的 UI 驱动输入。不提供辅助功能的应用程序应将此标志设置为 false。需要向桌面上的其他窗口驱动输入的应用程序(例如屏幕键盘)应将此值设置为 true。
true 该应用程序被允许绕过 UI 保护级别,以驱动输入到桌面上更高权限的窗口。此设置应仅用于 UI 辅助功能应用程序。

我写了一个非常小的应用程序(它只是等待一个按键输入)来演示清单文件的使用。下载可以在本文顶部找到。

还有一个函数 `CredUIPromptForCredentials`,用于向用户请求特定的凭据,并可用于获取管理员权限,但对用户本人来说不是很方便。

禁用它

如果您对所有这些询问您权限的对话框感到厌烦,您可以禁用 UAC。据我所知,有两种简单的方法。第一种在 UAC 团队博客上有详细描述,即进入控制面板 -> 管理工具 -> 本地安全策略。在左侧的树中点击本地策略,然后点击安全选项。滚动直到找到:“用户帐户控制:管理员的提升提示行为”。将当前值更改为:“无提示提升”。就像这样

另一种方法更激进。它是关于完全移除 UAC,并且需要重启机器。在管理工具中,不要点击本地安全策略,而是点击系统配置。在对话框中选择“工具”选项卡,然后滚动直到在“工具名称”下找到“禁用 UAC”。要执行该命令,请点击启动。

从命令行中可以看出,它只是执行 reg.exe 来添加一个键/值。完整的命令行是

C:\Windows\System32\cmd.exe /k %windir%\System32\reg.exe
  ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
  /v EnableLUA /t REG_DWORD /d 0 /f

要重新启用 UAC,它将 EnableLUA dword 设置为 1。

我不知道禁用 UAC 是不是一个好主意。可以肯定的是,很多程序员会因为太烦而不想让它启用。关于这个安全系统有很多批评。然而,我个人认为,在一个仍然拥有管理员权限的帐户上,这是唯一能保证最低限度安全的方法。如果需要更多安全性,就应该切换到普通用户帐户。

地址空间布局随机化

简称 ASLR。关于 Vista 上的这个新功能,有一个有趣的 博客。如您所知,随机化对于防止缓冲区溢出攻击很有用,并且已经在其他操作系统上实现。在 Vista 上,随机化在多个层面上进行

  • 映像基址
  • 堆栈
  • Heap

随着 Visual Studio 2005 新的 Service Pack 1 的推出,引入了 /dynamicbase 选项。这个选项使得像动态库一样重定位可执行文件成为可能,只需在 PE 头中添加一个重定位节。Windows Vista 上的所有可执行文件都是用这个选项编译的,这使得程序员无法假定特定数据的位置。映像的随机化有 256 种变体。Michael Howard 解释了原因

最后一点,我们确实没有像 PaX 和其他更激进的 ASLR 实现那样多的随机化。例如,映像随机化只有 8 位(256 种变体中的 1 种)。映像必须 64K 对齐,因此在 32 位系统上,理论上我们可以将映像随机化多达 15 位(32,768 种变体中的 1 种),但增量安全增益很小——如果你访问一个网站,浏览器崩溃了,你还会再访问那个网站 255 次吗?——而且这会以碎片化整个地址空间为代价,从而减少应用程序可用的连续内存并降低系统性能。我们认为我们达到了一个很好的平衡点。

我做了一些测试,似乎在 x86 上,与 x64 不同,映像基址只在重启时改变。在 x64 上,映像基址似乎每次执行都会改变。顺便说一句,x64 不应该有连续内存可用性的问题,因为地址空间比 x86 大得多。不幸的是,我不能做更多的测试,因为目前我正在 Vista x86 上运行。

另一方面,堆栈应该有 16,384 种可能的变化(14 位)。为了测试,我写了一个小程序来计算 N 次执行中的堆栈变化。我写得很快,所以实现也很粗糙。基本上,一个可执行文件调用另一个用 /dynamicbase 选项编译的可执行文件 N 次,并从中获取一个局部变量的堆栈地址。然后它评估该堆栈地址是否已被使用;如果没有,它就增加变化次数。为了将堆栈地址传达给父进程,动态基址可执行文件使用 `SendMessage`(这就足够了,因为 `wparam` 和 `lparam` 的大小与指针相同)。这个应用程序是为 x86 和 x64 编译的。

这是动态基址可执行文件的源代码

#include <Windows.h>
#include <tchar.h>

#define WM_STACKADDRESS        (WM_USER + 100)

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
             LPTSTR lpCmdLine, int nCmdShow)
{
    int x;

    return (int) SendMessage(FindWindow(NULL, _T("RandTest")),
        WM_STACKADDRESS, 0, (LPARAM) &x);
}

这是主可执行文件

#include <Windows.h>
#include <tchar.h>
#include "resource.h"

#define WM_STACKADDRESS        (WM_USER + 100)

ULONG_PTR *pAddresses = NULL;
UINT nAddresses = 0;

//
// Test Loop thread: not blocking for GUI
//

void TestLoop(ULONG_PTR p)
{
    UINT nExec = (UINT) p;

    STARTUPINFO si = { 0 };
    PROCESS_INFORMATION pi = { 0 };

    for (UINT x = 0; x < nExec; x++)
    {
        CreateProcess(_T("DynBaseApp.exe"), NULL, NULL, NULL, FALSE, 0,
            NULL, NULL, &si, &pi);

        WaitForSingleObject(pi.hProcess, 3000);
    }

    delete pAddresses;

    //
    // We're through with testing, show number of variations
    //

    TCHAR szMsg[100];

    wsprintf(szMsg, _T("The number of variations on %d executions is: %d."),
        nExec, nAddresses);

    MessageBox(NULL, szMsg, _T("Test Result"), MB_ICONINFORMATION);

    ExitThread(0);
}

LRESULT CALLBACK DlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {

    case WM_CLOSE:
        {
            EndDialog(hDlg, FALSE);
            break;
        }

    case WM_COMMAND:
        {
            switch ((WORD) wParam)
            {

            case IDC_TEST:
                {
                    //
                    // If executions != 0 start the loop
                    //

                    TCHAR nStrExec[20];

                    GetDlgItemText(hDlg, ED_EXECUTIONS, nStrExec, 20);

                    UINT nExec = _tcstoul(nStrExec, NULL, 10);

                    if (nExec == 0) break;

                    pAddresses = new ULONG_PTR [nExec];
                    nAddresses = 0;

                    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) &TestLoop,
                        (LPVOID) (ULONG_PTR) nExec, 0, NULL);

                    break;
                }
            }

            break;
        }

    case WM_STACKADDRESS:
        {
            //
            // It's a stack address being sended by DynBaseApp.exe
            // add it to the list if not already there
            //

            ULONG_PTR Addr = (ULONG_PTR) lParam;

            for (UINT x = 0; x < nAddresses; x++)
            {
                if (Addr == pAddresses[x]) return FALSE;
            }

            pAddresses[nAddresses] = Addr;
            nAddresses++;

            break;
        }
    }

    return FALSE;
}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
             LPTSTR lpCmdLine, int nCmdShow)
{
    return (int) DialogBox(hInstance, (LPCTSTR) IDD_RANDTEST, NULL,
                          (DLGPROC) DlgProc);
}

结果相当不错。在 16,385 次执行中,我得到的最小值是 4026(在 x64 上)。执行次数较少时,结果当然更好(100/100, 199/200, 296/300)。

要使用 /dynamicbase 选项编译可执行文件,只需在链接器命令行(项目 -> 属性 -> 链接器 -> 命令行 -> 其他选项)中添加“/dynamicbase”(不带括号)。

驱动程序签名

Vista 的一个重大变化是,驱动程序现在需要经过认证才能运行(至少在 64 位上),而为了获得认证,您必须提供驱动程序的 x64 版本。仅 x86 的提交不再被 WHQL(Windows 硬件质量实验室)接受。如果您对如何签名驱动程序一无所知,MSDN 上有一份很好的文档,当然还有官方页面。尽管如此,出于测试和调试的目的,仍然可以禁用驱动程序认证。重启您的系统并按 F8 进入高级启动选项。选择“禁用驱动程序签名强制”。

然而,如果您是或想成为设备驱动程序程序员,还有很多需要讨论的地方。在这种情况下,我建议您阅读 NT Insider,并针对这个问题阅读这篇文章。它描述了为驱动程序调试和测试设置 Vista 机器所需的所有步骤。

Patch Guard

本段(及其子段)不应仅限于 Vista。然而,关于 Vista 的 Patch Guard 有很多讨论。Patch Guard 并不是新闻,它最早是为 x64 引入的(不适用于 x86),在 Windows XP 和 2003 中就有了。我选择将本段放在 Vista 部分,是因为围绕这项技术的纷争给 Vista 带来了一些变化。Patch Guard 的意思是,不再可能修补内核数据,即使是受信任的组件也不行。事实上,一些公司,如赛门铁克,抗议这项技术,并声称微软利用它来阻止第三方开发的安全解决方案工作。这当然是无稽之谈。微软的安全产品也不会修补 Vista 的内核,而是像其他所有人一样使用有文档记录的接口。基本上,Patch Guard 检查系统数据的完整性,如果数据被破坏,它会调用 KeBugCheckEx 导致系统关闭。触发此行为的事情有

  • 修改系统服务表 (SDT)。
  • 修改中断描述符表(IDT)。
  • 修改全局描述符表(GDT)。
  • 使用非内核分配的内核堆栈。
  • 修补内核的任何部分(仅在基于 AMD64 的系统上检测到)。

仅当调试系统时,Patch Guard 才会被禁用。如果您的产品依赖于内核修补,您或许可以使用此 博客文章 中建议的替代方法。

显然,客户需要有效的安全解决方案,而这些解决方案的开发可以不依赖内核修补技术。一些内核修补的替代方案包括:

  • Windows Vista 包含“Windows 筛选平台”,该平台使软件能够执行面向网络的操作,例如数据包检测以及支持防火墙产品所需的其他活动。
  • 文件系统微筛选器模型允许软件参与文件系统活动,这可供反病毒软件使用。
  • 注册表通知挂钩,该功能在 Windows XP 中引入,并在 Windows Vista 中得到增强,允许软件参与系统中的注册表相关活动。

我将在“注册表筛选”段落中讨论第三种替代方案。

攻击

由于 Patch Guard 是一个软件实现,因此已经有人在这方面做出了一些努力。Uninformed 网站绕过了 Windows XP x64 上的 Patch Guard 保护。Joanna Rutkowska 通过修补驱动程序的分页文件(在磁盘上),绕过了 Vista RC1 的 Patch Guard。此 攻击 是在用户模式下完成的,因此需要能够使用 CreateFile 并带有写权限来打开磁盘,然后通过 WriteFile 修改数据。看起来 Vista RC2 解决了这个问题,它阻止了从用户模式下修改磁盘(即使有高权限)。然而,我做了一些测试,发现 CreateFile 仍然返回一个有效的句柄。正如我所见,修改某些磁盘部分(如引导扇区)仍然是被允许的。尽管如此,如果您的实用程序依赖于对磁盘的原始写入访问,您可能会遇到一些问题。事实上,这个限制甚至没有解决问题,因为理论上同样的技巧仍然可以在内核模式下使用。

注册表过滤

由于在 x64 上不再可能修补服务描述符表 (SDT),人们可能会好奇 Mark Russinovich 的 Regmon(即新的 Process Monitor)是如何在 x64 上工作的。答案很简单,从 Windows XP 开始,就不再需要挂钩 SDT 了。实际上,Windows XP 引入了一种官方且有文档记录的新方法来筛选注册表。此方法依赖于三个函数:CmRegisterCallback(在 XP 和 Vista 上支持)、CmRegisterCallbackEx(仅在 Vista 上支持)和 CmUnRegisterCallback。CmRegisterCallback/Ex 为每个注册表操作注册一个回调函数。该回调函数如下所示:

NTSTATUS RegistryCallback(
    IN PVOID  CallbackContext,
    IN PVOID  Argument1,
    IN PVOID  Argument2
    );

以下是其参数:

CallbackContext 驱动程序在注册此 RegistryCallback 例程时,作为 Context 参数传递给 CmRegisterCallback 或 CmRegisterCallbackEx 的值。
Argument1 一个 REG_NOTIFY_CLASS 类型的值,用于标识正在执行的注册表操作的类型,以及 RegistryCallback 例程是在注册表操作执行之前还是之后被调用。
Argument2 一个指向结构的指针,该结构包含特定于注册表操作类型的信息。结构类型取决于 Argument1 的 REG_NOTIFY_CLASS 类型值,如下表所示。有关不同操作系统版本可用的 REG_NOTIFY_CLASS 类型值的信息,请参阅 REG_NOTIFY_CLASS。

Argument1 携带注册表操作信息,Argument2 是指向一个结构的指针。以下是操作及其结构的列表:

操作 (Operation)

结构

RegNtDeleteKey REG_DELETE_KEY_INFORMATION
RegNtPreDeleteKey REG_DELETE_KEY_INFORMATION
RegNtPostDeleteKey REG_POST_OPERATION_INFORMATION
RegNtSetValueKey REG_SET_VALUE_KEY_INFORMATION
RegNtPreSetValueKey REG_SET_VALUE_KEY_INFORMATION
RegNtPostSetValueKey REG_POST_OPERATION_INFORMATION
RegNtDeleteValueKey REG_DELETE_VALUE_KEY_INFORMATION
RegNtPreDeleteValueKey REG_DELETE_VALUE_KEY_INFORMATION
RegNtPostDeleteValueKey REG_POST_OPERATION_INFORMATION
RegNtPostDeleteValueKey REG_POST_OPERATION_INFORMATION
RegNtSetInformationKey REG_SET_INFORMATION_KEY_INFORMATION
RegNtPreSetInformationKey REG_SET_INFORMATION_KEY_INFORMATION
RegNtPostSetInformationKey REG_POST_OPERATION_INFORMATION
RegNtRenameKey REG_RENAME_KEY_INFORMATION
RegNtPreRenameKey REG_RENAME_KEY_INFORMATION
RegNtPostRenameKey REG_POST_OPERATION_INFORMATION
RegNtEnumerateKey REG_ENUMERATE_KEY_INFORMATION
RegNtPreEnumerateKey REG_ENUMERATE_KEY_INFORMATION
RegNtPostEnumerateKey REG_POST_OPERATION_INFORMATION
RegNtEnumerateValueKey REG_ENUMERATE_VALUE_KEY_INFORMATION
RegNtPreEnumerateValueKey REG_ENUMERATE_VALUE_KEY_INFORMATION
RegNtPostEnumerateValueKey REG_POST_OPERATION_INFORMATION
RegNtQueryKey REG_QUERY_KEY_INFORMATION
RegNtPreQueryKey REG_QUERY_KEY_INFORMATION
RegNtPostQueryKey REG_POST_OPERATION_INFORMATION
RegNtQueryValueKey REG_QUERY_VALUE_KEY_INFORMATION
RegNtPreQueryValueKey REG_QUERY_VALUE_KEY_INFORMATION
RegNtPostQueryValueKey REG_POST_OPERATION_INFORMATION
RegNtQueryMultipleValueKey REG_QUERY_MULTIPLE_
VALUE_KEY_INFORMATION
RegNtPreQueryMultipleValueKey REG_QUERY_MULTIPLE_
VALUE_KEY_INFORMATION
RegNtPostQueryMultipleValueKey REG_POST_OPERATION_INFORMATION
RegNtPreCreateKey REG_PRE_CREATE_KEY_INFORMATION
RegNtPreCreateKeyEx REG_CREATE_KEY_INFORMATION
RegNtPostCreateKey REG_POST_CREATE_KEY_INFORMATION
RegNtPostCreateKeyEx REG_POST_OPERATION_INFORMATION
RegNtPreOpenKey REG_PRE_OPEN_KEY_INFORMATION
RegNtPreOpenKeyEx REG_OPEN_KEY_INFORMATION
RegNtPostOpenKey REG_POST_OPEN_KEY_INFORMATION
RegNtPostOpenKeyEx REG_POST_OPERATION_INFORMATION
RegNtKeyHandleClose REG_KEY_HANDLE_CLOSE_INFORMATION
RegNtPreKeyHandleClose REG_KEY_HANDLE_CLOSE_INFORMATION
RegNtPostKeyHandleClose REG_POST_OPERATION_INFORMATION
RegNtPreFlushKey REG_FLUSH_KEY_INFORMATION
RegNtPostFlushKey REG_POST_OPERATION_INFORMATION
RegNtPreLoadKey REG_LOAD_KEY_INFORMATION
RegNtPostLoadKey REG_POST_OPERATION_INFORMATION
RegNtPreUnLoadKey REG_UNLOAD_KEY_INFORMATION
RegNtPostUnLoadKey REG_POST_OPERATION_INFORMATION
RegNtPreQueryKeySecurity REG_QUERY_KEY_SECURITY_INFORMATION
RegNtPostQueryKeySecurity REG_POST_OPERATION_INFORMATION
RegNtPreSetKeySecurity REG_SET_KEY_SECURITY_INFORMATION
RegNtPostSetKeySecurity REG_POST_OPERATION_INFORMATION
RegNtCallbackContextCleanup REG_CALLBACK_CONTEXT_
CLEANUP_INFORMATION

根据 MSDN 的说法,访问这些结构中的指针应在 try/except 块中进行。回调函数也可以阻止操作的执行(它是一个真正的筛选器)。在 XP 上要实现这一点,只需返回一个不同于 STATUS_SUCCESS 的值即可。不幸的是,这样做之后,最初调用注册表函数的线程也会收到相同的错误。这就是为什么在 Vista 上支持一个新的值:STATUS_CALLBACK_BYPASS。通过返回此值,注册表操作实际上不会被执行,但线程不会收到错误值。这对于安全解决方案非常有用。

我编写了一个小(非常小)的注册表筛选器(请参阅文章顶部的 MyRegFilter 下载链接)来展示这个新方法是如何工作的。别对此太兴奋,我只花了 20 分钟写它,而且它并不那么好,但也许对某些人会有帮助。

#include <ntddk.h>

WCHAR DeviceName[] = L"\\Device\\MyRegFilter";
WCHAR SymLinkName[] = L"\\DosDevices\\MyRegFilter";

UNICODE_STRING usDeviceName;
UNICODE_STRING usSymbolicLinkName;

typedef struct _DEVICE_CONTEXT
{
    PDRIVER_OBJECT  pDriverObject;
    PDEVICE_OBJECT  pDeviceObject;

    LARGE_INTEGER RegCookie;
}
DEVICE_CONTEXT, *PDEVICE_CONTEXT, **PPDEVICE_CONTEXT;

PDEVICE_OBJECT  g_pDeviceObject  = NULL;
PDEVICE_CONTEXT g_pDeviceContext = NULL;

#define FILE_DEVICE_MYREGFILTER 0x8000


NTSTATUS DriverInitialize(PDRIVER_OBJECT  pDriverObject,
                          PUNICODE_STRING pusRegistryPath);
NTSTATUS DriverEntry(PDRIVER_OBJECT  pDriverObject,
                     PUNICODE_STRING pusRegistryPath);

NTSTATUS RegistryCallback(PVOID CallbackContext, PVOID Argument1,
                          PVOID Argument2);

#ifdef ALLOC_PRAGMA

#pragma alloc_text (INIT, DriverInitialize)
#pragma alloc_text (INIT, DriverEntry)

#endif

NTSTATUS DeviceDispatcher(PDEVICE_CONTEXT pDeviceContext, PIRP pIrp)
{
    PIO_STACK_LOCATION pisl;
    NTSTATUS ns = STATUS_NOT_IMPLEMENTED;

    pisl = IoGetCurrentIrpStackLocation(pIrp);

    switch (pisl->MajorFunction)
    {

    case IRP_MJ_CREATE:
    case IRP_MJ_CLEANUP:
    case IRP_MJ_CLOSE:
         case IRP_MJ_DEVICE_CONTROL:
        {
            ns = STATUS_SUCCESS;
            break;
        }
    }

    pIrp->IoStatus.Status = ns;
    pIrp->IoStatus.Information = 0;

    IoCompleteRequest(pIrp, IO_NO_INCREMENT);

    return ns;
}

NTSTATUS DriverDispatcher(PDEVICE_OBJECT pDeviceObject, PIRP pIrp)
{
    return (pDeviceObject == g_pDeviceObject ?
        DeviceDispatcher(g_pDeviceContext, pIrp)
        : STATUS_INVALID_PARAMETER_1);
}

VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
    //

    // Stop filtering the registry

    // Shouldn't be placed in the unload

    //


    CmUnRegisterCallback(g_pDeviceContext->RegCookie);

    IoDeleteSymbolicLink(&usSymbolicLinkName);
    IoDeleteDevice(pDriverObject->DeviceObject);
}

NTSTATUS DriverInitialize(PDRIVER_OBJECT pDriverObject,
                          PUNICODE_STRING pusRegistryPath)
{
    PDEVICE_OBJECT pDeviceObject = NULL;
    NTSTATUS ns = STATUS_DEVICE_CONFIGURATION_ERROR;

    RtlInitUnicodeString(&usDeviceName, DeviceName);
    RtlInitUnicodeString(&usSymbolicLinkName, SymLinkName);

    if ((ns = IoCreateDevice(pDriverObject, sizeof (DEVICE_CONTEXT),
        &usDeviceName, FILE_DEVICE_MYREGFILTER, 0, FALSE,
        &pDeviceObject)) == STATUS_SUCCESS)
    {
        if ((ns = IoCreateSymbolicLink(&usSymbolicLinkName,
            &usDeviceName)) == STATUS_SUCCESS)
        {
            g_pDeviceObject  = pDeviceObject;
            g_pDeviceContext = pDeviceObject->DeviceExtension;

            g_pDeviceContext->pDriverObject = pDriverObject;
            g_pDeviceContext->pDeviceObject = pDeviceObject;

        }
        else
        {
            IoDeleteDevice(pDeviceObject);
        }
    }

    return ns;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,
                     PUNICODE_STRING pusRegistryPath)
{
    PDRIVER_DISPATCH *ppdd;
    NTSTATUS ns = STATUS_DEVICE_CONFIGURATION_ERROR;

    if ((ns = DriverInitialize(pDriverObject, pusRegistryPath))
                                                            == STATUS_SUCCESS)
    {
        ppdd = pDriverObject->MajorFunction;

        ppdd[IRP_MJ_CREATE                  ] =
        ppdd[IRP_MJ_CREATE_NAMED_PIPE       ] =
        ppdd[IRP_MJ_CLOSE                   ] =
        ppdd[IRP_MJ_READ                    ] =
        ppdd[IRP_MJ_WRITE                   ] =
        ppdd[IRP_MJ_QUERY_INFORMATION       ] =
        ppdd[IRP_MJ_SET_INFORMATION         ] =
        ppdd[IRP_MJ_QUERY_EA                ] =
        ppdd[IRP_MJ_SET_EA                  ] =
        ppdd[IRP_MJ_FLUSH_BUFFERS           ] =
        ppdd[IRP_MJ_QUERY_VOLUME_INFORMATION] =
        ppdd[IRP_MJ_SET_VOLUME_INFORMATION  ] =
        ppdd[IRP_MJ_DIRECTORY_CONTROL       ] =
        ppdd[IRP_MJ_FILE_SYSTEM_CONTROL     ] =
        ppdd[IRP_MJ_DEVICE_CONTROL          ] =
        ppdd[IRP_MJ_INTERNAL_DEVICE_CONTROL ] =
        ppdd[IRP_MJ_SHUTDOWN                ] =
        ppdd[IRP_MJ_LOCK_CONTROL            ] =
        ppdd[IRP_MJ_CLEANUP                 ] =
        ppdd[IRP_MJ_CREATE_MAILSLOT         ] =
        ppdd[IRP_MJ_QUERY_SECURITY          ] =
        ppdd[IRP_MJ_SET_SECURITY            ] =
        ppdd[IRP_MJ_POWER                   ] =
        ppdd[IRP_MJ_SYSTEM_CONTROL          ] =
        ppdd[IRP_MJ_DEVICE_CHANGE           ] =
        ppdd[IRP_MJ_QUERY_QUOTA             ] =
        ppdd[IRP_MJ_SET_QUOTA               ] =
        ppdd[IRP_MJ_PNP                     ] = DriverDispatcher;
        pDriverObject->DriverUnload           = DriverUnload;

        //
        // Filter the registry
        //

        ns = CmRegisterCallback(RegistryCallback, g_pDeviceContext,
                                &g_pDeviceContext->RegCookie);

        if (!NT_SUCCESS(ns)) IoDeleteDevice(g_pDeviceObject);
    }

    return ns;
}

//
// Registry Filter Callback
//

NTSTATUS RegistryCallback(PVOID CallbackContext, PVOID Argument1,
                          PVOID Argument2)
{
    PDEVICE_CONTEXT pContext = (PDEVICE_CONTEXT) CallbackContext;
    REG_NOTIFY_CLASS Action  = (REG_NOTIFY_CLASS) Argument1;


    switch (Action)
    {

    case RegNtPreDeleteKey:
        {
            //
            // Pre DeleteKey
            //

            PREG_DELETE_KEY_INFORMATION pInfo
                                = (PREG_DELETE_KEY_INFORMATION) Argument2;

            DbgPrint("Delete Key\n");

            //
            // You can prevent this operation from happening
            // Without having the thread noticing it
            // Only on Windows Vista
            //
            //
            // return STATUS_CALLBACK_BYPASS;
            //

            break;
        }

    case RegNtPreCreateKeyEx:
        {
            //
            // Pre CreateKey
            //

            PREG_CREATE_KEY_INFORMATION pInfo
                                = (PREG_CREATE_KEY_INFORMATION) Argument2;

            DbgPrint("Create Key\n");

            break;
        }

    default:
        {
            //
            // Return STATUS_SUCCESS
            //

            break;
        }
    }

    return STATUS_SUCCESS;
}

在此代码示例中,我使用 DbgPrint 来获取注册表操作的通知。在 Vista 上,DbgPrint 的输出默认是禁用的。如果您想启用它,请遵循这些说明。

电源管理

Vista 中的电源管理得到了改进,不仅因为引入了新功能,还因为设备驱动程序开发人员的电源管理工作变得更容易了。这些 WinHec 文档 是一个很好的入门资源。其中一个重大新闻是引入了混合睡眠(默认的关闭模式)。在这种睡眠模式下,系统映像被写入磁盘上的休眠文件,系统可以从该文件恢复。驱动程序会通过 IRP_MN_SET_POWER (Parameters.Power.State == PowerSystemHybernate) 收到进入混合睡眠(S4 状态)的通知。使用 SYSTEM_POWER_STATE_CONTEXT 结构 (Parameters.Power.SystemPowerStateContext) 来确定状态转换过程。

此外,这可能不是很重要,但我碰巧读到了以下内容。在 Vista 中,不再允许(在用户模式下)静默取消关机。这意味着如果您的应用程序收到了一个 WM_QUERYENDSESSION 消息但没有返回 TRUE(以允许系统关机),Vista 将会弹出一个对话框,通知用户这一行为。

.NET Framework 3.0

.NET Framework 3.0 与 Vista 一同发布。不过,它也可以安装在 XP SP2 上。要在 Visual Studio 2005 中使用这个新框架引入的新技术,您需要两个扩展。一个是用于 Windows Presentation Foundation (WPF) 和 Windows Communication Foundation (WCF) 的扩展。另一个是用于 Windows Workflow Foundation (WWF) 的扩展。当然,我无法详尽地讨论这些技术,但我可以尝试为那些从未接触过它们的程序员提供一些见解。

Windows Presentation Foundation

我对这项技术非常热情,但对于像我这样的传统 C/C++ 程序员来说,需要花点时间才能理解它的工作原理。基本上,这是一种为桌面应用程序和网页创建图形用户界面 (GUI) 的新方法。与旧方法的主要区别在于,这些 GUI 是通过 XAML(可扩展应用程序标记语言)创建的,这是一种基于 XML 的语言。使用 WPF 的优点很多。您可以在几秒钟内使用 2D/3D、音频、视频、动画等。不再有 HWND,所有的工作都委托给了 GPU。在 MSDN TV 上有一些演示,展示了如何通过 WPF 设计出美观且高级的 GUI。WPF 在 GUI 开发和内部代码实现之间提供了很好的分离。此外,许多事情可以通过 XAML 实现,而无需使用 C#/VB 代码(smallWPF.zip 下载可在文章顶部找到)。

在这个小程序示例中,我将滑块绑定到列表框的值,以改变其外观(位置和阴影)。令人惊叹的是,列表框仍然可以使用,您可以滚动它、选择项目等。我不是说旋转列表框很有用,但这只是一个展示其可能性的例子。正如我所说,滑块是绑定到值的,这意味着我没有使用代码。这个应用程序所做的一切都是用 XAML 编写的。以下是所有代码:

<Window x:Class="SmallWPF.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="SmallWPF" Height="339" Width="454"
  xmlns:my="clr-namespace:System;assembly=mscorlib">
  <Grid>
    <Border BorderBrush="White" BorderThickness="5"
            HorizontalAlignment="Center"
      VerticalAlignment="Top">
      <ListBox Width="200" Height="200" Name="listBox1" >
        <ListBoxItem>Hello,</ListBoxItem>
        <ListBoxItem IsSelected="True">how are you?</ListBoxItem>
        <ListBoxItem>This</ListBoxItem>
        <ListBoxItem>is</ListBoxItem>
        <ListBoxItem>a</ListBoxItem>
        <ListBoxItem>3D</ListBoxItem>
        <ListBoxItem>ListBox!</ListBoxItem>
      </ListBox>
      <Border.BitmapEffect>
        <BitmapEffectGroup>
          <DropShadowBitmapEffect Color="Black"
            Direction="{Binding ElementName=MySlider4, Path=Value}"
            ShadowDepth="{Binding ElementName=MySlider5, Path=Value}"
            Softness="1" Opacity="0.5"/>
        </BitmapEffectGroup>
      </Border.BitmapEffect>
      <Border.RenderTransform>
        <TransformGroup>
          <SkewTransform CenterX="0" CenterY="0"
            AngleX="{Binding ElementName=MySlider1, Path=Value}"
            AngleY="{Binding ElementName=MySlider2, Path=Value}" />
          <RotateTransform Angle="{Binding ElementName=MySlider3, Path=Value}"/>
        </TransformGroup>
      </Border.RenderTransform>
    </Border>
    <Slider Height="21" Margin="42,0,0,43" Name="MySlider1"
      VerticalAlignment="Bottom"
      HorizontalAlignment="Left" Width="104" Minimum="0" Maximum="50" />
    <Slider Height="21" Margin="184,0,158,43" Name="MySlider2"
      VerticalAlignment="Bottom"
      Width="104" Minimum="0" Maximum="50" />
    <Slider Height="21" Margin="0,0,33,43" Name="MySlider3"
      VerticalAlignment="Bottom"
      HorizontalAlignment="Right" Width="104" Minimum="0" Maximum="50" />
    <Slider Height="21" Margin="42,0,0,15" Name="MySlider4"
      VerticalAlignment="Bottom"
      Width="104" Minimum="0" Maximum="200" HorizontalAlignment="Left" />
    <Slider Height="21" Margin="184,0,158,13" Name="MySlider5"
      VerticalAlignment="Bottom"
      Width="104" Minimum="0" Maximum="100" />
  </Grid>
</Window>

我使用此代码将一个值绑定到一个滑块:

AngleX="{Binding ElementName=MySlider1, Path=Value}"

ElementName 是要绑定的控件的名称,Path 是被绑定控件的属性,该属性应用于填充值。在本例中,MySlider1 的位置填充了 AngleX 字段。我还可以将控件的行为绑定到 C# 代码。但是,当然,这里不是讨论这项技术所有属性的地方。我只希望这一段能引起您的足够兴趣,让您想去阅读更多相关内容。

Windows Communication Foundation

WCF 是一个用于设计服务的接口。其背后的思想是为现有技术提供一个统一的编程模型:COM+ / .NET Enterprise services、MSMQ、.NET Remoting、ASP.NET Web Services、Web Services Enhancements (WSE)。此外,WCF 能够处理这些技术之间的互通,而无需程序员担心,并且是以可靠和安全的方式进行的。从我所读到的内容来看,这似乎是对过去所有问题的一个很好的最终解决方案,因为从现在开始,程序员不必考虑通信过程本身,这由 WCF 处理,意味着他不必担心正在与哪种技术以及从哪里进行通信。更多信息,请访问 Windows Communication Foundation 官方主页。不过,CodeProject 上有更多实际的示例。

Windows Workflow Foundation

WWF 是一种通过绑定到代码的可视化项目来形式化基于工作流的活动的好方法。好吧,这听起来有点奇怪,我再试着解释一下。基本上,如果您是一家公司,需要将一系列活动过程形式化,并希望有一个可视化模型的便利,那么 WWF 就是您所需要的。我不能详尽地讨论这个主题,因为我自己也没有广泛使用过这项技术。然而,由于许多程序员可能想知道这项技术到底是关于什么的,我将尝试一种简单的理解方法。这是我做的一个工作流图:

如您所见,该图被划分为单个活动(非常少,因为我缺乏想象力)。这些活动可以通过声明性规则进行绑定。这意味着如果我有一个需要满足的条件,我可以将该条件声明为一个属性。工作流组件有很多种,每种都有自己的属性。例如,代码工作流组件可以在一个 C# 文件中定义一个代码函数,当轮到该组件活动时执行。在这个小例子中,第一个活动是等待用户输入。之后,设置了一个声明性规则,将工作流细分为两个独立的活动流。

我希望,尽管我的工作流模型很糟糕,您还是理解了 Windows Workflow Foundation 的总体用途。如果您有兴趣了解更多,请查看官方主页,在那里您还可以找到许多代码示例。和往常一样,关于这个主题有大量的指南。

结论

文章到此结束。我希望您喜欢这篇文章,并且不反感这种对 x64 和 Windows Vista 这两个非常广泛的主题进行如此概括性的介绍。在写作过程中,我注意到我不得不在文章中加入很多图片,这对于网速慢的用户来说可能会是个问题。对此我感到抱歉,但这是没有将本文分成更多篇文章的直接后果。

© . All rights reserved.