Doubled integer precision is just a special case of arbitrary precision. At the level of assembly language, how this is implemented is dependent on the ISA. (Even though you asked specifically about x86, I will provide some additional information.)

For addition and subtraction, many ISAs (e.g., ARM, Power, x86) provide a carry (borrow for subtraction) bit and special add with carry and subtract with borrow instructions.

```
ADD r1, r2, r3; // r1 = r2 + r3, set or clear carry bit
ADDC r4, r5, r6; // r4 = r5 + r6 + carry bit (also sets/clears carry)
```

Some ISAs (e.g., Alpha [which was always 64-bit], MIPS) do not provide a carry bit and special add/subtract instructions. These may use a set-on-less-than instruction (setting a register to zero if not less than and to one if less than) to test whether the result is less than one of the operands (which determines whether the result generated a carry).

```
ADD r1, r2, r3; // r1 = r2 + r3
ADD r4, r5, r6; // r4 = r5 + r6
SLT r7, r1, r2; // r7 = (r1 < r2)? 1 : 0 (determining carry)
ADD r4, r4, r7; // r4 = r4 + r7 (adding in carry)
```

(The ISA designers who chose not to provide a carry bit did so because multiple precision operation is not common, a single carry bit introduces a data dependency that hinders superscalar operation, and special bits make register renaming for out-of-order execution more complex.)

For multiplication, doubled precision might not be enough to make sophisticated algorithms more efficient, so the straightforward method of generating partial products and summing the results might be preferred. This uses the formula (where a and c are the upper half of the doubled precision value):

```
(a + b) * (c + d) = a*c + a*d + b*c + b*d.
```

(For simplicity, the following assumes a doubled precision result with any excess being ignored. This means that the product term a*c is ignored completely.)

For an ISA (e.g., MIPS, x86) that provides both a high and a low result from a single multiply, the operation is reasonably straightforward. The following is a *rough* approximation for x86 (I am not familiar with the details):

```
MUL r2:r0, r3, r7; // multiply a*d
MOV r1, r0; // preserve low result of a*d
MUL r2:r0, r5, r6; // multiply b*c
MOV [temp], R0; // preserve low result on stack at temp
MUL r2:r0, r5, r7; // multiply b*d
ADD r2, r1; // add high b*d and low b*c for part of
// higher result
ADD r2, [temp]; // add partial higher result and low a*d
// doubled precision product is now in r2:r0
```

For an ISA (e.g., Alpha) that provides separate multiply for high result and multiple for low result instructions (multiplication naturally produces a doubled precision value), this operation is somewhat similar:

```
// a*c result is beyond doubled precision range
MULL r16, r1, r4; // low multiply result (a*d)
// high a*d result is beyond doubled precision range
MULL r17, r2, r3; // low multiply result (b*c)
// high b*c result is beyond doubled precision range
MULL r18, r2, r4; // low multiply result (b*d)
MULH r19, r2, r4; // high multiply result (b*d)
ADD r20, r19, r17; // sum high (b*d) and low (b*c)
// for part of higher result
ADD r20, r20, r16; // sum partial result and low (a*d)
// double precision result is in r20:r16
```

(Some ISAs do not provide a means of getting the higher result of a multiply. For such ISAs, the operands could be split into half-sized units and a quadruple precision multiplication performed, though there is probably a more sophisticated algorithm. ISAs with multiply add instructions can obviously merge the additions and two of the multiplies into multiply-adds.)

The above pseudo-assembly code assumes that overflow is not a concern (and is not optimized for performance). An actual implementation would handle such properly.

The GNU Multiple Precision Arithmetic Library, an open-source library for arbitrary precision arithmetic, would provide details of a real-world implementation.

### Address Space Limitations

The reason that a 32-bit processor can (in a simple sense) only use 4 GiB of memory is that it uses 32-bit pointers/addresses and use byte addressing (so 2^{32} addresses are available). Traditionally, even with virtual memory systems, supporting a physical address space (which includes memory-mapped I/O addresses, by the way) larger than 4 GiB was considered unnecessary. (A 4 GiB physical address space is also convenient for 32-bit page table entries, allowing the use of 10 bits to define validity, permissions, and other metadata when using 4 KiB pages. Using 64-bit page table entries would tend to use excess memory for small systems, and initial 32-bit systems were small.)

To extend the life of 32-bit ISAs, some ISAs (e.g., ARM, x86) added Physical Address Extensions which use a 64-bit page table entry to allow a larger physical address. This workaround is awkward, especially for the OS (which must manage all of the physical memory).

Some ISAs (like HP PA-RISC) provided a segmentation system which allowed user-level programs to address a larger address space by appending special purpose segment registers to the top of the address. (Power[PC] also uses segments, but the values cannot be modified by unprivileged [i.e., application] software.)

(In theory, an ISA could used a register pair to expand memory addressing capabilities, like some 8-bit processors. However, by that point moving to a 64-bit ISA generally makes more sense.)