标签:
from : http://goooder.bokee.com/2000373.html (雷立辉 整理)
简介:本文对如何将32位Windows程序平滑的支持和过渡到64位Windows操作系统做出了一个简单而系统的介绍。包括对于64位操作系统的版本,编程模型,一些移植原则甚至包括对驱动程序的移植原则的介绍。
作者介绍:系统分析员,现在在北京工作。作者的blog:http://goooder.blogchina.com.
前言:
或许大家还对32位的CPU及操作系统使用还是觉得非常的合乎日常需要,但Windows 64位已经悄悄的来到了各位的身边。不少软件厂商也纷纷宣称自己的软件已经支持64位操作系统了。
笔者在几个月之前就成功的实施了一个软件的64位操作系统移植。大致方案是,上层应用程序仍然使用32位程序,但将该系统软件的驱动程序统统的变成了64位。这也是让应用程序支持64位的最小代价。整个过程前后不到一个月(两个人月),就解决了大部分问题。而一些难啃的骨头都是因为以前的编码不太规范而引起的。因为这不是本文的主题,所以就此打住。
下面的内容就是我在工作过程中整理各种资料所得到的成果。为了文章的系统性,中间转贴了一些MSDN中文网站的内容。希望能对大家的工作有所帮助。
一.最有可能受益于64位的应用
l 需要大量的可寻址内存,因而系统总体内存需求超过4GB 的应用。例如那些采用大型数据集的应用(金融和科学建模软件)和基于主机的桌面应用(在不降低性能的情况下同时运行多个线程);
l 必须同时管理大量的用户或者应用线程,例如大规模的瘦客户端解决方案,大型数据库,以及用于客户关系管理(CRM)、供应链管理(SCM)、企业资源规划(ERP)和数字版权管理(DRM)系统中的解决方案的数据仓库应用;
l 需要通过实时加密和解密提高安全性的应用,包括电子商务应用和对专用或者分类数据的保护;
l 需要数学精度和浮点性能的应用,包括建模、模拟、统计和金融分析、图像/视频/信号处理、物理、医学研究、远程通信、加密和压缩;
l 需要大规模的、强大的数据库性能的应用,包括决策支持,搜索和索引,文档和内容管理,以及语音识别;
l 需要64 位计算的大内存寻址功能的应用,包括很多高性能计算(HPC)群集应用;
l 需要提供数字内容创建功能,例如计算机辅助设计、制造和工程(CAD、CAM 和CAE)、数字音乐制作和视频编辑,以及实时媒体流解决方案;
l 需要通过最大限度的性能实现逼真的影院级消费者体验,包括计算机游戏、数字视频和协作;
l 需要将以前只限于64 位工作站的功能移植到企业、消费者和计算机爱好者的台式机中,包括3D 建模、渲染、动画、模拟和软件开发。
二.Windows XP的64位版分类
微软在2003年3月28日发布了64位的Windows XP。64位的Windows XP称Windows XP 64-Bit Edition。其实就是64位版本的Windows XP Professional。根据不同的微处理器架构,它分为两个不同版本:
1.IA-64版的Windows XP
针对英特尔(Intel)的IA-64架构的安腾2(Itanium2)纯64位微处理器的Windows XP 64-Bit Edition Version 2003 for Itanium-based Systems。它是拥有64位寻址能力的强大的操作系统,主要面向顶级的高端IA-64架构的工作站,用在高端的科学运算,石油探测工艺,立体绘图,复杂的动画制作等等,是一种用在高效能运算(High Performance Computing)的强大的操作系统。估计它可能会改名为Windows XP Professional Itanium-based Edition。支持双处理器;最低支持1GB的内存,最高支持16GB的内存。
2.x64版的Windows XP
针对超微(AMD)的x64架构的皓龙(Opteron)与速龙64(Athlon64)所属的64位扩展微处理器的Windows XP 64-Bit Edition for 64-Bit Extended Systems。由于英特尔也发布了x64架构的Intel EM64T技术的至强(Xeon)与奔腾4(Pentium 4)的64位扩展微处理器,故微软将该版本的的Windows XP 64-Bit Edition改为Windows XP Professional x64 Edition,它支持AMD与Intel的x64架构。可以使用在一般x64架构的工作站,桌面电脑以及笔记本电脑,用途与32位Windows XP Professional一样,但具有64位寻址能力。支持双处理器;最低支持256MB的内存,最高支持16GB的内存。
Windows XP Professional x64 Edition与32位版本很相像
三.x64版的Windows操作系统的设计目标
x64版有5大特征,即:
1. 同时轻松支持32位Win32程序及64位程序;
2. 在64位运行的程序代码和32位运行的程序应该是同一份代码;
3. 使现有程序具有企业级应用性能;(Enable existing applications to scale to enterprise capacities)
4. 支持新的设计使之可以利用巨大地址空间及内存空间;
5. 支持32位既有程序。
x64 位平台并没有出现 Win64 API,它仍然是熟悉的 Win32 API(现在更合适的名称为 Windows API)。但它出现了一些新的兼容 64 位的数据类型,所以可能需要对代码进行少量的更改。这就意味着开发者可以从单个代码库构建代码的 32 位和 64 位版本,减少了由于维护两个代码库所带来的维护开销。
但是,在x64中,Microsoft 已经去除了一些旧的组件,如 Win16 子系统。所以Windows 64 位代码不支持16 位的Windows程序。也不支持 POSIX 和 OS/2 子系统。又出现了一个新的子系统,称为 WOW64。
四.x64新出现的子系统―WOW64
WOW64 是 Windows-32-on-Windows-64 的缩写。它为现有的 32 位应用程序提供了 32 位的模拟环境,可以使大多数 32 位应用程序在无需修改而直接运行在 Windows 64 位版本上。它类似于旧的 WOW32 子系统,负责在 Windows 32 位版本下运行 16 位的代码。
尽管x64 CPU本身具有 32 位兼容性模式,可以处理 IA-32 指令的实际执行,但WOW 层仍然必不可少。WOW子系统负责诸如在32位和64位模式之间进程切换以及模拟32位系统的服务。例如,32位和64 位程序具有不同的注册表配置单元,还有一个用于 32 位二进制文件的不同的系统目录,而且64位二进制文件仍然使用 System32 目录。因此,当 32 位应用程序安装到系统中时,WOW层会确保将32位二进制文件置于一个新的目录 SysWOW64中。这是通过如下方式实现的:根据应用程序是否运行在 WOW 下,截获对 API 的调用(如 GetSystemDirectory)并返回适当的目录。类似的问题可能会存在于注册表中。因为 32 位和 64 位的 COM 服务器都可以安装在系统上,并位于相同的类标识符 (CLSID) 下,因此 WOW 层需要将对注册表的调用重定向到适当的32位或64位配置单元中。WOW 层也会处理注册表中某些区域之间的镜像更改,以便使其更简单地支持32位和64位代码之间的交互操作。
WOW64 非常重要,因为当不关注性能和可伸缩性的问题时,它使开发者可以利用大多数现有的32位代码。它是两种方法的最佳结合。开发者可以将应用程序服务迁移到64位,同时将 Microsoft 管理控制台 (MMC) 配置管理单元保留为32位。Windows 64 位版本包括 MMC的32位和64位的版本。当选择保留管理工具为32位时,进程间的通讯可能会遇到某些问题,但是只要接口设计正确,诸如远程过程调用 (RPC) 的协议应该可以在32位和64位进程之间运行。有关 WOW64 的另外一点需要牢记:它并不是为要求高性能的应用程序而设计的。至少,WOW64子系统需要将32位参数扩展到64位,并且需要将64位的返回值截断为32位。在最糟糕的情况下,WOW64 子系统将需要进行内核调用,涉及到的不仅仅是到内核的转换,还有从处理器的32位兼容性模式到其本机64位模式的转换。在 WOW64 下运行时,应用程序将无法妥当地进行调整。对于那些要将其保留为32位的应用程序而言,必须在WOW64下测试它们。如果性能不能满足期望,则需要考虑将应用程序迁移到64位。
WOW64是在用户模式下实现的,作为ntdll.dll和内核之间的层。WOW64及其支持的一些 DLL仅仅是可以加载到32位进程中的64位的 DLL。对于所有其他情况,进程保持为纯进程。32位的进程无法加载64位的DLL,反之亦然。所以,请检查代码中的所有“LoadLibrary”调用是否有效。
有关 WOW64 的详细信息,请参阅 Microsoft_ Platform SDK 中的“64-bit Windows Programming - Running 32-bit Applications”。
五.64位windows内存地址空间映射
默认情况下,Windows 32位版本的地址空间限制在4GB,其中一半是为内核保留的。这限制了普通的应用程序只能使用2GB的有效虚拟内存。2 GB看起来好像很多,但是由于错误的分配算法、大型文件映射甚至过多的使用 DLL,地址空间很容易在应用程序中变得零碎。看一下任务管理器中的“VM Size”列,就会发现普通应用程序消耗的虚拟内存量。当然,就想过去的 DOS 时期(利用 XMS/EMS)一样,有很多种方法可以使 32 位的应用程序访问多于 4 GB 的物理内存。进入物理地址扩展 (PAE) 和地址窗口扩展(Address Windowing Extensions,AWE)。PAE 通过将地址位的数量从 32 扩展到 36 来工作,这样使应用程序可以寻址的空间达到 64 GB。AWE 使应用程序可以将大于 4 GB 的物理内存范围映射到虚拟地址空间中。这两种方法都引入了开销并增加了代码的复杂性。
Windows 64 位版本提供 16 TB 的有效寻址空间,其中一半可用于用户模式的应用程序。这意味着整个数据库可以移动到内存中,显著地提高了性能,或者整个网站可以缓存到内存中。它还可以使代码保留并委托到巨型的邻近虚拟内存块中,无需实际地担心虚拟内存碎片问题。这也考虑到了巨型文件映射对象或共享的内存部分。
下面是64 位体系结构和 32 位体系结构的比较表格:
下面是Windows系列内存地址空间分配比较:
...
六.Windows 64 位版本的 C/C++ 编程
1.64位编码指导原则
Windows 64 位版本使用 LLP64 数据模型。这意味着标准 C 类型 int 和 long 保持为 32 位整数。数据类型 size_t 将映射到处理器词大小(IA32 为 32 位,IA64 为 64 位),并且 __int64 是 64 位整数。在协助迁移 32 位代码时就会完成上述操作。意义在于您可以对应用程序的 32 位版本和 64 版本使用相同的代码库。
还有一个称为 LP64 的数据模型,它将标准的 C 类型 long 映射到 64 位整数,并使 int 保持为 32 位的整数。这种数据模型常见于 Unix 平台,但从单个代码库同时创建应用程序的 32 位和 64 位版本时可能有一些困难。您可能注意到了此处的常见主题。32 位平台与 64 位平台的思想就是应该能够从单个代码库中构建两个版本的应用程序。如果无法做到,那么您可能要重新审视您的设计。具有单个代码库就是巨大的胜利,尤其是如果您计划发行两个版本。
综合以上考虑,建议如下:
使用Windows64位或32位安全数据类型;
检查所有的指针运算及使用指针的地方;
改写所有嵌入的汇编代码;使用intrinsics或者native assembly code;
对于X64相关代码使用 #if defined (_AMD64__) 的预编译命令(没有定义__IA64__宏);
对于IA64相关的代码使用#if defined (__IA64__)命令;
编译x64程序使用AMD64处理器;
用 Visual C++ 创建在 64 位 Windows 操作系统中运行的应用程序时,应注意以下问题:
在 64 位 Windows 操作系统中,int 和 long 是 32 位值。
在 64 位 Windows 操作系统中,size_t、time_t 和 ptrdiff_t 是 64 位值。
在 32 位 Windows 操作系统中,time_t 是 32 位值。
应注意代码在哪里采用 int 值和将其作为 size_t 值或 time_t 值处理。数字有可能增长得比 32 位数大,并且数据在被传递回 int 存储时有可能被截断。
%x(十六进制 int 格式)printf 修饰符在 64 位 Windows 操作系统中不按预期的那样工作;它只对传递给它的值的前 32 位进行操作。
Windows 32 位操作系统使用 %I32x 显示整数。
Windows 64 位操作系统使用 %I64x 显示整数。
%p(指针的十六进制格式)在 64 位 Windows 操作系统中按预期的那样工作。
2./WP64:使编译器警告您潜在的问题
Microsoft_ Visual C 和 Microsoft_ Visual C++_ .NET 2002 编译器添加了 /WP64 开关,这使您可以测试 32 位代码的 64 位兼容性问题。编译器将发出有关指针截断和不正确转换的警告。将 32 位应用程序迁移到 Windows 64 位版本中前面的一个步骤就是打开这个标记,然后就像通常编译代码那样来编译您的代码。第一次会有几个错误。例如,请看下面这个代码片段:
DWORD i = 0;
size_t x = 100;
i = x; // C4267: warning C4267: ‘=‘ : conversion from
// ‘size_t‘ to ‘DWORD‘, possible loss of data.
在 32 位的平台上,这段代码能够很好的进行编译,因为 size_t 是 32 位的,但是在 64 位的平台上,size_t 就是 64 位的整数。启用 /WP64 后,编译器将会警告您类似的情况。
其他示例:
void func(DWORD context)
{
char* sz = (char*)context; // C4312: warning C4312:
// ‘type cast‘ : conversion
// from ‘DWORD‘ to ‘char *‘ of
// greater size
// Do something with sz..
}
char* string = "the quick brown fox jumped over the lazy dog.";
func((DWORD)string); // C4311: warning C4311: ‘type cast‘ :
// pointer truncation from ‘char *‘
// to ‘DWORD‘
在修复这些错误后,请测试您的 32 位代码。您希望确保 32 位的代码继续按预期那样工作。32 位和 64 位二进制文件应该从相同的代码库中构建。这就是编写不断前进的 Windows 应用程序的关键概念。开始时,您需要考虑 32 位和 64 位的问题,并且为应用程序编写可以运行在这两个平台上的代码。
3.多态类型
由于 Win32 API 是针对 C 的,在很多情况下,您都需要将整数转换成指针或者相反。在 32 位的硬件上不会有问题,其中指针的大小和整数的大小是相同的,但在 64 位的硬件上却完全不一样。这就是多态类型出现的原因。
对于特定的精度,您可以使用固定精度的数据类型。不管处理器的词大小如何,它们的大小都是一致的。大多数这些类型都在它们的名称中包含精度,可以从下面的表中看出:
此外,当您需要数据类型的精度随着处理器词大小变化时,请使用指针精度数据类型。这些类型又称为“多态”数据类型。这些类型通常以 _PTR 后缀结尾,如下面的表格所示:
通过整数参数传递参数或上下文信息的所有 Win32 API 都更改为使用这些新的类型。SetWindowLong 和 SetWindowLongPtr 函数都是很好的示例:
旧方法:
LONG SetWindowLong(
HWND hWnd,
int nIndex,
LONG dwNewLong);
新的多态方法:
LONG_PTR SetWindowLongPtr(
HWND hWnd,
int nIndex,
LONG_PTR dwNewLong);
请注意,该函数的 xxxPtr 版本使用新的多态类型。对于开发人员而言,通过在窗口的额外数据区域中存储指针来存储窗口的上下文信息是相当常见的。使用 SetWindowLong 函数在 Windows 32 位版本上存储指针的任何代码必须更改为调用 SetWindowLongPtr。该更改非常简单并且很快就可以完成,因为大多数更改要求使用多态类型。
另外,WindowProc 和 GetQueuedCompletionStatus 也是很好的示例:
LRESULT CALLBACK WindowProc(
HWND hWnd,
UINT uiMsg,
WPARAM wParam,
LPARAM lParam);
BOOL GetQueuedCompletionStatus(
HANDLE hCompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED* lpOverlapped,
DWORD dwMilliseconds);
WindowProc 使用 LPARAM,后者是多态类型。GetQueuedCompletionStatus 使用 ULONG_PTR,后者也是多态类型。这使那些假设整数的大小与指针大小相同的现有代码可以在进行很少修改的情况下继续工作。
Microsoft_ Visual Studio_ .NET 2002 的编译器包含两个新的优化模式:Link Time Code Generation(LTCG,又称 Whole Program Optimization)和 Profile Guided Optimization (PoGO)。代码优化在 Itanium 处理器上比在 x86 平台上更为重要,因为编译器对生产高效代码负有全部责任。对于编译器方面的知识,请参考MSDN中文网站http://www.microsoft.com/china/MSDN/library/windev/Windows2003/NFdnnetservws0364bitdev.mspx来获得更多此方面的内容。
七.将驱动移植到64位Windows操作系统
x64位操作系统和x32位操作系统的最大区别就是内存寻址方式的不同。而64位操作系统不支持32位的驱动程序,因为驱动程序和windows内核同处于一个地址空间中。这是移植32位驱动到64位驱动的最大原因。当然,64位驱动程序可以使用更大的分页内存,非分页内存及系统缓存。而且,你的设备从此就支持64位windows操作系统了。
1.在X64下的驱动程序安装
除了要把应用程序的32位驱动程序变成64位程序之外,驱动的安装程序和其它配置文件同样需要修改。也就是说,对于要在x64上运行的32位程序,它所依赖的驱动仍然需要是64位的。这些相关程序包括inf文件,device installers, class installers和co-installers。相关资料可查看MSDN Libarary DDK:Porting Your Driver to 64-Bit Windows。
所以,要改造应用程序的安装程序。方法是,让32位版的驱动安装为缺省安装选项,即用户插入安装光盘之后,依然运行32位安装程序。但当程序调用UpdateDriverForPlugAndPlayDevices返回值为ERROR_IN_WOW64时,这说明该安装程序正运行在64位Windows环境中。此时,这个安装程序应该调用CreateProcess函数来启动64位的安装进程。这个64位的安装进程通过调用64位驱动目录下的inf文件进行驱动安装。
2.驱动要支持32位IOCTL
某些IOCTL可能包含含有指针的结构,所以,要特别小心的区别对待它,必须根据被调用者解析结构或者输出结构。
有三种办法可以解决这个问题:
1. 尽量避免使用IOCTL传递包含有指针的结构;
2. 通过API IoIs32bitProcess()来判断上层调用者的程序类型;
3. 在64位程序中采用新的IOCTL命令;
例子:
IOCTL structure in header file
typedef struct _IOCTL_PARAMETERS {
PVOID Addr;
SIZE_T Length;
HANDLE Handle;
} IOCTL_PARAMETERS, *PIOCTL_PARAMETERS;
32-bit IOCTL structure
//
// This structure is defined
// inside the driver source code
//
typedef struct _IOCTL_PARAMETERS_32 {
VOID*POINTER_32 Addr;
INT32 Length;
VOID*POINTER_32 Handle;
} IOCTL_PARAMETERS_32, *PIOCTL_PARAMETERS_32;
32-Bit and 64-Bit IOCTL
#ifdef _WIN64
case IOCTL_REGISTER:
if (IoIs32bitProcess(Irp)) {
/* If this is a 32 bit process */
params32 = (PIOCTL_PARAMETERS_32)(Irp>AssociatedIrp.SystemBuffer);
if(irpSp->Parameters.DeviceIoControl.InputBufferLength < sizeof(IOCTL_PARAMETERS_32)) {
status = STATUS_INVALID_PARAMETER;
} else {
LocalParam.Addr = params32->Addr;
LocalParam.Handle = params32->Handle;
LocalParam.Length = params32->Length;
/* Handle the ioctl here */
status = STATUS_SUCCESS;
Irp->IoStatus.Information = sizeof(IOCTL_PARAMETERS);
}
} else { /* 64bit process IOCTL */
} else { /* 64bit process IOCTL */
params = (PIOCTL_PARAMETERS)
(Irp->AssociatedIrp.SystemBuffer);
if (irpSp->Parameters.DeviceIoControl.InputBufferLength
< sizeof(IOCTL_PARAMETERS)) {
status = STATUS_INVALID_PARAMETER;
} else {
RtlCopyMemory(&LocalParam, params,
sizeof(IOCTL_PARAMETERS));
/* Handle the ioctl here */
status = STATUS_SUCCESS;
}
Irp->IoStatus.Information = sizeof(IOCTL_PARAMETERS);
}
break;
3.64-Bit INF 文件要求
在Windows Server 2003SP1之后,64位驱动的安装被提高了要求。这可以简化用户的操作及提高安全性。
Inf文件中必须含有NTAmd64或者NTIA64之类的修饰符才行。具体做法是在[Manufacturer]和Models小节都需要添加此类的字段。
[Manufacturer]
%mycompany% = MyCompanyModels
[MyCompanyModels]
%MyDev% = mydevInstall,mydevHwid
[Manufacturer]
%mycompany% = MyCompanyModels,NTx86,NTAmd64
[MyCompanyModels.NTx86]
%MyDev% = mydevInstallx86,mydevHwid
[MyCompanyModels. NTAmd64]
%MyDev% = mydevInstallAmd64,mydevHwid
如果只需要在WindowsX64系统上安装,则只需要使用NTAmd64修饰符就可以了。更多请参考http://www.microsoft.com/whdc/driver/install/64INF_reqs.mspx。
4.编程中容易碰到的问题
1)指针的相关问题
如果原有项目的编程风格控制不严,指针类型混用,强制转换使用过多等等可能对移植是一个巨大的考验。另外,程序中存在结构之中根据具体数据类型来计算其它变量的位置此类的代码也需要重新检查。
使用指针的原则如下:
1. 不要将指针强制转换为int, long, ULONG, DWORD等类型,而应该使用UINT_PTR和INT_PTR;
2. 使用PtrToUlong()和PtrToLong()来截断指针;
3. 永远不要将已经截断的存贮在int或者ULONG中指针地址的重新合成一个新的指针地址;
4. 小心的计算缓冲区的大小,说不定缓冲区的长度比ULONG所能存储的最大数都大!
5. 小心的调用那些传出指针的函数;
对4可以举个例子:比如说有两个地址ptr2(高地址), ptr1(低地址),则len = ptr2 – ptr1 将有可能大于2的32次方。
2)结构的内存排列问题
在64位的操作系统上,结构的内存排列(structure alignment)也需要小心审查。内存排列的齐整有利于处理器的执行效率。如果打开了一些编译选项,为了对齐内存地址,编译器可能会将某些位置填空。在移植过程中,对结构中的变量顺序需要仔细检查,特别是在同一个头文件中使用不同的pack选项。比如下面的代码:
#pragma pack (1) /* 也可以使用编译选项/Zp(结构成员对齐)*/
struct AlignSample {
ULONG size;
void *ptr;
};
struct AlignSample s;
void foo(void *p) {
*p = p; // 将会导致访问异常
...
}
foo((PVOID)&s.ptr);
补救办法就是使用宏UNALIGNED:
void foo(void *p) {
struct AlignSample s;
*(UNALIGNED void *)&s.ptr = p;
}
当然,更好的办法就是首先将那些64位长度的数据类型变量放在结构的前端。
3)小心使用十六进制的常量,无符号整数
小心使用十六进制的常量,无符号整数等等。比如说下面的一些断言在64位系统中是错误的:
~((UINT64)(PAGE_SIZE-1)) == (UINT64)~(PAGE_SIZE-1)
PAGE_SIZE = 0x1000UL // Unsigned Long - 32 bits
PAGE_SIZE - 1 = 0x00000fff
等式左边:
// 无符号转换
(UINT64)(PAGE_SIZE -1 ) = 0x0000000000000fff
~((UINT64)(PAGE_SIZE -1 ))= 0xfffffffffffff000
等式右边:
~(PAGE_SIZE-1) = 0xfffff000
(UINT64)(~(PAGE_SIZE-1))=0x00000000fffff000
所以:
~((UINT64)(PAGE_SIZE-1))!= (UINT64)(~(PAGE_SIZE-1))
还有:
DWORD index = 0;
CHAR *p;
If (p[index – 1] == ‘0’)
上面的代码将会在64位系统上出错!因为在32位系统上
p[index-1] == p[0xffffffff] == p[-1]
这是对的。但在64位系统上:
p[index-1] == p[0x00000000ffffffff] != p[-1]
再如:
-1 != 0xFFFFFFFF
0xFFFFFFFF != invalid handle
DWORD总是32位,所以要查找原有程序中所有用DWORD存贮指针的代码。另外,别忘了使用 %I来打印指针地址,而且大于0x80000000的也未必是内核态地址了。
5.开发64位驱动的工具
64位的开发工具和32位的差不多,除了必须的Windows DDK 2003之外,Windbg,Driver Verifier等都是拿手的好工具。最后,下载一份最新的WHQL测试包进行WHQL测试也是值得推荐的。
目前,学会使用Windbg来调试64位用户态和核心态程序的方法是尤为必要的。也可以购买支持64位的VC环境Visual Stdio 2005来进行64位程序开发。
标签:
原文地址:http://www.cnblogs.com/crazii/p/4512760.html