Web编程技术营地
研究、演示、创新

数值的表示与转化

撰写时间:2025-02-17

修订时间:2025-02-17

负数在内存中的表示

正数与负数的不同表示

const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (i32.store (i32.const 0) (i32.const 30)) (i32.store (i32.const 4) (i32.const -30)) ) (start $store) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 8); let str = getHexAddrStrFromTypedArray(tarr, 4); pc.log('%s', str);

使用i32.store指令在内存中分别存储了30-30两个值。它们均各自占用4个字节的内存空间。

数值30在内存中相应4个字节分别为1E 00 00 00,而数值-30在内存中相应4个字节分别为E2 FF FF FF

两相比较,除高位部分分别为0x000xFF不一样之外,正数30最低字节的值0x1E不等于负数-30最低字节的值0xE2。这是因为计算机系统采取了补码two's complement)的方式来表示一个整数。

补码原理

单字节负数的补码

对于数值-30,分4步求出其补码。

第一步,取数值-30的绝对值30的二进制原码sign-magnitude)。

const { getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 30; pc.log('%d, %s, %s', num, getHexStr(num), getBinStr(num, 4));

第二步,求出对应负数的原码。即,将其符号位置为1,以表示负数。

则上面的二进制变为:

const { getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 0b1001_1110; pc.log('%s', getBinStr(num, 4));

第三步,求出负数的反码one's complement)。即,保留符号位,其余位取反。

则上面的二进制变为:

const { getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 0b1110_0001; pc.log('%s', getBinStr(num, 4));

第四步,求出负数的补码two's complement)。即,将反码的值加1

则最终结果的二进制及其十六进制分别为:

const { getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 0b1110_0010; pc.log('%s, %s', getBinStr(num, 4), getHexStr(num));

因此,单字节的十进制数值-30,其十六进制的值为0xE2

4字节负数的补码

当数值-30需要使用4字节来存储时,求其补码的方式与步骤与上节一样,但需将4个字节的内存区域纳入参与转换运算的范围。

第一步,正数的原码:

const { getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 30; pc.log('%d, %s, %s', num, getHexStr(num), getBinStr(num, 8, 32));

第二步,负数的原码:

const { getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 0B10000000_00000000_00000000_00011110; pc.log('%s', getBinStr(num, 8, 32));

第三步,负数的反码:

const { getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 0B11111111_11111111_11111111_11100001; pc.log('%s', getBinStr(num, 8, 32));

第四步,负数的补码:

const { getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 0B11111111_11111111_11111111_11100010; pc.log('%s, %s', getBinStr(num, 8, 32), getHexStr(num));

因此,4字节的十进制数值-30,其十六进制的值为0xFFFFFFE2

非负数的补码

0与正数也同样存在补码,但从概念上定义了:对于0与正数,它们的反码及补码均等于其原码,因此它们的补码不存在像负数一样的转换过程,直接取其原码值即可。

补码的作用

补码是计算机表示有符号整数的标准方式,主要目的是统一加减法运算,避免硬件额外区分正负数。

计算机使用补码的好处:

  1. 运算一致性。无论操作数是正还是负,加减法可直接使用同一套逻辑电路,简化了硬件设计。
  2. 消除冗余。补码解决了原码和反码中 +0 和 -0 表示不唯一的问题(补码中 0 只有 0 一种形式)。
  3. 兼容性。现代CPU的指令集和硬件设计均基于补码,因此所有整数(包括正数)均以补码形式存储。

负数存储的结构特点

综上,一个负数在内存中存储,其结构特点如下::

  1. 必须有明确、足够的位域来存储一个负数。
  2. 在内存中,除表示特定数值的字节(数值位域)之外,其余高位字节全部为0xFF(符号位域)。
  3. 在按小尾存储的系统中,其数值位域位于内存的最低地址。

负数的加载

因为负数在内存中存储的结构特点,当从内存加载特定字节,以得到其原有的带符号的数值时,应同时考虑原来存储该数值时的所有位域,及其存储数值时原来的偏移值。

正确的负数加载

下面是正确的加载方式。

const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (i32.store (i32.const 0) (i32.const -30)) ) (func (export "load3") (param $idx i32) (result i32 i32 i32) (i32.load8_s (local.get $idx)) (i32.load16_s (local.get $idx)) (i32.load (local.get $idx)) ) (start $store) ) `; const { memory, load3} = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 8); let str = getHexAddrStrFromTypedArray(tarr); pc.log('%s', str); let [num1, num2, num3] = load3(0); pc.log('%d, %s, %s', num1, getHexStr(num1), getBinStr(num1)); pc.log('%d, %s, %s', num2, getHexStr(num2), getBinStr(num2)); pc.log('%d, %s, %s', num3, getHexStr(num3), getBinStr(num3));

上面代码,对于一个32位的负数,分别调用i32load8_s, load16_s,及load指令,均正确地读出其原来数值-30

load8_s的内部细节

从第一节我们知道,30-30这两个值在32位域中如何表示。这里再次使用该节代码:

const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (i32.store (i32.const 0) (i32.const 30)) (i32.store (i32.const 4) (i32.const -30)) ) (start $store) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 8); let str = getHexAddrStrFromTypedArray(tarr, 4); pc.log('%s', str);

30的第8位至第31位,即高24位的值均为0;而-30的第8位至第31位,即高24位的值均为1

现在,只看值为-30的情况:

const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (i32.store (i32.const 0) (i32.const -30)) ) (start $store) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 4); let str = getHexAddrStrFromTypedArray(tarr, 4); pc.log('%s', str);

我们准备只取出第0个字节值为0xE2的数值。其二进制值如下:

const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let num = 0xE2; pc.log('%s', getBinStr(num, 4));

实际上,226-30,这2个十进制数,它们的二进制数值的最低权重的字节的值都等于1110_0010

const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (i32.store (i32.const 0) (i32.const 226)) (i32.store (i32.const 4) (i32.const -30)) ) (start $store) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 8); let str = getHexAddrStrFromTypedArray(tarr, 4); pc.log('%s', str); pc.log('%s', getBinStr(226, 4));

可见,对于一个在内存中存储的位长为1字节、值为1110_0010的二进制数,即可表示一个无符号的十进制数值226,也可表示一个有符号的且为负的十进制数值-30。当读取此二进制数值时,依赖于我们准备如何解释这个二进制数值。

如果我们要将此解读为无符号数值,则直接转换为十进制数值。

如果我们要将此解读为有符号数值,则:

如果它是非负数,则直接转换为带符号的十进制数值。

如果它是负数,则按以下步骤进行:

  1. 将值减1。得到1110_0001
  2. 符号位不变,其余位取反。得到1001_1110
  3. 将符号位置0。得到0001_1110,即十进制数值30
  4. 在该值前面添加-号并返回。

load8_s指令先取出第0个字节的值0xE2,根据其二进制数值,取出其符号位扩展为32位:

  • 若最高位符号位为1,则高24位均填充1
  • 若最高位符号位为0,则高24位均填充0

const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (i32.store (i32.const 0) (i32.const -30)) (i32.store (i32.const 4) (i32.load8_s (i32.const 0)) ) ) (start $store) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 8); let str = getHexAddrStrFromTypedArray(tarr, 4); pc.log('%s', str);

这意味着一个原为正数的32位数值,单独取出其第0个字节的数值再扩展为32位,也有可能变成一个负数。

const { getHexAddrStrFromTypedArray, getHexStr, getBinStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (i32.store (i32.const 0) (i32.const 0xE2)) (i32.store (i32.const 4) (i32.load8_s (i32.const 0)) ) ) (func (export "load") (result i32) (i32.load (i32.const 4)) ) (start $store) ) `; const { memory, load } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 8); let str = getHexAddrStrFromTypedArray(tarr, 4); pc.log('%s', str); pc.log('mem: %s <===> %d', getHexStr(0xE2), 0xE2); let num = load(); pc.log('loaded: %s <===> %d', getHexStr(num), num);

参考资源

Core

  1. Convertions