`
isiqi
  • 浏览: 16489890 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

【转载】WDM驱动中使用DeviceIoControl,CreateFile

阅读更多

2009-12-11 00:33 by IamEasy_Man, 59 visits, 网摘, 收藏, 编辑

标 题: 【成果3.4】WDM驱动中使用DeviceIoControl,CreateFile
作 者: 火影
时 间: 2008-01-08,23:49
链 接: http://bbs.pediy.com/showthread.php?t=57948

同样使用网上流传的WDM驱动Demo,自己添加一些注释,好像不算是自己的成果
驱动部分:
/*************************************************************************
/*
/* This file contains the implementation for mandatory part for
/* Pseudo Driver to work with the support of I/O Control Code
/* handling.
/*
/*************************************************************************/

#include <wdm.h>
#include "DrvMain.h"
#include "..\ShareFiles\Basic\WDMDefault.h"
#include "..\ShareFiles\PnP\PnP.h"
#include "..\ShareFiles\PM\PM.h"

UNICODE_STRING Global_sz_Drv_RegInfo;
UNICODE_STRING Global_sz_DeviceName;
PDEVICE_POWER_INFORMATION Global_PowerInfo_Ptr;

NTSTATUS
DriverEntry(
//DriverEntry的第一个参数是一个指针
//指向一个刚被初始化的驱动程序对象, 该对象就代表你的驱动程序。
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
DbgPrint("In DriverEntry : Begin\r\n");
//保存设备服务键键名
RtlInitUnicodeString(
&Global_sz_Drv_RegInfo,
RegistryPath->Buffer);
// Initialize function pointers

DriverObject->DriverUnload = DriverUnload;
//注意看这里
DriverObject->DriverExtension->AddDevice = AddDevice;

DriverObject->MajorFunction[IRP_MJ_CREATE] = PsdoDispatchCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = PsdoDispatchClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = PsdoDispatchDeviceControl;
DriverObject->MajorFunction[IRP_MJ_POWER] = PsdoDispatchPower;
DriverObject->MajorFunction[IRP_MJ_PNP] = PsdoDispatchPnP;

DbgPrint("In DriverEntry : End\r\n");

return STATUS_SUCCESS;
}

NTSTATUS
AddDevice(
//DriverObject 参数指向一个驱动程序对象
//就是你在DriverEntry 例程中初始化的那个驱动程序对象。PhysicalDeviceObject参
//数指向设备堆栈底部的物理设备对象
IN PDRIVER_OBJECT DriverObject,
IN PDEVICE_OBJECT PhysicalDeviceObject
)
{
ULONG DeviceExtensionSize;
PDEVICE_EXTENSION p_DVCEXT;
PDEVICE_OBJECT ptr_PDO;
NTSTATUS status;
ULONG IdxPwrState;

DbgPrint("In AddDevice : Begin\r\n");
//设备名称
RtlInitUnicodeString(
&Global_sz_DeviceName,
L"\\DosDevices\\PSEUDODEVICE");
//Get DEVICE_EXTENSION required memory space
DeviceExtensionSize = sizeof(DEVICE_EXTENSION);
//创建设备,WDM驱动在这里创建设备
status = IoCreateDevice(
DriverObject,
DeviceExtensionSize,
&Global_sz_DeviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
//存放设备对象指针
&ptr_PDO
);

if (NT_SUCCESS(status)) {
//Set Device Object Flags
//清除DO_DEVICE_INITIALIZING标志
//当这个标志设置时,I/O 管理器将拒绝任何打开该设备句柄的请求或向该设备对象上附着其它设备对象的请求。
//在驱动程序完成初始化后,必须清除这个标志
ptr_PDO->Flags &= ~DO_DEVICE_INITIALIZING;
//设备对象中有两个标志位需要在 AddDevice 中初始化,并且它们在以后也不会改变,它们是
//DO_BUFFERED_IO 和 DO_DIRECT_IO 标志。你只能设置并使用其中一个标志,它将决定你以何种方式处理
//来自用户模式的内存缓冲区
//在 buffered 方式中,I/O管理器先创建一个与用户模式数据缓冲区大小相等的系统缓冲区。而你的驱动程序将使用这个
//系统缓冲区工作。I/O 管理器负责在系统缓冲区和用户模式缓冲区之间复制数据。
//在 direct 方式中,I/O 管理器锁定了包含用户模式缓冲区的物理内存页,并创建一个称为 MDL(内存描述符表)的辅助数
//据结构来描述锁定页。因此你的驱动程序将使用 MDL 工作。
ptr_PDO->Flags |= DO_DIRECT_IO;

//Device Extension memory maps
//初始化设备扩展
p_DVCEXT = ptr_PDO->DeviceExtension;
//保存新设备对象
p_DVCEXT->DeviceObject = ptr_PDO;

//Initialize driver description string
RtlInitUnicodeString(
&p_DVCEXT->Device_Description,
L"This is a Pseudo Device Driver\r\n"
L"Created by mjtsai 2003/1/25\r\n");
//初始化自旋锁
IoInitializeRemoveLock(
&p_DVCEXT->RemoveLock,
'KCOL',
0,
0
);

//Initialize driver power state
p_DVCEXT->SysPwrState = PowerSystemWorking;
//全供电状态
p_DVCEXT->DevPwrState = PowerDeviceD0;
//Initialize device power information
//可暂时跳过
Global_PowerInfo_Ptr = ExAllocatePool(
NonPagedPool, sizeof(DEVICE_POWER_INFORMATION));
RtlZeroMemory(
Global_PowerInfo_Ptr,
sizeof(DEVICE_POWER_INFORMATION));
Global_PowerInfo_Ptr->SupportQueryCapability = FALSE;
Global_PowerInfo_Ptr->DeviceD1 = 0;
Global_PowerInfo_Ptr->DeviceD2 = 0;
Global_PowerInfo_Ptr->WakeFromD0 = 0;
Global_PowerInfo_Ptr->WakeFromD1 = 0;
Global_PowerInfo_Ptr->WakeFromD2 = 0;
Global_PowerInfo_Ptr->WakeFromD3 = 0;
Global_PowerInfo_Ptr->DeviceWake = 0;
Global_PowerInfo_Ptr->SystemWake = 0;
for (IdxPwrState = 0;
IdxPwrState < PowerSystemMaximum;
IdxPwrState++)
{
Global_PowerInfo_Ptr->DeviceState[IdxPwrState] = 0;
}

//Store next-layered device object
//Attach device object to device stack
//IoAttachDeviceToDeviceStack 的第一个参数是新创建的设备对象的地址
//第二个参数是设备对象指针。 AddDevice的第二个参数也是这个地址。
//返回值是紧接着你下面的任何设备对象的地址,它可以是设备对象,也可以是其它低
//级过滤器设备对象
//本例把新设备对象链接到底层物理设备对象之上
p_DVCEXT->NextDeviceObject =
IoAttachDeviceToDeviceStack(ptr_PDO, PhysicalDeviceObject);
}

DbgPrint("In AddDevice : End\r\n");

return status;
}

VOID
DriverUnload(
IN PDRIVER_OBJECT DriverObject
)
{
PDEVICE_EXTENSION p_DVCEXT;

DbgPrint("In DriverUnload : Begin\r\n");

p_DVCEXT = DriverObject->DeviceObject->DeviceExtension;
ExFreePool(Global_PowerInfo_Ptr);
RtlFreeUnicodeString(&Global_sz_Drv_RegInfo);
RtlFreeUnicodeString(
&p_DVCEXT->Device_Description);
//减少调用者设备对象和底层驱动的设备对象的连接
//减少对p_DVCEXT->DeviceObject的引用计数
//如果计数为0且底层驱动已经被标记为卸载操作,则底层驱动被卸载
IoDetachDevice(
p_DVCEXT->DeviceObject);
//从系统中移除一个设备对象,当p_DVCEXT->NextDeviceObject引用计数为0时。
//如果不为0,则标记为待删除。
IoDeleteDevice(
p_DVCEXT->NextDeviceObject);

DbgPrint("In DriverUnload : End\r\n");
return;
}

NTSTATUS
PsdoDispatchCreate(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
PIO_STACK_LOCATION p_IO_STK;
PDEVICE_EXTENSION p_DVCEXT;
NTSTATUS status;

DbgPrint("IRP_MJ_CREATE Received : Begin\r\n");
//获得当前IRP堆栈位置
p_IO_STK = IoGetCurrentIrpStackLocation(Irp);
//DeviceExtension该结构可用于保存每个设备实例的信息
p_DVCEXT = DeviceObject->DeviceExtension;
//增加计数
status = IoAcquireRemoveLock(&p_DVCEXT->RemoveLock, p_IO_STK->FileObject);
if (NT_SUCCESS(status)) {
CompleteRequest(Irp, STATUS_SUCCESS, 0);
DbgPrint("IRP_MJ_CREATE Received : End\r\n");
return STATUS_SUCCESS;
} else {
IoReleaseRemoveLock(&p_DVCEXT->RemoveLock, p_IO_STK->FileObject);
CompleteRequest(Irp, status, 0);
DbgPrint("IRP_MJ_CREATE Received : End\r\n");
return status;
}
}

NTSTATUS
PsdoDispatchClose(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
PIO_STACK_LOCATION p_IO_STK;
PDEVICE_EXTENSION p_DVCEXT;

DbgPrint("IRP_MJ_CLOSE Received : Begin\r\n");

p_IO_STK = IoGetCurrentIrpStackLocation(Irp);
p_DVCEXT = DeviceObject->DeviceExtension;
IoReleaseRemoveLock(&p_DVCEXT->RemoveLock,
p_IO_STK->FileObject);
CompleteRequest(Irp, STATUS_SUCCESS, 0);

DbgPrint("IRP_MJ_CLOSE Received : Begin\r\n");
return STATUS_SUCCESS;
}

NTSTATUS
PsdoDispatchDeviceControl(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
ULONG code, cbin, cbout, info, pwrinf_size;
PIO_STACK_LOCATION p_IO_STK;
PDEVICE_EXTENSION p_DVCEXT;
PDEVICE_POWER_INFORMATION pValue;
ULONG IdxPwrState;
NTSTATUS status;

p_IO_STK = IoGetCurrentIrpStackLocation(Irp);
p_DVCEXT = DeviceObject->DeviceExtension;
//DeviceIoControl访问码
code = p_IO_STK->Parameters.DeviceIoControl.IoControlCode;
//输入缓冲区长度
cbin = p_IO_STK->Parameters.DeviceIoControl.InputBufferLength;
//输出缓冲区长度
cbout = p_IO_STK->Parameters.DeviceIoControl.OutputBufferLength;
IoAcquireRemoveLock(&p_DVCEXT->RemoveLock, Irp);

switch(code)
{
case IOCTL_READ_DEVICE_INFO:
if (p_DVCEXT->Device_Description.Length > cbout)
{
cbout = p_DVCEXT->Device_Description.Length;
info = cbout;
} else {
info = p_DVCEXT->Device_Description.Length;
}
//将数据复制到系统缓冲区,I/O管理器负责将数据复制到用户缓冲区内
RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer,
p_DVCEXT->Device_Description.Buffer,
info);
status = STATUS_SUCCESS;
break;
//可跳过不看
case IOCTL_READ_POWER_INFO:
pwrinf_size = sizeof(DEVICE_POWER_INFORMATION);
if (pwrinf_size > cbout)
{
cbout = pwrinf_size;
info = cbout;
} else {
info = pwrinf_size;
}
//Display Related Device Power State
DbgPrint("Support Query Device Capability : %$r\n",
Global_PowerInfo_Ptr->SupportQueryCapability ? "Yes" : "No");
DbgPrint("DeviceD1 : %d\r\n", Global_PowerInfo_Ptr->DeviceD1);
DbgPrint("DeviceD2 : %d\r\n", Global_PowerInfo_Ptr->DeviceD2);
DbgPrint("WakeFromD0 : %d\r\n", Global_PowerInfo_Ptr->WakeFromD0);
DbgPrint("WakeFromD1 : %d\r\n", Global_PowerInfo_Ptr->WakeFromD1);
DbgPrint("WakeFromD2 : %d\r\n", Global_PowerInfo_Ptr->WakeFromD2);
DbgPrint("WakeFromD3 : %d\r\n", Global_PowerInfo_Ptr->WakeFromD3);
DbgPrint("SystemWake : %d\r\n", Global_PowerInfo_Ptr->SystemWake);
DbgPrint("DeviceWake : %d\r\n", Global_PowerInfo_Ptr->DeviceWake);
for (IdxPwrState = 0;
IdxPwrState < PowerSystemMaximum;
IdxPwrState++)
{
DbgPrint("DeviceState[%d] : %d\r\n",
IdxPwrState,
Global_PowerInfo_Ptr->DeviceState[IdxPwrState]);
}
#ifdef _DEF_HANDLE_BY_POWER_INFO_STRUCTURE
pValue = (PDEVICE_POWER_INFORMATION)
Irp->AssociatedIrp.SystemBuffer;
pValue->SupportQueryCapability = Global_PowerInfo_Ptr->SupportQueryCapability;
pValue->DeviceD1 = Global_PowerInfo_Ptr->DeviceD1;
pValue->DeviceD2 = Global_PowerInfo_Ptr->DeviceD2;
pValue->DeviceWake = Global_PowerInfo_Ptr->DeviceWake;
pValue->SystemWake = Global_PowerInfo_Ptr->SystemWake;
pValue->WakeFromD0 = Global_PowerInfo_Ptr->WakeFromD0;
pValue->WakeFromD1 = Global_PowerInfo_Ptr->WakeFromD1;
pValue->WakeFromD2 = Global_PowerInfo_Ptr->WakeFromD2;
pValue->WakeFromD3 = Global_PowerInfo_Ptr->WakeFromD3;
for (IdxPwrState = 0;
IdxPwrState < PowerSystemMaximum;
IdxPwrState++)
{
pValue->DeviceState[IdxPwrState] =
Global_PowerInfo_Ptr->DeviceState[IdxPwrState];
}
#else
RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer,
Global_PowerInfo_Ptr,
info);
#endif
status = STATUS_SUCCESS;
break;
default:
info = 0;
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}

IoReleaseRemoveLock(&p_DVCEXT->RemoveLock, Irp);

CompleteRequest(Irp, STATUS_SUCCESS, info);
return status;
}
/*
NTSTATUS CompleteRequest(
IN PIRP Irp,
IN NTSTATUS status,
IN ULONG_PTR info)
{

Irp->IoStatus.Status = status;
Irp->IoStatus.Information = info;
//驱动程序已经完成对IRP的处理,并把IRP交给I/O管理器。
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
*/
用户层部分:
#include "stdafx.h"
#include <winioctl.h>
/*
Self-Defined I/O Control Code
*/
//I/O Control Code for Device Information retrieval
//注意下面的缓冲模式与驱动程序中adddevice函数中初始的缓冲模式有些不同
//解释如下:
//那时我指出, 当读写请求到来时, 你必须记住在 AddDevice
//中指出的访问用户模式缓冲区所使用的模式,是 buffered模式还是 direct 模式(或者两者都不是)。控制请求也
//利用这些寻址方式,但有一些差异。不是用设备对象中的标志来指定全局寻址方式,而是用 IOCTL中的功能码
//的低两位来为每个IOCTL指定寻址方式
//此外,IOCTL中指定的缓冲方式并不影响普通读写 IRP的寻址缓冲区。
//#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) )
//#define METHOD_BUFFERED 0
//#define METHOD_IN_DIRECT 1
//#define METHOD_OUT_DIRECT 2
//#define METHOD_NEITHER 3
#define IOCTL_READ_DEVICE_INFO \
CTL_CODE( \
FILE_DEVICE_UNKNOWN, \
0x800, \
METHOD_BUFFERED, \
FILE_ANY_ACCESS)
int main(int argc, char* argv[])
{
//倒数第二个参数:我们以不指定FILE_FLAG_OVERLAPPED标志的情况下打开设备句柄。因此,在这之后对
//DeviceIoControl的调用将不返回,直到驱动程序对我们的请求做出回答。
//用来设置同步或异步
HANDLE hdevice = CreateFile("\\\\.\\PSEUDODEVICE", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hdevice == INVALID_HANDLE_VALUE)
{
printf("Unable to open PSEUDODEVICE device - error %d\n", GetLastError());
return 1;
}

wchar_t answer[512] = {'\0'};
DWORD junk;
//只需要输出缓冲区,注意answer为unicode
if (DeviceIoControl(hdevice, IOCTL_READ_DEVICE_INFO, NULL, 0, answer, sizeof(answer), &junk, NULL))
{
answer[junk] = 0;
MessageBoxW(NULL,answer,L"look me", 0);
wprintf(L"%s",answer);
}
else
printf("Error %d in call to DeviceIoControl\n", GetLastError());
CloseHandle(hdevice);
return 0;
}
如何编译代码:
进入ShareFiles文件夹,分别Build "Basic","PnP","PM",然后Build "IOCTL_PW"
如何安装驱动:
这里使用“控制面板”中的“添加硬件”的方式,选中从“磁盘安装”,选择“install"文件夹内的inf文件进行安装即可(普通的动态加载驱动程序的方法好像不适用于WDM驱动,每次都蓝屏)
分享到:
评论

相关推荐

    一个简单的WDM驱动例子

    用户模式的应用程序可以使用CreateFile API来打开这个设备,并通过DeviceIoControl发送控制代码到驱动。 5. **用户模式应用调用**:在用户模式下,我们需要一个测试程序来验证驱动的功能。这通常涉及到创建设备句柄...

    windows 驱动 应用层与驱动层通信(读、写文件) 源码

    在Windows中,应用程序通过调用API函数(如CreateFile, ReadFile, WriteFile等)进行文件操作,这些API函数会将请求传递到I/O管理器,然后由I/O管理器转发给相应的驱动程序。驱动程序则处理这些请求,完成实际的硬件...

    USB设备的WDM驱动程序设计(转)

    以创建设备句柄为例,当应用程序调用`CreateFile`函数时,内核通过参数中的设备名称找到对应的驱动程序,并向驱动程序发送主功能名域(MajorFunction)。这一过程涉及到的主要步骤如下: 1. **打开设备**:应用程序...

    windows wdm驱动的例子,采用异步完成的方式实现驱动程序和应用程序通信的程序

    测试程序可能使用CreateFile、DeviceIoControl等API与驱动程序通信,并通过事件或回调来等待异步操作的完成。 在实现异步完成时,驱动程序通常会执行以下步骤: - **注册I/O完成例程**:当驱动程序接收到I/O请求时...

    实战DeviceIoControl.rar_deviceiocontrol_driver.IoControl_pdiusbd12

    在Windows操作系统中,DeviceIoControl是一个非常重要的API函数,它为...在pdiusbd12这样的USB驱动中,熟练掌握DeviceIoControl的使用技巧,能够帮助开发者高效地进行设备编程和调试,从而更好地服务于各种应用需求。

    windows驱动开发WDM程序设计实务之I/O

    在Windows驱动开发中,WDM(Windows Driver Model)是一种广泛使用的模型,用于构建与硬件交互的软件组件。WDM驱动程序设计涵盖了多个方面,其中包括I/O(Input/Output)管理,这是驱动程序与系统和应用程序之间进行...

    windows驱动开发+exe调用驱动文件中的api

    在VC++中,可以创建一个Win32应用程序项目,利用CreateFile、DeviceIoControl等函数来与驱动通信。 在描述中提到的"可以用了,就是慢"可能指的是在调用驱动API时遇到了性能问题。性能问题可能源于多方面,包括驱动...

    设备驱动程序通知应用程序的5种方法

    在 Win32 应用程序中,使用 CreateFile() 函数动态加载设备驱动程序,然后定义一个回调函数 backFunc(),并将回调函数的地址作为参数,通过 DeviceIoControl() 传送给设备驱动程序。设备驱动程序获得回调函数的地址...

    windows驱动开发技术详解 加载和卸载.sys驱动程序的exe源代码.zip

    4. **NTDriverLoader.exe**:这个可执行文件可能是用于加载和卸载驱动的工具,它可能使用`CreateFile`、`DeviceIoControl`等API与驱动进行通信,执行加载(通过` ZwCreateSection`、`ZwMapViewOfSection`等内核API...

    cp.zip_vb设备驱动_驱动_驱动加载

    例如,可以使用CreateFile函数打开驱动设备,然后用DeviceIoControl函数与驱动进行通信。 在"www.pudn.com.txt"这个文件中,可能包含的是关于VB设备驱动编程的教程、代码示例或者是在pudn.com网站上分享的相关资源...

    台湾大学驱动教程

    这部分可能讲解了创建和使用设备接口类,以及如何通过CreateFile和DeviceIoControl等API进行通信。 7. **Writing Driver Programs.pdf**:这部分可能包含驱动程序开发的实际编程指导,包括驱动程序的生命周期、错误...

    鼠标左右键切换驱动源码及MFC加载驱动测试源码(vs2015)

    1. **驱动加载**:使用`CreateFile`和`DeviceIoControl`等函数加载和通信驱动。 2. **用户界面**:设计友好的图形界面,展示驱动状态和进行按键切换操作。 3. **错误处理**:捕获并处理驱动加载或操作过程中的异常。...

    vc 简单加载驱动源码

    在Windows系统中,NT驱动程序遵循Windows Driver Model (WDM),包括Kernel-Mode Drivers(KMDF)和User-Mode Drivers(UMDF)。 二、Visual C++与驱动开发 虽然Visual C++主要用于开发用户模式的应用程序,但通过...

    vb调取驱动例子

    2. **系统调用**:在VB中,通常通过P/Invoke(Platform Invoke)技术来调用Windows API,这些API中包含了与驱动相关的函数,如`CreateFile`、`DeviceIoControl`等,用于打开设备并发送控制命令。 3. **类模块引用**...

    wince驱动开发PPT文件

    在Windows CE(简称WinCE)操作系统中,驱动程序开发是一项至关重要的任务,它涉及到系统与硬件设备之间的交互,确保硬件功能得以充分利用。本PPT文件着重讲解了WinCE驱动开发的相关知识,包括本机驱动程序和流接口...

    Windows+CE+设备驱动程序开发指南.pdf

    此外,驱动还需要实现DeviceIoControl和CreateFile等系统调用的回调函数。 六、调试驱动程序 利用Kernel Debugger和User-Mode Debugger,开发者可以对驱动进行调试,检查运行时的内存状态、跟踪执行流程、捕获异常...

    EZ-USB通用驱动程序说明

    对于用户态的应用,你可以使用任何支持Win 32 功能的编译工具CreateFile()和DeviceIoControl()。所提供的例程就是运行在Microsoft Visual C++5.0 下。 在加载EZ-USB GPD时,本章将要描述一个USB设备驱动是如何加载...

    浅析设备驱动程序通知应用程序的几种方法

    4. 异步过程调用(Asynchronous Procedure Call, APC):在WDM模型中,设备驱动程序可以使用APC来异步通知应用程序。驱动程序在完成I/O操作后,可以调度一个APC,将其插入到应用程序线程的APC队列中。当线程进入空闲...

    2022年试谈Windows环境下输入输出程序设计(共52张PPT).pptx

    - 在Windows 2000/XP中,应用程序通常通过系统提供的API(如CreateFile、DeviceIoControl等)来发起I/O请求。这些API会自动处理I/O请求,调用合适的驱动程序,并在必要时转换为硬件可理解的命令。 - I/O请求的处理...

    USB驱动编程

    例如,`CreateFile`函数用于打开设备,`DeviceIoControl`用于发送I/O控制命令,而`ReadFile`和`WriteFile`则用于读写设备数据。 此外,调试驱动程序也是一项挑战,因为它们运行在内核模式下,错误可能导致系统崩溃...

Global site tag (gtag.js) - Google Analytics