• The Old New Thing

    The Itanium processor, part 1: Warming up


    The Itanium may not have been much of a commercial success, but it is interesting as a processor architecture because it is different from anything else commonly seen today. It's like learning a foreign language: It gives you an insight into how others view the world.

    The next two weeks will be devoted to an introduction to the Itanium processor architecture, as employed by Win32. (Depending on the reaction to this series, I might also do a series on the Alpha AXP.)

    I originally learned this information in order to be able to debug user-mode code as part of the massive port of several million lines of code from 32-bit to 64-bit Windows, so the focus will be on being able to read, understand, and debug user-mode code. I won't cover kernel-mode features since I never had to learn them.


    The Itanium is a 64-bit EPIC architecture. EPIC stands for Explicitly Parallel Instruction Computing, a design in which work is offloaded from the processor to the compiler. For example, the compiler decides which operations can be safely performed in parallel and which memory fetches can be productively speculated. This relieves the processor from having to make these decisions on the fly, thereby allowing it to focus on the real work of processing.

    Registers overview

    There are a lot of registers.

    • 128 general-purpose integer registers r0 through r127, each carrying 64 value bits and a trap bit. We'll learn more about the trap bit later.
    • 128 floating point registers f0 through f127.
    • 64 predicate registers p0 through p63.
    • 8 branch registers b0 through b7.
    • An instruction pointer, which the Windows debugging engine for some reason calls iip. (The extra "i" is for "insane"?)
    • 128 special-purpose registers, not all of which have been given meanings. These are called "application registers" (ar) for some reason. I will cover selected register as they arise during the discussion.
    • Other miscellaneous registers we will not cover in this series.

    Some of these registers are further subdivided into categories like static, stacked, and rotating.

    Note that if you want to retrieve the value of a register with the Windows debugging engine, you need to prefix it with an at-sign. For example ? @r32 will print the contents of the r32 register. If you omit the at-sign, then the debugger will look for a variable called r32.

    A notational note: I am using the register names assigned by the Windows debugging engine. The formal names for the registers are gr# for integer registers, fr# for floating point registers, pr# for predicate registers, and br# for branch registers.

    Static, stacked, and rotating registers

    These terms describe how the registers participate in register renumbering.

    Static registers are never renumbered.

    Stacked registers are pushed onto a register stack when control transfers into a function, and they pop off the register stack when control transfers out. We'll see more about this when we study the calling convention.

    Rotating registers can be cyclically renumbered during the execution of a function. They revert to being stacked when the function ends (and are then popped off the register stack). We'll see more about this when we study register rotation.

    Integer registers

    Of the 128 integer registers, registers r0 through r31 are static, and r32 through r127 are stacked (but they can be converted to rotating).

    Of the static registers, Win32 assigns them the following mnemonics which correspond to their use in the Win32 calling convention.

    Register Mnemonic Meaning
    r0 Reads as zero (writes will fault)
    r1 gp Global pointer
    r8r11 ret0ret3 Return values
    r12 sp Stack pointer
    r13 TEB

    Registers r4 through r7 are preserved across function calls. Well, okay, you should also preserve the stack pointer and the TEB if you know what's good for you, and there are special rules for gp which we will discuss later. The other static variables are scratch (may be modified by the function).

    Register r0 is a register that always contains the value zero. Writes to r0 trigger a processor exception.

    The gp register points to the current function's global variables. The Itanium has no absolute addressing mode. In order to access a global variable, you need to load it indirectly through a register, and the gp register points to the global variables associated with the current function. The gp register is kept up to date when code transfers between DLLs by means we'll discuss later. (This is sort of a throwback to the old days of MAKEPROCINSTANCE.)

    Every integer register contains 64 value bits and one trap bit, known as not-a-thing, or NaT. The NaT bit is used by speculative execution to indicate that the register values are not valid. We learned a little about NaT some time ago; we'll discuss it further when we reach the topic of control speculation. The important thing to know about NaT right now is that if you take a register which is tagged as NaT and try to do arithmetic with it, then the NaT bit is set on the output register. Most other operations on registers tagged as NaT will raise an exception.

    The NaT bit means that accessing an uninitialized variable can crash.

    void bad_idea(int *p)
     int uninitialized;
     *p = uninitialized; // can crash here!

    Since the variable uninitialized is uninitialized, the register assigned to it might happen to have the NaT bit set, left over from previous execution, at which point trying to save it into memory raises an exception.

    You may have noticed that there are four return value registers, which means that you can return up to 32 bytes of data in registers.

    Floating point registers

    Register Meaning
    f0 Reads as 0.0 (writes will fault)
    f1 Reads as 1.0 (writes will fault)

    Registers f0 through f31 are static, and f32 through f127 are rotating.

    By convention, registers f0 through f5 and f16 through f31 are preserved across calls. The others are scratch.

    That's about all I'm going to say about floating point registers, since they aren't really where the Itanium architecture is exciting.

    Predicate registers

    Instead of a flags register, the Itanium records the state of previous comparison operations in dedicated registers known as predicates. Each comparison operation indicates which predicates should hold the comparison result, and future instructions can test the predicate.

    Register Meaning
    p0 Reads as true (writes are ignored)

    Predicate registers p0 through p15 are static, and p16 through p63 are rotating.

    You can predicate almost any instruction, and the instruction will execute only if the predicate register is true. For example:

    (p1) add ret0 = r32, r33

    means, "If predicate p1 is true, then set register ret0 equal to the sum of r32 and r33. If not, then do nothing." The thing inside the parentheses is called the qualifying predicate (abbreviated qp).

    Instructions which execute unconditionally are internally represented as being conditional upon predicate register p0, since that register is always true.

    Actually, I lied when I said that the instruction will execute only if the qualifying predicate is true. There is one class of instructions which execute regardless of the state of the qualifying predicate; we'll learn about that when we get to them.

    The Win32 calling convention specifies that predicate registers p0 through p5 are preserved across calls, and p6 through p63 are scratch.

    There is a special pseudo-register called preds by the Windows debugging engine which consists of the 64 predicate registers combined into a single 64-bit value. This pseudo-register is used when code needs to save and restore the state of the predicate registers.

    Branch registers

    The branch registers are used for indirect jump instructions. The only things you can do with branch registers are load them from an integer register, copy them to an integer register, and jump to them. In particular, you cannot load them directly from memory or do arithmetic on them. If you want to do any of those things, you need to do it with an integer register, then transfer it to a branch register.

    The Win32 calling convention assigns the following meanings to the branch registers:

    Register Mnemonic Meaning
    b0 rp Return address

    The return address register is sometimes called br, but the disassembler calls it rp, so that's what we'll call it.

    The return address register is set automatically by the processor when a br.call instruction is executed.

    By convention, registers b1 through b5 are preserved across calls, while b6 and b7 are scratch. (Exercise: Is b0 preserved across calls?)

    Application registers

    There are a large number of application registers, most of which are not useful to user-mode code. We'll introduce the interesting ones as they arise. I've already mentioned one of them already: bsp is the ia64's second stack pointer.


    Okay, this was a whirlwind tour of the Itanium register set. I bet your head hurts already, and we haven't even started coding yet!

    In fact, we're not going to be coding for quite some time. Next time, we'll look at the instruction format.

  • The Old New Thing

    The curse of the redefinition of the symbol HLOG


    A customer was running into a compiler error complaining about redefinition of the symbol HLOG.

    #include <pdh.h>
    #include <lm.h>

    The result is

    lmerrlog.h(80): error C2373: 'HLOG' redefinition; different type modifiers
    pdh.h(70): See declaration of 'HLOG'

    "Our project uses both performance counters (pdh.h) and networking (lm.h). What can we do to avoid this conflict?"

    We've seen this before. The conflict arises from two problems.

    First is hubris/lack of creativity. "My component does logging. I need a handle to a log. I will call it HLOG because (1) I can't think of a better name, and/or (2) obviously I'm the only person who does logging. (Anybody else who wants to do logging should just quit their job now because it's been done.)"

    This wouldn't normally be a problem except that Win32 uses a global namespace. This is necessary for annoying reasons:

    • Not all Win32 languages support namespaces.
    • Even though C++ supports namespaces, different C++ implementations decorate differently, so there is no agreement on the external linkage. (Indeed, the decoration can change from one version of the C++ compiler to another!)

    Fortunately, in the case of HLOG, the two teams noticed the collision and came to some sort of understanding. If you include them in the order

    #include <lm.h>
    #include <pdh.h>

    then pdh.h detects that lm.h has already been included and avoids the conflicting definition.

    #ifndef _LMHLOGDEFINED_
    typedef PDH_HLOG     HLOG;

    The PDH log is always accessible via the name PDH_HLOG. If lm.h was not also included, then the PDH log is also accessible under the name HLOG.

    Sorry for the confusion.

  • The Old New Thing

    Corrupted file causes application to crash; is that a security vulnerability?


    A security vulnerability report came in that went something like this:

    We have found a vulnerability in the XYZ application when it opens the attached corrupted file. The error message says, "Unhandled exception: System.OverflowException. Value was either too large or too small for an Int16." For a nominal subscription fee, you can learn about similar vulnerabilities in Microsoft products in the future.

    Okay, so there is a flaw in the XYZ application where a file that is corrupted in a specific way causes it to suffer an unhandled exception trying to load the file.

    That's definitely a bug, and thanks for reporting it, but is it a security vulnerability?

    The attack here is that you create one of these corrupted files and you trick somebody into opening it. And then when they open it, the XYZ application crashes. The fact that an Overflow­Exception was raised strongly suggests that the application was diligent enough to do its file parsing under the checked keyword, or that the entire module was compiled with the /checked compiler option, so that any overflow or out-of-range errors raise an exception rather than being ignored. That way, the overflow cannot be used as a vector to another attack.

    What is missing from the story is that nobody was set up to catch the overflow exception, so the corrupted file resulted in the entire application crashing rather than some sort of "Sorry, this file is corrupted" error being displayed.

    Okay, so let's assess the situation. What have you gained by tricking somebody into opening the file? The program detects that the file is corrupted and crashes instead of using the values in it. There is no code injection because the overflow is detected at the point it occurs, before any decisions are made based on the overflowed value. Consequently, there is no elevation of privilege. All you got was a denial of service against the XYZ application. (The overflow checking did its job and stopped processing as soon as corruption was detected.)

    There isn't even data loss, because the problem occurred while loading up the corrupted file. It's not like the XYZ application had any old unsaved data.

    At the end of the day, the worst you can do with this crash is annoy somebody.

    Here's another way you can annoy somebody: Send them a copy of onestop.mid.

  • The Old New Thing

    When you think you found a problem with a function, make sure you're actually calling the function, episode 2


    A customer reported that the Duplicate­Handle function was failing with ERROR_INVALID_HANDLE even though the handle being passed to it seemed legitimate:

      // Create the handle here
      m_Event =
        ::CreateEvent(NULL, FALSE/*bManualReset*/,
                           FALSE/*bInitialState*/, NULL/*lpName*/));
      ... error checking removed ...
    // Duplicate it here
    HRESULT MyClass::CopyTheHandle(HANDLE *pEvent)
     HRESULT hr = S_OK;
     if (m_Event != NULL) {
      BOOL result = ::DuplicateHandle(
      if (!result) {
        // always fails with ERROR_INVALID_HANDLE
        return HRESULT_FROM_WIN32(GetLastError());
     } else {
      *pEvent = NULL;
     return hr;

    The handle in m_Event appears to be valid. It is non-null, and we can still set and reset it. But we can't duplicate it.

    Now, before claiming that a function doesn't work, you should check what you're passing to it and what it returns. The customer checked the m_Event parameter, but what about the other parameters? The function takes three handle parameters, after all, and they checked only one of them. According to the debugger, Duplicate­Handle was called with the parameters

    hSourceProcessHandle  = 0x0aa15b80
    hSourceHandle  = 0x00000ed8 m_Event, appears to be valid
    hTargetProcessHandle  = 0x0aa15b80
    lpTargetHandle  = 0x00b0d914
    dwDesiredAccess  = 0x00000000
    bInheritHandle  = 0x00000000
    dwOptions  = 0x00000002

    Upon sharing this information, the customer immediately saw the problem: The other two handle parameters come from the Get­Current­Process function, and that function was returning 0x0aa15b80 rather than the expected pseudo-handle (which is currently -1, but that is not contractual).

    The customer explained that their My­Class has a method with the name Get­Current­Process, and it was that method which was being called rather than the Win32 function Get­Current­Process. They left off the leading :: and ended up calling the wrong Get­Current­Process.

    By default, Visual Studio colors member functions and global functions the same, but you can change this in the Fonts and Colors options dialog. Under Show settings for, select Text Editor, and then under Display items you can customize the colors to use for various language elements. In particular, you can choose a special color for static and instance member functions.

    Or, as a matter of style, you could have a policy of not giving member functions the same name as global functions. (This has the bonus benefit of reducing false positives when grepping.)

    Bonus story: A different customer reported a problem with visual styles in the common tab control. After a few rounds of asking questions, coming up with theories, testing the theories, disproving the theories, the customer wrote back: "We figured out what was happening when we tried to step into the call to Create­Dialog­Indirect­ParamW. Someone else in our code base redefined all the dialog creation functions in an attempt to enforce a standard font on all of them, but in doing so, they effectively made our code no longer isolation aware, because in the overriding routines, they called Create­Dialog­Indirect­ParamW instead of Isolation­Aawre­Create­Dialog­Indirect­ParamW. Thanks for all the help, and apologies for the false alarm."

  • The Old New Thing

    Please enjoy the new eco-friendly printers, now arguably less eco-friendly


    Some years ago, the IT department replaced the printers multifunction devices with new reportedly eco-friendly models. One feature of the new devices is that when you send a job to the printer, it doesn't print out immediately. Printing doesn't begin until you go to the device and swipe your badge through the card reader. The theory here is that this cuts down on the number of forgotten or accidental printouts, where you send a job to a printer and forget to pick it up, or you click the Print button by mistake. If a job is not printed within a few days, it is automatically deleted.

    The old devices already supported secured printing, where the job doesn't come out of the printer until you go to the device and release the job. But with this change, secured printing is now mandatory. Of course, this means that even if you weren't printing something sensitive, you still have to stand there and wait for your document to print instead of having the job already completed and waiting for you.

    The new printing system also removes the need for job separator pages. Avoiding banner pages and eliminating forgotten print jobs are touted as the printer's primary eco-friendly features.

    Other functions provided by the devices are photocopying and scanning. With the old devices, you place your document on the glass or in the document hopper, push the Scan button, and the results are emailed to you. With the new devices, you place your document on the glass or in the document hopper, push the Scan button, and the results are emailed to you, plus a confirmation page is printed out.

    Really eco-friendly service there, printing out confirmation pages for every scanning job.

    The problem was fixed a few weeks later.

    Bonus chatter: Our fax machines also print confirmation pages, or at least they did the last time I used one, many years ago.

  • The Old New Thing

    How can I detect whether a keyboard is attached to the computer?


    Today's Little Program tells you whether a keyboard is attached to the computer. The short answer is "Enumerate the raw input devices and see if any of them is a keyboard."

    Remember: Little Programs don't worry about silly things like race conditions.

    #include <windows.h>
    #include <iostream>
    #include <vector>
    #include <algorithm>
    bool IsKeyboardPresent()
     UINT numDevices = 0;
      if (GetRawInputDeviceList(nullptr, &numDevices,
                                sizeof(RAWINPUTDEVICELIST)) != 0) {
       throw GetLastError();
     std::vector<RAWINPUTDEVICELIST> devices(numDevices);
     if (GetRawInputDeviceList(&devices[0], &numDevices,
                               sizeof(RAWINPUTDEVICELIST)) == (UINT)-1) {
      throw GetLastError();
     return std::find_if(devices.begin(), devices.end(),
        [](RAWINPUTDEVICELIST& device)
        { return device.dwType == RIM_TYPEKEYBOARD; }) != devices.end();
    int __cdecl main(int, char**)
     std::cout << IsKeyboardPresent() << std::endl;
     return 0;

    There is a race condition in this code if the number of devices changes between the two calls to Get­Raw­Input­Device­List. I will leave you to fix it before incorporating this code into your program.

  • The Old New Thing

    What did the Ignore button do in Windows 3.1 when an application encountered a general protection fault?


    In Windows 3.0, when an application encountered a general protection fault, you got an error message that looked like this:

    Application error
    CONTOSO caused a General Protection Fault in
    module CONTOSO.EXE at 0002:2403

    In Windows 3.1, under the right conditions, you would get a second option:

    An error has occurred in your application.
    If you choose Ignore, you should save your work in a new file.
    If you choose Close, your application will terminate.

    Okay, we know what Close does. But what does Ignore do? And under what conditions will it appear?

    Roughly speaking, the Ignore option becomes available if

    • The fault is a general protection fault,
    • The faulting instruction is not in the kernel or the window manager,
    • The faulting instruction is one of the following, possibly with one or more prefix bytes:
      • Memory operations: op r, m; op m, r; or op m.
      • String memory operations: movs, stos, etc.
      • Selector load: lds, les, pop ds, pop es.

    If the conditions are met, then the Ignore option became available. If you chose to Ignore, then the kernel did the following:

    • If the faulting instruction is a selector load instruction, the destination selector register is set to zero.
    • If the faulting instruction is a pop instruction, the stack pointer is incremented by two.
    • The instruction pointer is advanced over the faulting instruction.
    • Execution is resumed.

    In other words, the kernel did the assembly language equivalent of ON ERROR RESUME NEXT.

    Now, your reaction to this might be, "How could this possibly work? You are just randomly ignoring instructions!" But the strange thing is, this idea was so crazy it actually worked, or at least worked a lot of the time. You might have to hit Ignore a dozen times, but there's a good chance that eventually the bad values in the registers will get overwritten by good values (and it probably won't take long because the 8086 has so few registers), and the program will continue seemingly-normally.

    Totally crazy.

    Exercise: Why didn't the code have to know how to ignore jump instructions and conditional jump instructions?

    Bonus trivia: The developer who implemented this crazy feature was Don Corbitt, the same developer who wrote Dr. Watson.

  • The Old New Thing

    Why do I get ERROR_INVALID_HANDLE from GetModuleFileNameEx when I know the process handle is valid?


    Consider the following program:

    #define UNICODE
    #define _UNICODE
    #include <windows.h>
    #include <psapi.h>
    #include <stdio.h> // horrors! mixing C and C++!
    int __cdecl wmain(int, wchar_t **)
     STARTUPINFO si = { sizeof(si) };
     wchar_t szBuf[MAX_PATH] = L"C:\\Windows\\System32\\notepad.exe";
     if (CreateProcess(szBuf, szBuf, NULL, NULL, FALSE,
                       NULL, NULL, &si, &pi)) {
      if (GetModuleFileNameEx(pi.hProcess, NULL, szBuf, ARRAYSIZE(szBuf))) {
       wprintf(L"Executable is %ls\n", szBuf);
      } else {
       wprintf(L"Failed to get module file name: %d\n", GetLastError());
      TerminateProcess(pi.hProcess, 0);
     } else {
      wprintf(L"Failed to create process: %d\n", GetLastError());
     return 0;

    This program prints

    Failed to get module file name: 6

    and error 6 is ERROR_INVALID_HANDLE. "How can the process handle be invalid? I just created the process!"

    Oh, the process handle is valid. The handle that isn't valid is the NULL.

    "But the documentation says that NULL is a valid value for the second parameter. It retrieves the path to the executable."

    In Windows, processes are initialized in-process. (In other words, processes are self-initializing.) The Create­Process function creates a process object, sets the initial state of that object, copies some information into the address space of the new process (like the command line parameters), and sets the instruction pointer to the process startup code inside ntdll.dll. From there, the startup code in ntdll.dll pulls the process up by its bootstraps. It creates the default heap. It loads the primary executable and the associated bookkeeping that says "Here is the module information for the primary executable, in case anybody asks." It identifies all the DLLs referenced by the primary executable, the DLLs referenced by those DLLs, and so on. It loads each of the DLLs in turn, creating the module information that says "Here is another module that this process loaded, in case anybody asks," and then it initializes the DLLs in the proper order. Once all the process bootstrapping is complete, ntdll.dll calls the executable entry point, and the program takes control.

    An interesting take-away from this is that modules are a user-mode concept. Kernel mode does not know about modules. All kernel mode sees is that somebody in user mode asked to map sections of a file into memory.

    Okay, so if the process is responsible for managing its modules, how do functions like Get­Module­File­Name­Ex work? They issue a bunch of Read­Process­Memory calls and manually parse the in-memory data structures of another process. Normally, this would be considered "undocumented reliance on internal data structures that can change at any time," and in fact those data structures do change quite often. But it's okay because the people who maintain the module loader (and therefore would be the ones who change the data structures) are also the people who maintain Get­Module­File­Name­Ex (so they know to update the parser to match the new data structures).

    With this background information, let's go back to the original question. Why is Get­Module­File­Name­Ex failing with ERROR_INVALID_HANDLE?

    Observe that the process was created suspended. This means that the process object has been created, the initialization parameters have been injected into the new process's address space, but no code in the process has run yet. In particular, the startup code inside ntdll.dll hasn't run. This means that the code to add a module information entry for the main executable hasn't run.

    Now we can connect the dots. Since the module information entry for the main executable hasn't been added to the module table, the call to Get­Module­File­Name­Ex is going to try to parse the module table from the suspended Notepad process, and it will see that the table is empty. Actually, it's worse than that. The module table hasn't been created yet. The function then reports, "There is no module table entry for NULL," and it tells you that the handle NULL is invalid.

    Functions like Get­Module­File­Name­Ex and Create­Tool­help­32­Snapshot are designed for diagnostic or debugging tools. There are naturally race conditions involved, because the process you are inspecting is certainly free to load or unload a module immediately after the call returns, at which point your information may be out of date. What's worse, the process you are inspecting may be in the middle of updating its module table, in which case the call may simply fail with a strange error like ERROR_PARTIAL_COPY. (Protecting the data structures with a critical section isn't good enough because critical sections do not cross processes, and the process doing the inspecting is going to be using Read­Process­Memory, which doesn't care about critical sections.)

    In the particular example above, the code could avoid the problem by using the Query­Full­Process­Image­Name function to get the path to the executable.

    Bonus chatter: The Create­Tool­help­32­Snapshot function extracts the information in a different way from Get­Module­File­Name­Ex. Rather than trying to parse the information via Read­Process­Memory, it injects a thread into the target process and runs code to extract the information from within the process, and then marshals the results back. I'm not sure whether this is more crazy than using Read­Process­Memory or less crazy.

    Second bonus chatter: A colleague of mine chose to describe this situation more directly. "Let's cut to the heart of the matter. These APIs don't really work by the normally-accepted definitions of 'work'." These snooping-around functions are best-effort, so use them in situations where best-effort is better than nothing. For example, if you have a diagnostic tool, you're probably happy that it gets information at all, even if it may sometimes be incomplete. (Debuggers don't use any of these APIs. Debuggers receive special events to notify them of modules as they are loaded and unloaded, and those notifications are generated by the loader itself, so they are reliable.)

    Exercise: Diagnose this customer's problem: "If we launch a process suspended, the Get­Module­Information function fails with ERROR_INVALID_HANDLE."

    #include <windows.h>
    #include <psapi.h>
    #include <iostream>
    int __cdecl wmain(int, wchar_t **)
     STARTUPINFO si = { sizeof(si) };
     wchar_t szBuf[MAX_PATH] = L"C:\\Windows\\System32\\notepad.exe";
     if (CreateProcess(szBuf, szBuf, NULL, NULL, FALSE,
                       NULL, NULL, &si, &pi)) {
      DWORD addr;
      std::cin >> std::hex >> addr;
      MODULEINFO mi;
      if (GetModuleInformation(pi.hProcess, (HINSTANCE)addr,
                               &mi, sizeof(mi))) {
       wprintf(L"Got the module information\n");
      } else {
       wprintf(L"Failed to get module information: %d\n", GetLastError());
      TerminateProcess(hProcess, 0);
     } else {
      wprintf(L"Failed to create process: %d\n", GetLastError());
     return 0;

    Run Process Explorer, then run this program. When the program asks for an address, enter the address that Process Explorer reports for the base address of the module.

  • The Old New Thing

    Why doesn't the Print command appear when I select 20 files and right-click?

    This is explained in the MSDN documentation:

    When the number of items selected does not match the verb selection model or is greater than the default limits outlined in the following table, the verb fails to appear.

    Type of verb implementation Document Player
    Legacy 15 items 100 items
    COM 15 items No limit

    The problem here is that users will select a large number of files, then accidentally Print all of them. This fires up 100 copies of Notepad or Photoshop or whatever, and all of them start racing to the printer, and most of the time, the user is frantically trying to close 100 windows to stop the documents from printing, which is a problem because 100 new processes is putting a heavy load on the system, so it's slow to respond to all the frantic clicks, and even if the click manages to make it to the printing application, the application is running so slowly due to disk I/O contention that it takes a long time for it to respond to the click anyway.

    In panic, the user pulls the plug to the computer.

    The limit of 15 documents for legacy verbs tries to limit the scope of the damage. You will get at most 15 new processes starting at once, which is still a lot, but is significantly more manageable than 100 processes.

    Player verbs and COM-based verbs have higher limits because they are typically all handled by a single program, so there's only one program that you need to close. (Although there is one popular player that still runs a separate process for each media file, so if you select 1000 music files, right-click, and select "Add to playlist", it runs 1000 copies of the program, which basically turns your computer into a space heater. An arbitrary limit of 100 was chosen to keep the damage under control.)

    If you want to raise the 15-document limit, you can adjust the Multiple­Invoke­Prompt­Minimum setting. Note that this setting is not contractual, so don't get too attached to it.

  • The Old New Thing

    Hazy memories of the Windows 95 ship party


    One of the moments from the Windows 95 ship party (20 years ago today) was when one of the team members drove his motorcycle through the halls, leaving burns in the carpet.

    The funny part of that story (beyond the fact that it happened) is that nobody can agree on who it was! I seem to recall that it was Todd, but another of my colleagues remembers that it was Dave, and yet another remembers that it was Ed. We all remember the carpet burns, but we all blame it on different people.

    As one of my colleagues noted, "I'm glad all of this happened before YouTube."

    Brad Silverberg, the vice president of the Personal Systems Division (as it was then known), recalled that "I had a lot of apologizing to do to Facilities [about all the shenanigans that took place that day], but it was worth it."

Page 1 of 455 (4,542 items) 12345»